Skip to content
LogoLogo

3. Cryptography

3.1 Signature schemes

Scheme idAlgorithmPublic keySignature
0x01ML-DSA-65 (FIPS 204)1952 B3309 B
0x02ML-DSA-87 (FIPS 204)2592 B4627 B
0x03FN-DSA-512 (Falcon, draft FIPS 206), provisional: regtest only897 B666 B
0x10reserved: SLH-DSA (FIPS 205)——

FN-DSA-512 cuts a key plus signature from 5.2 KB to about 1.5 KB, so payments become about 3× smaller.

Status. It is provisional:

  • FIPS 206 is not final, and its encodings may still change.
  • It is in the scheme schedule (§8) of regtest only. On testnet and mainnet it is absent, so outputs and spends using it fail (scheme-output-disallowed, scheme-retired).
  • Before any public activation, the encoding below must be re-checked against the published standard, and the vectors regenerated.

Encoding (as implemented by the Rust crate fn-dsa 0.4.0; vectors in core/vectors/fn_dsa.json):

  • raw public key (897 B) = 0x09 ‖ NTT(h): 512 values in [0, q), q = 12289, packed 14 bits each, little-endian. The key carries h in the NTT domain. The NTT is negacyclic with twiddle ψ^bitrev9(k), ψ = 49 = 7² (7 being a primitive 2048-th root of unity mod q), and outputs in bit-reversed order.

  • signature (666 B) = 0x39 ‖ nonce(40) ‖ comp(s2):

    • comp codes each coefficient as a sign bit, 7 low bits, then the rest in unary;
    • |s2ᵢ| ≤ 840, "−0" is invalid, and the padding must be zero.
  • message representative: mu = SHAKE256(SHAKE256(raw_pk)[0:64] ‖ 0x00 ‖ len(ctx) ‖ ctx ‖ msg)[0:64], with ctx = the context string below (pure mode, no pre-hash).

  • hash-to-point: c = HashToPoint(nonce, mu). Read SHAKE256(nonce ‖ mu) as 16-bit little-endian words, keep w < 61445, and take w mod q.

  • valid iff s1 = c − INTT(NTT(s2) ⊙ NTT(h)) (centred mod q) has every |s1ᵢ| ≤ 840, and ‖s1‖² + ‖s2‖² ≤ 34,034,726.

  • Private keys are 32-byte seeds. Key generation draws its randomness from SHAKE256("ARRK/fn-dsa/keygen" ‖ seed) (wallet standard, not consensus). Signing is randomised.

  • Serialized public key = scheme_id ‖ raw_public_key.

  • key_hash(pk) = TH("pubkey", serialized_pk).

  • All signatures use the context string "ARRK-TX-v1" (ML-DSA and FN-DSA both take one). Signing may be hedged (randomised); verifiers MUST NOT assume deterministic signatures.

  • Private keys are 32-byte ML-DSA seeds (FIPS 204 ξ).

Scheme bits (for multisig masks): ML-DSA-65 = 0x01, ML-DSA-87 = 0x02, FN-DSA-512 = 0x04.

3.2 Key derivation (wallet standard, not consensus)

  • master = HMAC-SHA3-512(key="ARRK master seed v1", data=seed), with a 16–64 byte seed. seed_part = master[0:32], chain = master[32:64].
  • child(i) = HMAC-SHA3-512(chain, 0x00 ‖ seed_part ‖ be32(i | 0x80000000)). All derivation is hardened.
  • Standard path: m/9000'/0'/account'/change'/index'.
  • Recovery phrase: BIP-39 English mnemonics. 24 words (256-bit entropy) is the default; 12–21 words are accepted. seed = PBKDF2-HMAC-SHA512(NFKD(words), "mnemonic" ‖ NFKD(passphrase), 2048 iterations, 64 bytes), which is then used as the master seed above. Wallets MUST reject unknown words and bad checksums. Vectors: core/vectors/wallet.json.

3.3 Addresses

  • bech32m(hrp, [lock_kind] ‖ convertbits(payload, 8→5)) per BIP-350. Only bech32m is accepted.
  • HRP: arrk (mainnet), tarrk (testnet), rarrk (regtest).
  • PUBKEY_HASH (kind 1, prefix …1p), MULTISIG_HASH (kind 2, prefix …1z) and POLICY (kind 3, prefix …1r) have addresses. Burn outputs and reserved kinds (16–127) never do.

3.3.1 Coin addresses (wallet standard, not consensus)

A wallet MAY show a lock under the name of the coin it holds, so that a MICO wallet reads mico1…. The data part is unchanged; only the HRP differs, and both forms pay the same lock.

  • Coin HRP: lower(ticker) ‖ mark, where mark is "" on mainnet, -t on testnet and -r on regtest (other networks: -<hrp>). For example, MICO gives mico1…, mico-t1… and mico-r1….
  • The ticker matches [A-Z][A-Z0-9]{1,9}. The native tickers ARRK, TARRK and RARRK give the plain address, and their lower-case forms are never coin HRPs.
  • Decoding for a network accepts its plain HRP (ticker none) or any coin HRP of that network (ticker = the upper-cased prefix without the mark). Everything else is the wrong network.
  • The ticker in an address is a label: the output's asset id decides what moves. Tickers are unique on chain (§7.1), but a look-alike ticker (MIC0 for MICO) is a different coin, so wallets SHOULD show the asset id and warn on look-alikes. A wallet sending coin X to an address tagged with a different ticker SHOULD refuse or warn.
  • Nodes accept coin addresses wherever an address is taken. Vectors: core/vectors/addresses.json.

3.5 Signed messages (wallet standard, not consensus)

digest = TH("message", varbytes(utf8(origin)) ‖ varbytes(message)) (message ≤ 64 KiB), signed with ML-DSA under context "ARRK-MSG-v1". The origin binds the signature to the requesting site, and the context keeps message signatures from ever verifying as transaction signatures. Vectors: core/vectors/wallet.json (messages).