Skip to content

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 0x41 first byte.
  • checksum = SHA256(SHA256(addr21))[:4]; encoded form is Base58(addr21 || checksum) — always 34 characters starting with T.
  • The Bitcoin Base58 alphabet is used and leading zero bytes are preserved as leading '1' characters (the all-zero payload address T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb exercises this).
  • address.Parse auto-detects the representation (T… vs hex with optional 0x). Address.EVM() returns the 20-byte payload without 0x41 — the form used inside ABI words and in the EVM-emulated CREATE2 preimage.
  • The round trip is fuzzed (FuzzRoundTrip, committed corpus under internal/address/testdata/fuzz/); active exploration runs via make fuzz, while make check replays the committed corpus as ordinary tests.