nautechsystems/nautilus_trader · error
Persisted transaction signature is not EIP-2 normalized
Error message
Persisted transaction signature is not EIP-2 normalized
What it means
decode_signed_transaction enforces that the recovered signature is EIP-2 normalized (s <= n/2). A non-canonical s value allows signature malleability — the same transaction could appear under two hashes — so the pipeline refuses to accept such a signature on a persisted transaction.
Source
Thrown at crates/adapters/blockchain/src/execution/transaction.rs:219
anyhow::anyhow!("Persisted signed transaction is not a complete EIP-2718 envelope")
})?;
let TxEnvelope::Eip1559(signed) = envelope else {
anyhow::bail!("Persisted signed transaction is not EIP-1559");
};
anyhow::ensure!(
signed.signature().normalize_s().is_none(),
"Persisted transaction signature is not EIP-2 normalized"
);
let signer = signed
.signature()
.recover_address_from_prehash(&signed.signature_hash())
.context("failed to recover persisted transaction signer")?;
let hash = *signed.hash();
let tx = signed.tx();
let TxKind::Call(to) = tx.to else {
anyhow::bail!("Signed transaction creates a contract instead of calling a destination");
};
anyhow::ensure!(
tx.access_list.is_empty(),
"Signed transaction access list is not empty"
);
Ok(DecodedSignedTransaction {
hash,
signer,
chain_id: tx.chain_id,
nonce: tx.nonce,
to,
value: tx.value,
input: tx.input.clone(),
gas_limit: tx.gas_limit,
max_fee_per_gas: tx.max_fee_per_gas,
max_priority_fee_per_gas: tx.max_priority_fee_per_gas,
})
}
View on GitHub (pinned to 18893faf8b)
Solutions
- Re-sign the transaction with a signer that performs EIP-2 low-s normalization (all mainstream libraries do)
- Do not manually normalize and persist — normalize_s() output must be re-hashed/re-verified as a new signature
- Check whether stored bytes were mutated (compare against original persisted blob/hashes)
- Regenerate the intent and signed transaction end-to-end if the signer cannot be fixed
Defensive patterns
Strategy: try-catch
Try / catch
match validate_signed_transaction(&raw, &intent) {
Ok(()) => (),
Err(e) if e.to_string().contains("EIP-2 normalized") => {
// reject the payload; flag the signer as non-conforming, do not mutate bytes in place
}
Err(e) => return Err(e),
} Prevention
- Use well-established signers (alloy, ethers) that low-s normalize by default
- Never hand-construct or mutate ECDSA s values
- Verify a signature round-trip (recover signer) before persisting signed transactions
When it happens
Trigger: decode_signed_transaction (via its three callers) encounters a signed EIP-1559 transaction whose signature fails normalize_s() (returns Some, meaning s was in the high half of the curve order) — typically bytes signed by a non-conforming signer or hand-crafted/mutated signatures.
Common situations: Custom or buggy signing code that does not apply EIP-2 low-s normalization; a signature mutated in storage (bit flip making s high); test fixtures generated with raw ECDSA without normalization.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- Sealed transaction payload is {} bytes, exceeding the {} byt
- Sealed transaction payload is truncated
- Unsupported signed transaction payload envelope version {}
- Payload deployment ID does not match the configured key set
- Failed to load active execution intent: {e}
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/d42b13f22cb89e5e.
Report an issue: GitHub.