Skip to content
LogoLogo

9. Blocks

header (124 B) = version u32 | height u32 | prev_hash 32 | merkle_root 32 | witness_root 32 |
                 timestamp u64 | bits u32 | nonce u64
full header    = header ‖ auxpow                          (§9.6; in blocks, HEADERS, CMPCTBLOCK)
block          = full header ‖ varint n ‖ n × transaction (full serialization)
block_id       = TH("block", header)                      (the AuxPoW commits to it)

nonce is a free field: miners may use it as an extra nonce. Proof of work is the AuxPoW.

9.1 Context-free rules

  1. version == 1 (genesis era) or (version & 0xE0000000) == 0x20000000 (version bits, §9.4). Serialized size ≤ max_block_size (8,000,000).
  2. bits decodes to a positive target ≤ pow_limit, and the AuxPoW is valid for block_id with its parent header hashing below the target (§9.6).
  3. timestamp ≤ local_time + 900 (time-too-new; peers MUST NOT be penalised for this one).
  4. At least one transaction. The first is a coinbase and no other transaction is. The coinbase lock_time == header.height.
  5. Every transaction passes §5.1. No duplicate txids.
  6. merkle_root = merkle(txids) and witness_root = merkle(wtxids).

9.2 Contextual rules

  1. height == parent.height + 1.
  2. bits == expected_bits(parent) (§10).
  3. timestamp > median(timestamps of parent and its up-to-10 ancestors).
  4. Transactions are applied in order. Each non-coinbase transaction passes §6 against the state left by the previous ones, so a transaction may spend outputs created earlier in the same block, but not later ones.
  5. Coinbase outputs pass the scheme output rule, and Σ coinbase outputs ≤ subsidy(h) + Σ fees. Unclaimed value is destroyed and accounted as burned.
  6. Filter commitment (height ≥ 1): the coinbase's last burn output with a 36-byte payload starting with "ARKF" MUST carry the block's filter header (§9.5). Otherwise the block fails with bad-cb-filter-missing or bad-cb-filter.
  7. State commitment (height ≥ 1, checked before any transaction is applied): the coinbase's last burn output with a 36-byte payload starting with "ARKU" MUST carry commitment(state after block max(h − delay, 0) on this block's branch) (§9.7). Otherwise the block fails with bad-cb-utxo-missing or bad-cb-utxo.
  8. Finality (§9.8): a block at a height the node holds as final with a different hash is rejected (checkpoint-conflict, not penalised).

9.3 Fork choice

The valid chain with the most cumulative work, where work(bits) = 2²⁵⁶ / (target + 1). On equal work the first-seen chain is kept. Switching chains is atomic: if any block on the new branch fails §9.2, that block and its descendants are marked invalid and the node stays on its current chain.

9.4 Soft-fork deployments (version bits)

Each network lists deployments: (name, bit 0–28, start_height, timeout_height, period, threshold, min_activation_height, lock_in_on_timeout). A block signals for a deployment if (version & 0xE0000000) == 0x20000000 and the bit is set; version-1 headers signal nothing.

The state is fixed per period and is computed from the block's own ancestry. At each period start s:

  • DEFINED becomes STARTED if s ≥ start_height.
  • STARTED becomes LOCKED_IN if at least threshold of the previous period's blocks signalled. Otherwise, once s ≥ timeout_height, it becomes FAILED, or LOCKED_IN if lock_in_on_timeout.
  • LOCKED_IN becomes ACTIVE if s ≥ min_activation_height.

The rules a deployment defines (for example a reserved lock kind's spending rules) apply to blocks whose state is ACTIVE. Miners signal every STARTED or LOCKED_IN deployment. Current deployments: none on mainnet or testnet. Regtest has testdummy (bit 28, start 0, timeout 1,000,000, period 20, threshold 15), which exercises the mechanism.

9.5 Compact block filters

  • Items: every non-burn output lock in the block (coinbase included) and every spent output's lock, as lock bytes. Every non-ARRK asset id among those outputs, as "A" ‖ asset_id.
  • Filter: a Golomb-coded set with P = 19 and M = 784,931:
    key     = parent_hash[0:16]                        (the block's own hash would be circular)
    map(x)  = (int_be(TH("filter/item", key ‖ x)[0:8]) × N × M) >> 64      (N = #distinct items)
    filter  = varint N ‖ Golomb–Rice(sorted mapped values as deltas; quotient unary, P-bit remainder,
              MSB-first, zero-padded)                  (N = 0: just varint 0)
    
  • Headers: filter_header(b) = TH("filter/header", TH("filter", filter_b) ‖ filter_header(parent)), with genesis's parent header being 32 zero bytes.
  • Commitment: the commitment output is a burn, so it is excluded from the filter, which keeps it non-circular. A light client checks that a filter matches its header, and that the header is committed in a coinbase covered by the block's proof of work.

9.6 Merged mining (AuxPoW)

Every block proves its work with an AuxPoW in Namecoin's format. That lets existing merged-mining pools (Namecoin, Dogecoin, Syscoin) add Arrekta through createauxblock / submitauxblock (§14) without software changes, and lets Bitcoin hashrate secure Arrekta at no extra cost to miners.

9.6.1 Encoding (CompactSize as in §1; integers little-endian):

auxpow = coinbase_tx (a Bitcoin transaction, non-witness serialization, ≤ 32,768 B)
       ‖ parent_hash 32                (ignored)
       ‖ varint n (≤ 32) ‖ n × hash32 ‖ i32 coinbase_index
       ‖ varint m (≤ 30) ‖ m × hash32 ‖ i32 chain_index
       ‖ parent_header 80              (Bitcoin header: version, prev, merkle, time u32, bits u32, nonce u32)

9.6.2 Validity for block id B (error codes in brackets):

  1. coinbase_index == 0 [auxpow-not-coinbase], m ≤ 30 [auxpow-chain-branch], and the coinbase is not exactly 64 bytes [auxpow-coinbase-size].
  2. Merkle steps use SHA256d(left ‖ right); the low bit of the index picks the side.
    • root = merkle(B, chain_branch, chain_index).
    • merkle(SHA256d(coinbase_tx), coinbase_branch, 0) MUST equal parent_header[36:68] [auxpow-merkle-root].
  3. In the scriptSig of the coinbase's first input:
  4. int_le(SHA256d(parent_header)) ≤ target(bits) [high-hash]. The parent's own bits are irrelevant.

chain_id = 0x0D07 on every network. Its low bits differ from Namecoin (1), Dogecoin (98) and Syscoin (16), so the weak slot function never collides with them. Merged-mining RPC values (hash, _target) use Bitcoin's reversed byte order.

9.6.3 Solo mining uses a standalone parent:

  • a coinbase with one null input whose scriptSig is fa be 6d 6d ‖ reverse(B) ‖ 01000000 ‖ 00000000 ‖ extra_nonce;
  • one empty output;
  • a parent header with version 0x20000000, zero prev and merkle = txid, iterating its nonce.

There is one proof-of-work rule. Genesis blocks use this form, with the parent nonce recorded as the network's genesis_nonce.

9.7 Chain-state commitment

The consensus state is the UTXO set, the asset registry and the freeze list. Each entry is a state item:

U ‖ txid 32 ‖ vout u32 ‖ height u32 ‖ coinbase u8 ‖ txout
A ‖ asset_id 32 ‖ var ticker ‖ var name ‖ u8 decimals ‖ u64 max_supply ‖ u8 flags (1 mint, 2 freeze, 4 clawback)
  ‖ metadata_hash 32 ‖ u64 issued ‖ u64 burned ‖ issue_txid 32 ‖ u32 issue_height ‖ opt authority ‖ opt control
F ‖ asset_id 32 ‖ lock ‖ u32 height                         (opt = 00, or 01 ‖ 32 bytes)

LtHash16:

  • expand(x) = SHAKE256("ARRK/lthash16" ‖ x) gives 2048 bytes, read as 1024 little-endian u16 lanes.
  • The state hash is the lane-wise sum (mod 2¹⁶) of expand(item) over all items; removing an item subtracts it.
  • commitment = TH("utxo/commitment", lanes as 2048 bytes).

The hash is order-independent and updated per change. It rests on a lattice assumption (Lewi et al., ePrint 2019/227, as used by Solana's SIMD-0215), not on discrete logarithms like MuHash, so it stays sound against quantum computers.

Delay: block h commits to the state after block max(h − delay, 0), with delay 100 on mainnet/testnet and 10 on regtest. Miners commit to settled state, side branches validate normally, and a snapshot taken at H is proven once H + delay is mined.

9.7.2 Snapshots (arrekta-node snapshot export|import FILE):

"ARKS" ‖ u8 1 ‖ magic 4 ‖ u32 H ‖ tip 32 ‖ lthash 2048 ‖ varint (H+1) × var full_header ‖
varint k × (u32 height ‖ commitment 32)   (heights H−delay+1 … H) ‖ filter_header(H) 32 ‖
varint m × var state_item ‖ TH("snapshot", all previous bytes)

Import (into an empty data directory only) does the following:

  1. checks every header like headers-first sync: links, AuxPoW, bits and median time;
  2. rebuilds the LtHash from the items and requires it to equal both lthash and the commitment listed for H;
  3. makes H final (§9.8), since there is no undo data below it.

Block H + delay then proves the snapshot by proof of work: it must commit to the state after H, so a self-consistent forgery fails there. Item order is not significant.

9.8 Signed checkpoints (finality)

A network MAY define a checkpoint signer group: threshold m of up to 32 post-quantum keys. Regtest's group is 2-of-3, derived from the public seeds 0x31…, 0x32…, 0x33… with ExtendedSeed.master(seed).private_key(). Mainnet and testnet have none until the genesis ceremony. The design follows Dash ChainLocks (DIP-0008).

checkpoint = u8 1 ‖ u32 height ‖ block_hash 32 ‖ u8 has_next ‖ [var next_set] ‖ varint k (≤32) ×
             (u8 signer_index ‖ var signature)
message    = TH("checkpoint", magic ‖ u32 height ‖ block_hash ‖ u8 has_next ‖ [var next_set])
signer_set = u8 m ‖ varint n ‖ n × var serialized_pubkey          (signature context "ARRK-CHECKPOINT-v1")

Validation:

Merging and rotation:

  • Signature sets for the same (height, hash, next_set) merge. At m valid signatures the checkpoint is final.
  • A final checkpoint with next_set hands over to that group for all later heights. That is rotation without a software release.

Finality floor (also set by a snapshot base). When a block becomes final at height h:

  • blocks at h with another hash, and their descendants, become invalid;
  • if the active chain conflicts, the node disconnects back below h and follows the best valid chain, even one with less work;
  • no reorganisation crosses the floor.

Checkpoints are policy-free consensus inputs. A node that never sees one still follows the most-work chain; a node that does see one cannot be reorganised below it by rented hashpower.

Signer role (ARREKTA_CHECKPOINT_SEED, interval 10, depth 6): a signer signs the active block at each multiple of the interval once it is depth deep. First seen wins, and a signer never signs two hashes at one height.

9.9 Finality proofs (interop standard, not consensus)

A self-contained proof that a transaction is in a block made final by a checkpoint, checked against a trusted signer set alone (docs/INTEROP.md §2). Nodes serve them at GET /v1/txs/{txid}/proof.

proof = u8 1 ‖ var tx (full serialization) ‖ varint b (≤ 32) × (u8 side ‖ sibling 32)
        ‖ varint n (1..4096) × header 124 ‖ varint r (≤ 64) × var handover ‖ var checkpoint

side is 1 when the sibling is on the left (node = TH("merkle/node", left ‖ right), §2). Decoders reject trailing bytes, bad sides and unknown versions.

Verification, given a trusted signer_set and the network magic:

  1. txid(tx) folded up the branch equals headers[0].merkle_root [proof-bad-merkle].
  2. Each headers[i] has prev_hash = hash(headers[i−1]) and height = headers[i−1].height + 1 [proof-broken-headers].
  3. Hand-overs, in order: each has a next_set, heights strictly ascend and stay below the checkpoint's height, and each is final (§9.8) under the current set, which then becomes its next_set [proof-bad-handover].
  4. checkpoint names the last header (height and hash) [proof-checkpoint-mismatch] and is final under the current set [proof-not-final]; bad signatures fail as in §9.8.

The proof establishes (txid, height, block_hash, checkpoint_height, signer_set_id). Proof of work is not part of it. Nodes build proofs from the lowest final checkpoint at or above the block on the active chain, and include every hand-over below it.