nautechsystems/nautilus_trader · error
Verified swap decision header changed before signing
Error message
Verified swap decision header changed before signing
What it means
This client re-verifies the block header anchoring a swap quote immediately before signing an EIP-1559 transaction. When swap anchors are supplied, the freshly verified decision header must be byte-identical to `anchors.state`; if the chain has produced a new block (or a reorg occurred) since the quote was anchored, the header hash/content differs and the client refuses to sign so the swap executes against the exact state the quote was priced for.
Source
Thrown at crates/adapters/blockchain/src/execution/client.rs:2321
)?
} else {
match decision_header {
Some(verified) => verified,
None => {
let now_unix_secs = current_unix_secs()?;
required_verification(
self.verification
.verify_decision_header(now_unix_secs)
.await,
"pre-sign decision header",
)?
}
}
};
let decision_header = decision_header_verification.value;
if let Some(anchors) = swap_anchors {
anyhow::ensure!(
decision_header == anchors.state,
"Verified swap decision header changed before signing"
);
}
let decision_ancestry = self.verify_decision_ancestry(decision_header).await?;
validate_transaction_authorization(authorization, to, value, &input)?;
let deployment_verification = required_verification(
self.verification
.verify_deployment_manifest(&self.deployment_manifest, decision_header.number)
.await,
"pre-sign deployment manifest",
)?;
let authorization_decisions = match authorization {
Some(authorization) => {
self.verify_transaction_authorization(authorization, decision_header.number)
.await?
}
None => Vec::new(),View on GitHub (pinned to 18893faf8b)
Solutions
- Re-fetch the swap quote to obtain fresh anchors for the current chain head, then retry signing immediately.
- Reduce the delay between quote anchoring and signing (move signing closer to quote time, avoid long queuing).
- Only act on quotes anchored to blocks at or beyond the chain's safe/finalized height to avoid reorg-induced mismatches.
- Confirm the wallet is pointed at the intended chain/RPC endpoint; a load-balancer behind on blocks can produce mismatched headers.
Example fix
// before: reusing a long-lived anchor let prepared = client.prepare_and_sign_with_anchors(intent_id, created, to, value, input, Some(&old_anchors), None, None).await?; // after: refresh anchors right before signing let fresh_quote = quoter.refresh(&old_anchors).await?; let prepared = client.prepare_and_sign_with_anchors(intent_id, created, to, value, input, Some(&fresh_quote.anchors), None, None).await?;
Defensive patterns
Strategy: validation
Validate before calling
pub fn anchors_fresh(anchors: &SwapQuoteAnchors, current_head: u64, max_lag_blocks: u64) -> bool {
current_head.saturating_sub(anchors.state.number) <= max_lag_blocks
} Try / catch
// Rust: treat as retryable-stale
match client.prepare_and_sign_with_anchors(...).await {
Err(e) if e.to_string().contains("changed before signing") => re_quote_and_retry().await?,
other => other?,
} Prevention
- Sign as soon as possible after quoting; do not queue anchored intents for long periods.
- Only accept quotes anchored to finalized blocks.
- Monitor anchor age and re-quote proactively before it can go stale.
When it happens
Trigger: Calling the transaction preparation path (prepare_and_sign_with_anchors) with a `SwapQuoteAnchors` whose `state` header no longer matches the re-verified header at `anchors.state.number` — i.e. a new block was mined at that height's successor, the anchor went stale during quote-to-sign latency, or a chain reorg replaced the anchored block.
Common situations: Slow pipeline between fetching a swap quote and signing (quote anchored many seconds earlier on a fast blocktime chain); operator pauses or retries that outlive the anchor block; reorgs on a chain with weak finality; anchoring against a testnet/fork that advances quickly.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Decision header changed before signing
- Durable finalized header tip conflicts with independent sour
- Pool state block {} changed from {} to {}; refresh the profi
- Swap decision header conflicts with profiler ancestry
- Empty profiler ancestry does not end at the decision header
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/c64a8996eb1d6a4f.
Report an issue: GitHub.