nautechsystems/nautilus_trader · critical · anyhow::Error
Finalized transaction does not match the authenticated…
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.
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…
- A recoverable execution for wallet
- Active execution intent
- Canonical head changed during signer-nonce replacement scan
- Canonical nonce advanced without an authenticated signer…
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/f9f6dd78718842ca.
Report an issue: GitHub.
Appendix: 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)