-
-
Notifications
You must be signed in to change notification settings - Fork 200
security: rust wallet signed transfers use incompatible signature format #2114
Description
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:
nonceisu64→ JSON number:"nonce":1733420000000 - Server:
nonceisstr(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-walletcrate (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
- Build the Rust wallet:
cd rustchain-wallet && cargo build - Generate a wallet, fund it, attempt a transfer
- 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:
transaction.rs: Replaceserialize_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 amountclient.rs: Change request payload field names fromfrom/to/amounttofrom_address/to_address/amount_rtc, convert amount from smallest units to RTC units, serialize nonce as string- Tests: Add 7 focused tests verifying canonical message format compatibility
Distinction from Prior Submissions
- Security: unsigned AttestationReport allows wallet tampering and replay #2055/security: sign AttestationReport critical fields #2056: AttestationReport signing gap (
/attest/submit) — different endpoint, different purpose - Security: MiningProof nonce replay allows proof reuse across blocks #2057/security: reject replayed MiningProof nonces #2058: MiningProof nonce replay — mining layer
- Security: BFT PRE-PREPARE messages bypass signature and freshness checks #2061/security: verify BFT PRE-PREPARE messages before accept #2062: BFT PRE-PREPARE auth bypass — consensus layer
- This finding: Rust wallet client↔server interoperability at
/wallet/transfer/signed— no overlap