nautechsystems/nautilus_trader · error

Retained terminal receipt is above the verified finalized bo

Error message

Retained terminal receipt is above the verified finalized boundary

What it means

When migrating a retained terminal (finalized/reverted) intent, the client fetches its receipt and requires the receipt's block number to be at or below the currently verified `finalized` block. This error is thrown when the stored transaction receipt points to a block that the node no longer considers finalized — i.e. the receipt cannot be proven final under the current finalized header, so the terminal status cannot be safely migrated. It guards against trusting receipts from unfinalized or reorged-out blocks.

Source

Thrown at crates/adapters/blockchain/src/execution/client.rs:5505

                nonce < next_canonical_nonce,
                "Retained active nonce {nonce} is above canonical nonce {next_canonical_nonce}"
            );

            let receipt_verification = required_verification(
                self.verification
                    .verify_receipt(&B256::from_str(&current.transaction_hash).with_context(
                        || {
                            format!(
                                "Retained transaction hash {} is invalid",
                                current.transaction_hash
                            )
                        },
                    )?)
                    .await,
                "migration receipt",
            )?;
            let receipt = receipt_verification.value.clone();
            anyhow::ensure!(
                receipt.block_number <= finalized.number,
                "Retained terminal receipt is above the verified finalized boundary"
            );
            let inclusion_verification = required_verification(
                self.verification.verify_block(receipt.block_number).await,
                "migration inclusion header",
            )?;
            anyhow::ensure!(
                inclusion_verification.value.hash == receipt.block_hash
                    && finalized_headers.iter().any(|header| {
                        header.number == receipt.block_number
                            && header.hash == receipt.block_hash.to_string()
                    }),
                "Retained terminal receipt is not on the verified finalized ancestry"
            );
            let tx_hash = B256::from_str(&current.transaction_hash)
                .context("Retained transaction hash is invalid")?;
            let included = IncludedTransaction {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Wait for the node to sync so its finalized block number is >= the receipt's block number, then re-run migration.
  2. Verify you are connected to the same chain/network the receipts were produced on; fix the RPC/chain config and retry.
  3. Re-verify the intent's transaction on-chain (fetch a fresh receipt) and update the stored terminal record if the original receipt was from a reorged block.
  4. If the receipt's block genuinely lost finality, revert the intent to a recoverable state so it can be re-prepared and re-signed.

Example fix

// before (migrating against a lagging node)
let finalized = provider.latest_finalized().await?;  // block 100
// receipt.block_number = 105 -> error

// after (gate migration on finality catch-up)
if finalized.number < receipt.block_number {
    wait_for_finality(receipt.block_number).await?;
}
Defensive patterns

Strategy: retry

Validate before calling

async fn finality_caught_up(provider: &Provider, receipt_block: u64) -> Result<bool, Error> {
    let finalized = provider.get_finalized_block().await?;
    Ok(finalized.number >= receipt_block)
}

Type guard

fn receipt_is_final(receipt_block: u64, finalized_number: u64) -> bool {
    receipt_block <= finalized_number
}

Try / catch

// retry until the node's finalized head passes the receipt block
loop {
    match run_migration().await {
        Err(e) if e.to_string().contains("above the verified finalized boundary") => {
            tokio::time::sleep(Duration::from_secs(12)).await; // ~1 block
            continue;
        }
        other => break other,
    }
}

Prevention

When it happens

Trigger: Raised by `anyhow::ensure!(receipt.block_number <= finalized.number, ...)` when: the node's finalized head moved backwards or the migration runs against a different/forked chain than the one that produced the receipt; the node is behind and its `finalized` block is older than the receipt's block; or the persisted receipt references a block from an uncle/reorged fork.

Common situations: Switching RPC providers or networks (testnet vs fork) while retaining state; a chain reorg invalidating previously 'final' receipts (weak-finality chains); running migration immediately after node startup before finalized head catches up; replaying state from a snapshot ahead of the current node's finalized block.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/54402029f37efd67. Report an issue: GitHub.