Skip to content

Commit fa4785a

Browse files
mainnet-patrkalis
andauthored
Add vmResourceUsage method to TransactionBuilder (#354)
Co-authored-by: Rosco Kalis <[email protected]>
1 parent d349ebf commit fa4785a

File tree

3 files changed

+78
-1
lines changed

3 files changed

+78
-1
lines changed

packages/cashscript/src/TransactionBuilder.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
isUnlockableUtxo,
1818
isStandardUnlockableUtxo,
1919
StandardUnlockableUtxo,
20+
VmResourceUsage,
21+
isContractUnlocker,
2022
} from './interfaces.js';
2123
import { NetworkProvider } from './network/index.js';
2224
import {
@@ -172,6 +174,44 @@ export class TransactionBuilder {
172174
return debugLibauthTemplate(this.getLibauthTemplate(), this);
173175
}
174176

177+
getVmResourceUsage(verbose: boolean = false): Array<VmResourceUsage> {
178+
// Note that only StandardUnlockableUtxo inputs are supported for debugging, so any transaction with custom unlockers
179+
// cannot be debugged (and therefore cannot return VM resource usage)
180+
const results = this.debug();
181+
const vmResourceUsage: Array<VmResourceUsage> = [];
182+
const tableData: Array<Record<string, any>> = [];
183+
184+
const formatMetric = (value: number, total: number, withPercentage: boolean = false): string =>
185+
`${formatNumber(value)} / ${formatNumber(total)}${withPercentage ? ` (${(value / total * 100).toFixed(0)}%)` : ''}`;
186+
const formatNumber = (value: number): string => value.toLocaleString('en');
187+
188+
const resultEntries = Object.entries(results);
189+
for (const [index, input] of this.inputs.entries()) {
190+
const [, result] = resultEntries.find(([entryKey]) => entryKey.includes(`input${index}`)) ?? [];
191+
const metrics = result?.at(-1)?.metrics;
192+
193+
// Should not happen
194+
if (!metrics) throw new Error('VM resource could not be calculated');
195+
196+
vmResourceUsage.push(metrics);
197+
tableData.push({
198+
'Contract - Function': isContractUnlocker(input.unlocker) ? `${input.unlocker.contract.name} - ${input.unlocker.abiFunction.name}` : 'P2PKH Input',
199+
Ops: metrics.evaluatedInstructionCount,
200+
'Op Cost Budget Usage': formatMetric(metrics.operationCost, metrics.maximumOperationCost, true),
201+
SigChecks: formatMetric(metrics.signatureCheckCount, metrics.maximumSignatureCheckCount),
202+
Hashes: formatMetric(metrics.hashDigestIterations, metrics.maximumHashDigestIterations),
203+
});
204+
}
205+
206+
if (verbose) {
207+
console.log('VM Resource usage by inputs:');
208+
console.table(tableData);
209+
}
210+
211+
return vmResourceUsage;
212+
}
213+
214+
// TODO: rename to getBitauthUri()
175215
bitauthUri(): string {
176216
console.warn('WARNING: it is unsafe to use this Bitauth URI when using real private keys as they are included in the transaction template');
177217
return getBitauthUri(this.getLibauthTemplate());

packages/cashscript/src/interfaces.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Transaction } from '@bitauth/libauth';
1+
import { AuthenticationProgramStateResourceLimits, type Transaction } from '@bitauth/libauth';
22
import type { NetworkProvider } from './network/index.js';
33
import type SignatureTemplate from './SignatureTemplate.js';
44
import { Contract } from './Contract.js';
@@ -164,3 +164,5 @@ export interface ContractOptions {
164164
}
165165

166166
export type AddressType = 'p2sh20' | 'p2sh32';
167+
168+
export type VmResourceUsage = AuthenticationProgramStateResourceLimits['metrics'];

packages/cashscript/test/debugging.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,3 +678,38 @@ describe('Debugging tests', () => {
678678
}
679679
});
680680
});
681+
682+
describe('VM Resources', () => {
683+
it('Should output VM resource usage', async () => {
684+
const provider = new MockNetworkProvider();
685+
686+
const contractSingleFunction = new Contract({ ...artifactTestSingleFunction, contractName: 'SingleFunction' }, [], { provider });
687+
const contractZeroHandling = new Contract({ ...artifactTestZeroHandling, contractName: 'ZeroHandling' }, [0n], { provider });
688+
689+
provider.addUtxo(contractSingleFunction.address, randomUtxo());
690+
provider.addUtxo(contractZeroHandling.address, randomUtxo());
691+
provider.addUtxo(aliceAddress, randomUtxo());
692+
693+
const tx = new TransactionBuilder({ provider })
694+
.addInputs(await contractSingleFunction.getUtxos(), contractSingleFunction.unlock.test_require_single_function())
695+
.addInputs(await contractZeroHandling.getUtxos(), contractZeroHandling.unlock.test_zero_handling(0n))
696+
.addInput((await provider.getUtxos(aliceAddress))[0], new SignatureTemplate(alicePriv).unlockP2PKH())
697+
.addOutput({ to: aliceAddress, amount: 1000n });
698+
699+
console.log = jest.fn();
700+
console.table = jest.fn();
701+
702+
const vmUsage = tx.getVmResourceUsage();
703+
expect(console.log).not.toHaveBeenCalled();
704+
expect(console.table).not.toHaveBeenCalled();
705+
706+
tx.getVmResourceUsage(true);
707+
expect(console.log).toHaveBeenCalledWith('VM Resource usage by inputs:');
708+
expect(console.table).toHaveBeenCalled();
709+
710+
jest.restoreAllMocks();
711+
712+
expect(vmUsage[0]?.hashDigestIterations).toBeGreaterThan(0);
713+
expect(vmUsage[2]?.hashDigestIterations).toBeGreaterThan(0);
714+
});
715+
});

0 commit comments

Comments
 (0)