diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index a52f3838..81d39874 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -17,6 +17,8 @@ import { isUnlockableUtxo, isStandardUnlockableUtxo, StandardUnlockableUtxo, + VmResourceUsage, + isContractUnlocker, } from './interfaces.js'; import { NetworkProvider } from './network/index.js'; import { @@ -172,6 +174,44 @@ export class TransactionBuilder { return debugLibauthTemplate(this.getLibauthTemplate(), this); } + getVmResourceUsage(verbose: boolean = false): Array { + // 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 = []; + const tableData: Array> = []; + + 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()); diff --git a/packages/cashscript/src/interfaces.ts b/packages/cashscript/src/interfaces.ts index 8e89e39e..badfc116 100644 --- a/packages/cashscript/src/interfaces.ts +++ b/packages/cashscript/src/interfaces.ts @@ -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'; @@ -164,3 +164,5 @@ export interface ContractOptions { } export type AddressType = 'p2sh20' | 'p2sh32'; + +export type VmResourceUsage = AuthenticationProgramStateResourceLimits['metrics']; diff --git a/packages/cashscript/test/debugging.test.ts b/packages/cashscript/test/debugging.test.ts index 3183bb64..0cd18b9d 100644 --- a/packages/cashscript/test/debugging.test.ts +++ b/packages/cashscript/test/debugging.test.ts @@ -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); + }); +});