nautechsystems/nautilus_trader · critical · anyhow::Error

Finalized transaction does not match the authenticated signe

Error message

Finalized transaction does not match the authenticated signed payload and persisted intent

What it means

This error is thrown by `verify_finalized_transaction_identity` in the blockchain execution client after a transaction is included on chain. It re-decodes the raw signed transaction that was submitted and ensures the on-chain receipt, the signed payload, and the persisted execution intent all agree on hash, signer, chain id, nonce, recipient (`to`), calldata (`input`), and value. It exists as a post-inclusion defense-in-depth check: if the chain finalized a transaction that does not exactly match what was signed and what was recorded as intent, execution must be treated as unverified and fail hard.

Source

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

async fn verify_finalized_transaction_identity(
    included: &IncludedTransaction,
    intent: &ExecutionIntentRow,
    nonce: u64,
    raw_transaction: &[u8],
    verification: &VerificationCoordinator,
    wallet_address: Address,
    chain_id: u32,
    deployment_manifest: &BlockchainDeploymentManifest,
    trace_purpose: &str,
) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
    let transaction_verification = required_verification(
        verification.verify_transaction(&included.tx_hash).await,
        "finalized transaction",
    )?;
    let transaction = &transaction_verification.value;
    let signed = decode_signed_transaction(raw_transaction)?;
    let (expected_to, expected_input, expected_value) = persisted_call_fields(intent)?;
    anyhow::ensure!(
        included.receipt.transaction_hash == included.tx_hash
            && signed.hash == included.tx_hash
            && signed.signer == wallet_address
            && signed.chain_id == u64::from(chain_id)
            && signed.nonce == nonce
            && signed.to == expected_to
            && signed.input == expected_input
            && signed.value == expected_value,
        "Finalized transaction does not match the authenticated signed payload and persisted intent"
    );
    validate_rpc_transaction_matches_payload(transaction, raw_transaction)
        .context("finalized transaction identity mismatch")?;

    let trace_verification = required_verification(
        verification.verify_call_trace(&included.tx_hash).await,
        "finalized call trace",
    )?;
    validate_call_trace(

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Compare the intended recipient, calldata, and value in the persisted execution intent against what was actually signed and submitted; if the intent row is stale or wrong, regenerate the intent and resubmit a fresh transaction.
  2. Verify the signing wallet address matches `executor.wallet_address` and that the raw transaction bytes passed to this check are the exact bytes submitted (no re-signing or re-encoding in between).
  3. Check that the configured chain id / RPC endpoint is the intended network and that the signed chain id matches it.
  4. Check for nonce reuse or replacement: query the wallet's nonce history; if the transaction was replaced (e.g. by a speed-up with different fields), treat the earlier intent as superseded and re-run execution for the new intent.
  5. If the RPC receipt/transaction lookups are suspect, re-fetch from a trusted or secondary node to rule out indexer corruption before retrying.

Example fix

// before: submitting via a generic signer not tied to the configured executor wallet
let raw = signer.sign_transaction(tx).await?;
executor.submit(&raw).await?; // executor.wallet_address != signer.address()

// after: assert signing identity before submission so mismatches surface early
anyhow::ensure!(signer.address() == executor.wallet_address, "signer wallet does not match executor wallet");
let raw = signer.sign_transaction(tx).await?;
executor.submit(&raw).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Before finalization checks, confirm the submitted bytes still match the persisted intent.
let signed = decode_signed_transaction(&raw_transaction)?;
let (to, input, value) = persisted_call_fields(&intent)?;
assert_eq!(signed.signer, wallet_address, "wrong signing wallet");
assert_eq!(signed.chain_id, chain_id as u64, "wrong chain");
assert_eq!(signed.nonce, nonce, "nonce mismatch");
assert_eq!((signed.to, signed.input.clone(), signed.value), (to, input, value), "payload drifted from intent");

Type guard

fn matches_intent(signed: &DecodedSignedTransaction, intent: &ExecutionIntentRow, wallet: Address, chain_id: u32, nonce: u64) -> bool {
    let Ok((to, input, value)) = persisted_call_fields(intent) else { return false };
    signed.signer == wallet
        && signed.chain_id == u64::from(chain_id)
        && signed.nonce == nonce
        && signed.to == to
        && signed.input == input
        && signed.value == value
}

Try / catch

match verify_finalized_transaction(&included, &intent, nonce, &raw, &executor, purpose).await {
    Ok(decisions) => apply(decisions),
    Err(e) if e.to_string().contains("does not match the authenticated signed payload") => {
        alert_security_review(included.tx_hash); // do NOT auto-retry; investigate signer/nonce/intent drift
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: `anyhow::ensure!` at client.rs:4346 fails when any of these hold: `included.receipt.transaction_hash != included.tx_hash`; the decoded `signed.hash` differs from `included.tx_hash`; `signed.signer != wallet_address` (signed by a different wallet); `signed.chain_id` mismatches the executor chain id (e.g. replayed or submitted to the wrong network); `signed.nonce` differs from the intended nonce (transaction replaced/rebroadcast with a different nonce); or `signed.to`/`signed.input`/`signed.value` differ from the fields persisted in the execution intent row (`persisted_call_fields`).

Common situations: A wallet or signing service was rotated or reconfigured so the transaction was signed with a different key; the same raw transaction was rebroadcast after a nonce mismatch/repacement with different fields; the persisted intent row in the database was mutated or written by a different code path/version than the transaction builder; the RPC node or indexer returned a receipt for the wrong transaction hash; a chain fork or wrong-endpoint (testnet vs mainnet) configuration causes chain-id mismatch.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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