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
- Get Started
- Initiate a withdrawal (see Withdraw Overview).
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
Updated 22 days ago