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
version == 1(genesis era) or(version & 0xE0000000) == 0x20000000(version bits, §9.4). Serialized size ≤max_block_size(8,000,000).bitsdecodes to a positive target ≤pow_limit, and the AuxPoW is valid forblock_idwith its parent header hashing below the target (§9.6).timestamp ≤ local_time + 900(time-too-new; peers MUST NOT be penalised for this one).- At least one transaction. The first is a coinbase and no other transaction is.
The coinbase
lock_time == header.height. - Every transaction passes §5.1. No duplicate txids.
merkle_root = merkle(txids)andwitness_root = merkle(wtxids).
9.2 Contextual rules
height == parent.height + 1.bits == expected_bits(parent)(§10).timestamp > median(timestamps of parent and its up-to-10 ancestors).- 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.
- Coinbase outputs pass the scheme output rule, and
Σ coinbase outputs ≤ subsidy(h) + Σ fees. Unclaimed value is destroyed and accounted as burned. - 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 withbad-cb-filter-missingorbad-cb-filter. - 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 carrycommitment(state after block max(h − delay, 0) on this block's branch)(§9.7). Otherwise the block fails withbad-cb-utxo-missingorbad-cb-utxo. - 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
thresholdof the previous period's blocks signalled. Otherwise, onces ≥ timeout_height, it becomes FAILED, or LOCKED_IN iflock_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
lockbytes. Every non-ARRK asset id among those outputs, as"A" ‖ asset_id. - Filter: a Golomb-coded set with
P = 19andM = 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):
coinbase_index == 0[auxpow-not-coinbase],m ≤ 30[auxpow-chain-branch], and the coinbase is not exactly 64 bytes [auxpow-coinbase-size].- 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 equalparent_header[36:68][auxpow-merkle-root].
- In the scriptSig of the coinbase's first input:
reverse(root)MUST occur [auxpow-missing-root].- The magic
fa be 6d 6dMUST occur [auxpow-missing-header], exactly once [auxpow-multiple-headers], and immediately before the root [auxpow-header-position]. - 8 bytes MUST follow the root [
auxpow-missing-size]:size u32 ‖ nonce u32, withsize == 2^m[auxpow-size]. chain_index == expected_index(nonce, chain_id, m)[auxpow-wrong-index], whererand = nonce·1103515245 + 12345; rand += chain_id; rand = rand·1103515245 + 12345(all mod 2³²),index = rand mod 2^m.
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, zeroprevandmerkle = 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:
- checks every header like headers-first sync: links, AuxPoW,
bitsand median time; - rebuilds the LtHash from the items and requires it to equal both
lthashand the commitment listed for H; - 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:
- Signer indices are unique.
- A bad signature or an unknown index invalidates the message:
checkpoint-bad-signatureorcheckpoint-bad-signer, scored 100.
Merging and rotation:
- Signature sets for the same
(height, hash, next_set)merge. Atmvalid signatures the checkpoint is final. - A final checkpoint with
next_sethands 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:
txid(tx)folded up the branch equalsheaders[0].merkle_root[proof-bad-merkle].- Each
headers[i]hasprev_hash = hash(headers[i−1])andheight = headers[i−1].height + 1[proof-broken-headers]. - 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 itsnext_set[proof-bad-handover]. checkpointnames 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.