perf: improve tx land rate (#1429)

* Checkpoint

* Checkpoint

* Continue

* Revert

* Revert

* Revert

* Update proposer

* Clean

* Lint
This commit is contained in:
guibescos 2024-04-09 11:24:45 +01:00 committed by GitHub
parent 0e885e3ca7
commit 972a9a1e1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 192 additions and 70 deletions

View File

@ -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;
} }
}
} }
} }
} }

View File

@ -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
) )

32
package-lock.json generated
View File

@ -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": {

View File

@ -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"
} }
} }

View File

@ -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");
}
}
} }