Resolve Nonce Holes

Overview

Nonces are single-use numbers that prevent replay attacks by ensuring each transaction is unique. A nonce hole occurs when a transaction broadcasts with a nonce that skips the expected sequential value, creating a gap. This gap often results from network failures, crashes, or out-of-order execution. Therefore, resolving nonce holes is critical to ensure that transactions with higher nonces don't remain stuck.

Common causes of nonce holes include:

  • Dropped Messages - If messages containing nonces are lost, other parties may expect a missing nonce.
  • Asynchronous Execution - Some protocols assume a sequential nonce usage, but real-world execution may be out of order.
  • Crashes and Restarts - A participant may lose track of the last used nonce if no persistence mechanism exists.

Prerequisites

1. Identify Stuck Transactions

Endpoint: Get list of potentially stuck transactions and their nonces for eth-like coins

export COIN="<ASSET_ID>"
export WALLET_ID="<YOUR_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"

curl -X GET \
  "https://app.bitgo-test.com/api/v2/$COIN/wallet/$WALLET_ID/potentialStuckTxs" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Step Result

Stuck transactions return:

  • "cause": "nonceHole"
  • "userActionDisabled": false
[
  {
    "nonce": 200000, # save this nonce value for use in the following step
    "txHex": "01000000000101d...a53aec8b11400",
    "txId": "b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26",
    "sendTransfer": {
      "baseValueString": "2000000",
      "bitgoOrg": "BitGo Trust",
      "coin": "hteth",
      "date": "2025-01-24T14:15:22Z",
      "enterprise": "59cd72485007a239fb00282ed480da1f",
      "history": [
        {
          "action": "created",
          "date": "2025-01-24T14:15:22Z",
          "txid": "b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26",
          "user": "59cd72485007a239fb00282ed480da1f"
        }
      ],
      "id": "59cd72485007a239fb00282ed480da1f",
      "organization": "59cd72485007a239fb00282ed480da1f",
      "pendingApproval": "664ed267aad92c62a183ac5f28883495",
      "state": "initialized",
      "txid": "b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26",
      "type": "send",
      "valueString": "2000000",
      "wallet": "59cd72485007a239fb00282ed480da1f",
      "walletType": "custodial"
    },
    "cause": "nonceHole", # identifies the cause of the stuck transaction is a nonce hole
    "gasAccelerationFee": {
      "gasPrice": "200000",
      "maxFeePerGas": "200000",
      "maxPriorityFeePerGas": "200000"
    },
    "userActionDisabled": false # identifies that the transaction is stuck
  }
]

2. Resolve Nonce Holes

Create a new transaction that includes the nonce value returned in the prior step.

Note: Withdrawal flows differ by wallet type. View the end-to-end transaction flow and integration guides for your wallet type at Withdraw Overview.

Endpoints:

export WALLET_ID="<WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export NONCE="<NONCE_VALUE_OF_STUCK_TRANSACTION>"

curl -X POST "https://app.bitgo-test.com/api/v2/wallet/$WALLET_ID/txrequests" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "idempotencyKey": "string",
    "intent": {
      "intentType": "fillNonce",  # Required to fill the nonce hole
      "nonce": "'"$NONCE"'", # Required to fill the nonce hole
      "sequenceId": "4dEeycHsDSsCAG1zYPKGSxyPkHQ",
      "feeOptions": {
        "unit": "baseUnit",
        "formula": "fixed",
        "feeType": "base",
        "gasLimit": 0,
        "gasPrice": 0
      },
      "receiveAddress": "string",
      "senderAddressIndex": 0
    },
    "videoApprovers": [
      "585951a5df8380e0e3063e9f",
      "585951a5df8380e0e304a553"
    ],
    "apiVersion": "lite",
    "preview": false
  }'
export COIN="<ASSET_ID>"
export WALLET_ID="<YOUR_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export AMOUNT="<AMOUNT_IN_BASE_UNITS>"
export ADDRESS="<DESTINATION_ADDRESS>"
export NONCE="<NONCE_VALUE_OF_STUCK_TRANSACTION>"

curl -X POST \
  https://app.bitgo-test.com/api/v2/$COIN/wallet/$WALLET_ID/tx/build \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "type": "fillNonce", # Required to fill the nonce hole
    "nonce": "'"$NONCE"'", # Required to fill the nonce hole
    "idfSignedTimestamp": "2025-01-28T10:55:38.732Z",
    "idfUserId": "628ca09d1b78a6022750254f0777561a",
    "idfVersion": 1,
    "instant": false,
    "preview": false,
    "recipients": [
      {
        "amount": "'"$AMOUNT"'",
        "address": "'"$ADDRESS"'" # If sending to another Go Account, use the walletId parameter instead
      }
    ],
    "sequenceId": "4dEeycHsDSsCAG1zYPKGSxyPkHQ"
}'
export BITGO_EXPRESS_HOST="<YOUR_LOCAL_HOST>"
export COIN="<OFC_ASSET_ID>"
export WALLET_ID="<YOUR_WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export ADDRESS="<DESTINATION_ADDRESS_OR_ID_HASH>"
export GO_ACCOUNT="<DESTINATION_WALLET_ID>" // If withdrawing to another Go Account
export AMOUNT="<AMOUNT_IN_BASE_UNITS>"
export WALLET_PASSPHRASE="<YOUR_WALLET_PASSPHRASE>"
export NONCE="<NONCE_VALUE_OF_STUCK_TRANSACTION>"

curl -X POST \
  http://$BITGO_EXPRESS_HOST/api/v2/$COIN/wallet/$WALLET_ID/sendcoins \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "address": "'"$ADDRESS"'",
    "walletId": "'"$GO_ACCOUNT"'", # If withdrawing to another Go Account, pass "walletId" instead of "address"
    "amount": "'"$AMOUNT"'",
    "walletPassphrase": "'"$WALLET_PASSPHRASE"'",
    "type": "fillNonce", # Required to fill the nonce hole
    "nonce": "'"$NONCE"'" # Required to fill the nonce hole
}'
export WALLET_ID="<WALLET_ID>"
export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>"
export NONCE="<NONCE_VALUE_OF_STUCK_TRANSACTION>"

curl -X POST "https://app.bitgo-test.com/api/v2/wallet/$WALLET_ID/txrequests" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d '{
    "idempotencyKey": "string",
    "intent": {
      "intentType": "fillNonce",  # Required to fill the nonce hole
      "nonce": "'"$NONCE"'", # Required to fill the nonce hole
      "sequenceId": "4dEeycHsDSsCAG1zYPKGSxyPkHQ",
      "feeOptions": {
        "unit": "baseUnit",
        "formula": "fixed",
        "feeType": "base",
        "gasLimit": 0,
        "gasPrice": 0
      },
      "receiveAddress": "string",
      "senderAddressIndex": 0
    },
    "videoApprovers": [
      "585951a5df8380e0e3063e9f",
      "585951a5df8380e0e304a553"
    ],
    "apiVersion": "lite",
    "preview": false
  }'
import * as dotenv from "dotenv";
import {EnvironmentName} from "bitgo";
import {BitGoAPI} from "@bitgo/sdk-api";
import {Hteth} from "@bitgo/sdk-coin-eth";

dotenv.config();

const config = {
    USERNAME: process.env.USERNAME as string,
    PASSWORD: process.env.PASSWORD as string,
    ENV: process.env.ENV as EnvironmentName,
    OTP: process.env.OTP as string,
    ENTERPRISE_ID: process.env.ENTERPRISE_ID as string,
    WALLET_ID: process.env.WALLET_ID as string,
    WALLET_PASS_PHRASE: process.env.WALLET_PASS_PHRASE as string
};

const bitgo = new BitGoAPI({env: config.ENV});
bitgo.register("hteth", Hteth.createInstance);
const coin = bitgo.coin("hteth");

async function auth() {
    await bitgo.authenticate({
        username: config.USERNAME,
        password: config.PASSWORD,
        otp: config.OTP,
    });
    await bitgo.lock();
    await bitgo.unlock({otp: "000000", duration: 3600});
}

async function main() {
    await auth();
    const destinatonAddress = "0x1037c88b10fbd0754b9fbd3ba7be41fe7cb61f59";
    const wallet = await coin.wallets().get({id: config.WALLET_ID});
    const sendAmount = wallet.balanceString() ?? 0;
    const res = await wallet.sendMany({
        walletPassphrase: config.WALLET_PASS_PHRASE,
        recipients: [{address: destinatonAddress, amount: sendAmount}],
        type: "fillNonce", // Required to fill the nonce hole
        nonce: 200000, // Use the nonce value from the stuck transaction
    });
    console.log(res);
}

main().catch((err) => console.error(err));

Step Result

{
  "txRequestId": "string",
  "version": 0,
  "latest": true,
  "walletId": "string",
  "walletType": "cold",
  "enterpriseId": "string",
  "state": "initialized",
  "date": {},
  "createdDate": {},
  "userId": "string",
  "initiatedBy": "string",
  "updatedBy": "string",
  "intent": {
    "intentType": "fillNonce",
    "nonce": "string",
    "sequenceId": "string",
    "comment": "string",
    "feeOptions": {
      "unit": "baseUnit",
      "formula": "fixed",
      "feeType": "base",
      "gasLimit": 0,
      "gasPrice": 0
    },
    "receiveAddress": "string",
    "senderAddressIndex": 0
  },
  "pendingApprovalId": "string",
  "isCanceled": true,}

Next

Continue the transaction flow for your wallet. For more details, see Withdraw Overview.

See Also