Przeglądaj źródła

perf: improve tx land rate (#1429)

* Checkpoint

* Checkpoint

* Continue

* Revert

* Revert

* Revert

* Update proposer

* Clean

* Lint
guibescos 1 rok temu
rodzic
commit
972a9a1e1d

+ 15 - 40
governance/xc_admin/packages/xc_admin_common/src/propose.ts

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

+ 10 - 5
governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals.tsx

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

+ 31 - 1
package-lock.json

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

+ 2 - 1
target_chains/solana/sdk/js/solana_utils/package.json

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

+ 134 - 23
target_chains/solana/sdk/js/solana_utils/src/transaction.ts

@@ -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",
+  });
+
+  // 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 provider = new AnchorProvider(connection, wallet, opts);
-  await provider.sendAll(transactions);
+      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");
+    }
+  }
 }