CRYPTO.md — Signing Format & Address Derivation¶
Living reference for internal/crypto and internal/address. Tier-1 authority
remains docs/ARCHITECTURE.md §5.1; this file documents the concrete contracts
and the reasons behind them.
The signature format transform (why the reorder exists)¶
decred's ecdsa.SignCompact is not a drop-in for the TRON/Ethereum wire
format:
| Layout | Order | V encoding |
|---|---|---|
| decred output | [V | R | S] |
V = 27 + recoveryID |
| TRON expects | R || S || V |
V ∈ {0, 1} |
crypto.Sign performs exactly that reorder — copy c[1:] (R||S), then
sig[64] = c[0] - 27 — and it is the only call site of SignCompact in
the codebase (CLAUDE.md §3.2). java-tron tolerates legacy V ≥ 27 by
normalizing, but SunSail always emits the canonical {0, 1} form.
crypto.RecoverSigner accepts both when verifying foreign signatures.
Properties decred guarantees and SunSail asserts in tests (never assumes): deterministic RFC 6979 nonces and canonical lower-half S.
Defensive post-check¶
Before any signature leaves the package, Sign recovers the public key from
its own output and requires the recovered address to equal the signer's. The
payload passed to Sign must be SHA-256(raw_data) of the serialized
transaction — the txid.
Acceptance vectors¶
internal/crypto/testdata/vectors.json holds real java-tron-accepted
signatures: broadcast TRON mainnet transactions captured from public chain
data (raw_data hex, on-chain signature, owner address; one vector carries a
legacy V = 28). The recovery invariant test recomputes each txid as
SHA-256(raw_data) and requires RecoverCompact over the R||S||V signature to
reconstruct the owner's address. The vectors — not this document — are the
acceptance authority. On-chain acceptance itself is exercised by the
differential test (TestDifferential_SignatureAcceptedByNile).
Address derivation¶
Externally owned accounts: addr21 = 0x41 || keccak256(pubkey_uncompressed[1:])[12:]
— the 64 coordinate bytes are hashed, the 0x04 serialization prefix is not.
Keccak-256 is the legacy variant (x/crypto/sha3.NewLegacyKeccak256); the
standard library's crypto/sha3 implements final SHA-3 padding and produces
different digests — using it would derive wrong addresses everywhere.
Base58Check (internal/address, zero dependencies)¶
- Raw form: 21 bytes, mandatory
0x41first byte. checksum = SHA256(SHA256(addr21))[:4]; encoded form isBase58(addr21 || checksum)— always 34 characters starting withT.- The Bitcoin Base58 alphabet is used and leading zero bytes are preserved
as leading
'1'characters (the all-zero payload addressT9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwbexercises this). address.Parseauto-detects the representation (T…vs hex with optional0x).Address.EVM()returns the 20-byte payload without0x41— the form used inside ABI words and in the EVM-emulated CREATE2 preimage.- The round trip is fuzzed (
FuzzRoundTrip, committed corpus underinternal/address/testdata/fuzz/); active exploration runs viamake fuzz, whilemake checkreplays the committed corpus as ordinary tests.