Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/cashscript/src/TransactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
isUnlockableUtxo,
isStandardUnlockableUtxo,
StandardUnlockableUtxo,
VmResourceUsage,
isContractUnlocker,
} from './interfaces.js';
import { NetworkProvider } from './network/index.js';
import {
Expand Down Expand Up @@ -172,6 +174,44 @@ export class TransactionBuilder {
return debugLibauthTemplate(this.getLibauthTemplate(), this);
}

getVmResourceUsage(verbose: boolean = false): Array<VmResourceUsage> {
// Note that only StandardUnlockableUtxo inputs are supported for debugging, so any transaction with custom unlockers
// cannot be debugged (and therefore cannot return VM resource usage)
const results = this.debug();
const vmResourceUsage: Array<VmResourceUsage> = [];
const tableData: Array<Record<string, any>> = [];

const formatMetric = (value: number, total: number, withPercentage: boolean = false): string =>
`${formatNumber(value)} / ${formatNumber(total)}${withPercentage ? ` (${(value / total * 100).toFixed(0)}%)` : ''}`;
const formatNumber = (value: number): string => value.toLocaleString('en');

const resultEntries = Object.entries(results);
for (const [index, input] of this.inputs.entries()) {
const [, result] = resultEntries.find(([entryKey]) => entryKey.includes(`input${index}`)) ?? [];
const metrics = result?.at(-1)?.metrics;

// Should not happen
if (!metrics) throw new Error('VM resource could not be calculated');

vmResourceUsage.push(metrics);
tableData.push({
'Contract - Function': isContractUnlocker(input.unlocker) ? `${input.unlocker.contract.name} - ${input.unlocker.abiFunction.name}` : 'P2PKH Input',
Ops: metrics.evaluatedInstructionCount,
'Op Cost Budget Usage': formatMetric(metrics.operationCost, metrics.maximumOperationCost, true),
SigChecks: formatMetric(metrics.signatureCheckCount, metrics.maximumSignatureCheckCount),
Hashes: formatMetric(metrics.hashDigestIterations, metrics.maximumHashDigestIterations),
});
}

if (verbose) {
console.log('VM Resource usage by inputs:');
console.table(tableData);
}

return vmResourceUsage;
}

// TODO: rename to getBitauthUri()
bitauthUri(): string {
console.warn('WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template');
return getBitauthUri(this.getLibauthTemplate());
Expand Down
4 changes: 3 additions & 1 deletion packages/cashscript/src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Transaction } from '@bitauth/libauth';
import { AuthenticationProgramStateResourceLimits, type Transaction } from '@bitauth/libauth';
import type { NetworkProvider } from './network/index.js';
import type SignatureTemplate from './SignatureTemplate.js';
import { Contract } from './Contract.js';
Expand Down Expand Up @@ -164,3 +164,5 @@ export interface ContractOptions {
}

export type AddressType = 'p2sh20' | 'p2sh32';

export type VmResourceUsage = AuthenticationProgramStateResourceLimits['metrics'];
35 changes: 35 additions & 0 deletions packages/cashscript/test/debugging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,3 +678,38 @@ describe('Debugging tests', () => {
}
});
});

describe('VM Resources', () => {
it('Should output VM resource usage', async () => {
const provider = new MockNetworkProvider();

const contractSingleFunction = new Contract({ ...artifactTestSingleFunction, contractName: 'SingleFunction' }, [], { provider });
const contractZeroHandling = new Contract({ ...artifactTestZeroHandling, contractName: 'ZeroHandling' }, [0n], { provider });

provider.addUtxo(contractSingleFunction.address, randomUtxo());
provider.addUtxo(contractZeroHandling.address, randomUtxo());
provider.addUtxo(aliceAddress, randomUtxo());

const tx = new TransactionBuilder({ provider })
.addInputs(await contractSingleFunction.getUtxos(), contractSingleFunction.unlock.test_require_single_function())
.addInputs(await contractZeroHandling.getUtxos(), contractZeroHandling.unlock.test_zero_handling(0n))
.addInput((await provider.getUtxos(aliceAddress))[0], new SignatureTemplate(alicePriv).unlockP2PKH())
.addOutput({ to: aliceAddress, amount: 1000n });

console.log = jest.fn();
console.table = jest.fn();

const vmUsage = tx.getVmResourceUsage();
expect(console.log).not.toHaveBeenCalled();
expect(console.table).not.toHaveBeenCalled();

tx.getVmResourceUsage(true);
expect(console.log).toHaveBeenCalledWith('VM Resource usage by inputs:');
expect(console.table).toHaveBeenCalled();

jest.restoreAllMocks();

expect(vmUsage[0]?.hashDigestIterations).toBeGreaterThan(0);
expect(vmUsage[2]?.hashDigestIterations).toBeGreaterThan(0);
});
});
Loading