Skip to content

Commit 972a9a1

Browse files
authored
perf: improve tx land rate (#1429)
* Checkpoint * Checkpoint * Continue * Revert * Revert * Revert * Update proposer * Clean * Lint
1 parent 0e885e3 commit 972a9a1

File tree

5 files changed

+192
-70
lines changed

5 files changed

+192
-70
lines changed

governance/xc_admin/packages/xc_admin_common/src/propose.ts

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,19 @@ import SquadsMesh, { getIxAuthorityPDA, getTxPDA } from "@sqds/mesh";
2424
import { MultisigAccount } from "@sqds/mesh/lib/types";
2525
import { mapKey } from "./remote_executor";
2626
import { WORMHOLE_ADDRESS } from "./wormhole";
27-
import { TransactionBuilder } from "@pythnetwork/solana-utils";
27+
import {
28+
TransactionBuilder,
29+
sendTransactions,
30+
} from "@pythnetwork/solana-utils";
2831
import {
2932
PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET,
3033
PriorityFeeConfig,
3134
} from "@pythnetwork/solana-utils";
35+
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
3236

3337
export const MAX_EXECUTOR_PAYLOAD_SIZE =
3438
PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET - 687; // Bigger payloads won't fit in one addInstruction call when adding to the proposal
3539
export const MAX_INSTRUCTIONS_PER_PROPOSAL = 256 - 1;
36-
export const MAX_NUMBER_OF_RETRIES = 10;
3740

3841
type SquadInstruction = {
3942
instruction: TransactionInstruction;
@@ -385,52 +388,24 @@ export class MultisigVault {
385388

386389
async sendAllTransactions(transactions: Transaction[]) {
387390
const provider = this.getAnchorProvider({
388-
preflightCommitment: "processed",
389-
commitment: "processed",
391+
preflightCommitment: "confirmed",
392+
commitment: "confirmed",
390393
});
391394

392-
let needToFetchBlockhash = true; // We don't fetch blockhash everytime to save time
393-
let blockhash: string = "";
394-
for (let [index, tx] of transactions.entries()) {
395-
console.log("Trying to send transaction: " + index);
396-
let numberOfRetries = 0;
397-
let txHasLanded = false;
398-
399-
while (!txHasLanded) {
395+
for (const [index, tx] of transactions.entries()) {
396+
console.log("Trying transaction: ", index, " of ", transactions.length);
397+
let retry = true;
398+
while (true)
400399
try {
401-
if (needToFetchBlockhash) {
402-
blockhash = (await provider.connection.getLatestBlockhash())
403-
.blockhash;
404-
needToFetchBlockhash = false;
405-
}
406-
tx.feePayer = tx.feePayer || provider.wallet.publicKey;
407-
tx.recentBlockhash = blockhash;
408-
provider.wallet.signTransaction(tx);
409-
await sendAndConfirmRawTransaction(
400+
await sendTransactions(
401+
[{ tx, signers: [] }],
410402
provider.connection,
411-
tx.serialize(),
412-
provider.opts
403+
this.squad.wallet as NodeWallet
413404
);
414-
txHasLanded = true;
405+
break;
415406
} catch (e) {
416-
if (numberOfRetries >= MAX_NUMBER_OF_RETRIES) {
417-
// Cap the number of retries
418-
throw Error("Maximum number of retries exceeded");
419-
}
420-
const message = (e as any).toString().split("\n")[0];
421-
if (
422-
message ==
423-
"Error: failed to send transaction: Transaction simulation failed: Blockhash not found"
424-
) {
425-
// If blockhash has expired, we need to fetch a new one
426-
needToFetchBlockhash = true;
427-
} else {
428-
await new Promise((r) => setTimeout(r, 3000));
429-
}
430407
console.log(e);
431-
numberOfRetries += 1;
432408
}
433-
}
434409
}
435410
}
436411
}

governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -400,12 +400,17 @@ const Proposal = ({
400400
squads.wallet.publicKey,
401401
squads.connection
402402
)
403-
builder.addInstruction({ instruction, signers: [] })
404-
const versionedTxs = await builder.buildVersionedTransactions(
405-
DEFAULT_PRIORITY_FEE_CONFIG
406-
)
403+
builder.addInstruction({
404+
instruction,
405+
signers: [],
406+
computeUnits: 10000,
407+
})
408+
const transactions = builder.buildLegacyTransactions({
409+
computeUnitPriceMicroLamports: 150000,
410+
tightComputeBudget: true,
411+
})
407412
await sendTransactions(
408-
versionedTxs,
413+
transactions,
409414
squads.connection,
410415
squads.wallet as Wallet
411416
)

package-lock.json

Lines changed: 31 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

target_chains/solana/sdk/js/solana_utils/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
},
4444
"dependencies": {
4545
"@coral-xyz/anchor": "^0.29.0",
46-
"@solana/web3.js": "^1.90.0"
46+
"@solana/web3.js": "^1.90.0",
47+
"bs58": "^5.0.0"
4748
}
4849
}

target_chains/solana/sdk/js/solana_utils/src/transaction.ts

Lines changed: 134 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { AnchorProvider, Wallet } from "@coral-xyz/anchor";
1+
import { Wallet } from "@coral-xyz/anchor";
22
import {
33
ComputeBudgetProgram,
4-
ConfirmOptions,
54
Connection,
65
PACKET_DATA_SIZE,
76
PublicKey,
@@ -11,6 +10,7 @@ import {
1110
TransactionMessage,
1211
VersionedTransaction,
1312
} from "@solana/web3.js";
13+
import bs58 from "bs58";
1414

1515
/**
1616
* If the transaction doesn't contain a `setComputeUnitLimit` instruction, the default compute budget is 200,000 units per instruction.
@@ -40,6 +40,7 @@ export type InstructionWithEphemeralSigners = {
4040
export type PriorityFeeConfig = {
4141
/** This is the priority fee in micro lamports, it gets passed down to `setComputeUnitPrice` */
4242
computeUnitPriceMicroLamports?: number;
43+
tightComputeBudget?: boolean;
4344
};
4445

4546
/**
@@ -186,14 +187,19 @@ export class TransactionBuilder {
186187
async buildVersionedTransactions(
187188
args: PriorityFeeConfig
188189
): Promise<{ tx: VersionedTransaction; signers: Signer[] }[]> {
189-
const blockhash = (await this.connection.getLatestBlockhash()).blockhash;
190+
const blockhash = (
191+
await this.connection.getLatestBlockhash({ commitment: "confirmed" })
192+
).blockhash;
190193

191194
return this.transactionInstructions.map(
192195
({ instructions, signers, computeUnits }) => {
193196
const instructionsWithComputeBudget: TransactionInstruction[] = [
194197
...instructions,
195198
];
196-
if (computeUnits > DEFAULT_COMPUTE_BUDGET_UNITS * instructions.length) {
199+
if (
200+
computeUnits > DEFAULT_COMPUTE_BUDGET_UNITS * instructions.length ||
201+
args.tightComputeBudget
202+
) {
197203
instructionsWithComputeBudget.push(
198204
ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits })
199205
);
@@ -226,21 +232,33 @@ export class TransactionBuilder {
226232
buildLegacyTransactions(
227233
args: PriorityFeeConfig
228234
): { tx: Transaction; signers: Signer[] }[] {
229-
return this.transactionInstructions.map(({ instructions, signers }) => {
230-
const instructionsWithComputeBudget = args.computeUnitPriceMicroLamports
231-
? [
232-
...instructions,
235+
return this.transactionInstructions.map(
236+
({ instructions, signers, computeUnits }) => {
237+
const instructionsWithComputeBudget: TransactionInstruction[] = [
238+
...instructions,
239+
];
240+
if (
241+
computeUnits > DEFAULT_COMPUTE_BUDGET_UNITS * instructions.length ||
242+
args.tightComputeBudget
243+
) {
244+
instructionsWithComputeBudget.push(
245+
ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits })
246+
);
247+
}
248+
if (args.computeUnitPriceMicroLamports) {
249+
instructionsWithComputeBudget.push(
233250
ComputeBudgetProgram.setComputeUnitPrice({
234251
microLamports: args.computeUnitPriceMicroLamports,
235-
}),
236-
]
237-
: instructions;
252+
})
253+
);
254+
}
238255

239-
return {
240-
tx: new Transaction().add(...instructionsWithComputeBudget),
241-
signers: signers,
242-
};
243-
});
256+
return {
257+
tx: new Transaction().add(...instructionsWithComputeBudget),
258+
signers: signers,
259+
};
260+
}
261+
);
244262
}
245263

246264
/**
@@ -295,8 +313,16 @@ export class TransactionBuilder {
295313
}
296314
}
297315

316+
export const isVersionedTransaction = (
317+
tx: Transaction | VersionedTransaction
318+
): tx is VersionedTransaction => {
319+
return "version" in tx;
320+
};
321+
322+
const TX_RETRY_INTERVAL = 500;
323+
298324
/**
299-
* Send a set of transactions to the network
325+
* Send a set of transactions to the network based on https://github.com/rpcpool/optimized-txs-examples
300326
*/
301327
export async function sendTransactions(
302328
transactions: {
@@ -305,12 +331,97 @@ export async function sendTransactions(
305331
}[],
306332
connection: Connection,
307333
wallet: Wallet,
308-
opts?: ConfirmOptions
334+
maxRetries?: number
309335
) {
310-
if (opts === undefined) {
311-
opts = AnchorProvider.defaultOptions();
312-
}
336+
const blockhashResult = await connection.getLatestBlockhashAndContext({
337+
commitment: "confirmed",
338+
});
339+
340+
// Signing logic for versioned transactions is different from legacy transactions
341+
for (const transaction of transactions) {
342+
const { signers } = transaction;
343+
let tx = transaction.tx;
344+
if (isVersionedTransaction(tx)) {
345+
if (signers) {
346+
tx.sign(signers);
347+
}
348+
} else {
349+
tx.feePayer = tx.feePayer ?? wallet.publicKey;
350+
tx.recentBlockhash = blockhashResult.value.blockhash;
351+
352+
if (signers) {
353+
for (const signer of signers) {
354+
tx.partialSign(signer);
355+
}
356+
}
357+
}
358+
359+
tx = await wallet.signTransaction(tx);
360+
361+
// In the following section, we wait and constantly check for the transaction to be confirmed
362+
// and resend the transaction if it is not confirmed within a certain time interval
363+
// thus handling tx retries on the client side rather than relying on the RPC
364+
let confirmedTx = null;
365+
let retryCount = 0;
366+
367+
try {
368+
// Get the signature of the transaction with different logic for versioned transactions
369+
const txSignature = bs58.encode(
370+
isVersionedTransaction(tx)
371+
? tx.signatures?.[0] || new Uint8Array()
372+
: tx.signature ?? new Uint8Array()
373+
);
313374

314-
const provider = new AnchorProvider(connection, wallet, opts);
315-
await provider.sendAll(transactions);
375+
const confirmTransactionPromise = connection.confirmTransaction(
376+
{
377+
signature: txSignature,
378+
blockhash: blockhashResult.value.blockhash,
379+
lastValidBlockHeight: blockhashResult.value.lastValidBlockHeight,
380+
},
381+
"confirmed"
382+
);
383+
384+
confirmedTx = null;
385+
while (!confirmedTx) {
386+
confirmedTx = await Promise.race([
387+
confirmTransactionPromise,
388+
new Promise((resolve) =>
389+
setTimeout(() => {
390+
resolve(null);
391+
}, TX_RETRY_INTERVAL)
392+
),
393+
]);
394+
if (confirmedTx) {
395+
break;
396+
}
397+
if (maxRetries && maxRetries < retryCount) {
398+
break;
399+
}
400+
console.log(
401+
"Retrying transaction: ",
402+
txSignature,
403+
" Retry count: ",
404+
retryCount
405+
);
406+
retryCount++;
407+
408+
await connection.sendRawTransaction(tx.serialize(), {
409+
// Skipping preflight i.e. tx simulation by RPC as we simulated the tx above
410+
// This allows Triton RPCs to send the transaction through multiple pathways for the fastest delivery
411+
skipPreflight: true,
412+
// Setting max retries to 0 as we are handling retries manually
413+
// Set this manually so that the default is skipped
414+
maxRetries: 0,
415+
preflightCommitment: "confirmed",
416+
minContextSlot: blockhashResult.context.slot,
417+
});
418+
}
419+
} catch (error) {
420+
console.error(error);
421+
}
422+
423+
if (!confirmedTx) {
424+
throw new Error("Failed to land the transaction");
425+
}
426+
}
316427
}

0 commit comments

Comments
 (0)