Skip to content

security: rust wallet signed transfers use incompatible signature format #2114

@createkr

Description

@createkr

Rust wallet signs incompatible message format — /wallet/transfer/signed signatures always fail server verification

Summary

The Rust wallet crate (rustchain-wallet/) signs transactions using a different message format than what the Python server (rustchain_v2_integrated_v2.2.1_rip200.py) expects for signature verification at POST /wallet/transfer/signed. This makes the Rust wallet completely non-functional — every signed transfer it produces will fail server-side verification.

Affected Components

  • Client: rustchain-wallet/src/transaction.rs (serialize_for_signing()), rustchain-wallet/src/client.rs (submit_transaction())
  • Server: node/rustchain_v2_integrated_v2.2.1_rip200.py (wallet_transfer_signed(), line ~6691)

Root Cause

Three layers of incompatibility between the Rust wallet's signing format and the server's verification format:

L1: Request payload field names (HTTP 400 — validation fails immediately)

Field Rust sends Server expects
Sender "from" "from_address"
Recipient "to" "to_address"
Amount "amount" (u64, smallest units) "amount_rtc" (f64, RTC units)

The server's validate_wallet_transfer_signed() in payout_preflight.py requires from_address, to_address, and amount_rtc. The Rust wallet sends from, to, and amount — causing immediate HTTP 400 rejection.

L2: Signed message field set (signature would never verify)

The Rust wallet signs 7 fields: from, to, amount, fee, nonce, timestamp, memo
The server reconstructs 5 fields: from, to, amount, memo, nonce

The fee and timestamp fields are included in the Rust-signed message but absent from the server's reconstruction, producing a different byte string → signature mismatch.

L3: Nonce type in signed message (signature would never verify)

  • Rust: nonce is u64 → JSON number: "nonce":1733420000000
  • Server: nonce is str(nonce_int) → JSON string: "nonce":"1733420000000"

L4: JSON encoding differences (signature would never verify)

  • Rust: serde_json::to_string() — default separators (", ", ": "), insertion-order keys
  • Server: json.dumps(sort_keys=True, separators=(",",":")) — compact separators, alphabetically sorted keys

Concrete Example

What the Rust wallet signs:

{"from":"RTCabc...","to":"RTCdef...","amount":1000000,"fee":1000,"nonce":1733420000000,"timestamp":1733420000,"memo":""}

What the server reconstructs for verification:

{"amount":1.0,"from":"RTCabc...","memo":"","nonce":"1733420000000","to":"RTCdef..."}

These are completely different byte strings. The Ed25519 signature will never verify.

Impact

  • Severity: High — the Rust wallet cannot submit any signed transfers to the Python node
  • Scope: All users of rustchain-wallet crate (CLI wallet, programmatic usage)
  • Other clients unaffected: Python CLI (rustchain_wallet_cli.py), JS light client (app.js), and SDK clients already use the correct canonical format

Reproduction

  1. Build the Rust wallet: cd rustchain-wallet && cargo build
  2. Generate a wallet, fund it, attempt a transfer
  3. The server returns HTTP 400 (missing required fields) or HTTP 401 (invalid signature)

Fix

The fix aligns the Rust wallet with the server's canonical format:

  1. transaction.rs: Replace serialize_for_signing() to produce the exact canonical JSON: {"amount":X.X,"from":"...","memo":"...","nonce":"...","to":"..."} with sorted keys, compact separators, string nonce, and RTC-unit amount
  2. client.rs: Change request payload field names from from/to/amount to from_address/to_address/amount_rtc, convert amount from smallest units to RTC units, serialize nonce as string
  3. Tests: Add 7 focused tests verifying canonical message format compatibility

Distinction from Prior Submissions

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions