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

View File

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

32
package-lock.json generated
View File

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

View File

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

View File

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