Astreum

BLOCKCHAIN CONSENSUS

Consensus

The consensus layer governs block production, chain validation, fork resolution, and the treasury. It uses a validator-scheduled model with adjustable difficulty and automatic fork tracking.

Validator Scheduling

  • Validators are registered with a stake in the treasury trie via TREASURY_DEPOSIT. The current validator for a given block height is selected by stake-weighted random choice: all staked validators' public keys are collected from the treasury trie, a PRNG is seeded with the block hash, and a validator is drawn proportionally to its stake.
  • A node is eligible to create blocks only if it has a configured validation_secret_key. When the selection algorithm picks this node's public key, the node begins block production.
  • The validation worker thread runs continuously: it checks the head state to determine the current validator for the next block, and if the local node is the scheduled validator, it creates and signs a new block.

Block Creation

  • When the node is the scheduled validator, it collects pending transactions from its incoming queue, applies them to the current state, and builds a new block.
  • Each transaction is applied in order: balance transfers, code execution, account storage operations, and fee collection. The resulting state changes are recorded in the accounts trie.
  • After applying all transactions, the validator computes the new accounts trie root hash, signs the block header with its Ed25519 validation key, and stores the block.
  • The block is then advertised to peers via the storage advertisement system.

Difficulty Adjustment

  • Difficulty targets 2 second block spacing (configurable). The algorithm is simple: each block compares its timestamp to the previous block's timestamp.
  • If the spacing is ≤ 1 s, difficulty increases by 1 (blocks are too fast).
  • If the spacing exceeds the target (2 s), difficulty decreases by 1 (blocks are too slow).
  • If the spacing is exactly at the target, difficulty stays the same.
  • Difficulty is clamped to a minimum of 1. The base difficulty is the previous block's difficulty.

Nonce and Proof-of-Work

  • To create a valid block, the validator must find a nonce such that the BLAKE3 hash of the block expression has at least difficulty leading zero bits.
  • The nonce search is a simple increment loop starting from 0. For each candidate nonce, the block is re-serialized and re-hashed until the difficulty target is met.
  • The block's expr_id is set to the hash that meets the target. Both the block hash and the nonce are stored.

Genesis Block

  • When a node starts with no latest_block, and no latest_block_hash is configured, the validation worker creates a genesis block automatically.
  • The genesis block has height 0, previous_block_hash = ZERO32 (all zeros), and a default difficulty of 1.
  • The genesis block establishes the initial accounts trie and the chain identity (chain_id).

Fork Resolution

  • The fork verification worker runs in a background thread. It monitors peer-reported fork heads and verifies each one.
  • Light pass (header verification) — walks the block headers backward from the fork tip, verifying signatures and structural integrity.
  • Heavy pass (transaction verification) — re-applies every transaction in every block along the fork, verifying state transitions.
  • Anchor detection — the walker identifies where the fork connects to known chain state: a shared fork head, an intersection with another fork, root/genesis, or a known verified anchor.
  • Self-validation skip — blocks the local node itself created are skipped in the heavy pass (the node already validated them during creation).
  • If a block fails verification, malicious_block_hash is set and the fork is rejected. If verification succeeds, the fork is stored in node.forks with its anchor and chain fork position.

Transaction Lifecycle

  1. Creation — a client constructs a Transaction object, sets the fields, signs it with the sender's Ed25519 private key, and serializes it.
  2. Submission — the client sends the serialized transaction as a TRANSACTION (topic 5) P2P message. It propagates through the network.
  3. Inclusion — the current validator collects pending transactions and includes them in a new block. Each transaction's counter must match the sender's account counter (replay protection).
  4. Application — within the block, each transaction is applied sequentially: the amount is transferred, the cost limit is consumed, the code is executed in the VM, and account state is updated.
  5. Receipt — after execution, a Receipt is generated recording fees, status, logs, and any minted tokens.
  6. Verification — other nodes verify the block by re-applying all transactions and checking that receipts and state transitions match.

Treasury

The treasury is a built-in account at the hardcoded address 0x0101...01 (32 bytes of 0x01). It manages validator stakes and issues secured loans. Four transaction codes target the treasury:

TREASURY_DEPOSIT (0x20)

  • Deposit stake into the treasury. The amount is transferred from the sender to the treasury account.
  • The sender's TreasuryUserRecord is stored in the treasury's data trie keyed by the sender's public key. The record tracks balance (total stake deposited), loans_root_hash (root of the loans radix trie), and total_interest_paid.
  • recipient must be the treasury address. If the sender has no existing record, the deposit fails.

TREASURY_BORROW (0x21)

  • Borrow tokens against the sender's stake as a secured loan. Sets amount to 0 (the requested amount is encoded in the data payload).
  • The data payload is an 18-byte borrow request encoding: version (1 byte) | loan_type (1 byte) | payment_interval_blocks (8 bytes LE) | payment_count (8 bytes LE).
  • The actual disbursed amount is a discounted amount calculated from the windowed rate fraction and the payment schedule. The treasury's balance is debited by this discounted amount and credited to the sender.
  • A TreasuryLoanRecord is created with fields: creation block, loan type, discounted amount, payment amount, payment interval, next payment block, and payment count. The loan is stored in the sender's loans trie keyed by the borrow transaction hash.
  • The sender's total staked balance must exceed the scheduled total repayments (payment_amount × payment_count) plus any existing loan obligations.

TREASURY_REPAY (0x22)

  • Make one or more payments on an existing loan. recipient must be the treasury address.
  • The data field contains the 32-byte transaction hash of the borrow transaction being repaid. The amount must be a multiple of the loan's payment_amount.
  • Each full payment amount advances the next_payment_block_number by one interval. Interest is calculated as the difference between the discounted amount and the sum of payment amounts paid so far, prorated across remaining payments.
  • The repaid amount is credited to the treasury's balance.

TREASURY_CLOSE (0x23)

  • Close a treasury position and reclaim the sender's staked balance.
  • Requires that all outstanding loans be fully repaid before the position can be closed.

Key structures

  • TreasuryUserRecord — 3-field expression: (balance loans_root_hash total_interest_paid). Stored in the treasury's data trie keyed by sender public key.
  • TreasuryLoanRecord — 7-field expression: (creation_block_number payment_interval_blocks payment_amount next_payment_block_number loan_type payment_count discounted_amount). Stored in the sender's loans trie keyed by the borrow transaction hash.
  • Windowed rate — the discount rate is computed from era-level statistics using a windowed fraction. The rate numerator and denominator are looked up based on the loan's duration, enabling time-adjusted discounting.

Fees

  • Transaction fee — base fee per transaction, charged to the sender.
  • Storage fee — fee for data written to on-chain account storage (acc.put) or transaction logs (tx.log). Based on the size of the stored data.
  • Data fee — fee proportional to the transaction's data payload size.
  • Execution fee — fee based on VM meter consumption during code execution.
  • Cost limit — the sender sets a maximum total meter cost. If the combined fees exceed this limit, the transaction fails.
  • All fees are denominated in the chain's native token. The total fees for a block are recorded in total_transaction_fee and total_storage_fee.

Minting

  • New tokens are minted as storage mining rewards through the STORAGE_PAYMENT transaction. When a storage contract's mint flag is set, the payout is credited to the storage provider's balance as newly created supply.
  • Each block records its total_mint — the sum of newly created tokens in that block.
  • The cumulative mint across all blocks is tracked in the block's statistics field alongside cumulative fees and stakes.

Payment Channels

Payment channels enable off-chain value transfer between two parties. Each channel is a Channel expression stored in the account's channels radix trie, keyed by the counterparty's public key. Three transaction codes manage channels:

Channel model

  • A channel has 3 fields: balance (current channel balance), counter (monotonic version counter for replay protection), and withdrawal_window (minimum block delay before unilateral withdrawal is allowed).
  • Serializes as a 3-element link chain: (balance counter withdrawal_window).

CHANNEL_UPDATE (0x10)

  • Update an existing channel's state. recipient must equal sender (the channel is stored in the sender's account).
  • The data payload is 32 bytes (counterparty public key) optionally followed by 8 bytes (new withdrawal window). The counter is incremented by 1 and the amount is added to the channel balance.
  • The withdrawal window can only be increased, never decreased. This prevents a counterparty from shortening the timeout.

CHANNEL_WITHDRAW (0x11)

  • Unilaterally withdraw funds from a channel. Sets amount to 0 (the requested withdrawal amount is encoded in the data payload).
  • The data payload is 73 bytes: op(1) | counter(8) | amount(8) | signature(64). The signature is produced by the counterparty over a message containing (chain_id payer recipient counter amount) and verified with the counterparty's Ed25519 public key.
  • The withdrawal amount is debited from the channel balance and credited to the sender. The remainder stays in the channel.

CHANNEL_CLOSE (0x12)

  • Close a channel and settle final balances. recipient must equal sender.
  • The remaining channel balance is returned to the sender's main account balance.