perf: improve tx land rate (#1429)
* Checkpoint * Checkpoint * Continue * Revert * Revert * Revert * Update proposer * Clean * Lint
This commit is contained in:
parent
0e885e3ca7
commit
972a9a1e1d
|
@ -24,16 +24,19 @@ import SquadsMesh, { getIxAuthorityPDA, getTxPDA } from "@sqds/mesh";
|
|||
import { MultisigAccount } from "@sqds/mesh/lib/types";
|
||||
import { mapKey } from "./remote_executor";
|
||||
import { WORMHOLE_ADDRESS } from "./wormhole";
|
||||
import { TransactionBuilder } from "@pythnetwork/solana-utils";
|
||||
import {
|
||||
TransactionBuilder,
|
||||
sendTransactions,
|
||||
} from "@pythnetwork/solana-utils";
|
||||
import {
|
||||
PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET,
|
||||
PriorityFeeConfig,
|
||||
} from "@pythnetwork/solana-utils";
|
||||
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
|
||||
|
||||
export const MAX_EXECUTOR_PAYLOAD_SIZE =
|
||||
PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET - 687; // Bigger payloads won't fit in one addInstruction call when adding to the proposal
|
||||
export const MAX_INSTRUCTIONS_PER_PROPOSAL = 256 - 1;
|
||||
export const MAX_NUMBER_OF_RETRIES = 10;
|
||||
|
||||
type SquadInstruction = {
|
||||
instruction: TransactionInstruction;
|
||||
|
@ -385,52 +388,24 @@ export class MultisigVault {
|
|||
|
||||
async sendAllTransactions(transactions: Transaction[]) {
|
||||
const provider = this.getAnchorProvider({
|
||||
preflightCommitment: "processed",
|
||||
commitment: "processed",
|
||||
preflightCommitment: "confirmed",
|
||||
commitment: "confirmed",
|
||||
});
|
||||
|
||||
let needToFetchBlockhash = true; // We don't fetch blockhash everytime to save time
|
||||
let blockhash: string = "";
|
||||
for (let [index, tx] of transactions.entries()) {
|
||||
console.log("Trying to send transaction: " + index);
|
||||
let numberOfRetries = 0;
|
||||
let txHasLanded = false;
|
||||
|
||||
while (!txHasLanded) {
|
||||
for (const [index, tx] of transactions.entries()) {
|
||||
console.log("Trying transaction: ", index, " of ", transactions.length);
|
||||
let retry = true;
|
||||
while (true)
|
||||
try {
|
||||
if (needToFetchBlockhash) {
|
||||
blockhash = (await provider.connection.getLatestBlockhash())
|
||||
.blockhash;
|
||||
needToFetchBlockhash = false;
|
||||
}
|
||||
tx.feePayer = tx.feePayer || provider.wallet.publicKey;
|
||||
tx.recentBlockhash = blockhash;
|
||||
provider.wallet.signTransaction(tx);
|
||||
await sendAndConfirmRawTransaction(
|
||||
await sendTransactions(
|
||||
[{ tx, signers: [] }],
|
||||
provider.connection,
|
||||
tx.serialize(),
|
||||
provider.opts
|
||||
this.squad.wallet as NodeWallet
|
||||
);
|
||||
txHasLanded = true;
|
||||
break;
|
||||
} catch (e) {
|
||||
if (numberOfRetries >= MAX_NUMBER_OF_RETRIES) {
|
||||
// Cap the number of retries
|
||||
throw Error("Maximum number of retries exceeded");
|
||||
}
|
||||
const message = (e as any).toString().split("\n")[0];
|
||||
if (
|
||||
message ==
|
||||
"Error: failed to send transaction: Transaction simulation failed: Blockhash not found"
|
||||
) {
|
||||
// If blockhash has expired, we need to fetch a new one
|
||||
needToFetchBlockhash = true;
|
||||
} else {
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
}
|
||||
console.log(e);
|
||||
numberOfRetries += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -400,12 +400,17 @@ const Proposal = ({
|
|||
squads.wallet.publicKey,
|
||||
squads.connection
|
||||
)
|
||||
builder.addInstruction({ instruction, signers: [] })
|
||||
const versionedTxs = await builder.buildVersionedTransactions(
|
||||
DEFAULT_PRIORITY_FEE_CONFIG
|
||||
)
|
||||
builder.addInstruction({
|
||||
instruction,
|
||||
signers: [],
|
||||
computeUnits: 10000,
|
||||
})
|
||||
const transactions = builder.buildLegacyTransactions({
|
||||
computeUnitPriceMicroLamports: 150000,
|
||||
tightComputeBudget: true,
|
||||
})
|
||||
await sendTransactions(
|
||||
versionedTxs,
|
||||
transactions,
|
||||
squads.connection,
|
||||
squads.wallet as Wallet
|
||||
)
|
||||
|
|
|
@ -59586,7 +59586,8 @@
|
|||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@coral-xyz/anchor": "^0.29.0",
|
||||
"@solana/web3.js": "^1.90.0"
|
||||
"@solana/web3.js": "^1.90.0",
|
||||
"bs58": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.4.0",
|
||||
|
@ -59600,6 +59601,19 @@
|
|||
"typescript": "^4.6.3"
|
||||
}
|
||||
},
|
||||
"target_chains/solana/sdk/js/solana_utils/node_modules/base-x": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
|
||||
"integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw=="
|
||||
},
|
||||
"target_chains/solana/sdk/js/solana_utils/node_modules/bs58": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
|
||||
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
|
||||
"dependencies": {
|
||||
"base-x": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"target_chains/sui/cli": {
|
||||
"name": "pyth-sui-cli",
|
||||
"version": "0.0.1",
|
||||
|
@ -71281,12 +71295,28 @@
|
|||
"@types/jest": "^29.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.20.0",
|
||||
"@typescript-eslint/parser": "^5.20.0",
|
||||
"bs58": "^5.0.0",
|
||||
"eslint": "^8.13.0",
|
||||
"jest": "^29.4.0",
|
||||
"prettier": "^2.6.2",
|
||||
"quicktype": "^23.0.76",
|
||||
"ts-jest": "^29.0.5",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"base-x": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz",
|
||||
"integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw=="
|
||||
},
|
||||
"bs58": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
|
||||
"integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
|
||||
"requires": {
|
||||
"base-x": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/primitive": {
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@coral-xyz/anchor": "^0.29.0",
|
||||
"@solana/web3.js": "^1.90.0"
|
||||
"@solana/web3.js": "^1.90.0",
|
||||
"bs58": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { AnchorProvider, Wallet } from "@coral-xyz/anchor";
|
||||
import { Wallet } from "@coral-xyz/anchor";
|
||||
import {
|
||||
ComputeBudgetProgram,
|
||||
ConfirmOptions,
|
||||
Connection,
|
||||
PACKET_DATA_SIZE,
|
||||
PublicKey,
|
||||
|
@ -11,6 +10,7 @@ import {
|
|||
TransactionMessage,
|
||||
VersionedTransaction,
|
||||
} from "@solana/web3.js";
|
||||
import bs58 from "bs58";
|
||||
|
||||
/**
|
||||
* 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 = {
|
|||
export type PriorityFeeConfig = {
|
||||
/** This is the priority fee in micro lamports, it gets passed down to `setComputeUnitPrice` */
|
||||
computeUnitPriceMicroLamports?: number;
|
||||
tightComputeBudget?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -186,14 +187,19 @@ export class TransactionBuilder {
|
|||
async buildVersionedTransactions(
|
||||
args: PriorityFeeConfig
|
||||
): Promise<{ tx: VersionedTransaction; signers: Signer[] }[]> {
|
||||
const blockhash = (await this.connection.getLatestBlockhash()).blockhash;
|
||||
const blockhash = (
|
||||
await this.connection.getLatestBlockhash({ commitment: "confirmed" })
|
||||
).blockhash;
|
||||
|
||||
return this.transactionInstructions.map(
|
||||
({ instructions, signers, computeUnits }) => {
|
||||
const instructionsWithComputeBudget: TransactionInstruction[] = [
|
||||
...instructions,
|
||||
];
|
||||
if (computeUnits > DEFAULT_COMPUTE_BUDGET_UNITS * instructions.length) {
|
||||
if (
|
||||
computeUnits > DEFAULT_COMPUTE_BUDGET_UNITS * instructions.length ||
|
||||
args.tightComputeBudget
|
||||
) {
|
||||
instructionsWithComputeBudget.push(
|
||||
ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits })
|
||||
);
|
||||
|
@ -226,21 +232,33 @@ export class TransactionBuilder {
|
|||
buildLegacyTransactions(
|
||||
args: PriorityFeeConfig
|
||||
): { tx: Transaction; signers: Signer[] }[] {
|
||||
return this.transactionInstructions.map(({ instructions, signers }) => {
|
||||
const instructionsWithComputeBudget = args.computeUnitPriceMicroLamports
|
||||
? [
|
||||
...instructions,
|
||||
return this.transactionInstructions.map(
|
||||
({ instructions, signers, computeUnits }) => {
|
||||
const instructionsWithComputeBudget: TransactionInstruction[] = [
|
||||
...instructions,
|
||||
];
|
||||
if (
|
||||
computeUnits > DEFAULT_COMPUTE_BUDGET_UNITS * instructions.length ||
|
||||
args.tightComputeBudget
|
||||
) {
|
||||
instructionsWithComputeBudget.push(
|
||||
ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits })
|
||||
);
|
||||
}
|
||||
if (args.computeUnitPriceMicroLamports) {
|
||||
instructionsWithComputeBudget.push(
|
||||
ComputeBudgetProgram.setComputeUnitPrice({
|
||||
microLamports: args.computeUnitPriceMicroLamports,
|
||||
}),
|
||||
]
|
||||
: instructions;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
tx: new Transaction().add(...instructionsWithComputeBudget),
|
||||
signers: signers,
|
||||
};
|
||||
});
|
||||
return {
|
||||
tx: new Transaction().add(...instructionsWithComputeBudget),
|
||||
signers: signers,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -295,8 +313,16 @@ export class TransactionBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
export const isVersionedTransaction = (
|
||||
tx: Transaction | VersionedTransaction
|
||||
): tx is VersionedTransaction => {
|
||||
return "version" in tx;
|
||||
};
|
||||
|
||||
const TX_RETRY_INTERVAL = 500;
|
||||
|
||||
/**
|
||||
* Send a set of transactions to the network
|
||||
* Send a set of transactions to the network based on https://github.com/rpcpool/optimized-txs-examples
|
||||
*/
|
||||
export async function sendTransactions(
|
||||
transactions: {
|
||||
|
@ -305,12 +331,97 @@ export async function sendTransactions(
|
|||
}[],
|
||||
connection: Connection,
|
||||
wallet: Wallet,
|
||||
opts?: ConfirmOptions
|
||||
maxRetries?: number
|
||||
) {
|
||||
if (opts === undefined) {
|
||||
opts = AnchorProvider.defaultOptions();
|
||||
}
|
||||
const blockhashResult = await connection.getLatestBlockhashAndContext({
|
||||
commitment: "confirmed",
|
||||
});
|
||||
|
||||
const provider = new AnchorProvider(connection, wallet, opts);
|
||||
await provider.sendAll(transactions);
|
||||
// Signing logic for versioned transactions is different from legacy transactions
|
||||
for (const transaction of transactions) {
|
||||
const { signers } = transaction;
|
||||
let tx = transaction.tx;
|
||||
if (isVersionedTransaction(tx)) {
|
||||
if (signers) {
|
||||
tx.sign(signers);
|
||||
}
|
||||
} else {
|
||||
tx.feePayer = tx.feePayer ?? wallet.publicKey;
|
||||
tx.recentBlockhash = blockhashResult.value.blockhash;
|
||||
|
||||
if (signers) {
|
||||
for (const signer of signers) {
|
||||
tx.partialSign(signer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx = await wallet.signTransaction(tx);
|
||||
|
||||
// In the following section, we wait and constantly check for the transaction to be confirmed
|
||||
// and resend the transaction if it is not confirmed within a certain time interval
|
||||
// thus handling tx retries on the client side rather than relying on the RPC
|
||||
let confirmedTx = null;
|
||||
let retryCount = 0;
|
||||
|
||||
try {
|
||||
// Get the signature of the transaction with different logic for versioned transactions
|
||||
const txSignature = bs58.encode(
|
||||
isVersionedTransaction(tx)
|
||||
? tx.signatures?.[0] || new Uint8Array()
|
||||
: tx.signature ?? new Uint8Array()
|
||||
);
|
||||
|
||||
const confirmTransactionPromise = connection.confirmTransaction(
|
||||
{
|
||||
signature: txSignature,
|
||||
blockhash: blockhashResult.value.blockhash,
|
||||
lastValidBlockHeight: blockhashResult.value.lastValidBlockHeight,
|
||||
},
|
||||
"confirmed"
|
||||
);
|
||||
|
||||
confirmedTx = null;
|
||||
while (!confirmedTx) {
|
||||
confirmedTx = await Promise.race([
|
||||
confirmTransactionPromise,
|
||||
new Promise((resolve) =>
|
||||
setTimeout(() => {
|
||||
resolve(null);
|
||||
}, TX_RETRY_INTERVAL)
|
||||
),
|
||||
]);
|
||||
if (confirmedTx) {
|
||||
break;
|
||||
}
|
||||
if (maxRetries && maxRetries < retryCount) {
|
||||
break;
|
||||
}
|
||||
console.log(
|
||||
"Retrying transaction: ",
|
||||
txSignature,
|
||||
" Retry count: ",
|
||||
retryCount
|
||||
);
|
||||
retryCount++;
|
||||
|
||||
await connection.sendRawTransaction(tx.serialize(), {
|
||||
// Skipping preflight i.e. tx simulation by RPC as we simulated the tx above
|
||||
// This allows Triton RPCs to send the transaction through multiple pathways for the fastest delivery
|
||||
skipPreflight: true,
|
||||
// Setting max retries to 0 as we are handling retries manually
|
||||
// Set this manually so that the default is skipped
|
||||
maxRetries: 0,
|
||||
preflightCommitment: "confirmed",
|
||||
minContextSlot: blockhashResult.context.slot,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
if (!confirmedTx) {
|
||||
throw new Error("Failed to land the transaction");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue