nautechsystems/nautilus_trader · error

signature_hex called before sign

Error message

signature_hex called before sign

What it means

`SignedAction::signature_hex` formats the ECDSA/EIP-712 signature as a `0x`-prefixed hex string, but the signature bytes are only populated by calling `sign` first. The struct stores `signature: Option<[u8; 65]>`, and reading it before signing panics. This is a documented API-ordering requirement (the doc comment states the panic condition).

Source

Thrown at crates/adapters/derive/src/signing/eip712.rs:235

            signer
                .sign_hash_sync(&typed_data_hash)
                .map_err(|e| TypedDataError::SigningFailed {
                    message: e.to_string(),
                })?;
        let bytes = signature.as_bytes();
        self.signature = Some(bytes);
        Ok(bytes)
    }

    /// Returns the signature as a `0x`-prefixed 130-character hex string.
    /// Panics if [`SignedAction::sign`] has not yet been called.
    ///
    /// # Panics
    ///
    /// Panics if [`SignedAction::sign`] has not been called.
    #[must_use]
    pub fn signature_hex(&self) -> String {
        let bytes = self.signature.expect("signature_hex called before sign");
        format!("0x{}", alloy_primitives::hex::encode(bytes))
    }

    /// Returns the signed action's subaccount id.
    #[must_use]
    pub const fn subaccount_id(&self) -> u64 {
        self.ctx.subaccount_id
    }

    /// Returns the signed action's nonce.
    #[must_use]
    pub const fn nonce(&self) -> u64 {
        self.ctx.nonce
    }

    /// Returns the signed action's session-key signer address.
    #[must_use]
    pub const fn signer_address(&self) -> Address {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Always call `sign(&private_key)` on the SignedAction before `signature_hex()`.
  2. Refactor to a typestate or a `sign` that returns a `SignedActionWithSignature` so unsigned actions cannot expose a signature.
  3. If only the action payload is needed (hashing/serialization), use methods that don't require the signature.
  4. Add an assertion or debug check before use to catch the ordering mistake early.

Example fix

// before
let action = SignedAction { action, signature: None, .. };
let sig = action.signature_hex(); // panics
// after
let mut action = SignedAction { action, signature: None, .. };
action.sign(&private_key)?;
let sig = action.signature_hex();
Defensive patterns

Strategy: validation

Validate before calling

debug_assert!(action.signature.is_some(), "call sign() before signature_hex()");
if action.signature.is_none() {
    return Err(/* signature not yet computed */);
}

Prevention

When it happens

Trigger: Building a `SignedAction` (e.g. via the action-construction helpers) and calling `signature_hex()` before invoking `sign(private_key)`; also when `sign` fails silently or is skipped on a code path (e.g. early return, conditional signing).

Common situations: Developers wiring up Derive authentication for the first time forget the sign step; test harnesses constructing actions for serialization checks call signature_hex on unsigned actions; refactors move the sign call after logging code that reads the signature.

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


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