Skip to content

Commit 1450a7e

Browse files
authored
Merge pull request #38 from paritytech/pg/update-node-env
Token benchmarks
2 parents 8c01e8a + 603b0d0 commit 1450a7e

File tree

26 files changed

+2420
-161
lines changed

26 files changed

+2420
-161
lines changed

cli/token_benchmark.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env -S deno run --env-file --allow-all
2+
3+
import { env } from '../tools/lib/index.ts'
4+
import { abis } from '../codegen/abis.ts'
5+
import { MyToken } from '../codegen/addresses.ts'
6+
import { privateKeyToAccount } from 'viem/accounts'
7+
import { generatePrivateKey } from 'viem/accounts'
8+
import { parseEther } from 'viem'
9+
10+
const code = await env.wallet.getCode({ address: MyToken })
11+
if (!code) {
12+
console.error(`Deploy contract first with "deno task deploy"`)
13+
Deno.exit(1)
14+
} else {
15+
console.log(`MyToken code size: ${code.length / 2 - 1} bytes`)
16+
}
17+
18+
const ITERATIONS = 300
19+
20+
// Generate address pool (first half senders, second half receivers)
21+
console.log('Generating address...')
22+
const addressPool = Array.from({ length: ITERATIONS * 2 }, () => {
23+
const privateKey = generatePrivateKey()
24+
const account = privateKeyToAccount(privateKey)
25+
return { privateKey, address: account.address, account }
26+
})
27+
const senderAccounts = addressPool.slice(0, ITERATIONS)
28+
const receiverAddresses = addressPool.slice(ITERATIONS).map((a) => a.address)
29+
console.log(
30+
`Generated ${ITERATIONS} sender accounts and ${ITERATIONS} receiver addresses`,
31+
)
32+
33+
// Prefund all addresses to bring accounts into existence
34+
{
35+
console.log('Prefunding all addresses...')
36+
const nonce = await env.wallet.getTransactionCount(env.wallet.account)
37+
const fundHashes = await Promise.all(
38+
addressPool.map((account, index) => {
39+
return env.wallet.sendTransaction({
40+
to: account.address,
41+
value: parseEther('1'),
42+
nonce: nonce + addressPool.length - 1 - index,
43+
})
44+
}),
45+
)
46+
console.log(`Sent ${fundHashes.length} funding transactions`)
47+
await env.wallet.waitForTransactionReceipt({ hash: fundHashes[0] })
48+
console.log('All addresses funded')
49+
}
50+
51+
{
52+
// Simulate and execute mint with env.wallet
53+
const mintRequest = {
54+
address: MyToken,
55+
abi: abis.MyToken,
56+
functionName: 'mint',
57+
args: [env.wallet.account.address, 10_000_000_000_000_000_000_000_000n],
58+
} as const
59+
const mintHash = await env.wallet.writeContract(mintRequest)
60+
const mintReceipt = await env.wallet.waitForTransactionReceipt({
61+
hash: mintHash,
62+
})
63+
64+
// Simulate and execute transfer with env.wallet
65+
const transferRequest = {
66+
address: MyToken,
67+
abi: abis.MyToken,
68+
functionName: 'transfer',
69+
args: [receiverAddresses[0], 1n],
70+
} as const
71+
const transferHash = await env.wallet.writeContract(transferRequest)
72+
const transferReceipt = await env.wallet.waitForTransactionReceipt({
73+
hash: transferHash,
74+
})
75+
76+
// Build all transactions for the loop
77+
console.log('Building all transactions...')
78+
const allRequests = []
79+
for (let i = 0; i < ITERATIONS; i++) {
80+
const sender = senderAccounts[i]
81+
const receiverAddress = receiverAddresses[i]
82+
83+
// Mint transaction from sender (nonce 0)
84+
allRequests.push(
85+
env.wallet.writeContract({
86+
...mintRequest,
87+
nonce: 0,
88+
account: sender.account,
89+
gas: mintReceipt.gasUsed,
90+
gasPrice: mintReceipt.effectiveGasPrice,
91+
}),
92+
)
93+
94+
// // Transfer transaction from sender (nonce 1)
95+
allRequests.push(
96+
env.wallet.writeContract({
97+
...transferRequest,
98+
args: [receiverAddress, 1n],
99+
nonce: 1,
100+
account: sender.account,
101+
gas: transferReceipt.gasUsed,
102+
gasPrice: mintReceipt.effectiveGasPrice,
103+
}),
104+
)
105+
}
106+
107+
// Send all transactions at once
108+
console.log('Sending all transactions...')
109+
const allHashes = await Promise.all(allRequests)
110+
console.log(
111+
`Sent ${allHashes.length} transactions (${ITERATIONS} mint + ${ITERATIONS} transfer)`,
112+
)
113+
114+
console.log('Waiting for last transaction...')
115+
env.wallet.waitForTransactionReceipt({
116+
hash: allHashes[allHashes.length - 1],
117+
})
118+
119+
console.log(`\nSuccessfully processed ${ITERATIONS} mint + transfer pairs`)
120+
}

cli/token_deploy.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { deploy } from '../tools/lib/index.ts'
2+
import { readBytecode } from '../utils/index.ts'
3+
4+
await deploy({
5+
name: { name: 'MyToken', mappedTo: 'MyTokenEvm' },
6+
args: [],
7+
})
8+
9+
await deploy({
10+
name: { name: 'MyToken', mappedTo: 'MyTokenPvm' },
11+
args: [],
12+
bytecodeType: 'polkavm', // Specify `pvm` for PVM bytecode deployment
13+
})
14+
await deploy({
15+
name: { name: 'MyToken', mappedTo: 'MyTokenInk' },
16+
args: [],
17+
bytecode: readBytecode('./ink/ink_erc20/target/ink/ink_erc20.polkavm'),
18+
})
19+
20+
await deploy({
21+
name: { name: 'MyToken', mappedTo: 'MyTokenRustWithAlloc' },
22+
args: [],
23+
bytecode: readBytecode('./rust/contract_with_alloc/contract.polkavm'),
24+
})
25+
26+
await deploy({
27+
name: { name: 'MyToken', mappedTo: 'MyTokenRustNoAlloc' },
28+
args: [],
29+
bytecode: readBytecode('./rust/contract_no_alloc/contract.polkavm'),
30+
})

cli/token_execute.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env -S deno run --env-file --allow-all
2+
import { env } from '../tools/lib/index.ts'
3+
import { abis } from '../codegen/abis.ts'
4+
import {
5+
MyTokenEvm,
6+
MyTokenInk,
7+
MyTokenPvm,
8+
MyTokenRustNoAlloc,
9+
MyTokenRustWithAlloc,
10+
} from '../codegen/addresses.ts'
11+
import { Hex, parseEther } from 'viem'
12+
import Table from 'cli-table3'
13+
14+
const recipient: Hex = '0x3d26c9637dFaB74141bA3C466224C0DBFDfF4A63'
15+
16+
// fund recipient if needed
17+
{
18+
// endow the account wallet with some funds if needed
19+
const endowment = parseEther('1')
20+
const balance = await env.wallet.getBalance({ address: recipient })
21+
if (balance == 0n) {
22+
console.log(`funding ${recipient}`)
23+
const hash = await env.wallet.sendTransaction({
24+
to: recipient,
25+
value: endowment,
26+
})
27+
await env.wallet.waitForTransactionReceipt({ hash })
28+
}
29+
}
30+
31+
const addresses = [
32+
{ address: MyTokenEvm, name: 'EVM - solidity' },
33+
{ address: MyTokenPvm, name: 'PVM - solidity' },
34+
{ address: MyTokenInk, name: 'PVM - Ink!' },
35+
{ address: MyTokenRustWithAlloc, name: 'PVM - Rust with alloc' },
36+
{ address: MyTokenRustNoAlloc, name: 'PVM - Rust no alloc' },
37+
] as const
38+
39+
type CodeSizeEntry = {
40+
name: string
41+
size: number
42+
}
43+
44+
type StatEntry = {
45+
operation: string
46+
gas: bigint
47+
weight: { ref_time: bigint; proof_size: bigint }
48+
}
49+
50+
const codeSizes: CodeSizeEntry[] = []
51+
const stats: StatEntry[] = []
52+
53+
for (const { name, address } of addresses) {
54+
const code = await env.wallet.getCode({ address })
55+
if (!code) {
56+
console.error(`Deploy contract first with "deno task deploy"`)
57+
Deno.exit(1)
58+
}
59+
codeSizes.push({
60+
name,
61+
size: code.length / 2 - 1,
62+
})
63+
}
64+
65+
// mint token
66+
for (const { name, address } of addresses) {
67+
const { request } = await env.wallet.simulateContract({
68+
address,
69+
abi: abis.MyToken,
70+
functionName: 'mint',
71+
args: [
72+
'0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac',
73+
10_000n,
74+
],
75+
})
76+
77+
const hash = await env.wallet.writeContract(request)
78+
const receipt = await env.wallet.waitForTransactionReceipt({ hash })
79+
const weight = await env.debugClient.postDispatchWeight(hash)
80+
81+
stats.push({
82+
operation: `mint (${name})`,
83+
gas: receipt.gasUsed,
84+
weight,
85+
})
86+
}
87+
88+
// transfer token
89+
for (const { name, address } of addresses) {
90+
const { request } = await env.wallet.simulateContract({
91+
address,
92+
abi: abis.MyToken,
93+
functionName: 'transfer',
94+
args: [recipient, 1n],
95+
})
96+
97+
const hash = await env.wallet.writeContract(request)
98+
const receipt = await env.wallet.waitForTransactionReceipt({ hash })
99+
const weight = await env.debugClient.postDispatchWeight(hash)
100+
101+
stats.push({
102+
operation: `transfer (${name})`,
103+
gas: receipt.gasUsed,
104+
weight,
105+
})
106+
}
107+
108+
// Separate mint and transfer operations
109+
const mintStats = stats.filter((s) => s.operation.includes('mint'))
110+
const transferStats = stats.filter((s) => s.operation.includes('transfer'))
111+
112+
function createOperationTable(operationStats: StatEntry[], title: string) {
113+
const minGas = operationStats.reduce(
114+
(min, s) => s.gas < min ? s.gas : min,
115+
operationStats[0].gas,
116+
)
117+
const minRefTime = operationStats.reduce(
118+
(min, s) => s.weight.ref_time < min ? s.weight.ref_time : min,
119+
operationStats[0].weight.ref_time,
120+
)
121+
const minProofSize = operationStats.reduce(
122+
(min, s) => s.weight.proof_size < min ? s.weight.proof_size : min,
123+
operationStats[0].weight.proof_size,
124+
)
125+
126+
const table = new Table({
127+
head: [
128+
'Implementation',
129+
'Gas Used',
130+
'Gas %',
131+
'Ref Time',
132+
'Ref Time %',
133+
'Proof Size',
134+
'Proof Size %',
135+
],
136+
style: {
137+
head: ['cyan'],
138+
},
139+
})
140+
141+
for (const stat of operationStats) {
142+
const gasPercent = ((Number(stat.gas) / Number(minGas)) * 100).toFixed(
143+
1,
144+
)
145+
const refTimePercent =
146+
((Number(stat.weight.ref_time) / Number(minRefTime)) * 100).toFixed(
147+
1,
148+
)
149+
const proofSizePercent =
150+
((Number(stat.weight.proof_size) / Number(minProofSize)) * 100)
151+
.toFixed(1)
152+
153+
// Extract implementation name (EVM or PVM) from operation
154+
const implName = stat.operation.match(/\((.*?)\)/)?.[1] ||
155+
stat.operation
156+
157+
table.push([
158+
implName,
159+
stat.gas.toString(),
160+
`${gasPercent}%`,
161+
stat.weight.ref_time.toString(),
162+
`${refTimePercent}%`,
163+
stat.weight.proof_size.toString(),
164+
`${proofSizePercent}%`,
165+
])
166+
}
167+
168+
console.log(`\n## ${title}\n`)
169+
console.log(table.toString())
170+
}
171+
172+
createOperationTable(mintStats, 'Mint Operation')
173+
createOperationTable(transferStats, 'Transfer Operation')
174+
175+
// Output code size table
176+
const minCodeSize = codeSizes.reduce(
177+
(min, c) => c.size < min ? c.size : min,
178+
codeSizes[0].size,
179+
)
180+
181+
const codeSizeTable = new Table({
182+
head: ['Implementation', 'Code Size (bytes)', 'Size %'],
183+
style: {
184+
head: ['cyan'],
185+
},
186+
})
187+
188+
for (const entry of codeSizes) {
189+
const sizePercent = ((entry.size / minCodeSize) * 100).toFixed(1)
190+
codeSizeTable.push([
191+
entry.name,
192+
entry.size.toString(),
193+
`${sizePercent}%`,
194+
])
195+
}
196+
197+
console.log('\n' + codeSizeTable.toString())

contracts/MyToken.sol

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.27;
3+
4+
/// @title Minimal ERC-20-like token with only mint and transfer entry points.
5+
contract MyToken {
6+
uint256 public totalSupply;
7+
8+
mapping(address => uint256) private balances;
9+
10+
event Transfer(address indexed from, address indexed to, uint256 value);
11+
12+
error InsufficientBalance();
13+
14+
/// @notice Transfers `amount` tokens from the caller to `to`.
15+
function transfer(address to, uint256 amount) external {
16+
uint256 senderBalance = balances[msg.sender];
17+
if (senderBalance < amount) {
18+
revert InsufficientBalance();
19+
}
20+
21+
unchecked {
22+
balances[msg.sender] = senderBalance - amount;
23+
balances[to] += amount;
24+
}
25+
26+
emit Transfer(msg.sender, to, amount);
27+
}
28+
29+
/// @notice Permissionless mint mirroring the ink! implementation.
30+
/// Emits a Transfer event with the zero address as the sender.
31+
function mint(address to, uint256 amount) external {
32+
balances[to] += amount;
33+
totalSupply += amount;
34+
emit Transfer(address(0), to, amount);
35+
}
36+
}

0 commit comments

Comments
 (0)