no_std-compatible Rust toolkit for multi-chain transaction signing — twelve networks, zero hand-written cryptography, cross-implementation KATs.
Signer composes thin wrappers around k256 (secp256k1 ECDSA and BIP-340 Schnorr) and ed25519-dalek into a capability-driven trait surface for Aptos, Bitcoin, Ethereum, Solana, Cosmos, Tron, Sui, TON, Filecoin, Spark, XRP Ledger, and Nostr. Every library crate builds under no_std + alloc; private keys wrap in ZeroizeOnDrop, Debug prints [REDACTED], and every chain-specific output is pinned against the relevant RFC / BIP / EIP vectors plus an independent @noble/curves reference.
See also
kobe— the companion HD-wallet derivation toolkit that feedsSigner::from_derivedwith BIP-39 / BIP-32 / SLIP-10 accounts.
Shell (macOS / Linux):
curl -fsSL https://sh.qntx.fun/signer | shPowerShell (Windows):
irm https://sh.qntx.fun/signer/ps | iexsigner evm sign-message -k "0x4c0883a6.." -m "Hello, Ethereum!" # EIP-191
signer btc sign-message -k "4c0883a6..." -m "Hello, Bitcoin!" # BIP-137
signer sui sign-tx -k "9d61b19d..." -t "0000..." # BLAKE2b intent
signer cosmos sign-tx -k "4c0883a6..." -t "<SignDoc hex>" # ADR-036 input
signer xrpl sign-tx -k "4c0883a6..." -t "<tx fields hex>" # STX\0 + SHA-512/2 + DER
signer nostr sign-hash -k "nsec10allq0g..." -x "5e6ea04f..." # NIP-19 accepted
signer evm address -k "0x4c0883a6..." # EIP-55 checksummed
signer --json evm sign-message -k "0x4c0883a6..." -m "test" # agent-friendlyuse signer_evm::{Sign as _, SignMessage as _, Signer};
let signer = Signer::from_hex(
"4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318",
)?;
let raw = signer.sign_hash(&[0x42u8; 32])?; // v = 0 | 1 (raw parity, feeds RLP)
let msg = signer.sign_message(b"hello")?; // v = 27 | 28 (EIP-191 wire)
println!("Address: {}", signer.address());
println!("Signature: {}", msg.to_hex());SignOutput is a discriminated enum (Ecdsa { signature, v } / EcdsaDer(Vec<u8>) / Ed25519([u8; 64]) / Ed25519WithPubkey { signature, public_key } / Schnorr { signature, xonly_public_key }) — callers pattern-match on the variant that matches their chain's wire format instead of juggling Option metadata.
Chains without a canonical off-chain scheme deliberately do not implement SignMessage; callers build the domain-specific preimage themselves and pass it through the chain's inherent Signer::sign_transaction method (not a trait method — transaction-bytes semantics differ irreconcilably across chains, so Sign only contains sign_hash):
// Cosmos: build the ADR-036 `StdSignDoc` externally (e.g. via `kobe cosmos`)
// and hand its canonical bytes to `sign_transaction`.
use signer_cosmos::Signer;
let signer = Signer::from_hex("4c0883a6...")?;
let sign_doc = build_adr036_sign_doc("cosmos1...", b"hello");
let signature = signer.sign_transaction(sign_doc.as_bytes())?;Bitcoin message signing picks the BIP-137 header byte for the target address type; the default matches Bitcoin Core's signmessage:
use signer_btc::{BitcoinMessageAddressType, SignMessage as _, Signer};
let signer = Signer::from_hex("4c0883a6...")?;
// Default: compressed-P2PKH header (v = 31 | 32), Bitcoin Core compatible.
let compressed = signer.sign_message(b"Hi")?;
// Explicit: native SegWit (bech32) header, v = 39 | 40.
let bech32 = signer.sign_message_with(
BitcoinMessageAddressType::SegwitBech32,
b"Hi",
)?;Enable the kobe feature to construct signers from kobe derived accounts:
use kobe::Wallet;
use kobe_evm::Deriver;
use signer_evm::Signer;
let wallet = Wallet::from_mnemonic("abandon abandon ... about", None)?;
let account = Deriver::new(&wallet).derive(0)?;
let signer = Signer::from_derived(&account)?;
println!("Address: {}", signer.address());| Chain | Crate | Curve | Sighash | Off-chain message |
|---|---|---|---|---|
| Bitcoin | signer-btc |
secp256k1 | double-SHA-256 | BIP-137 (four header variants) |
| Ethereum | signer-evm |
secp256k1 | Keccak-256 | EIP-191, EIP-712 |
| Cosmos | signer-cosmos |
secp256k1 | SHA-256 | ADR-036 StdSignDoc (external) |
| Tron | signer-tron |
secp256k1 | SHA-256 (raw_data txID) |
TRON prefix, wire v = 27/28 |
| Filecoin | signer-fil |
secp256k1 | BLAKE2b-256 over CID bytes | BLAKE2b-256 |
| Spark | signer-spark |
secp256k1 | double-SHA-256 | BIP-137 (compressed P2PKH) |
| TON | signer-ton |
Ed25519 | raw | raw (caller builds the preimage) |
| XRP Ledger | signer-xrpl |
secp256k1 | STX\0 + SHA-512-half, DER |
none (no canonical spec) |
| Solana | signer-svm |
Ed25519 | raw | raw Ed25519 |
| Sui | signer-sui |
Ed25519 | BLAKE2b-256 intent + BCS | PersonalMessage intent |
| Aptos | signer-aptos |
Ed25519 | SHA3-256 domain + BCS | raw Ed25519 |
| Nostr | signer-nostr |
Schnorr BIP-340 | SHA-256 (NIP-01 event id) | raw BIP-340 (caller frames it) |
- 12 chains — Aptos, Bitcoin, Ethereum, Solana, Cosmos, Tron, Sui, TON, Filecoin, Spark, XRP Ledger, Nostr
- Zero hand-written cryptography —
k256for secp256k1 ECDSA and BIP-340 Schnorr,ed25519-dalekfor Ed25519; hashing viasha2/sha3/blake2/ripemd; encoding viabech32/bs58 - Capability-split traits — mandatory
Sign::sign_hashas the primitive-level interface (32 bytes in,SignOutputout) plus opt-inSignMessage,ExtractSignableBytes,EncodeSignedTransaction; each chain's protocol-levelsign_transactionis an inherent method on itsSigner(transaction-bytes semantics differ irreconcilably across chains, so a trait would be a false abstraction). Capability gaps surface at compile time, not a runtimeErr. - Type-safe digests —
sign_hashtakes&[u8; 32];verify_prehash*dispatches strictly on wire length (64-byte compact, 65-byte recoverable, DER) - Discriminated
SignOutput—Ecdsa { signature, v }/EcdsaDer(Vec<u8>)/Ed25519([u8; 64])/Ed25519WithPubkey/Schnorr, withvbyte semantics fully documented per producer (raw parity, EIP-191, BIP-137 four ranges) - Cross-implementation KATs — RFC 6979 deterministic ECDSA, RFC 8032 Test Vectors 1–3, BIP-340
test-vectors.csvindices 0–3/5/6, EIP-712 "Mail" example (be609aee…30957bd2), BIP-137 four-variant header, plusecrecover/verifymessage/ intent-digest round-trips — no self-confirming dumps - Standard error contract —
Sign::Error: core::error::Error + From<SignError> + Send + Sync + 'static, interoperating with?,Box<dyn Error>, andthiserrorout of the box - Fallible entropy — every signer exposes
try_random() -> Result<Self, SignError>; the panickingrandom()is a thin wrapper for std environments no_std+alloc— every library crate compiles onthumbv7m-none-eabiunder CI; embedded / WASM ready- Security hardened —
ZeroizeOnDrop,Debugredacted to[REDACTED],Cloneintentionally removed,Send + Syncrequired - Kobe integration — optional HD wallet bridging via the
kobefeature flag (feedsSigner::from_derivedwith kobe derived accounts) - Strict linting — Clippy
pedantic+nursery+correctness(deny),rust_2018_idiomsdeny, zero warnings on nightly
See crates/README.md for the full crate table, dependency graph, and feature flag reference.
This library has not been independently audited. Use at your own risk.
- Private keys wrapped in
zeroize— wiped from memory on drop Debugimpl prints[REDACTED]— no key material reaches logsCloneintentionally removed — prevents uncontrolled key duplication- Random generation uses OS-provided CSPRNG via
getrandom; prefertry_random()on embedded / WASM targets where entropy can legitimately fail SignandSignMessagerequireSend + Sync— safe to share across async executors- No key material is logged or persisted by the workspace
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT License (LICENSE-MIT or https://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project shall be dual-licensed as above, without any additional terms or conditions.