CRYPTARCHIA-PROTOCOL

FieldValue
NameCryptarchia Protocol
Slug92
Statusraw
CategoryStandards Track
EditorDavid Rusu [email protected]
ContributorsÁlvaro Castro-Castilla [email protected], Giacomo Pasini [email protected], Thomas Lavaur [email protected], Mehmet [email protected], Marcin Pawlowski [email protected], Daniel Sanchez Quiros [email protected], Youngjoon Lee [email protected], Filip Dimitrijevic [email protected]

Timeline

  • 2026-08-315fa8ee4 — docs(blockchain): compress the block proposal with 16-byte transaction reference prefixes (#389)
  • 2026-08-2748fbc82 — docs(blockchain): order channel configurations and prevent their replay (#396)
  • 2026-08-265a9bee0 — docs(blockchain): make execution steps clearer (#417)
  • 2026-08-265e42374 — docs(blockchain): specify uint32 for epoch number (#420)
  • 2026-08-19a2a85cb — RFC Bedrock: Cryptarchia with uncle references (#385)
  • 2026-07-27628684e — docs(blockchain): fix CHANNEL_DEPOSIT execution, block limits and storage market formatting (#381)
  • 2026-07-205b20a2d — docs(blockchain) Channel Participation in PoS (#364)
  • 2026-07-03709cf7f — Bedrock-RFC: Remove Concept of a Session (#365)
  • 2026-05-2967e498e — chore: fix math issues (#350)
  • 2026-05-28d45eed2 — Chore: mirror blochain specs into github/mdbook (#347)

Revision History

VersionChangesDate
1.0.0Initial revision.2026-01-20
1.0.1Replaced Logos Blockchain name with Logos Blockchain2026-04-17
1.0.2Added details for block root computation2026-05-26
1.1.0Precise and make clearer that the max block size is the max body size, and fix the verification of the number of transaction per block to be <= 10242026-07-27
1.1.1Corrected MAX_BLOCK_SIZE to 2 MiB, to match the implementation.2026-08-05
1.2.0Added uncle references: a block carries the signed headers of the fork blocks it references, every carried entry must be valid for the block itself to be valid, and the Total Stake Inference counts their slots. Replaced block_root with body_root, which commits to those headers as well as to the transactions.2026-08-06
1.2.1Defined EpochNumber, the type of an epoch number, as a 32-bit unsigned integer encoded little-endian wherever it is serialized or hashed.2026-08-25
1.2.2Precise, in Chain Maintenance, that the execution layer validates the block by applying its transactions in the order they appear starting from the parent block's execution state.2026-08-25
1.2.3Update the test vectors to reflect the parent added in the CHANNEL_CONFIG payload2026-08-27
1.2.4Reordered the ProofOfLeadership fields to the wire order defined by the Canonical Encoding, and noted that the Block ID preimage absorbs the same fields in a different order by design. The Header layout and the block_id preimage are otherwise unchanged.2026-08-28

Introduction

Cryptarchia is the consensus protocol of the Logos Blockchain’s Bedrock layer. This document specifies how Bedrock comes to agreement to a single history of blocks.

The values that Cryptarchia optimizes for are resilience and privacy. These come at the cost of block times and finality. These values have significant implications on user experience and we should understand them well.

Resilience

In consensus, we are presented with a choice of prioritizing either safety or liveness in the presence of catastrophic failure (this is a re-formalization of the CAP theorem). Choosing safety means the chain never forks, instead the chain halts until the network heals. On the other hand, choosing liveness (a la Bitcoin/Ethereum) means that block production continues but finality will stall, leading to confusion around which blocks are on the honest chain.

On the surface both options seem to provide similar guarantees. If finality is delayed indefinitely, is this not equivalent to a halted chain? The differences come down to how safety or liveness is implemented.

Prioritizing Safety

Chains that provide a safety guarantee do so using quorum-based consensus. This requires a known set of participants (i.e. a permissioned network) and extensive communication between them to reach agreement. This restricts the number of participants in the network. Furthermore, quorum based consensus can only tolerate up to 1/3rd of the participants becoming faulty.

A small participant set and low threshold for faults generally pushes these networks to put large barriers to entry, either through large staking requirements or politics.

Prioritizing Liveness

Chains that prioritize liveness generally do so by relying on fork choice rules such as the longest chain rule from Nakamoto consensus. These protocols allow each participant to make a local choice about which fork to follow, and therefore do not require quorums and thus can be permissionless.

Additionally, due to a lack of quorums, these protocols can be quite message efficient. Thus, participation does not need to be artificially reduced to remain within bandwidth restrictions.

These protocols tolerate up to 1/2 of participants becoming faulty. The large fault tolerance threshold and the large number of participants provides for much higher resilience to corruption.

Privacy

The motivation behind the design of Cryptarchia can be boiled down to this statement:

A block proposer should not feel the need to self-censor when proposing a block.

Working to give leaders confidence in this statement has had ripple effects throughout the protocol, including that:

  • The block proposals should not be linkable to a leader. An adversary should not be able to connect together the block proposals of a leader in order to build a profile. In particular, one should not be able to infer a proposer's stake from their past on-chain activity.
  • Cryptarchia must not reveal the stake of the leader - that is, it must be a Private Proof of Stake (PPoS) protocol. If the activity of the leader reveals their stake values (e.g. through weighted voting), then this value can be used to reduce the anonymity set for the leader by bucketing the leader as high/low stake and can open him up to targeting.
  • Leaders should be protected against network triangulation attacks. This is outside of the scope of this document, but it suffices to say that in-protocol cryptographic privacy is not sufficient to guarantee a leader's privacy. This topic is dealt with directly in Blend Protocol.

Limitations of Cryptarchia V1

Despite our best efforts, we cannot provide perfect privacy and censorship resistance to all parties. In particular:

  • We are unable to protect leaders from leaking information about themselves based on the contents of blocks they propose. The tagging attack is an example of this, where an adversary may distribute a transaction to only a small subset of the network. If the block proposal includes this transaction, the adversary learns that the leader was one of those nodes in that subset.
  • The leader is a single point of failure (SPOF). Despite all the efforts we go through to protect the leader, the network can be easily censored by the leader. The leader may choose to exclude certain types of transactions from blocks, leading to a worse UX for targeted parties.

As far as we can tell, these limitations are not insurmountable and we have sketches towards solutions that we will develop in following iterations of the protocol.

Overview

Cryptarchia is a probabilistic consensus protocol with properties similar to Bitcoin’s Nakamoto Consensus.

At a high level, Cryptarchia divides time into slots and at each slot, a leadership lottery is run. To participate in the lottery, a node must have held stake in the chain in the form of a note for a minimum time period. Given a sufficiently aged note, you can check if it has won a slot lottery by cryptographically flipping a weighted coin. The weight of the coin is proportional to the value of your note, thus higher valued notes lead to increased chances of winning. To ensure privacy and avoid revealing the note value, this lottery result is proven within a ZK proof system.

Our design starts from the solid foundation provided by Ouroboros Crypsinous: Privacy-Preserving Proof-of-Stake eprint.iacr.org and builds upon it, incorporating the latest research at the intersection of cryptography, consensus and network engineering.

Protocol

Constants

SymbolNameDescriptionValue
slot activation coefficientThe target rate of occupied slots. Not all slots contain blocks, many are empty. (see ANALYSIS-BLOCK-TIMES-BLEND-NETWORK for analysis leading to the choice of value)1/30
security parameterBlock depth finality. Blocks deeper than on any given chain are considered immutable.2160 blocks
noneslot lengthThe duration of a single slot.1 second
MAX_BLOCK_SIZEmax block sizeThe maximum size of the block body (not including the header)2 MiB (2,097,152 bytes)
MAX_BLOCK_TXSmax block transactionsThe maximum number of transactions in a block1024
uncle reference windowThe window width in expected block-intervals (each of slots): the slot of the parent of a referenced uncle must precede the slot of the referencing block by at most slots (360 at ). Anchoring the window to the parent bounds how far back the referencing chain must be retained to verify the uncle; a virtual upper bound on applies (see the note in Uncle Selection).12
MAX_UNCLESmax uncle referencesThe maximum number of uncles a block may reference.4

Notation

SymbolNameDescriptionValue
slot security parameterSufficient slots such that blocks have been produced with high probability.
the block treeThis is the block tree observed by a node.
tips of block tree The set of concurrent forks of some block tree .
tip of local chainThe chain that a node considers to be the honest chain.
the latest immutable blockThe latest block which was committed (finalized) by the chain maintenance.
slot numberIndex of slot. denotes the genesis slot.
epoch numberIndex of epoch. denotes the genesis epoch.

Latest Immutable Block

The latest immutable block is the most recent block considered permanently finalized. The blocks deeper than in the local chain are never to be reorganized.

This is maintained locally by the Chain Maintenance procedure. When the Online fork choice rule is in use, corresponds to the -deep block. However, it may be deeper than the -deep block if the fork choice rule has been switched from Online to Bootstrap. Unlike the -deep block, does not advance as new blocks are added unless the Online fork choice rule is used.

The details of fork choice rule transitions are defined in the bootstrap spec: Cryptarchia Bootstrapping & Synchronization

Slot

Time is divided up into slots of equal length, where one instance of the leadership lottery is held in each slot. A slot is said to be occupied if some validator has won the leadership lottery and proposed a block for that slot, otherwise the slot is said to be unoccupied.

Epoch

Cryptarchia has a few global variables that are adjusted periodically in order for consensus to function. Namely, we need:

  • Dynamic participation, thus the eligible notes must be refreshed regularly.
  • An unpredictable source of randomness for the leadership lottery. This source of randomness is derived from in-protocol activity and thus must be selected carefully to avoid giving adversaries an advantage.
  • Approximately constant block production rate achieved by dynamically adjusting the lottery difficulty based on observed participation levels.

The order in which these variables are calculated is important and is done w.r.t. the epoch schedule.

Epochs are numbered consecutively from 0, the epoch the Genesis block belongs to. That number is an EpochNumber, a 32-bit unsigned integer, encoded little-endian over 4 bytes wherever it is serialized or hashed.

Epoch Schedule

An epoch is divided into 3 phases, as outlined below.

Epoch PhasePhase LengthDescription
Stake Distribution Snapshot slotsA snapshot of note commitments are taken at the beginning of the epoch. We wait for this value to finalize before entering the next phase.
Buffer phase slotsAfter the stake distribution is finalized, we wait another slot finality period before entering the next phase. This is to further ensure that there is at least one honest leader contributing to the epoch nonce randomness. If an adversary can predict the nonce, they can grind their coin secret keys to gain an advantage.
Lottery Constants Finalization slotsOn the slot into the epoch, the epoch nonce and the inferred total stake can be computed. We wait another slots for these values to finalize.

The epoch length is the sum of the individual phases: slots.

Epoch State

The epoch state holds the variables derived over the course of the epoch schedule. It is the 3-tuple described below.

SymbolNameDescriptionValue
Eligible Leader Notes CommitmentA commitment to the set of notes eligible for leadership.See Eligible Leader Notes
Epoch NonceRandomness used in the leadership lottery (selected once per epoch)See Epoch Nonce
Inferred Total Stake (Lottery Difficulty)Total stake inferred from watching the results of the lottery during the course of the epoch. is used as the stake relativization constant for the following epoch.See Total Stake Inference

Eligible Leader Notes

A note is eligible to participate in the leadership lottery if it has not been spent and was a member of the note set at the beginning of the previous epoch, i.e. they are members of .

Note Ageing

If an adversary knows the epoch nonce , they may grind a note that wins the lottery more frequently than should be statistically expected. Thus, it’s critical that notes participating in the lottery are sufficiently old to ensure that they have no predictive power over .

Epoch Nonce

The epoch nonce is evolved after each block.

Given block where

  • is the parent of block
  • is the slot that is occupying.
  • is the epoch nonce entropy contribution from the block’s leadership proof

Then, is derived as

where is the domain separator EPOCH_NONCE_V1, maps the slot number to the corresponding scalser in Poseidon’s scalar field and is Poseidon2 as specified in Common Cryptographic Components .

The epoch nonce used in the next epoch is where is the last block before the start of the “Lottery Constants Finalization” phase in the epoch schedule.

Total Stake Inference

Given that stake is private in Cryptarchia, and that we want to maintain an approximately constant block rate, we must therefore adjust the difficulty of the slot lottery somehow based on the level of participation. The inference counts the number of occupied slots of the honest chain — the slots holding a canonical block or one of the uncle blocks it references. Therefore, lottery wins lost to forks still contribute to the estimate, and each slot is counted once however many blocks fall in it. The details can be found in the following document:

Total Stake Inference

Epoch State Pseudocode

At the start of each epoch, each validator must derive the new epoch state variables. This is done through the following protocol:

:

The genesis epoch state is hardcoded upon chain initialization.

    

The epoch state is derived w.r.t. observations in the previous epoch. Here we compute the slot at the start of the previous epoch. We will query observations relative to this slot.

    

Notes eligible for leadership lottery are those present in the commitment root at the start of the previous epoch.

    

The epoch nonce for epoch is the value of at the beginning of the lottery constants finalization phase in the epoch schedule

    

Total active stake is inferred from the number of blocks produced in the previous epoch during the stake freezing phase. It is also derived from the previous estimate of total stake, thus we recurse here to retrieve the previous epochs estimate

    

The number of distinct occupied slots during the first slots of the previous epoch: a slot counts if it holds a block of the chain of and/or one or more uncles referenced by the blocks of that chain that themselves lie in the window, and each slot is counted at most once. Referenced uncles are genuine lottery wins that were lost to forks; counting them gives a more accurate estimate of consensus participation. No filtering is needed here: an uncle carried by a block of the chain was validated as a condition of that block's acceptance (see Uncle References), so every node holding the chain counts exactly the same uncles.

    

    

The referenced uncles are the headers carried in the uncle_headers field of the blocks of the chain that fall inside the window. A block outside the window contributes nothing — neither its own slot nor the slots of the uncles it carries — so the count of a window is closed the moment that window ends: no block proposed afterwards can alter the estimate of a past epoch, however far back an uncle it carries may reach. Each entry was checked against the rules of Uncle References when its referencing block was validated — the parent of the uncle lies on the chain while the uncle itself does not, the uncle precedes its referencer and its parent does so by at most slots, its Proof of Leadership verifies against inputs derived from the chain, and its signature verifies over its header — so no entry needs to be re-examined or skipped here.

    

    

    

    

Leadership Lottery

A lottery is run for every slot to decide who is eligible to propose a block. For each slot, we can have 0 or more winners. In fact, it’s desirable to have short slots and many empty slots to allow for the network to propagate blocks and to reduce the chances of two leaders winning the same slot which are guaranteed forks.

Proof of Leadership

The specifications of how a leader can prove that they have won the lottery are specified in the following document:

Leader Rewards

As an incentive for producing blocks, leaders are rewarded with every block proposal. The rewarding protocol is specified in Anonymous Leaders Reward Protocol.

Block Chain

Fork Choice Rule

We use two fork choice rules, one during bootstrapping and a second once a node completes bootstrapping.

During bootstrapping, we must be resilient to malicious peers feeding us false chains, this calls for a more expensive fork choice rule that can differentiate between malicious long-range attacks and honest chains.

After bootstrapping we commit to the most honest looking chain we found and switch to a fork choice rule that rejects chains that diverge by more than blocks

Cryptarchia Fork Choice Rule

Block ID

Block ID is defined by the hash of the block header Block Header, where hash is Blake2b as specified in Common Cryptographic Components. The header commits to the whole block body through (see Block Construction, Validation and Execution), so the block ID transitively commits to the signed uncle headers the block carries as well as to its transactions: two blocks with the same ID are identical byte for byte.

def block_id(header: Header) -> hash
    return hash(
        b"BLOCK_ID_V1",
        header.bedrock_version,
        header.parent_block,
        header.slot.to_bytes(8, byteorder='little'),
        header.body_root,
        # PoL fields
        header.proof_of_leadership.leader_voucher,
        header.proof_of_leadership.entropy_contribution,
        header.proof_of_leadership.proof.serialize(),
        header.proof_of_leadership.leader_key.compressed(),
    )

Block Header

class Header:                                # 297 bytes
    bedrock_version: byte                    # 1 bytes
    parent_block: hash                       # 32 bytes
    slot: int                                # 8 bytes
    body_root: hash                          # 32 bytes
    proof_of_leadership: ProofOfLeadership   # 224 bytes

class ProofOfLeadership:                     # 224 bytes
    proof: Groth16Proof                      # 128 bytes
    entropy_contribution: zkhash             # 32 bytes
    leader_key: Ed25519PublicKey             # 32 bytes
    leader_voucher: zkhash                   # 32 bytes

The field order above is the wire order defined in Canonical Encoding. The Block ID preimage above absorbs the same ProofOfLeadership fields in a different order; that is deliberate, since the preimage is a domain-separated enumeration of header fields rather than a re-serialization of the header.

Block

Block Construction, Validation and Execution

Uncle References

A block may reference up to MAX_UNCLES uncles (see Constants). An uncle is a valid fork block that is not part of the chain of the referencing block but shares a common ancestor with it. A block references an uncle by carrying its full signed header in the uncle_headers field of both the block proposal and the reconstructed block (Block Construction, Validation and Execution); the block ID of an uncle is derived from the carried header as and is not recorded separately. Following the list convention of the Mantle Transaction Encoding"Any lists are length-prefixed with fixed width uints" — the list is serialized as a 1-byte little-endian element count followed by that many fixed-size entries (one byte suffices for the MAX_UNCLES bound), so it carries its own length. The header holds no uncle data of its own and is fixed-size; it commits to the carried entries — signatures included — through , and hence so does the Block ID. Any node holding a block therefore also holds the signed headers of the uncles it references, however it obtained that block, and cannot hold a variant of them. The size of a proposal varies with the number of referenced uncles; the indistinguishability of proposals required by the Blend Protocol is preserved at the message layer instead: Payload Formatting fixes every dispersed payload body to the maximum payload size Max_Body_Length — set from the maximum proposal size — padding shorter proposals with random data.

The only purpose of an uncle reference is to feed the Total Stake Inference, which infers the total active stake from the number of occupied slots — the slots in which at least one leader won the lottery. A referenced uncle is a genuine lottery win, backed by a valid Proof of Leadership, that was lost to a fork (predominantly caused by network delays); its slot was occupied, but without the reference the canonical chain would not observe it. Counting the slots of the referenced uncles alongside those of the canonical blocks recovers those occupied slots and gives a more accurate estimate of the total active stake.

At the moment a block is produced it is not yet known which branch of a fork will become canonical: some nodes build on one branch and some on the other. By referencing the blocks of the competing branches as uncles, whichever branch ultimately becomes canonical still counts the lottery wins of the branches that lost. This is the intent of uncle references — to let each branch count its counterpart branches — and it is what improves the total stake estimate.

A block is valid only if every uncle it carries is valid. The rules below are validity conditions, checked as step 10 of Block Header Validation: a block carrying an entry that fails any of them is rejected, exactly as one carrying an invalid Proof of Leadership is. Gating validity this way is safe because every rule is a function of the chain being extended and of the carried entry alone — never of the block tree , the node's pruning state, or the node's network history. All nodes therefore reach the same verdict on the same block; no node can be starved of the inputs needed to decide, since the entry travels inside the very block whose validity depends on it; and an adversary manipulating which fork blocks are visible can neither make a block valid for some nodes and invalid for others nor thereby influence block inclusion. Requiring validity also keeps fabricated uncles out of the chain permanently, instead of leaving every node that ever synchronizes to detect and discard the same entries again.

An entry of the uncle_headers list of a block is valid — we say holds — only if all of the following hold:

  • The parent of the uncle is part of the chain of the referencing block: . Hence the uncle is the first block of its fork, and its chain is a prefix of the chain of . Blocks deeper in a fork branch cannot be referenced: verifying their Proof of Leadership requires the ledger state of the fork branch, which cannot be reconstructed from the chain of (see the verification rule below).
  • The uncle itself is not part of the chain of the referencing block: — equivalently, is not the block of the chain of at slot .
  • The uncle precedes the referencing block, and the parent of the uncle lies within the uncle reference window (see Constants): and . The window is anchored to the parent rather than to the uncle itself because the parent is the block whose historical state the remaining checks need: as of is required to verify the Proof of Leadership below, and anchoring here bounds how far back the chain of must be retained to supply it. Anchoring to would not bound it: a leader may build on a stale tip, so is unbounded and a block could demand a ledger root arbitrarily deep in the chain. The anchor also matches Fork Pruning, which prunes by divergence depth — and the parent of a first-fork block is exactly that divergence point. Since by step 5 of Block Header Validation, this rule implies : the uncle's own slot falls inside the window as a consequence, not as a separate condition.
  • The Proof of Leadership of verifies against public inputs derived from the chain of : the slot, and taken from the header of ; the epoch state of the epoch of as derived on the chain of ; and as of , which is a historical ledger root of the chain of because the parent lies on that chain. Since the chain of the uncle is a prefix of the chain of , a genuine fork win was proven against exactly these values and verifies; a fabricated header does not. These are the same inputs a node derives to validate the Proofs of Leadership of the canonical blocks themselves; in particular is a function of the executed chain, so this check requires a full node's possession of the chain — headers alone do not suffice, exactly as they do not suffice to validate canonical blocks.
  • The carried signature verifies over the header of : , with taken from that header — the same binding required of a canonical proposal by step 9 of Block Header Validation. This ensures the uncle is a block authorized by the leader who won the lottery, not a fabricated header wrapped around a replayed proof.

A proposer can always determine in advance whether an entry will be accepted: the chain of is fixed the moment it selects the parent of , and every rule reads only that chain and the entry itself, so proposer and validators evaluate identically and a well-implemented proposer never builds a block that others reject. For the same reason all nodes — including a node bootstrapping from genesis — accept exactly the same blocks and, having accepted them, count exactly the same uncles and derive exactly the same estimate. A carried uncle is checked exactly as a received proposal is — the Proof of Leadership and the signature binding it to the header — minus the chain-context steps that do not apply to a fork block. One caveat is recorded for honesty: the signature proves that the winning leader authorized this header, not that the block was published in its slot — the owner of a winning note can fabricate and self-sign such a header later. This is benign: the Proof of Leadership still attests a genuine lottery win at that slot, which is precisely the signal the Total Stake Inference measures, and the count is taken per distinct slot, so neither replay nor self-wrapping can add occupied slots beyond genuine wins.

Uncle references are not required to be unique, and duplicates are permitted — the same uncle may be referenced by more than one block on the canonical chain, and a single block may carry the same entry twice. Each occurrence is validated on its own and a duplicate passes the rules above exactly as the first occurrence does; it merely wastes one of the MAX_UNCLES entries. Duplication is harmless because the Total Stake Inference counts occupied slots, not references: it forms the set of slots occupied by the canonical chain together with the slots of the uncles that chain references, and counts each slot once. A slot therefore contributes at most once to the estimate no matter how many blocks fall in it — the same uncle referenced several times, two distinct uncles that share a slot, and an uncle that shares its slot with a canonical block each add a single occupied slot, or none if that slot is already counted. This matches the slot lottery, which activates a slot with probability regardless of how many leaders win it.

Uncle Selection

When constructing a block at slot , the proposer fills the uncle_headers field by selecting from the accepted fork blocks in its block tree that are valid uncles for — the first blocks of the competing branches that 's own chain does not contain. Only a block whose parent is on 's chain qualifies (see the validity rules above), so only such blocks are candidates; carrying anything else would make itself invalid. Because the Total Stake Inference counts occupied slots, the proposer further narrows the candidate set to only the uncles that would add a new occupied slot: it excludes any uncle whose slot is already occupied on the chain that extends (the ancestors of ) — whether that slot holds a canonical block or an uncle the chain already references — since referencing it would waste one of the entries without changing the count. This last narrowing is a best-effort optimization and is not a validity rule: a proposer that references an already-occupied slot produces a block that is still valid, merely one entry poorer. The preceding conditions are not optional — validation enforces every one of them.

An uncle is not excluded because some other branch referenced it, nor because its slot is occupied on some other branch. The Total Stake Inference counts only the slots occupied by the canonical chain and its uncles, and at production time it is unknown which branch will become canonical. If a competing branch referenced an uncle but that branch is later discarded, its reference does not count; must therefore remain free to reference the same uncle, so that its lottery win is still counted should 's branch win.

def uncle_candidates(B) -> Set[Block]:
    # Slots already occupied on the chain B extends: the slots of B's ancestors (canonical
    # blocks) and the slots of the uncles those ancestors already reference. The Total Stake
    # Inference counts occupied slots, so an uncle whose slot is already occupied adds nothing —
    # referencing it would waste an entry. Slots occupied on *other* branches are deliberately
    # not excluded: they do not count unless that branch becomes canonical.
    referenced_uncles = { U for A in ancestors(B) for (U, _) in A.uncle_headers }
    occupied = { sl_A for A in ancestors(B) } | { sl_U for U in referenced_uncles }

    # Accepted fork blocks that are valid uncles for B and would add a new occupied slot
    # (see the validity rules in Uncle References).
    return { U for U in T if
               parent(U) in ancestors(B)          # first block of its fork: the only referenceable kind
               and not is_ancestor(U, B)          # a fork, not on B's chain
               and sl_B > sl_U                    # the uncle precedes B
               and sl_B - sl_parent(U) <= W / f   # its parent is within the uncle window
               and sl_U not in occupied           # its slot is not already occupied on B's chain
           }

The proposer then selects at most of these candidates by deterministically taking those with the oldest parent first. A candidate leaves the window when its parent ages out of it, so ordering by parent slot is ordering by time to expiry:

def select_uncles_oldest(B) -> array[SignedHeader]:   # at most MAX_UNCLES entries, in selection order
    ordered = sorted(uncle_candidates(B), key=lambda U: (sl_parent(U), sl_U, block_id(U)))
    selected, slots = [], set()
    for U in ordered:
        if len(selected) == MAX_UNCLES:
            break
        if sl_U in slots:              # at most one uncle per slot: a second adds no occupied slot
            continue
        slots.add(sl_U)
        selected.append(signed_header(U))   # header of U with the signature it arrived under
    return selected                    # the list carries its own length; no padding entries

This deterministic rule is simple and, without communication between proposers, robust. Two non-communicating proposers with the same candidate set produce the same selection, but they also tend to produce competing blocks that extend the same tip, of which only one becomes part of the canonical chain — the Fork Choice Rule discards the rest. The identical uncle references on the discarded competitors therefore cost nothing, and along a single canonical chain, excluding the slots already occupied on a block's own chain keeps successive blocks from re-counting a slot.

Which candidates a proposer selects is proposer-local: it may reference fewer uncles than it could, or pass over a candidate for another, and its block remains valid. What it may reference is not — every carried entry must satisfy the validity rules above, and a block carrying one that does not is rejected (see Block Header Validation). The procedure given here is therefore a recommendation for filling the entries well, not a consensus rule. Because uncle references carry no fork-choice weight and grant no reward, a proposer has no incentive to deviate from it, and deviating within the rules only affects the accuracy of the Total Stake Inference.

A referenced uncle never becomes part of the chain:

  • The transactions of the uncle are not executed and have no effect on the ledger state (see Block Execution).
  • The uncle carries no weight in the Fork Choice Rule.
  • The uncle grants no block reward.

The only effect of a referenced uncle is its contribution to the Total Stake Inference. Nothing further gates it: an uncle carried by a block of the chain has already been verified as a condition of that block's validity, so the inference counts its slot — whenever that slot and its referencing block both fall in the observation window — without re-examining the entry.

Note: The uncle reference window spans expected block-intervals — slots, 360 at the default and — and MAX_UNCLES is 4. Forks are predominantly caused by network delays and resolve within a few slots, so this window comfortably captures the forks worth referencing. carries a virtual upper bound of — equivalently — virtual in that no validation rule evaluates it: it constrains the choice of the constant itself, limiting the expense of lineage checking — how far back the chain of a referencing block must be reachable to verify an uncle — and any retuning of must stay within it. The bound keeps the window strictly inside the finalization window, which is what the proposer needs: a candidate whose parent is within the window is still present in its block tree, since only forks that diverged deeper than the latest immutable block — at most slots back — are pruned (see Fork Pruning); because the window is anchored to the parent, which is the divergence point, the two conditions are stated over the same quantity. The Total Stake Inference needs no such bound: it counts only the blocks of its observation window and the uncles those blocks carry, so the count of a past window is closed once the window ends whatever the value of , and recomputing for a past epoch always yields the same value.

Note: The validity rules are functions only of the referencing chain and the carried entry, and the signed headers are carried inside the blocks themselves (the uncle_headers field of Block Construction, Validation and Execution) — availability is structural, and referencing an uncle is inseparable from publishing its signed header. Every node therefore accepts exactly the same blocks and, from them, computes the same and the same , at any time; a bootstrapping node reproduces the estimate of every past epoch from the chain data alone. An adversary manipulating the visibility of fork blocks can thus affect neither block inclusion nor the agreement of the estimate — only whether its own lottery wins are referenced at all. Because a block carrying an unverifiable entry is rejected outright, no such entry ever becomes chain data, and the cost of checking one is paid once by the receiving node rather than by every node that later synchronizes. The carried signed headers are retained as part of the chain data, bounded by MAX_UNCLES entries per block.

Block Header Validation

Given block and the block tree where:

  • is the header defined in Header
  • is the list of signed headers of the uncles the block references, defined in Block
  • is the sequence of transactions in the block

We say returns True if all of the following constraints hold, otherwise it returns False.

  1. Ensure bedrock version number.

  2. Ensure the block body, i.e. the serialized sequence of transactions, does not exceed the maximum allowed size. The header is not part of the body and does not count towards this limit.

  3. Ensure the number of transactions in the block does not exceed the limit.

  4. Ensure the body root commits to both parts of the block body. It is computed as

def body_root(uncle_headers: list[SignedHeader], transactions: list[SignedMantleTx]) -> hash:
    return hash(
        b"BODY_ROOT_V1",
        serialize(uncle_headers),  # 1-byte count, then the fixed 361-byte entries
        merkle_root(transactions),
    )

where is the root of the Merkle tree built from transaction hashes (see Mantle - Mantle Transaction Hash) as leaves, using 0 to represent the hash of an empty transaction and padding the leaves to the closest power of two, and is the list encoding of Block Proposal — a 1-byte little-endian element count followed by that many entries, so an empty list encodes as a single zero byte. The serialized list is committed directly, with no inner hash: the element count and the fixed 361-byte entries keep the flat preimage unambiguous, and the single BODY_ROOT_V1 tag versions the whole construction. Because the signatures of the carried uncle headers are inside , they are committed here and hence in the Block ID; a copy of a block that differs in any of those bytes has a different ID and is a different block.

  1. Ensure the block’s slot comes after the parent block’s slot.

  2. Ensure this block’s slot time has elapsed. Local time is used in this validation. See Clocks for discussion around clock synchronization.

  3. Ensure we have already accepted the block’s parent into the block tree.

  4. Ensure the block comes after the latest immutable block. Assuming that prunes all forks diverged deeper than , this step, along with step 5, ensures that is descendant from . If all forks cannot be pruned completely in the implementation, this step must be replaced with , which checks whether is an ancestor of .

  5. Verify the leader’s right to propose and ensure it is the one proposing this block: Given leadership proof , where

  • is the slot lottery win proof as defined in Proof of Leadership
  • is the public key committed to in .
  • is a signature.

A leaders proposal is valid if

  • Ensure that the leader who won the lottery is actually proposing this block since PoL’s are not bound to blocks directly.
  1. Ensure every carried uncle is valid, as defined by the rules of Uncle References: the parent of lies on the chain of , itself does not, precedes and the slot of its parent precedes by at most , its Proof of Leadership verifies against inputs derived from the chain of , and verifies over under the of that header. A block carrying an entry that fails any of these is rejected.

The list bound len(uncle_headers) <= MAX_UNCLES is a constraint of the serialization schema, rejected at decode time (a block that does not parse is no block), and so is not restated here. Duplicate entries are permitted: each is validated independently and a repeat passes exactly as the first occurrence does.

Every input to these checks comes from the chain of or from the carried entry itself, both of which any node validating necessarily has — the entries travel inside and are committed by its . Validity is therefore a function of the block and the chain it extends alone, so all nodes agree on it and an adversary influencing what nodes know of forks cannot influence block inclusion.

Chain Maintenance

We define the chain maintenance procedure that governs how the block tree is updated.

Note: It’s assumed that block contents have already been validated by the execution layer, by applying the transactions of the block in the order they appear starting from the parent block’s execution state, as defined in Block Proposal Validation.

:

if :

Either we’ve already seen or it’s invalid, in both cases we ignore this block

    

Explicitly commit to the -deep block if the Online Fork Choice Rule is being used.

    

Commit

We define the procedure that commits to the block, which is deep from . This procedure computes the new latest immutable block .

Compute the latest immutable block, which is deep from .

Prune all forks diverged deeper than , so that future blocks on those forks can be rejected by Block Header Validation.

Fork Pruning

We define the fork pruning procedure that removes all blocks which are part of forks diverged deeper than a certain block.

If is a fork diverged deeper than , prune the fork.

    

    

      

Remove all blocks in the chain within range from .

    

    

Versioning and Protocol Upgrades

Protocol versions are signalled through the bedrock_version field of the block header. Protocol upgrades need to be co-ordinated well in advance to ensure that node operators have enough time to update their node. We will use block height to schedule the activation of protocol updates. E.g. bedrock version 35 will be active after block height 32000.

Annexes

Proof of Stake vs. Proof of Work

From a privacy and resiliency point of view, Proof of Work is highly attractive. The amount of hashing power of a node is private, they can provide a new public key for each block he mines ensuring that his blocks cannot be connected by this identity, and PoW is not susceptible to long range attacks as is PoS. Unfortunately, it is wasteful and demands that leaders have powerful machines. We want to ensure strong decentralization by having a low barrier to entry and we believe we can achieve a good enough level of security given by having participants have an economic stake in the protocol.

Clocks

Cryptarchia depends on honest nodes having relatively in-sync clocks. We are currently rely on NTP to synchronize clocks, this may be improved upon in the future, borrowing ideas from Ouroboros Chronos: Permissionless Clock Synchronization via Proof-of-Stake  eprint.iacr.org

References

  1. Ouroboros Crypsinous: Privacy-Preserving Proof-of-Stake eprint.iacr.org
  2. Ouroboros Chronos: Permissionless Clock Synchronization via Proof-of-Stake  eprint.iacr.org

Test Vectors

The operations used to derive the transaction Merkle root are the same as those defined in Test Vectors. That root is no longer a header field on its own: it is one of the two inputs to the body_root defined in step 4 of Block Header Validation, alongside the serialized carried uncle_headers. The vectors below are generated by the implementation's generate_body_root_test_vectors (core/src/header/mod.rs) at commit e14c7fd1.

InputOutput
empty block (no transaction)merkle_root(transactions): 0x0000000000000000000000000000000000000000000000000000000000000000
one transaction per operation kind:
- leaf[0]: 0x6ab0046084f3ce8dad90eb28afe5692ad92d5d0588a4e868ad38d0d841d7a60e (Transfer)
- leaf[1]: 0x15c4361f33089c446b8f2f7747ec211d5d031da529a3ee6b62e9df670f996dbf (ChannelConfig)
- leaf[2]: 0x50e5674eea7fa17f531a51159ea7c3cab843fb1c8e8bf9bd5518a8aad08865d3 (ChannelInscribe)
- leaf[3]: 0xd52da59d9db42391363d6c4f96447536e5dfff747b91b88320310b07581a8dee (ChannelDeposit)
- leaf[4]: 0x6f57c77dc872cc3f01380fbd57a97e9f7998a1cd8b24e84594ceba796cfa0822 (ChannelWithdraw)
- leaf[5]: 0x2c04be946507e2b8c239b85b03cf476a8be5af8e4de853660d0447a46ea460fc (ChannelTransfer)
- leaf[6]: 0x9ce9fa694b4c801eca6c9a1d3dca6401952404bda8c144fb16e03e3872fd475e (SDPDeclare)
- leaf[7]: 0x3555b3d8f5d05ea5d69efb17aab7639474738bcb4bfee8d354107433d781ef9c (SDPWithdraw)
- leaf[8]: 0x0a91ab8271016f212061e6b45ea35c95cfa0f9a70c5225508f284b2657f4d931 (SDPActive)
- leaf[9]: 0xc992f1a63a7ea665a3766fae6b032df3db12ef386caf0ef1f3654afedbc51c6c (LeaderClaim)
merkle_root(transactions): 0x65f481d9f0cdb38f2166299c40f4e74bec7332df72281daec7a6547a098ff08b
uncle_headers: empty list (encoded as the single byte 0x00)
- merkle_root(transactions): 0x65f481d9f0cdb38f2166299c40f4e74bec7332df72281daec7a6547a098ff08b
body_root: 0xd279012d8ce1c7db4812b900c29174f2657b3fa243270fc8ebeeb5f1c0a29cec
uncle_headers: 2 entries (encoded as the count byte 0x02 followed by the two 361-byte entries below, each a 297-byte header and its 64-byte signature)
- uncle_headers[0]: 0x016666666666666666666666666666666666666666666666666666666666666666660000000000000066666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666660000000000000000000000000000000000000000000000000000000000000034b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a7466600000000000000000000000000000000000000000000000000000000000000563913f1ba7ad4129a077acd56278e743fd45120226dd315fa49f3a9c5d07af6a174ab84d4555a279afe053e79c8bb794be3f7d2e71e92b8da1b490687cb8306
- uncle_headers[1]: 0x0177777777777777777777777777777777777777777777777777777777777777777700000000000000777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777700000000000000000000000000000000000000000000000000000000000000c853ad0f0cd2b619aea92ceec4fd56a24d6499d584ce79257e45cfd8139b60a77700000000000000000000000000000000000000000000000000000000000000ad17e45d503a16fb41c25c4b3025956c63b31015871e957f3562b47cebce784e5b392ce3dd05214afe09102e0d2ed8211a83b81f18231963a226198fd528df0c
- merkle_root(transactions): 0x65f481d9f0cdb38f2166299c40f4e74bec7332df72281daec7a6547a098ff08b
body_root: 0x7f3854d9c24cfdb5e30f37a74ace03d06adacbf807d55a89e4935d90f286e831
Header:
- bedrock_version: 0x01
- parent_block: 0x1111111111111111111111111111111111111111111111111111111111111111
- slot: 42 (0x2a; encoded little-endian)
- body_root: 0xd279012d8ce1c7db4812b900c29174f2657b3fa243270fc8ebeeb5f1c0a29cec (the empty-uncle_headers value above)
- leader_voucher: 0x4444000000000000000000000000000000000000000000000000000000000000
- entropy_contribution: 0x5555000000000000000000000000000000000000000000000000000000000000
- proof: 0x2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
- leader_key: 0x17cb79fb2b4120f2b1ec65e4198d6e08b28e813feb01e4a400839b85e18080ce
block_id: 0xb5232b5462d6d802b2e77185e3fd7124af713e36818db1438d921e8c980232bb