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
- 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.
- 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).
- Check that the configured chain id / RPC endpoint is the intended network and that the signed chain id matches it.
- 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.
- 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
- Always sign with the exact wallet configured on the executor and assert the signer address before submission.
- Submit the raw signed bytes unchanged; never re-encode or re-sign between signing and verification.
- Persist the intent row (to/input/value/nonce/chain_id) atomically with the signing step.
- Guard against nonce replacement (speed-up/cancel flows) so the verified intent corresponds to the actually mined transaction.
- Pin the RPC endpoint to the intended network and assert chain id at startup.
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.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Verified call-trace root differs from the authenticated tran
- Chain ID mismatch at connect: expected {expected_chain_id},
- Canonical head changed during signer-nonce replacement scan
- Verified finalized transaction count advanced without an act
- Execution intent {} has a signed transaction {} that was not
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/f9f6dd78718842ca.
Report an issue: GitHub.