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

  1. Re-sign the transaction with a signer that performs EIP-2 low-s normalization (all mainstream libraries do)
  2. Do not manually normalize and persist — normalize_s() output must be re-hashed/re-verified as a new signature
  3. Check whether stored bytes were mutated (compare against original persisted blob/hashes)
  4. 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

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


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/d42b13f22cb89e5e. Report an issue: GitHub.