BLOCK-CONSTRUCTION-VALIDATION-AND-EXECUTION
| Field | Value |
|---|---|
| Name | Block Construction, Validation and Execution |
| Slug | 93 |
| Status | raw |
| Category | Standards Track |
| Editor | Marcin Pawlowski [email protected] |
| Contributors | Thomas Lavaur [email protected], Daniel Sanchez Quiros [email protected], David Rusu [email protected], Álvaro Castro-Castilla [email protected], Mehmet Gonen [email protected], Filip Dimitrijevic [email protected] |
Timeline
- 2026-08-31 —
5fa8ee4— docs(blockchain): compress the block proposal with 16-byte transaction reference prefixes (#389) - 2026-08-26 —
5a9bee0— docs(blockchain): make execution steps clearer (#417) - 2026-08-19 —
a2a85cb— RFC Bedrock: Cryptarchia with uncle references (#385) - 2026-07-27 —
628684e— docs(blockchain): fix CHANNEL_DEPOSIT execution, block limits and storage market formatting (#381) - 2026-07-03 —
709cf7f— Bedrock-RFC: Remove Concept of a Session (#365) - 2026-05-28 —
d45eed2— Chore: mirror blochain specs into github/mdbook (#347) - 2026-05-18 —
58b5698— chore(blockchain): migrate contributor emails to @logos.co (#338) - 2026-02-09 —
afd94c8— chore: add math support (#287) - 2026-01-19 —
f24e567— Chore/updates mdbook (#262) - 2026-01-16 —
89f2ea8— Chore/mdbook updates (#258)
Revisions History
| Version | Changes | Date |
|---|---|---|
| 1.0.0 | Initial revision. | 2025-12-03 |
| 1.1.0 | Removed service_rewards due to updated SERVICE-REWARD-DISTRIBUTION-PROTOCOL. Extended the Block Execution logic with rewards distribution due to updated SERVICE-REWARD-DISTRIBUTION-PROTOCOL. Removed Block Samples subsection of the Batch verification of ZK proofs from the Annex. Reordered the Block Execution steps to enable immediate use of reward notes as inputs for transactions included in the proposal. | 2026-03-27 |
| 1.1.1 | [RFC] Simplify Mantle Transaction and Refactor Ledger Operations | 2026-05-06 |
| 1.1.2 | Precise that the maximum block size applies to the block body only. | 2026-07-27 |
| 1.1.3 | Corrected MAX_BLOCK_SIZE to 2 MiB, to match the implementation. | 2026-08-05 |
| 1.2.0 | Added the uncle_headers field — the signed headers of the referenced uncles — to the Proposal and to the newly defined Block, and replaced block_root with body_root in the Header, which commits to them, signatures included, as well as to the transactions. Due to updated Cryptarchia Protocol (uncle references). | 2026-08-06 |
| 1.2.1 | Precise the state each transaction of a block is validated against: the transactions are validated and executed one after the other in the order they appear, each against the state the preceding ones left, which makes block validity order-dependent. Precise that a block whose validation fails at any point is not executed at all. | 2026-08-24 |
| 1.3.0 | Compressed Block Proposal: 16-byte transaction reference prefixes and a variable-length references list, reducing the proposal from 34,574 bytes to at most 18,192. Added the Canonical Encoding section. | 2026-08-18 |
Introduction
In this document, we present the specification defining the construction of the block proposal, its validation, and execution. We define the block proposal construction that contains references to transactions (from the mempool) instead of a complete transaction to limit its length. The raw block body increases with the size of transactions it contains up to MAX_BLOCK_SIZE, which is 2 MiB and covers the transactions only, and the proposal compresses its size down to at most ≈18.2 kB (18,192 bytes), which saves the bandwidth necessary to broadcast new blocks.
Overview
For the consensus protocol to make progress, a new leader is elected through the leader lottery. The new leader is in possession of a proof of leadership (PoL) that confirms that it is indeed the leader. The main objective of the leader is to construct a new block, hence becoming a block builder, and share it with other members of the network as a block proposer. The block must be correctly constructed; otherwise, it will be rejected by the consensus nodes who are validating every block. Only a block that validates in full modifies the state of the chain: the transactions it includes are interpreted by all nodes, one after the other in the order they appear, and the state of the chain is modified according to the instructions embedded in them.
High-level Flow
Below, we present a high-level description of the block lifecycle. The main focus of this section is to build an intuition on the block construction, validation, and execution.
-
A leader is selected. The leader becomes a block builder.
-
The block builder constructs a block proposal.
- The block builder selects the latest block (parent) as the reference point for the chain state update.
- The block builder selects valid Mantle Transactions (as defined in Mantle) from its mempool and includes references to them in the proposal.
- The block builder populates the block header of the block proposal.
-
The block proposer sends the block proposal to the Blend network.
-
The validators receive the block proposal.
-
The validators validate the block proposal.
- They validate the block header.
- They retrieve complete transactions from their mempool that are referred in the block.
- They validate each transaction included in the block, in the order the transactions appear, each against the state the preceding ones left.
-
The validators execute the block proposal.
- They append the
leader_voucherof the block to the set of reward vouchers, which the following epoch starts with. - They execute the Service Reward Distribution Protocol to generate reward notes locally and include them in the ledger.
- They derive the new blockchain state from the previous one by executing transactions as defined in Mantle, in that same pass, adopting the result only once the whole block has validated.
- They append the
Constructions
Hash
We are using two hashing algorithms that have the same output length of 256 bits (32 bytes) that are Poseidon2 and Blake2b.
Block Proposal
A block proposal, instead of containing complete Mantle Transactions of an unlimited size, contains short fixed-size references to the transactions. It also carries the full signed headers of the uncles it references, so that every node holding the block holds those headers as well. Its size therefore varies with both the number of referenced transactions and the number of referenced uncles: from 364 bytes, up to the maximum of 18,192 bytes at MAX_BLOCK_TXS references and MAX_UNCLES uncles. The indistinguishability of proposals required by the Blend Protocol is provided at the message layer: every dispersed proposal is padded up to the maximum payload size Max_Body_Length = 18192 bytes — set from the maximum proposal size — by Payload Formatting.
We define the following message structure:
class Proposal: # 364..18192 bytes
header: Header # 297 bytes
uncle_headers: list[SignedHeader] # 1 + u * 361 bytes, u <= MAX_UNCLES
references: References # 2..16386 bytes (2-byte count + entries)
signature: Ed25519Signature # 64 bytes
class SignedHeader: # 361 bytes
header: Header # 297 bytes
signature: Ed25519Signature # 64 bytes
Where:
headeris the header of the proposal; defined below: Header.uncle_headersis a variable-size list carrying the full signed headers of the referenced uncles, with at mostMAX_UNCLESentries. Each entry holds an uncle block header together with the signature of that header under itsleader_key— the header and signature as originally received with the uncle's own proposal. This list is the reference: the block ID (Cryptarchia Protocol) of an uncle is derived from its carried header and is not recorded separately. Like every list, it is serialized as a 1-byte little-endian element count followed by that many entries, and a decoder must reject a count exceedingMAX_UNCLES. Theunclesfield having been removed from the Header, each entry is a fixed 361 bytes — a 297-byte header plus a 64-byte signature — so the list of carried headers parses unambiguously from its element count alone. The whole list, signatures included, is committed byheader.body_rootand therefore by the block ID, so no two blocks sharing an ID can differ in a carried signature. The proposal length reveals how many uncles are referenced — proposal indistinguishability is provided by the message-layer padding of Payload Formatting, not by the encoding. The proposer chooses which uncles to reference according to Uncle Selection, and every entry must satisfy the validity rules of Uncle References or the block is rejected. The same list is carried over into the reconstructed Block, which is what makes every referenced uncle structurally available: those rules can be evaluated by any node holding the chain, including nodes bootstrapping from genesis that never receive the proposal.referencesis a variable-length list of up toMAX_BLOCK_TXSreferences to transactions, each being a 16-byte (REFERENCE_PREFIX_LENGTH) prefix of the transaction hash defined in Mantle Transaction.signatureis the signature of the completeheaderusing theleader_keyfrom theProofOfLeadership; the size of theEd25519Signaturetype is 64 bytes.
The proposal carries no padding of its own. Proposal indistinguishability is provided at the message layer: Payload Formatting already mandates a fixed body length (Max_Body_Length) for every dispersed payload, with shorter messages padded with random data and the true length carried in body_length. An in-proposal zero-padded layout would duplicate that guarantee while charging every proposal the full MAX_BLOCK_TXS cost even when it references few transactions. The padding lies outside the signed proposal and is discarded via body_length on decapsulation, so no consensus meaning ever attaches to it.
Note that this makes the random-padding requirement of Payload Formatting load-bearing for the first time. While the proposal was a constant size it always filled the body exactly and the padding path never fired for a real proposal. Implementations must pad with random data, not zeros, or the padding region will itself distinguish proposals by their reference count.
Header
class Header: # 297 bytes
bedrock_version: byte # 1 byte
parent_block: hash # 32 bytes
slot: SlotNumber # 8 bytes
body_root: hash # 32 bytes
proof_of_leadership: ProofOfLeadership # 224 bytes
Where:
bedrock_versionis the version of the proposal message structure that supports other protocols defined in linked reference; its size is 1 byte and is fixed to0x01.parent_blockis the block ID (Cryptarchia Protocol) of the parent block, validated and accepted by the block builder. It is used for the derivation of theAgedLedgerandLatestLedgervalues necessary for validating the PoL; the size of thehashis 32 bytes.slotis the consensus slot number; the size of theSlotNumbertype is 8 bytes.body_rootis the commitment to the block body — both the carrieduncle_headersand the transactions. It is computed as defined in step 4 of Block Header Validation, which combines the serializeduncle_headerslist with the root of the Merkle tree constructed from the full transaction hashes (defined in Mantle Transaction) — the same hashes used for constructing themempool_transactionsreferences list; the size of thehashis 32 bytes. Because that Merkle root is taken over the full hashes,body_rootuniquely binds the proposal to a specific ordered transaction selection even when two transactions share the samereferencesprefix, and it also binds the number of references; see Binding of the reference list. Since the block ID is taken over the header, committing the uncle headers here is what makes two blocks with the same ID identical byte for byte.proof_of_leadershipis the proof confirming that the sender is the leader; defined below: Proof of Leadership.
References
Each reference is a fixed-length prefix of the transaction hash rather than the full hash. The prefix length is the protocol parameter REFERENCE_PREFIX_LENGTH = 16 bytes, and the prefix is taken by:
REFERENCE_PREFIX_LENGTH = 16 # bytes
def prefix(hash_input: bytes, length: int) -> bytes:
return hash_input[:length]
class References: # 2..16386 bytes
mempool_transactions: list[bytes] # UINT16 count + len * REFERENCE_PREFIX_LENGTH
Where mempool_transactions is a variable-length list of up to MAX_BLOCK_TXS references to transactions, each being prefix(mantle_txhash(tx), REFERENCE_PREFIX_LENGTH) of the transaction hash defined in Mantle Transaction.
The list is not padded. As specified in Canonical Encoding, it is serialized as a 2-byte little-endian element count followed by that many REFERENCE_PREFIX_LENGTH-byte entries, so its encoded size is 2 + len(mempool_transactions) * REFERENCE_PREFIX_LENGTH bytes — 2 bytes when the proposal references no transaction, and 2 + 1024 * 16 = 16386 bytes at MAX_BLOCK_TXS.
A decoder must reject a count greater than MAX_BLOCK_TXS before allocating for it or performing any mempool lookup, on every ingress path.
Proof of Leadership
class ProofOfLeadership: # 224 bytes
proof: Groth16Proof # 128 bytes
entropy_contribution: zkhash # 32 bytes
leader_key: Ed25519PublicKey # 32 bytes
leader_voucher: RewardVoucher # 32 bytes
Where:
proofis the proof confirming that the proposal is constructed by the leader; the size of theGroth16Prooftype is 128 bytes (2 compressed and 1 compressed BN256 elements).entropy_contributionis the output of the PoL contribution for Cryptarchia entropy; the size of thezkhashtype is 32 bytes.leader_keyis the one-timeEd25519PublicKeyused for signing theProposal. This binds the content of the proposal with theProofOfLeadership; the size of theEd25519PublicKeytype is 32 bytes.leader_voucheris the voucher value used for retrieving the reward by the leader for proposal; the size of theRewardVoucheris 32 bytes.
Field order. The order above is the wire order, i.e. the order in which these fields are concatenated by Canonical Encoding. The
block_idpreimage in Cryptarchia Protocol absorbs the same four fields in a different order (leader_voucher,entropy_contribution,proof,leader_key). This is deliberate, not an inconsistency:block_idis a domain-separated enumeration of header fields rather than a re-serialization of the header, so the two orders are independent and both are normative. Changing either one changes a different thing — the wire format in the first case, block identity in the second.
Canonical Encoding
This section defines the byte-level wire format of the block proposal. It follows the same conventions as the Mantle Transaction Encoding, in particular "All multi-byte integers use little-endian encoding" and "Any lists are length-prefixed with fixed width uints", and reuses its primitive terminals rather than redefining them.
The encoding is canonical: every valid proposal has exactly one byte representation, and every byte string decodes to at most one proposal. The following rules are normative.
- All multi-byte integers are little-endian.
- The fields of a structure are concatenated in the order in which they are declared, with no separators, no alignment padding, and no per-field framing.
- A list is encoded as a fixed-width little-endian element count followed by that many elements, each encoded individually. The width is given per list below — a
UINT16forReferences, a single byte forUncleHeaders, whoseMAX_UNCLESbound one byte covers — following the Mantle Transaction Encoding convention that lists are "length-prefixed with fixed width uints". The count must not exceed the list's declared bound. - Decoding must consume the input exactly. A decoder must reject input that ends before the structure is complete, and must equally reject input with bytes remaining after it. Accepting trailing bytes would allow two distinct wire messages to decode to the same proposal, which is a parser differential and therefore a consensus-split risk.
Proposal = Header UncleHeaders References Ed25519Signature
Header = Version ParentBlock Slot BodyRoot ProofOfLeadership
Version = Byte ; fixed to 0x01
ParentBlock = Hash32
Slot = UINT64
BodyRoot = Hash32
UncleHeaders = UncleCount *SignedHeader
UncleCount = Byte ; MUST NOT exceed MAX_UNCLES
SignedHeader = Header Ed25519Signature
ProofOfLeadership = Groth16 EntropyContribution LeaderKey LeaderVoucher
EntropyContribution = FieldElement
LeaderKey = Ed25519PublicKey
LeaderVoucher = FieldElement
References = ReferenceCount *Reference
ReferenceCount = UINT16 ; MUST NOT exceed MAX_BLOCK_TXS
Reference = 16BYTE ; REFERENCE_PREFIX_LENGTH bytes
The terminals Byte, UINT16, UINT64, Hash32, FieldElement, Groth16, Ed25519PublicKey and Ed25519Signature are those defined in Mantle Transaction Encoding. Note in particular that FieldElement is a little-endian BN254 field element, which fixes the byte order of entropy_contribution and leader_voucher.
This yields the following sizes, where n is the number of references:
| Structure | Encoded size | Minimum | Maximum |
|---|---|---|---|
Header | 1 + 32 + 8 + 32 + 224 | 297 | 297 |
ProofOfLeadership | 128 + 32 + 32 + 32 | 224 | 224 |
SignedHeader | 297 + 64 | 361 | 361 |
UncleHeaders | 1 + 361u | 1 | 1,445 |
References | 2 + 16n | 2 | 16,386 |
Proposal | 297 + (1 + 361u) + (2 + 16n) + 64 | 364 | 18,192 |
The maximum of 18,192 bytes, at n = MAX_BLOCK_TXS references and u = MAX_UNCLES uncles, is what Payload Formatting uses as Max_Body_Length.
Block
A block is what a proposal becomes once its references have been resolved against the mempool, and it is the unit that nodes store, execute and serve to their peers — in particular over Downloading Blocks during synchronization, where proposals are never transferred.
class Block:
header: Header # 297 bytes
signature: Ed25519Signature # 64 bytes
uncle_headers: list[SignedHeader] # 1 + n × 361 bytes, n <= MAX_UNCLES
transactions: list[SignedMantleTx] # up to MAX_BLOCK_SIZE
Where:
headeris the header of the proposal the block was reconstructed from, unchanged; defined above: Header.signatureis the proposal signature over thatheader, carried over unchanged. It is retained becauseblock_idcommits to the header alone and therefore does not cover the signature, so a node that receives the block without ever seeing the proposal — as happens during synchronization — would otherwise have nothing to check against step 9 of Block Header Validation; the size of theEd25519Signaturetype is 64 bytes.uncle_headersis the list carried over verbatim from the proposal, defined above: Block Proposal. It is committed byheader.body_root, so it cannot be altered, reordered or truncated without changing the block ID, and every entry must satisfy the validity rules of Uncle References for the block to be valid.transactionsis the sequence of Mantle Transactions resolved fromreferencesduring Block Proposal Reconstruction.
The uncle_headers list is part of the block rather than only of the proposal because both Block Header Validation and the Total Stake Inference need it. A node that obtained the chain by synchronizing from its peers — which transfers blocks, not proposals — would otherwise hold no record of the referenced uncles at all: it could not apply the validity rules of Uncle References to the blocks it downloads, could not compute , and therefore could not derive the difficulty that gates Proof-of-Leadership validity in the following epoch. Carrying the list in the block makes both the validation and the inference reproducible from chain data alone, for a bootstrapping node exactly as for a node that followed the chain live.
As with the header, uncle_headers does not count towards MAX_BLOCK_SIZE, which bounds the block body — the serialized transactions — only. The uncle payload is bounded independently by MAX_UNCLES entries per block.
Proposal Construction
In this section, we explain how the block proposal structure presented above is populated by the consensus leader.
The block proposal is constructed by the leader of the current slot. The node becomes a leader only after successfully generating a valid PoL for a given (Epoch, Slot).
Prerequisites
Before constructing the proposal, the block builder must:
- Select a valid parent block referenced by
ParentBlockon which they will extend the chain. - Derive the required Ledger state snapshots
AgedLedgerandLatestLedgerfrom the state of the chain including the last block. - Select a valid unspent note winning the PoL.
- Generate a valid PoL proving leadership eligibility for
(Epoch, Slot)based on the selected note. Attach the PoL to a one-time Ed25519 public key used to sign the block proposal.
Only after the PoL is generated can the block proposal be constructed (see Proof of Leadership).
Construction Procedure
- Initialize proposal metadata with the last known state of the blockchain. Set the:
header:bedrock_versionparent_blockslotbody_root— left unset here; it is computed in step 4, once both parts of the body are knownproof_of_leadership:leader_voucherentropy_contributionproofleader_key
uncle_headers: the full signed headers — header and signature as received with the uncle's own proposal — of the uncles chosen by Uncle Selection, in selection order. Every entry must satisfy the validity rules of Uncle References against the chain the proposal extends, or the resulting block will be rejected; because those rules read only that chain and the entry itself, the proposer can verify this before publishing.
- Construct the
mempool_transactionsobject: - Select Mantle transactions:
- Choose up to
MAX_BLOCK_TXSvalidSignedMantleTxfrom the local mempool. - Ensure the selected transactions, in the order they are placed in, form a sequence that is valid under Block Proposal Validation.
- Choose up to
No prefix-collision avoidance is needed at selection time. At REFERENCE_PREFIX_LENGTH = 16 a collision between two distinct transaction hashes is infeasible to encounter or to manufacture (see Prefix length and collision resistance), so a 16-byte prefix identifies a transaction as unambiguously as the full hash does for reconstruction purposes.
- Derive references values:
references: list[bytes] = [prefix(mantle_txhash(tx), REFERENCE_PREFIX_LENGTH)
for tx in mempool_transactions]
- Compute the
header.body_rootover both parts of the body, as defined in step 4 of Block Header Validation: the serializeduncle_headerslist from step 1, combined with the root of the Merkle tree constructed over the full transaction hashes of the selected transactions used to buildreferences. Thereferenceslist is left exactly as long as the selection; it is not padded. This step therefore comes after both uncle selection and transaction selection. - Sign the block proposal header, where
headeris its canonical encoding as defined in Canonical Encoding — the 297 bytes of the header alone, withoutuncle_headers, withoutreferencesand without the signature itself.
signature = Ed25519.sign(leader_secret_key, header)
- Assemble the block proposal.
proposal = Proposal(
header,
uncle_headers,
references,
signature
)
The PoL must have been generated beforehand and bound to the same Ledger view as mentioned in the Prerequisites.
The constructed proposal can now be broadcast to the network for validation.
Block Proposal Reconstruction
Given a block proposal, we assume transaction maturity. This means that the block proposal must include transactions from the mempool that have had enough time to spread across the network to reach all nodes. This ensures that transactions are widely known and recognized before block reconstruction.
This transaction maturity assumption holds true because the block proposal must be sent through the Blend Network before it reaches validators and can be reconstructed. The Blend Network introduces significant delay, ensuring that transactions referenced in the proposal have reached all network participants. This approach is crucial for maintaining smooth network operation and reducing the risk that proposals get rejected due to transactions being unavailable to some validators. Moreover, by increasing the number of nodes that have seen the transaction, anonymity is also enhanced as the set of nodes with the same view is larger. This may result in increased difficulty—or even practical prevention—of executing deanonymization attacks such as tagging attacks.
Upon receipt of a block proposal, validators must confirm the presence of all referenced transactions within their local mempool. This verification is an absolute requirement—if even a single referenced transaction is missing from the validator's mempool, that validator must reject the proposal. This stringent validation protocol ensures only widely-distributed transactions are included in the blockchain, safeguarding against potential network state fragmentation. The check is against a local mempool, so see Reference Resolution for what such a rejection does and does not establish about the block.
The process works as follows:
- Transaction is added to the node mempool.
- Node sends the transaction to all its neighbors.
- Neighbors add the transaction to their own mempools and propagate it to their neighbors—transaction is gossiped throughout the network.
- Block builder selects a transaction from its local mempool, which is guaranteed to be propagated through the network due to steps 1-3.
- Block builder constructs a block proposal with references to selected transactions.
- Block proposal is sent through the Blend Network, which requires multiple rounds of gossiping. This introduces a delay that ensures the transaction has reached most of the network participants' mempools.
- Block proposal is received by validators.
- Validators match each reference prefix in
referencesagainst the transactions in their local mempool. - If every reference resolves to a mempool transaction and the resolved set reproduces
header.body_root, the block proposal is reconstructed and proceeds to further validation steps; otherwise the validator rejects it, without that being a finding of invalidity — see Reference Resolution.
Reference Resolution
A reference is a 16-byte (REFERENCE_PREFIX_LENGTH) prefix of the transaction hash. Because two distinct transaction hashes cannot be made or found to share a 16-byte prefix at any feasible cost (see Prefix length and collision resistance), a validator resolves each reference to exactly one local mempool transaction:
def resolve(reference, mempool):
matches = [tx for tx in mempool
if prefix(mantle_txhash(tx), REFERENCE_PREFIX_LENGTH) == reference]
return matches[0] if len(matches) == 1 else None
A reference resolves only when the match is unique. Zero matches means the transaction is absent locally; two or more would mean a prefix collision, which is infeasible to manufacture and vanishingly unlikely to occur by chance, and is treated as unresolved rather than searched, so that resolution never branches. Because the match is unique when it exists, the result does not depend on the order in which the mempool is scanned.
Reconstruction considers every entry of references; the list has no padding. Nothing in the encoding forbids the same reference appearing more than once: each occurrence resolves independently to the same transaction, the reconstructed sequence contains that transaction more than once, and header.body_root decides — as for any sequence — whether that is what the proposer committed to. Whether such a sequence is valid is not a reconstruction question; it is decided by Mantle transaction validation and execution, where a repeated transaction conflicts with itself, spending the same notes twice. A proposal whose reference count exceeds MAX_BLOCK_TXS is rejected at decode time, as specified in References. The proposal is reconstructed when every reference resolves to a transaction and the body root over the carried uncle_headers and the resolved transactions' full hashes reproduces header.body_root; otherwise it is rejected. Resolution is a function of the proposal and the validator's mempool alone, so two validators holding the same mempool always reach the same decision.
A reference resolving to no local transaction means the referenced transaction has not reached this validator's mempool, which the transaction maturity assumption above is designed to prevent. The validator rejects the proposal and does not build on it. It must not record that outcome as a verdict on block_id: the block may be received again, in full, through chain synchronisation, and is then validated on its merits. This is unlike a failure implied by the header bytes alone — a wrong version, an invalid proof of leadership — which condemns the block the header names, identically at every node, and is final. Note that not every mempool-independent failure is of that kind: a frame that does not decode or a bad signature condemns only the received copy, because the bytes at fault lie outside the header that block_id is computed from. Block Proposal Validation states the full classification.
The header.body_root check is what makes resolution safe against the residual, cryptographically-negligible case of a prefix matching the wrong transaction: a mismatched set never reproduces the root.
Binding of the reference list
Neither the reference entries nor their count are covered by signature or by block_id, both of which range over header only. header.body_root is therefore the sole mechanism binding a proposal to its reference list and to the number of references: any altered reference set, and any altered count, fails to reproduce header.body_root and the proposal is rejected. The argument is given in Why body_root alone binds the reference list.
Two operational consequences follow:
- Tampered copies of a genuine proposal are cheap to produce, since
referencesis unauthenticated.block_idis computable from the 297-byte header alone and is shared by every variant of one proposal, which cuts both ways: once a copy has been accepted, every later copy carrying thatblock_idcan be dropped at a glance, collapsing all tampered variants into a single unit of reconstruction work — but a rejected copy must not suppress later ones, or the first tampered variant to arrive would censor the genuine proposal behind it. Duplicate suppression onblock_idis therefore keyed on acceptance, never on receipt. - Reconstruction must not be the first expensive step. It is a mempool scan, so it should follow signature and PoL verification, and an unauthenticated proposal must be discarded before any mempool scanning takes place.
Reconstruction assembles the Block from the proposal's header and signature, its uncle_headers copied over verbatim, and the transactions resolved from references in the order the references appear. The uncle_headers list is retained rather than discarded: it is not recoverable from the mempool, it is committed by header.body_root and so cannot be dropped without invalidating the block, and once the proposal has been consumed the block is the only carrier of the signed uncle headers that Block Header Validation and the Total Stake Inference need.
Block Proposal Validation
This section defines the procedure followed by a Logos Blockchain node to validate a received block proposal.
Given a proposal, a proposed block consisting of a header, uncle_headers, references and a signature. This block proposal is considered valid if the following conditions are met, checked in the order given so that the cheapest checks discard a malformed or unauthenticated proposal first.
The order is constrained as well as economical. Block Header Validation is defined over a block , but a proposal carries references rather than transactions, so the rules that range over transactions cannot be evaluated until reconstruction has produced them. They are therefore applied in the step where their operand exists, and each is named below.
-
Decoding The received bytes must decode to a
proposalunder Canonical Encoding: the frame must be consumed exactly, with no trailing bytes, thereferenceselement count must not exceedMAX_BLOCK_TXS, and theuncle_headerselement count must not exceedMAX_UNCLES. These checks precede any allocation proportional to a count and any mempool lookup. Because reconstruction produces exactly one transaction per reference, the first of them also discharges rule 3 of Block Header Validation,length(transactions) <= MAX_BLOCK_TXS. -
Header Validation The
headermust satisfy the rules of Block Header Validation that range over the header and the block tree alone: the bedrock version, the slot ordering against the parent, the wallclock check, the presence of the parent in the block tree, the height against the latest immutable block, and the leader's right to propose — that is, rules 1 and 5 through 9. These need no mempool access, and rule 9 coverssignatureand the proof of leadership, so an unauthenticated proposal is discarded here, before any mempool scanning. -
Uncle Validation Every entry of
uncle_headersmust be a valid uncle of the block, which is rule 10 of Block Header Validation (see Uncle References). Like the previous step this reads only the chain being extended and the carried entry, so it needs no mempool access and precedes reconstruction. At this point the carried entries are not yet authenticated:signaturedoes not cover them, and they are bound to the header only byheader.body_root, which is not checked until the next step. A failure here therefore condemns the received copy, not the block — it is handled exactly like a reconstruction failure, with no verdict recorded againstblock_id. A finding that the block carries an invalid uncle requires entries whose binding has been confirmed: a full block obtained through chain synchronisation, whosebody_rootis checkable immediately, is judged by rule 10 on its merits. -
Block Proposal Reconstruction Every
referencesprefix must resolve to a local mempool transaction, as defined in Reference Resolution, and the body root computed over the carrieduncle_headersand the resolved transactions must equalheader.body_root. This step producestransactionsand discharges rule 4 of Block Header Validation. A mismatch here means the message was corrupted in transit or malformed by its proposer, not that the block it names is invalid. Corruption of the header itself cannot reach this step — it would have failed the signature check in step 2 — so a mismatching copy has a genuine header over a tampered or damaged body, and carries the sameblock_idas the block it names. When that block is genuine, its well-formed bytes are held by the proposer and by every node that accepted it, and the proposal may be re-requested from any of them. That shared identity is exactly why no verdict about the block may be recorded from the mismatch: condemning theblock_idof a corrupted copy would condemn the genuine block with it. -
Block Body Validation With
transactionsnow available, the remaining rule of Block Header Validation applies: rule 2,bytes(transactions) <= MAX_BLOCK_SIZE. -
Mempool Transactions Validation
mempool_transactionsmust refer to a valid sequence of Mantle Transactions from the mempool. The transactions are validated in the order thereferencesresolve them, against a state that advances with them: each transaction is validated against the state the transactions preceding it left, the first one against the state the block inherits once the steps of Block Execution that precede it have been applied. Each transaction must be valid in that state according to the rules defined in the Mantle, which validates and executes its Operations along that same progression. Validation and execution are therefore one pass over one state, not two.
Block validity is consequently order-dependent, and the order the transactions appear in is normative. Two transactions consuming the same note make the block invalid whatever their order, since the second consumption finds the note gone; a transaction consuming a note an earlier transaction created is valid in that order and invalid in the reverse one.
In order to verify ZK proofs, they are batched for verification as explained in Batch verification of ZK proofs to get better performance. Batching covers the proof checks alone and does not change the state a transaction is validated in: the public inputs of every proof are taken from the state its transaction is reached in, and the state-dependent assertions still run in sequence.
If any of the above checks fail, the block proposal must be rejected. What the rejection establishes depends on which bytes the failed check read. block_id is computed from the 297-byte header alone, so every byte outside the header — uncle_headers, references, signature, trailing bytes — can be altered in a copy without changing the block_id it names, and none of those bytes are authenticated until header.body_root is confirmed in step 4. A failure detected in them is a property of the received copy, not of the block: a frame that does not decode (step 1), a bad signature (step 2), an invalid uncle entry (step 3) and a failure to reconstruct (step 4) each discard the copy without recording a verdict against block_id. Only a failure implied by the header bytes themselves — a wrong version, an invalid proof of leadership — condemns the block the header names, identically at every node. Treating any of the former as final would let an attacker censor a genuine block by circulating tampered copies of it: one flipped bit in the trailing signature, or one substituted uncle entry, leaves block_id unchanged. The mempool-dependence of step 4 adds one further distinction — a reference that fails to resolve locally may resolve at another node — described in Reference Resolution. Independently of what a rejection establishes about the block, nothing of the proposal is executed. The state progression the pass builds is a working one, adopted as the new chain state only once the last transaction of the block has validated: a single failed check anywhere in the block, in any transaction, in any Operation of any transaction, invalidates the whole block, so a node rejecting it holds exactly the state it held before it started processing it.
This ordering is specific to the proposal path. A block received in full through chain synchronisation carries its transactions from the start, so Block Header Validation applies to it as written, with no ordering constraint.
Block Execution
This section specifies how a Logos Blockchain node executes a valid block proposal to update its local state.
Given a ValidBlock that has successfully passed proposal validation, the node must, in this order:
- Append the
leader_vouchercontained in the block to the set of reward vouchers when the following epoch starts. - Execute the reward distribution protocol defined in Service Reward Distribution Protocol to generate reward notes locally and include them in the ledger.
- Execute the Mantle Transactions included in the block in the order they appear, using the execution rules defined in the Mantle.
Steps 1 and 2 read the epoch and the state of the Service Declaration Protocol, never the transactions of the block, which is what lets them run before those transactions are validated and makes the reward notes of step 2 available to them.
The three steps stand or fall together, on a block that has validated in full: a block that fails validation at any point is not executed at all. The voucher of step 1 is appended to the set the following epoch starts with, so it lands at the epoch boundary rather than with the other two.
The carried uncle_headers are not executed. A referenced uncle is not part of the chain; therefore, its transactions have no effect on the ledger state. The uncles are used only as evidence of consensus participation for the Total Stake Inference.
Annex
Prefix length and collision resistance
REFERENCE_PREFIX_LENGTH = 16 is chosen so that a collision between two 16-byte prefixes cannot be manufactured. Two distinct attacks have to be priced separately, because they cost very differently.
Write REFERENCE_PREFIX_LENGTH for the prefix length in bits, so at the chosen parameter.
Targeted collision. To make a reference resolve to the wrong transaction — for example to censor one specific transaction T — an attacker must produce a transaction whose prefix equals T's. That is a fixed target, matched with probability per attempt, so it costs ≈ work: infeasible by an enormous margin.
Self-collision. To merely create ambiguity — two transactions sharing a prefix, so a reference matches more than one — the attacker does not need a specific target. They generate their own candidate transactions and wait for any two to collide. This is a birthday search, which finds a colliding pair after only ≈ candidates rather than : the number of candidate pairs grows as the square of the number of candidates, so a collision appears once , i.e. . Grinding a candidate is cheap — mantle_txhash covers the MantleTx alone and not the op_proofs of the enclosing SignedMantleTx, so each candidate costs one encoding and one hash, and nothing is signed until a pair is found.
The self-collision cost is the one that governs the parameter, because it is the cheaper of the two and it is what an attacker needs to force reconstruction failures. At the 8-byte prefix of an earlier revision () it was only ≈ — under a second of GPU hashing — which is why that revision needed a construction-side ambiguity bound and a validator-side reconstruction cap to survive grindable collisions. At 16 bytes the birthday cost is ≈ : about 58 years on one GPU at hashes per second, and still ~214 days against a 100× adversary. Manufacturing even a single ambiguous pair is therefore infeasible, and the whole class of grinding attacks disappears along with the machinery that managed it. Every reference resolves to exactly one transaction, reconstruction is a deterministic lookup, and header.body_root remains the backstop for the residual, cryptographically-negligible random collision.
The cost is that references are twice as long as at 8 bytes, so the maximum proposal grows by 8,192 bytes. That still leaves it at 18,192 bytes against the 34,574 of a full-hash layout carrying the same uncles — a ≈1.9× reduction — and it buys reconstruction that no adversary can perturb — for a consensus structure, the stronger property is worth the bytes.
Why body_root alone binds the reference list
This annex substantiates Binding of the reference list.
It is not the case that tampering with references is caught by the signature. An attacker who truncates or extends the list and adjusts the 2-byte count to match produces a frame that is still well-formed and whose header bytes are untouched, so signature still verifies. Framing gives unambiguity, not tamper-detection.
What rules the tampering out is body_root. It is a domain-separated hash over the serialized uncle_headers and the Merkle root of the transaction hashes (Block Header Validation), so by collision-resistance of that outer hash it binds the Merkle root exactly, and the Merkle root binds the transaction set in two steps:
- The Merkle root is taken over the full 32-byte transaction hashes, with the leaf set padded to the next power of two using all-zero leaves. Changing the number of leaves across a power-of-two boundary changes the depth of the tree and therefore the root.
- Within a boundary, an added leaf would have to hash to the all-zero value to leave the root unchanged. Reconstruction rejects any reference that resolves to no mempool transaction, so every leaf must be the hash of a real Mantle Transaction, and producing one whose hash is all-zero is infeasible.
Relative to a design that carries the count in the signed header, this removes one of two independent mechanisms rather than adding one: the count would be bound both by the signature and by body_root, and is now bound by body_root alone. That is an acceptable trade because the count is not a security binding. Its only role is to remove the parsing ambiguity between a genuine all-zero reference and zero padding, and an explicit length prefix removes that ambiguity directly, at the same cost in bytes and without a fixed-size layout.
Batch verification of ZK proofs
Proofs of Claim
-
For each proof of Claim, the verifier collects the classic Groth16 elements required for verification. It includes the proof , and the public values for each proof of claim.
-
The verifier draws one random value for each proof .
-
The verifier computes:
-
for .
-
-
-
They test if .
Note that this batch verification of Groth16 proofs is the same as what is described in the Zcash paper, Appendix B.2.
ZkSignatures
The verifier follows the same procedure as in Proofs of Claim but with the Groth16 proofs of ZkSignatures.