{"record":{"id":"64dd820a0ba6442e","repo":"nautechsystems/nautilus_trader","slug":"canonical-head-changed-during-signer-nonce-replace","errorCode":null,"errorMessage":"Canonical head changed during signer-nonce replacement scan","messagePattern":"Canonical head changed during signer-nonce replacement scan","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/adapters/blockchain/src/execution/client.rs","lineNumber":1813,"sourceCode":"        from_block: u64,\n    ) -> anyhow::Result<Option<RpcTransaction>> {\n        let head = self.http_rpc_client.latest_block().await?;\n        let mut found = None;\n\n        for number in from_block..=head.number {\n            let block = self.http_rpc_client.block_by_number(number, true).await?;\n            if let Some(transaction) = block.transactions.into_iter().find(|transaction| {\n                transaction.from == self.wallet_address && transaction.nonce == nonce\n            }) {\n                found = Some(transaction);\n                break;\n            }\n        }\n        let stable_head = self\n            .http_rpc_client\n            .block_by_number(head.number, false)\n            .await?;\n        anyhow::ensure!(\n            stable_head.hash == head.hash,\n            \"Canonical head changed during signer-nonce replacement scan\"\n        );\n        Ok(found)\n    }\n\n    fn release_slot(&self) {\n        *self.in_flight.lock().expect(\"in-flight mutex poisoned\") = None;\n    }\n}\n\nfn current_execution_hash(\n    intent_id: i64,\n    hashes: &[ExecutionTransactionHashRow],\n) -> anyhow::Result<&ExecutionTransactionHashRow> {\n    let mut current = hashes.iter().filter(|row| row.current);\n    let row = current\n        .next()","sourceCodeStart":1795,"sourceCodeEnd":1831,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/2114cf6f761429e0adb5ca9596fcd7b895b16011/crates/adapters/blockchain/src/execution/client.rs#L1795-L1831","documentation":"find_nonce_transaction scans blocks from a start height to the head looking for the signer's nonce, then re-fetches the head block to confirm the scan covered a stable chain view. If the re-fetched head hash differs from the hash captured before scanning, blocks moved (a reorg or rapid head advance) during the scan, so any result would be unreliable and the scan aborts.","triggerScenarios":"Replacement/recovery scans (find_nonce_transaction) executed while the chain head advances or reorganizes: common on busy networks where several blocks are produced during a multi-block scan, or during a live reorg event.","commonSituations":"Scanning long block ranges after downtime on fast L2s (block times under a second); RPC nodes behind load balancers serving inconsistent heads; genuine reorg storms.","solutions":["Retry the operation: the scan restarts against the new head and usually succeeds once the view is stable","Narrow the scan range (start closer to the current head) so it completes within one or two blocks","Use an RPC endpoint with sticky sessions/consistent heads if the error repeats constantly","If it recurs on every attempt, check the network for an ongoing reorg incident"],"exampleFix":"# before: single scan attempt\nresult = find_nonce_transaction(nonce, from_block)  # raises on head change\n\n# after: retry until the chain view stays stable\nfor _ in range(5):\n    try:\n        result = find_nonce_transaction(nonce, from_block)\n        break\n    except Exception as e:\n        if 'Canonical head changed' not in str(e):\n            raise\n        from_block = max(from_block, latest_height() - 64)","handlingStrategy":"retry","validationCode":"# Bound the scan window to a couple of recent blocks to make head moves unlikely\nlatest = w3.eth.get_block('latest')\nfrom_block = max(from_block, latest.number - 12)  # ~2-3s of blocks on L2\nassert w3.eth.get_block(latest.number)['hash'] == latest.hash, 'unstable node view'","typeGuard":null,"tryCatchPattern":"for attempt in range(5):\n    try:\n        found = find_nonce_transaction(nonce, from_block)\n        break\n    except Exception as e:\n        if 'Canonical head changed' not in str(e):\n            raise\n        time.sleep(2)  # head moved mid-scan; retry against the new head\nelse:\n    raise RuntimeError('nonce scan could not get a stable head')","preventionTips":["Keep block-scan ranges short and start near the head rather than from ancient heights","Use RPC endpoints with consistent views (sticky sessions) for scan-heavy recovery flows"],"tags":["blockchain","chain-reorg","nonce","rpc","retryable"],"backgroundTag":"chain-reorg","analyzedSha":"2114cf6f761429e0adb5ca9596fcd7b895b16011","analyzedAt":"2026-08-21T11:28:30.864Z","schemaVersion":2},"datasetVersion":"2026-08-22T14:17:55.899Z"}