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 { MultisigAccount } from "@sqds/mesh/lib/types";
|
||||||
import { mapKey } from "./remote_executor";
|
import { mapKey } from "./remote_executor";
|
||||||
import { WORMHOLE_ADDRESS } from "./wormhole";
|
import { WORMHOLE_ADDRESS } from "./wormhole";
|
||||||
import { TransactionBuilder } from "@pythnetwork/solana-utils";
|
import {
|
||||||
|
TransactionBuilder,
|
||||||
|
sendTransactions,
|
||||||
|
} from "@pythnetwork/solana-utils";
|
||||||
import {
|
import {
|
||||||
PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET,
|
PACKET_DATA_SIZE_WITH_ROOM_FOR_COMPUTE_BUDGET,
|
||||||
PriorityFeeConfig,
|
PriorityFeeConfig,
|
||||||
} from "@pythnetwork/solana-utils";
|
} from "@pythnetwork/solana-utils";
|
||||||
|
import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet";
|
||||||
|
|
||||||
export const MAX_EXECUTOR_PAYLOAD_SIZE =
|
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
|
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_INSTRUCTIONS_PER_PROPOSAL = 256 - 1;
|
||||||
export const MAX_NUMBER_OF_RETRIES = 10;
|
|
||||||
|
|
||||||
type SquadInstruction = {
|
type SquadInstruction = {
|
||||||
instruction: TransactionInstruction;
|
instruction: TransactionInstruction;
|
||||||
|
@ -385,52 +388,24 @@ export class MultisigVault {
|
||||||
|
|
||||||
async sendAllTransactions(transactions: Transaction[]) {
|
async sendAllTransactions(transactions: Transaction[]) {
|
||||||
const provider = this.getAnchorProvider({
|
const provider = this.getAnchorProvider({
|
||||||
preflightCommitment: "processed",
|
preflightCommitment: "confirmed",
|
||||||
commitment: "processed",
|
commitment: "confirmed",
|
||||||
});
|
});
|
||||||
|
|
||||||
let needToFetchBlockhash = true; // We don't fetch blockhash everytime to save time
|
for (const [index, tx] of transactions.entries()) {
|
||||||
let blockhash: string = "";
|
console.log("Trying transaction: ", index, " of ", transactions.length);
|
||||||
for (let [index, tx] of transactions.entries()) {
|
let retry = true;
|
||||||
console.log("Trying to send transaction: " + index);
|
while (true)
|
||||||
let numberOfRetries = 0;
|
|
||||||
let txHasLanded = false;
|
|
||||||
|
|
||||||
while (!txHasLanded) {
|
|
||||||
try {
|
try {
|
||||||
if (needToFetchBlockhash) {
|
await sendTransactions(
|
||||||
blockhash = (await provider.connection.getLatestBlockhash())
|
[{ tx, signers: [] }],
|
||||||
.blockhash;
|
|
||||||
needToFetchBlockhash = false;
|
|
||||||
}
|
|
||||||
tx.feePayer = tx.feePayer || provider.wallet.publicKey;
|
|
||||||
tx.recentBlockhash = blockhash;
|
|
||||||
provider.wallet.signTransaction(tx);
|
|
||||||
await sendAndConfirmRawTransaction(
|
|
||||||
provider.connection,
|
provider.connection,
|
||||||
tx.serialize(),
|
this.squad.wallet as NodeWallet
|
||||||
provider.opts
|
|
||||||
);
|
);
|
||||||
txHasLanded = true;
|
break;
|
||||||
} catch (e) {
|
} 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);
|
console.log(e);
|
||||||
numberOfRetries += 1;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -400,12 +400,17 @@ const Proposal = ({
|
||||||
squads.wallet.publicKey,
|
squads.wallet.publicKey,
|
||||||
squads.connection
|
squads.connection
|
||||||
)
|
)
|
||||||
builder.addInstruction({ instruction, signers: [] })
|
builder.addInstruction({
|
||||||
const versionedTxs = await builder.buildVersionedTransactions(
|
instruction,
|
||||||
DEFAULT_PRIORITY_FEE_CONFIG
|
signers: [],
|
||||||
)
|
computeUnits: 10000,
|
||||||
|
})
|
||||||
|
const transactions = builder.buildLegacyTransactions({
|
||||||
|
computeUnitPriceMicroLamports: 150000,
|
||||||
|
tightComputeBudget: true,
|
||||||
|
})
|
||||||
await sendTransactions(
|
await sendTransactions(
|
||||||
versionedTxs,
|
transactions,
|
||||||
squads.connection,
|
squads.connection,
|
||||||
squads.wallet as Wallet
|
squads.wallet as Wallet
|
||||||
)
|
)
|
||||||
|
|
|
@ -59586,7 +59586,8 @@
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coral-xyz/anchor": "^0.29.0",
|
"@coral-xyz/anchor": "^0.29.0",
|
||||||
"@solana/web3.js": "^1.90.0"
|
"@solana/web3.js": "^1.90.0",
|
||||||
|
"bs58": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.4.0",
|
||||||
|
@ -59600,6 +59601,19 @@
|
||||||
"typescript": "^4.6.3"
|
"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": {
|
"target_chains/sui/cli": {
|
||||||
"name": "pyth-sui-cli",
|
"name": "pyth-sui-cli",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
|
@ -71281,12 +71295,28 @@
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.4.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.20.0",
|
"@typescript-eslint/eslint-plugin": "^5.20.0",
|
||||||
"@typescript-eslint/parser": "^5.20.0",
|
"@typescript-eslint/parser": "^5.20.0",
|
||||||
|
"bs58": "^5.0.0",
|
||||||
"eslint": "^8.13.0",
|
"eslint": "^8.13.0",
|
||||||
"jest": "^29.4.0",
|
"jest": "^29.4.0",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"quicktype": "^23.0.76",
|
"quicktype": "^23.0.76",
|
||||||
"ts-jest": "^29.0.5",
|
"ts-jest": "^29.0.5",
|
||||||
"typescript": "^4.6.3"
|
"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": {
|
"@radix-ui/primitive": {
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coral-xyz/anchor": "^0.29.0",
|
"@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 {
|
import {
|
||||||
ComputeBudgetProgram,
|
ComputeBudgetProgram,
|
||||||
ConfirmOptions,
|
|
||||||
Connection,
|
Connection,
|
||||||
PACKET_DATA_SIZE,
|
PACKET_DATA_SIZE,
|
||||||
PublicKey,
|
PublicKey,
|
||||||
|
@ -11,6 +10,7 @@ import {
|
||||||
TransactionMessage,
|
TransactionMessage,
|
||||||
VersionedTransaction,
|
VersionedTransaction,
|
||||||
} from "@solana/web3.js";
|
} 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.
|
* 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 = {
|
export type PriorityFeeConfig = {
|
||||||
/** This is the priority fee in micro lamports, it gets passed down to `setComputeUnitPrice` */
|
/** This is the priority fee in micro lamports, it gets passed down to `setComputeUnitPrice` */
|
||||||
computeUnitPriceMicroLamports?: number;
|
computeUnitPriceMicroLamports?: number;
|
||||||
|
tightComputeBudget?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -186,14 +187,19 @@ export class TransactionBuilder {
|
||||||
async buildVersionedTransactions(
|
async buildVersionedTransactions(
|
||||||
args: PriorityFeeConfig
|
args: PriorityFeeConfig
|
||||||
): Promise<{ tx: VersionedTransaction; signers: Signer[] }[]> {
|
): 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(
|
return this.transactionInstructions.map(
|
||||||
({ instructions, signers, computeUnits }) => {
|
({ instructions, signers, computeUnits }) => {
|
||||||
const instructionsWithComputeBudget: TransactionInstruction[] = [
|
const instructionsWithComputeBudget: TransactionInstruction[] = [
|
||||||
...instructions,
|
...instructions,
|
||||||
];
|
];
|
||||||
if (computeUnits > DEFAULT_COMPUTE_BUDGET_UNITS * instructions.length) {
|
if (
|
||||||
|
computeUnits > DEFAULT_COMPUTE_BUDGET_UNITS * instructions.length ||
|
||||||
|
args.tightComputeBudget
|
||||||
|
) {
|
||||||
instructionsWithComputeBudget.push(
|
instructionsWithComputeBudget.push(
|
||||||
ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits })
|
ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits })
|
||||||
);
|
);
|
||||||
|
@ -226,21 +232,33 @@ export class TransactionBuilder {
|
||||||
buildLegacyTransactions(
|
buildLegacyTransactions(
|
||||||
args: PriorityFeeConfig
|
args: PriorityFeeConfig
|
||||||
): { tx: Transaction; signers: Signer[] }[] {
|
): { tx: Transaction; signers: Signer[] }[] {
|
||||||
return this.transactionInstructions.map(({ instructions, signers }) => {
|
return this.transactionInstructions.map(
|
||||||
const instructionsWithComputeBudget = args.computeUnitPriceMicroLamports
|
({ instructions, signers, computeUnits }) => {
|
||||||
? [
|
const instructionsWithComputeBudget: TransactionInstruction[] = [
|
||||||
...instructions,
|
...instructions,
|
||||||
|
];
|
||||||
|
if (
|
||||||
|
computeUnits > DEFAULT_COMPUTE_BUDGET_UNITS * instructions.length ||
|
||||||
|
args.tightComputeBudget
|
||||||
|
) {
|
||||||
|
instructionsWithComputeBudget.push(
|
||||||
|
ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (args.computeUnitPriceMicroLamports) {
|
||||||
|
instructionsWithComputeBudget.push(
|
||||||
ComputeBudgetProgram.setComputeUnitPrice({
|
ComputeBudgetProgram.setComputeUnitPrice({
|
||||||
microLamports: args.computeUnitPriceMicroLamports,
|
microLamports: args.computeUnitPriceMicroLamports,
|
||||||
}),
|
})
|
||||||
]
|
);
|
||||||
: instructions;
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tx: new Transaction().add(...instructionsWithComputeBudget),
|
tx: new Transaction().add(...instructionsWithComputeBudget),
|
||||||
signers: signers,
|
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(
|
export async function sendTransactions(
|
||||||
transactions: {
|
transactions: {
|
||||||
|
@ -305,12 +331,97 @@ export async function sendTransactions(
|
||||||
}[],
|
}[],
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
wallet: Wallet,
|
wallet: Wallet,
|
||||||
opts?: ConfirmOptions
|
maxRetries?: number
|
||||||
) {
|
) {
|
||||||
if (opts === undefined) {
|
const blockhashResult = await connection.getLatestBlockhashAndContext({
|
||||||
opts = AnchorProvider.defaultOptions();
|
commitment: "confirmed",
|
||||||
}
|
});
|
||||||
|
|
||||||
const provider = new AnchorProvider(connection, wallet, opts);
|
// Signing logic for versioned transactions is different from legacy transactions
|
||||||
await provider.sendAll(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