Skip to content

Commit 9f54572

Browse files
authored
Merge pull request #7785 from BitGo/WIN-8401
feat(sol): add transaction size benchmark tool
2 parents 2d778e1 + 3ebe108 commit 9f54572

File tree

2 files changed

+314
-0
lines changed

2 files changed

+314
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Solana SDK Scripts
2+
3+
## Transaction Size Benchmark
4+
5+
Determines safe transaction limits for Solana legacy transactions (1232 byte limit).
6+
7+
**Run:**
8+
```bash
9+
npx tsx modules/sdk-coin-sol/scripts/transaction-size-benchmark.ts
10+
```
11+
12+
**Output:**
13+
- Console: Test results and recommended limits
14+
- File: `transaction-size-benchmark-results.json`
15+
16+
**Tests:**
17+
- Token transfers with ATA creation (new recipients)
18+
- Token transfers without ATA creation (existing accounts)
19+
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
/**
2+
* Transaction Size Benchmark Script
3+
*
4+
* This script empirically tests Solana transaction size limits by building
5+
* transactions with varying numbers of recipients and measuring the serialized
6+
* payload size. This helps determine safe, conservative limits for production use.
7+
*
8+
* Solana Legacy Transaction Constraints:
9+
* - Maximum transaction size: 1232 bytes
10+
* - Each instruction includes program ID, accounts, and instruction data
11+
* - Account metadata and signatures add to the total size
12+
*
13+
* Run: npx tsx modules/sdk-coin-sol/scripts/transaction-size-benchmark.ts
14+
*/
15+
16+
import { TransactionBuilderFactory, KeyPair } from '../src';
17+
import { coins } from '@bitgo/statics';
18+
19+
const SOLANA_LEGACY_TX_SIZE_LIMIT = 1232;
20+
const TEST_BLOCKHASH = 'GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi';
21+
const TEST_PRIVATE_KEY = '5jtsd9SmUH5mFZL7ywNNmqmxxdXw4FQ5GJQprXmrdX4LCuUwMBivCfUX2ar8hGdnLHDGrVKkshW1Ke21vZhPiLyr';
22+
const TEST_TOKEN = 'tsol:usdt';
23+
const TEST_AMOUNT = '1000000000';
24+
25+
interface BenchmarkResult {
26+
recipientCount: number;
27+
withAtaCreation: boolean;
28+
instructionCount: number;
29+
payloadSize: number;
30+
isVersioned: boolean;
31+
success: boolean;
32+
error?: string;
33+
errorStack?: string;
34+
}
35+
36+
/**
37+
* Generates an array of unique test recipient addresses
38+
*/
39+
function generateRecipients(count: number): string[] {
40+
const baseAddresses = [
41+
'GYFtQoFCRvGS3WgJe32nRi8LarbSH81rp32SeTmd8mqT',
42+
'9AKiRaA3NusuW9WcFz5NQ7K1vqf9FMLtb1hFXqc7imkx',
43+
'FiLHbfatU4keyGzZ1KjC57L3iYmVUi2azJ1ozSZsZWD6',
44+
'6XKXKPfk5bHmHV4eL2ZFZ5ubcMu6YLed86niBfx7P4Pg',
45+
'CfhjtJ7W1HHmwXPo9HAkG25gDJPav64iN9Z3nx8eCWV8',
46+
'DNNSTu2fkj45u6oQuH1fBc3tyDjnnbvhAESrZGhH3uxa',
47+
'EHPBg7n93e1TWW7oXDwpivQTjZmpavzN9T6HEqAN4bz1',
48+
'EUZfVoaJgApESLe3Bx8d7xyqg1pucgeydBLQPiRhi6nE',
49+
'EqE7fJ89HZTAE8MxmJtxCqLhW1ozfhaqZigM5cYxRdHx',
50+
'CDFk1LXXbWqkd8q9cEsnc3nX44uKgrkDb48b9keBrt8H',
51+
'EFDfayERUomCbAKurccrAbEYmkyZ1ytazgkiBMBGM851',
52+
'EPPE9i37rnwWFzagoEHQtBM644p52q54UUGqemJPkAQu',
53+
'3UKwF59ZmRPXq8ooQDjMmzYUiH861KwhJPzjcYfPXDTG',
54+
'Cr2yAAa7zEkSLJGebkscXQKNTpmgg5ECC4tCkZHHpNvG',
55+
'8wQ2VzVEtYDzKaJ5XqGskkQAJZT5YJwHiUTPa7juCF4w',
56+
'3URWW579b4ugLecHca69feGTM7LKPZ3jDrdTegosVE24',
57+
'3pL2GxpS8QJoNXhXicnEQ2nvXxFbSy7CoJCRxiWtAuQs',
58+
'Cei1xdDuZmQxU6FNAcrrmM7aR7amF72osySgS8afpv5m',
59+
'5Dd1qGYbbdudLi6uCdDodZw1CJ15j2zWnb7ZxQ68em5a',
60+
'4fviuPUgg3uJp7J2eqLCmeuiHh6zUbn8iXm6JcECNH7T',
61+
'4g9w2CsTR5h6urpuvm5R5w7UGPKUuCmrSSbj5BWAygq2',
62+
'3PRwJ1PjrwF7rBCSsNjAH2s8Pr2XdbSm9koGcVLmrnaz',
63+
'3p9ViY8efykqjixyZGVY2hPLVpHcfsUDKrrHqCUWyYPb',
64+
'3oANXoBY6b5qgwRf1yES6kx1hf7rDGbEWkopcmXxau1u',
65+
'3NqKx8uf3V1bTBtkniMzxVsuqW7SZmTnhJpRFuV6aAFz',
66+
'3NVNrcyJjZmH7xytX2ayPZRucS1YBywwZ5DMzjGRtJvu',
67+
'4iLLJqwHSdZPBbDPFpG8XiDazX8jMEam8fPQWwrG3qGP',
68+
'3wbfKiB6uZuZ3KhjVcU9qc5M4yaWyx6mAUJA56tCD1vJ',
69+
'4QheUEWRi8nesT94mRxiizZnAtRQTnk45jb4oaKMoo1f',
70+
'4PowojicaKCxgupfPCZK5DT7QB1U3N7xh8Wq7N7VoCvw',
71+
'F3rTU6cFx9Aqa9jmuq5rSZN7p8bu1pmU9EbNBPeKHz7t',
72+
'6XREPthmB7mTwLTHv54DZM14rgAuMa1NgXsmPeFgrqwu',
73+
'5LTQgjYbbBsZj3V5rPmkcAkPcJcbCT8PQTskz2rWL3kW',
74+
'7aG7LiNQ4hMNWwXTZRrpYVpPZDxY1Ea9YcbHuEjXXKVT',
75+
'DNyKPeaGfoPYzPmNG4osMZJdyJBZbeMUfNKAAt5RSw23',
76+
'DpFPcPk454MhoGv71DoaoHSJ5qWnKcVAo9euVoPWPprZ',
77+
'E4T6wq3aF4LivPfgr5sBKFUJLTZz7sND8duJDYK2EbXG',
78+
'EeM3aXzgt3Qf7tNV4RBe4vg83gtyGZqUQrJumzwtTUvN',
79+
'EqBF851o6HknqDy5Ji8PCtyxo48ACjBVPhC1NSpAKbEx',
80+
'EQVV74ZC332uKpZXrCVU7xzqTBBTdom3DERsEnRNqmAF',
81+
'EvnEyrSG5qSnxsCSqxdhbbKrcg2oiikKiTtYhwbAMnKT',
82+
'EBxE1QUeYuKsCCYuDsf6ngUV4ApmGUdc913b1oCLCwus',
83+
'5GjFQdcCpB9Wj9CJFbMfKTfB7X9h6FSdqrxG6HaJAmUF',
84+
'EY1wKHGmTUK89aKjZ8qmycyx5gQjBDgeWPfKcwQf2Hvd',
85+
'EzLYbB2JcSPf9FwiWonD2XfEd3S89ghuMUH27UPvQzgJ',
86+
'2n2xqWM9Z18LqxfJzkNrMMFWiDUFYA2k6WSgSnf6EnJs',
87+
'DesU7XscZjng8yj5VX6AZsk3hWSW4sQ3rTG2LuyQ2P4H',
88+
'Azz9EmNuhtjoYrhWvidWx1Hfd14SNBsYyzXhA9Tnoca8',
89+
'5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen',
90+
];
91+
92+
return baseAddresses.slice(0, Math.min(count, baseAddresses.length));
93+
}
94+
95+
/**
96+
* Tests building a transaction with a specific number of recipients
97+
*/
98+
async function testTransactionSize(recipientCount: number, withAtaCreation: boolean): Promise<BenchmarkResult> {
99+
const coinConfig = coins.get('tsol');
100+
const factory = new TransactionBuilderFactory(coinConfig);
101+
const authAccount = new KeyPair({ prv: TEST_PRIVATE_KEY });
102+
const sender = authAccount.getKeys().pub;
103+
const recipients = generateRecipients(recipientCount);
104+
105+
try {
106+
const txBuilder = factory.getTokenTransferBuilder();
107+
txBuilder.nonce(TEST_BLOCKHASH);
108+
txBuilder.sender(sender);
109+
110+
for (const recipientAddress of recipients) {
111+
txBuilder.send({
112+
address: recipientAddress,
113+
amount: TEST_AMOUNT,
114+
tokenName: TEST_TOKEN,
115+
});
116+
117+
if (withAtaCreation) {
118+
txBuilder.createAssociatedTokenAccount({
119+
ownerAddress: recipientAddress,
120+
tokenName: TEST_TOKEN,
121+
});
122+
}
123+
}
124+
125+
txBuilder.memo('Benchmark test transaction');
126+
txBuilder.sign({ key: authAccount.getKeys().prv });
127+
128+
const tx = await txBuilder.build();
129+
const payload = tx.signablePayload;
130+
const instructionCount = tx.toJson().instructionsData?.length || 0;
131+
const isVersioned = tx.isVersionedTransaction();
132+
133+
return {
134+
recipientCount,
135+
withAtaCreation,
136+
instructionCount,
137+
payloadSize: payload.length,
138+
isVersioned,
139+
success: true,
140+
};
141+
} catch (error) {
142+
const errorMessage = error instanceof Error ? error.message : String(error);
143+
const errorStack = error instanceof Error ? error.stack : undefined;
144+
145+
return {
146+
recipientCount,
147+
withAtaCreation,
148+
instructionCount: 0,
149+
payloadSize: 0,
150+
isVersioned: false,
151+
success: false,
152+
error: errorMessage,
153+
errorStack,
154+
};
155+
}
156+
}
157+
158+
/**
159+
* Performs binary search to find the maximum safe recipient count for LEGACY transactions
160+
*/
161+
async function findMaxRecipients(withAtaCreation: boolean): Promise<number> {
162+
let left = 1;
163+
let right = 50;
164+
let maxSafe = 0;
165+
166+
while (left <= right) {
167+
const mid = Math.floor((left + right) / 2);
168+
const result = await testTransactionSize(mid, withAtaCreation);
169+
170+
// Only consider legacy transactions within the size limit as safe
171+
if (result.success && !result.isVersioned && result.payloadSize <= SOLANA_LEGACY_TX_SIZE_LIMIT) {
172+
maxSafe = mid;
173+
left = mid + 1;
174+
} else {
175+
right = mid - 1;
176+
}
177+
}
178+
179+
return maxSafe;
180+
}
181+
182+
/**
183+
* Main benchmark execution
184+
*/
185+
async function runBenchmark(): Promise<void> {
186+
console.log('Solana Transaction Size Benchmark');
187+
console.log('Limit: 1232 bytes for legacy transactions\n');
188+
189+
// Test WITH ATA creation
190+
console.log('[1/2] Testing WITH ATA Creation:');
191+
const withAtaResults: BenchmarkResult[] = [];
192+
const testCountsWithAta = [1, 5, 10, 12, 13, 14, 15, 16, 17, 18, 20, 25, 30];
193+
194+
for (const count of testCountsWithAta) {
195+
const result = await testTransactionSize(count, true);
196+
withAtaResults.push(result);
197+
198+
if (result.success && result.payloadSize <= SOLANA_LEGACY_TX_SIZE_LIMIT) {
199+
const txType = result.isVersioned ? 'versioned' : 'legacy';
200+
console.log(
201+
` ${count} recipients: ${result.payloadSize} bytes, ${result.instructionCount} instructions (${txType})`
202+
);
203+
} else {
204+
console.log(` ${count} recipients: FAILED`);
205+
if (result.success && result.payloadSize > SOLANA_LEGACY_TX_SIZE_LIMIT) {
206+
console.log(
207+
` Error: Transaction size ${result.payloadSize} bytes exceeds ${SOLANA_LEGACY_TX_SIZE_LIMIT} byte limit`
208+
);
209+
} else {
210+
console.log(` Error: ${result.error}`);
211+
if (result.errorStack) {
212+
const stackLines = result.errorStack.split('\n').slice(0, 4);
213+
console.log(` Stack: ${stackLines.join('\n ')}`);
214+
}
215+
}
216+
}
217+
}
218+
219+
const maxWithAta = await findMaxRecipients(true);
220+
const conservativeWithAta = Math.floor(maxWithAta * 0.9);
221+
console.log(`\n Maximum: ${maxWithAta} recipients`);
222+
console.log(` Conservative (10% buffer): ${conservativeWithAta} recipients\n`);
223+
224+
// Test WITHOUT ATA creation
225+
console.log('[2/2] Testing WITHOUT ATA Creation:');
226+
const withoutAtaResults: BenchmarkResult[] = [];
227+
const testCountsWithoutAta = [1, 10, 20, 30, 35, 38, 39, 40, 41, 42, 45, 50];
228+
229+
for (const count of testCountsWithoutAta) {
230+
const result = await testTransactionSize(count, false);
231+
withoutAtaResults.push(result);
232+
233+
if (result.success && result.payloadSize <= SOLANA_LEGACY_TX_SIZE_LIMIT) {
234+
const txType = result.isVersioned ? 'versioned' : 'legacy';
235+
console.log(
236+
` ${count} recipients: ${result.payloadSize} bytes, ${result.instructionCount} instructions (${txType})`
237+
);
238+
} else {
239+
console.log(` ${count} recipients: FAILED`);
240+
if (result.success && result.payloadSize > SOLANA_LEGACY_TX_SIZE_LIMIT) {
241+
console.log(
242+
` Error: Transaction size ${result.payloadSize} bytes exceeds ${SOLANA_LEGACY_TX_SIZE_LIMIT} byte limit`
243+
);
244+
} else {
245+
console.log(` Error: ${result.error}`);
246+
if (result.errorStack) {
247+
const stackLines = result.errorStack.split('\n').slice(0, 4);
248+
console.log(` Stack: ${stackLines.join('\n ')}`);
249+
}
250+
}
251+
}
252+
}
253+
254+
const maxWithoutAta = await findMaxRecipients(false);
255+
const conservativeWithoutAta = Math.floor(maxWithoutAta * 0.9);
256+
console.log(`\n Maximum: ${maxWithoutAta} recipients`);
257+
console.log(` Conservative (10% buffer): ${conservativeWithoutAta} recipients\n`);
258+
259+
// Summary
260+
console.log('RECOMMENDED LIMITS (for legacy transactions):');
261+
console.log(` With ATA creation: ${conservativeWithAta} recipients`);
262+
console.log(` Without ATA creation: ${conservativeWithoutAta} recipients`);
263+
console.log('\nNote: Transactions exceeding limits may build as versioned transactions automatically.');
264+
265+
// Export results
266+
const exportData = {
267+
timestamp: new Date().toISOString(),
268+
solanaLegacyTxSizeLimit: SOLANA_LEGACY_TX_SIZE_LIMIT,
269+
withAtaCreation: {
270+
maxSafe: maxWithAta,
271+
conservative: conservativeWithAta,
272+
testResults: withAtaResults,
273+
},
274+
withoutAtaCreation: {
275+
maxSafe: maxWithoutAta,
276+
conservative: conservativeWithoutAta,
277+
testResults: withoutAtaResults,
278+
},
279+
};
280+
281+
const fs = await import('fs');
282+
fs.writeFileSync('transaction-size-benchmark-results.json', JSON.stringify(exportData, null, 2));
283+
console.log('\nResults saved to: transaction-size-benchmark-results.json');
284+
}
285+
286+
// Execute benchmark
287+
runBenchmark()
288+
.then(() => {
289+
console.log('Benchmark completed successfully');
290+
process.exit(0);
291+
})
292+
.catch((error) => {
293+
console.error('Benchmark failed:', error);
294+
process.exit(1);
295+
});

0 commit comments

Comments
 (0)