4

When sending a transaction using Solana web3, it sometimes shows this error:
Error: failed to send transaction: Transaction simulation failed: Blockhash not found

What is the proper way of dealing with this error other than retrying for x amount of times?
Is there a way to guarantee this issue won't happen when sending transactions?

Here is an example of how I'm sending a transaction:

const web3 = require("@solana/web3.js")
const bs58 = require('bs58')

const publicKey = new web3.PublicKey(new Uint8Array(bs58.decode("BASE_58_PUBLIC_KEY").toJSON().data))
const secretKey = new Uint8Array(bs58.decode("BASE_58_SECRET_KEY").toJSON().data)

const connection = new web3.Connection(
  "https://api.mainnet-beta.solana.com", "finalized",
  {
    commitment: "finalized",
    confirmTransactionInitialTimeout: 30000
  }
)
const transaction = new web3.Transaction().add(
  web3.SystemProgram.transfer({
    fromPubkey: publicKey,
    toPubkey: publicKey,
    lamports: 1
  })
)
web3.sendAndConfirmTransaction(
  connection,
  transaction,
  [{publicKey: publicKey, secretKey: secretKey}],
  {commitment: "finalized"}
)


How can I improve this to avoid the Blockhash not found error?

Yilmaz
  • 35,338
  • 10
  • 157
  • 202
Brynne Harmon
  • 65
  • 1
  • 5
  • I ended up doing a retry backoff as I can't think of anything else. Please let me know if there's a better way to do this! – Brynne Harmon Jan 15 '22 at 03:04
  • 1
    Hey, can you post your solution. I have the same error and none of the reference below solve it. Thank you! – Julia Jan 24 '22 at 23:03

4 Answers4

3

Retrying is not a bad thing! In some situations, it's actually the preferred way to handle dropped transactions. For example, that means doing:

// assuming you have a transaction named `transaction` already
const blockhashResponse = await connection.getLatestBlockhashAndContext();
const lastValidBlockHeight = blockhashResponse.context.slot + 150;
const rawTransaction = transaction.serialize();
let blockheight = await connection.getBlockHeight();

while (blockheight < lastValidBlockHeight) {
  connection.sendRawTransaction(rawTransaction, {
    skipPreflight: true,
  });
  await sleep(500);
  blockheight = await connection.getBlockHeight();
}

You may like to read through this cookbook entry about retrying transactions: https://solanacookbook.com/guides/retrying-transactions.html

Specifically, it explains how to implement some retry logic: https://solanacookbook.com/guides/retrying-transactions.html#customizing-rebroadcast-logic

And what retrying means specifically: https://solanacookbook.com/guides/retrying-transactions.html#when-to-re-sign-transactions

Jon C
  • 7,019
  • 10
  • 17
  • For the last article you linked, it states to only retry after a blockhash is invalidated, which can be checked by [isBlockhashValid](https://docs.solana.com/developing/clients/jsonrpc-api#isblockhashvalid). Wouldn't this rpc call be able to fail, since nodes can be out of sync? – Brynne Harmon Jan 15 '22 at 19:51
  • What I'm currently doing is I'm using a fibonacci retry backoff, with a timeout of 2 minutes. Once it's past 2 minutes, it'll stop retrying. The reason I chose 2 minutes is because this [article](https://docs.solana.com/implemented-proposals/durable-tx-nonces#problem) says a blockhash is valid for approx 2 minutes. Is my approach alright? Or should I do the ``isBlockhashValid`` checks rather than having a 2 minute timeout, in case the time for a blockhash to expire changes one day? – Brynne Harmon Jan 15 '22 at 19:54
  • 1
    I'm not totally sure about this, but I think the blockhash validity will be pretty standard across nodes and shouldn't fall too far out of sync, since it was already observed. The main issue that can come up is using a blockhash from a minority fork, but that's a whole different issue. I would recommend using the `isBlockhashValid` every so often to double check rather than hardcoding 2 minutes. – Jon C Jan 15 '22 at 21:41
  • 1
    It's been some time but the article was recently updated, instead of using `isBlockhashValid` the recommendation is to use `getEpochInfo` and compare `blockHeight` with the tx's `lastValidBlockHeight`. If the nodes are out of sync it's possible to save the context slot at which the `blockHeight` is higher (i.e. the slot when the tx has expired) and wait for the status check slot (`getSignatureStatus`) to be equal or ahead. If the status is still null at that slot then it's safe to assume the tx expired. – Moyom Aug 19 '22 at 16:54
  • This is a link-only answer - answers on StackOverflow are expected to have code. – mikemaccana Sep 09 '22 at 13:03
1

You can use the maxRetries option: For web3 / Solana native:

web3.sendAndConfirmTransaction(
  connection,
  transaction,
  {
     maxRetries: 5
  }
)

Or for SPL tokens:

const signature = await transfer(
  connection,
  sender,
  senderTokenAccount.address,
  recipientTokenAccount.address,
  sender.publicKey,
  amount,
  [],
  {
    maxRetries: 6,
  }
);
mikemaccana
  • 110,530
  • 99
  • 389
  • 494
Ian Samz
  • 1,743
  • 20
  • 20
  • 1
    I like this answer better as it has some actual code in it - thanks! I've added the SPL version to it, hope you don't mind. – mikemaccana Sep 08 '22 at 16:42
0

The Strata foundation has a package for solana and it's production grade.

I've used it at production applications.

    const signature = await sendAndConfirmWithRetry(
      connection, 
      tx.serialize(), 
      {
        maxRetries: 5,
        skipPreflight: true
      },
      "processed");
att
  • 617
  • 8
  • 17
0

From here

RecentBlockhash is an important value for a transaction. Your transaction will be rejected if you use an expired recent blockhash (after 150 blocks)

solana has block time of 400ms. that means

150 * 400ms = 60 seconds

That is why you need to query and send transaction very fast. otherwise, your transaction will be dropped for good and you will get that error. correct error response should have been "Blockhash expired"

Yilmaz
  • 35,338
  • 10
  • 157
  • 202