3. Cryptography
3.1 Signature schemes
| Scheme id | Algorithm | Public key | Signature |
|---|---|---|---|
0x01 | ML-DSA-65 (FIPS 204) | 1952 B | 3309 B |
0x02 | ML-DSA-87 (FIPS 204) | 2592 B | 4627 B |
0x03 | FN-DSA-512 (Falcon, draft FIPS 206), provisional: regtest only | 897 B | 666 B |
0x10 | reserved: 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):compcodes 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, keepw < 61445, and takew 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) andPOLICY(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,-ton testnet and-ron regtest (other networks:-<hrp>). For example, MICO givesmico1…,mico-t1…andmico-r1…. - The ticker matches
[A-Z][A-Z0-9]{1,9}. The native tickersARRK,TARRKandRARRKgive 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 (
MIC0forMICO) 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).