Skip to content
This repository was archived by the owner on Jul 10, 2026. It is now read-only.

Commit fce303b

Browse files
committed
feat: add benchmarks
1 parent f82399f commit fce303b

3 files changed

Lines changed: 316 additions & 0 deletions

File tree

Nargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ members = [
1616
[benchmark]
1717
token = "benchmarks/token_contract.benchmark.ts"
1818
nft = "benchmarks/nft_contract.benchmark.ts"
19+
multitoken = "benchmarks/multitoken_contract.benchmark.ts"
1920
vault = "benchmarks/vault_contract.benchmark.ts"
2021
escrow = "benchmarks/escrow_contract.benchmark.ts"
2122
logic = "benchmarks/logic_contract.benchmark.ts"
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import type { Wallet } from '@aztec/aztec.js/wallet';
2+
import { AztecAddress } from '@aztec/aztec.js/addresses';
3+
import type { ContractFunctionInteractionCallIntent } from '@aztec/aztec.js/authorization';
4+
5+
// Import the new Benchmark base class and context
6+
import { Benchmark, BenchmarkContext } from '@defi-wonderland/aztec-benchmark';
7+
8+
import { MultiTokenContract } from '../src/artifacts/MultiToken.js';
9+
import {
10+
deployMultiTokenWithMinter,
11+
initializeMultiTokenTransferCommitment,
12+
setupTestSuite,
13+
ID_A,
14+
} from '../src/ts/test/utils.js';
15+
16+
// Extend the BenchmarkContext from the new package
17+
interface MultiTokenBenchmarkContext extends BenchmarkContext {
18+
cleanup: () => Promise<void>;
19+
wallet: Wallet;
20+
deployer: AztecAddress;
21+
accounts: AztecAddress[];
22+
multiTokenContract: MultiTokenContract;
23+
commitments: bigint[];
24+
}
25+
26+
// --- Helper Functions ---
27+
28+
function amt(x: bigint | number) {
29+
// MultiToken carries NO decimals (its constructor takes only name/symbol/minter/auth_contract, unlike the
30+
// Token contract's `decimals` arg), so amounts are raw u128 values. We keep the sibling token benchmark's
31+
// magnitudes (mint 100, move 10) without the 10**18 scaling that `parseUnits` would apply.
32+
return BigInt(x);
33+
}
34+
35+
// Use export default class extending Benchmark
36+
export default class MultiTokenContractBenchmark extends Benchmark {
37+
/**
38+
* Sets up the benchmark environment for the MultiTokenContract.
39+
* Creates wallet, gets accounts, and deploys the contract.
40+
*/
41+
async setup(): Promise<MultiTokenBenchmarkContext> {
42+
const { cleanup, wallet, accounts } = await setupTestSuite(true);
43+
const [deployer] = accounts;
44+
// minter = deployer, auth_contract = ZERO (ARC-403 hook disabled) — mirrors the default JS suite deploy.
45+
const multiTokenContract = await deployMultiTokenWithMinter(wallet, deployer, deployer, AztecAddress.ZERO);
46+
47+
// Pre-initialize the partial notes consumed by transfer_private_to_commitment / transfer_public_to_commitment.
48+
// The commitment is id-agnostic (the completer binds the id at completion); the payer (alice) must be the
49+
// completer, and the note must come from a PRIOR settled tx (the helper settles each one).
50+
const [alice, bob] = accounts;
51+
const commitment_1 = await initializeMultiTokenTransferCommitment(multiTokenContract, alice, bob, alice);
52+
const commitment_2 = await initializeMultiTokenTransferCommitment(multiTokenContract, alice, bob, alice);
53+
54+
const commitments = [commitment_1, commitment_2];
55+
56+
return { cleanup, wallet, deployer, accounts, multiTokenContract, commitments };
57+
}
58+
59+
/**
60+
* Returns the list of MultiTokenContract methods to be benchmarked.
61+
* Ordering matters: the mints seed the balances that the following transfers/burns/commitments spend.
62+
* Every op is an `alice` self-spend of token id `ID_A` (nonce = 0, so no authwit is needed).
63+
*/
64+
getMethods(context: MultiTokenBenchmarkContext): ContractFunctionInteractionCallIntent[] {
65+
const { multiTokenContract, accounts, wallet, commitments } = context;
66+
const [alice, bob] = accounts;
67+
const owner = alice;
68+
const id = ID_A;
69+
70+
const methods: ContractFunctionInteractionCallIntent[] = [
71+
// Mint methods
72+
{
73+
caller: alice,
74+
action: multiTokenContract.withWallet(wallet).methods.mint_to_private(owner, id, amt(100)),
75+
},
76+
{
77+
caller: alice,
78+
action: multiTokenContract.withWallet(wallet).methods.mint_to_public(owner, id, amt(100)),
79+
},
80+
81+
// Transfer methods
82+
{
83+
caller: alice,
84+
action: multiTokenContract.withWallet(wallet).methods.transfer_private_to_public(owner, bob, id, amt(10), 0),
85+
},
86+
{
87+
caller: alice,
88+
action: multiTokenContract.withWallet(wallet).methods.transfer_private_to_private(owner, bob, id, amt(10), 0),
89+
},
90+
{
91+
caller: alice,
92+
action: multiTokenContract.withWallet(wallet).methods.transfer_public_to_private(owner, bob, id, amt(10), 0),
93+
},
94+
{
95+
caller: alice,
96+
action: multiTokenContract.withWallet(wallet).methods.transfer_public_to_public(owner, bob, id, amt(10), 0),
97+
},
98+
99+
// Burn methods
100+
{
101+
caller: alice,
102+
action: multiTokenContract.withWallet(wallet).methods.burn_private(owner, id, amt(10), 0),
103+
},
104+
{
105+
caller: alice,
106+
action: multiTokenContract.withWallet(wallet).methods.burn_public(owner, id, amt(10), 0),
107+
},
108+
109+
// Partial notes methods
110+
{
111+
caller: alice,
112+
action: multiTokenContract.withWallet(wallet).methods.initialize_transfer_commitment(bob, owner),
113+
},
114+
{
115+
caller: alice,
116+
action: multiTokenContract
117+
.withWallet(wallet)
118+
.methods.transfer_private_to_commitment(owner, id, commitments[0], amt(10), 0),
119+
},
120+
{
121+
caller: alice,
122+
action: multiTokenContract
123+
.withWallet(wallet)
124+
.methods.transfer_public_to_commitment(owner, id, commitments[1], amt(10), 0),
125+
},
126+
];
127+
128+
return methods.filter(Boolean);
129+
}
130+
131+
async teardown(context: MultiTokenBenchmarkContext): Promise<void> {
132+
await context.cleanup();
133+
}
134+
}

src/ts/test/utils.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import { VaultDeployerContract, VaultDeployerContractArtifact } from '../../../s
6060
import { NFTContract } from '../../../src/artifacts/NFT.js';
6161
import { TestLogicContract } from '../../../src/artifacts/TestLogic.js';
6262
import { EscrowContract } from '../../../src/artifacts/Escrow.js';
63+
import { MultiTokenContract } from '../../../src/artifacts/MultiToken.js';
6364

6465
import { expect } from 'vitest';
6566

@@ -766,3 +767,183 @@ export async function expectNFTTransferEvents(
766767
expect(events[i].token_id).toEqual(expected[i].token_id);
767768
}
768769
}
770+
771+
// --- MultiToken Utils ---
772+
//
773+
// MultiToken is token-shaped (public + private balances, commitments, ARC-403 authwit hook) but
774+
// NFT-shaped in that every balance-bearing op carries an `id: Field`. Its event is the 4-field
775+
// `TransferSingle{from,to,id,amount}` (eventSelector 0x2429b477) — the existing 3-field `Transfer`
776+
// helpers cannot be reused (see the design-contract E-01 and the plan's divergence guard).
777+
778+
/** Human-readable name/symbol used by the MultiToken deploy helper (kept as constants so tests can round-trip them). */
779+
export const MULTITOKEN_NAME = 'MultiToken';
780+
export const MULTITOKEN_SYMBOL = 'MTK';
781+
782+
/** Token-id fixture used across the MultiToken happy-path tests. */
783+
export const ID_A = 1n;
784+
785+
/**
786+
* Packs a short (<=31 byte) ASCII string into a single Field, big-endian — the encoding used by the
787+
* Noir `FieldCompressedString` that MultiToken's ctor takes for name/symbol (codegen shape `{ value }`).
788+
* The MultiToken ctor stores exactly the Field it is given, so the deploy-and-read-back round-trip in
789+
* tests holds regardless of any subtle byte-order difference; a real string is used so the value is meaningful.
790+
* @param s - The string to pack (max 31 bytes).
791+
* @returns The packed Field.
792+
*/
793+
export function fieldFromShortString(s: string): Fr {
794+
const bytes = new TextEncoder().encode(s);
795+
if (bytes.length > 31) {
796+
throw new Error(`String "${s}" is too long to pack into a single Field (max 31 bytes)`);
797+
}
798+
let acc = 0n;
799+
for (const b of bytes) {
800+
acc = (acc << 8n) + BigInt(b);
801+
}
802+
return new Fr(acc);
803+
}
804+
805+
/**
806+
* Normalises the value of a decoded `FieldCompressedString` (returned by `name()`/`symbol()`) to a bigint,
807+
* tolerating whether the SDK decodes the struct to `{ value }` (bigint or Fr) or to a bare scalar.
808+
*/
809+
export function compressedStringToBigInt(result: any): bigint {
810+
const v = result?.value ?? result;
811+
if (typeof v === 'bigint') return v;
812+
if (typeof v === 'number') return BigInt(v);
813+
return v.toBigInt();
814+
}
815+
816+
/**
817+
* Deploys the MultiToken contract with a specified minter (and optional ARC-403 auth contract).
818+
* @param wallet - The wallet to deploy the contract with.
819+
* @param deployer - The account that sends the deploy tx.
820+
* @param minter - The address stored as the (immutable) minter.
821+
* @param authContract - Optional ARC-403 hook contract address; `AztecAddress.ZERO` (default) disables the hook.
822+
* @returns A deployed MultiTokenContract instance.
823+
*/
824+
export async function deployMultiTokenWithMinter(
825+
wallet: Wallet,
826+
deployer: AztecAddress,
827+
minter: AztecAddress,
828+
authContract: AztecAddress = AztecAddress.ZERO,
829+
options?: DeployOptions,
830+
): Promise<MultiTokenContract> {
831+
const { contract } = await MultiTokenContract.deployWithOpts(
832+
{ method: 'constructor_with_minter', wallet },
833+
{ value: fieldFromShortString(MULTITOKEN_NAME) },
834+
{ value: fieldFromShortString(MULTITOKEN_SYMBOL) },
835+
minter,
836+
authContract,
837+
).send({ ...options, from: deployer });
838+
return contract as MultiTokenContract;
839+
}
840+
841+
// TODO: Replace wallet internals (privateExecutionResult) with simulate() + send() to get private return values via public API.
842+
/**
843+
* Initializes a MultiToken transfer commitment (partial note) and returns its commitment Field.
844+
* Mirrors `initializeTransferCommitment` (the ONLY sanctioned wallet-internals escape hatch) — the
845+
* MultiToken `initialize_transfer_commitment(to, completer)` is id-AGNOSTIC (the completer binds the id
846+
* at completion), so the signature is identical to the Token/NFT variant. Reaches into
847+
* `WalletWithInternals` to extract the partial-note commitment from `provenTx.privateExecutionResult`.
848+
* @param token - The MultiToken contract instance.
849+
* @param caller - The account that sends (and settles) the initialize tx.
850+
* @param to - The address of the note recipient.
851+
* @param completer - The address allowed to complete the partial note.
852+
* @returns Partial note commitment.
853+
*/
854+
export async function initializeMultiTokenTransferCommitment(
855+
token: MultiTokenContract,
856+
caller: AztecAddress,
857+
to: AztecAddress,
858+
completer: AztecAddress,
859+
): Promise<bigint> {
860+
const interaction = token.methods.initialize_transfer_commitment(to, completer);
861+
const executionPayload = await interaction.request();
862+
const w = token.wallet as unknown as WalletWithInternals;
863+
const feeOptions = await w.completeFeeOptions(caller, executionPayload.feePayer, undefined);
864+
const txRequest = await w.createTxExecutionRequestFromPayloadAndFee(executionPayload, caller, feeOptions);
865+
const provenTx = await w.pxe.proveTx(txRequest, { scopes: w.scopesFrom(caller), senderForTags: caller });
866+
867+
const entrypoint = provenTx.privateExecutionResult.entrypoint;
868+
const nestedResults = entrypoint.nestedExecutionResults;
869+
const returnValues = nestedResults[0].returnValues;
870+
const commitment = returnValues[0].toBigInt();
871+
872+
const tx = await provenTx.toTx();
873+
const txHash = tx.getTxHash();
874+
await node.sendTx(tx);
875+
await waitForTx(node, txHash);
876+
877+
return commitment;
878+
}
879+
880+
// --- MultiToken Transfer Event Utils ---
881+
882+
/** Represents a decoded MultiToken TransferSingle event (4 fields: from, to, id, amount). */
883+
export type MultiTokenTransferEvent = {
884+
from: AztecAddress;
885+
to: AztecAddress;
886+
id: bigint;
887+
amount: bigint;
888+
};
889+
890+
/**
891+
* Queries the node for public logs emitted in a transaction by a specific MultiToken contract,
892+
* and decodes them as `TransferSingle` events (4 fields; `id` distinguishes it from the 3-field
893+
* Token/NFT `Transfer`). An empty array serves the "no public events" privacy assertions.
894+
*
895+
* @param txHash - The transaction hash to query logs for.
896+
* @param contractAddress - The MultiToken contract address to filter logs by.
897+
* @returns An array of decoded MultiTokenTransferEvent objects.
898+
*/
899+
export async function getMultiTokenTransferEvents(
900+
txHash: TxHash,
901+
contractAddress: AztecAddress,
902+
): Promise<MultiTokenTransferEvent[]> {
903+
const response = await node.getPublicLogs({
904+
txHash,
905+
contractAddress,
906+
});
907+
908+
const eventMetadata = MultiTokenContract.events.TransferSingle;
909+
const expectedFieldCount = 4; // from, to, id, amount
910+
911+
return response.logs
912+
.filter((extLog) => {
913+
const eventFields = extLog.log.getEmittedFieldsWithoutTag();
914+
return eventFields.length === expectedFieldCount;
915+
})
916+
.map((extLog) => {
917+
const eventFields = extLog.log.getEmittedFieldsWithoutTag();
918+
return decodeFromAbi([eventMetadata.abiType], eventFields) as MultiTokenTransferEvent;
919+
});
920+
}
921+
922+
/**
923+
* Asserts that the TransferSingle events emitted by a specific MultiToken contract in a transaction
924+
* match the expected events exactly (count and content, order-sensitive).
925+
*
926+
* Comment convention above expectMultiTokenTransferEvents calls: `operation: TransferSingle(from, to, id, amount)`
927+
* - Mint to public: `// mint_to_public: TransferSingle(0x0, alice, id, AMOUNT)`
928+
* - Mint to commitment:`// mint_to_commitment: TransferSingle(0x0, PRIVATE, id, AMOUNT)`
929+
* - No events: `// transfer_private_to_private: (no public events)`
930+
*
931+
* @param txHash - The transaction hash to query logs for.
932+
* @param contractAddress - The MultiToken contract address to filter logs by.
933+
* @param expected - The expected TransferSingle events in order.
934+
*/
935+
export async function expectMultiTokenTransferEvents(
936+
txHash: TxHash,
937+
contractAddress: AztecAddress,
938+
expected: MultiTokenTransferEvent[],
939+
): Promise<void> {
940+
const events = await getMultiTokenTransferEvents(txHash, contractAddress);
941+
942+
expect(events.length).toBe(expected.length);
943+
for (let i = 0; i < expected.length; i++) {
944+
expect(events[i].from).toEqual(expected[i].from);
945+
expect(events[i].to).toEqual(expected[i].to);
946+
expect(events[i].id).toEqual(expected[i].id);
947+
expect(events[i].amount).toEqual(expected[i].amount);
948+
}
949+
}

0 commit comments

Comments
 (0)