Finalization Flow
This document traces the complete block finalization flow including BLS aggregation, threshold signatures, and light client verification.
Overview
Section titled “Overview”Block finalization uses threshold BLS signatures - each validator signs with their DKG share, and 2f+1 signatures are aggregated into a single compact certificate. Light clients can verify finality using only the aggregate public key.
Block proposed --> Validators vote (BLS shares) --> Aggregate 2f+1 sigs -->Finality certificate --> Persist --> Light client verifiesArchitecture Stack
Section titled “Architecture Stack”Simplex Consensus (BFT voting) |Marshal (certificate persistence) |Application Actor (block execution, finality proofs) |Ashen Store (persistent finalized blocks)1. Block Proposal
Section titled “1. Block Proposal”Proposal Flow
Section titled “Proposal Flow”async fn handle_propose_request(&mut self, round: Round) { // 1. Get parent block let parent = self.tip.as_ref().unwrap_or(&genesis);
// 2. Select transactions from txpool let txs = self.txpool.select_block_txs(max_txs, max_gas);
// 3. Load parent VRF output for randomness let parent_vrf = if let Some(proof) = self.load_finality_proof(parent_height) { proof.vrf_output() } else { Hash::default() };
// 4. Build execution context let ctx = ExecutionContext { timestamp, validator_set_id, epoch, view, parent_vrf_output: parent_vrf, proposer: self.address, };
// 5. Execute and compute state root let (header, _) = self.exec.build_block_preview(parent, &txs, ctx)?;
// 6. Cache block (Arc for efficiency) let block = Arc::new(Block { header, execution }); self.blocks_by_hash.insert(block_hash, block.clone()); self.pending_proposals.put(round, block);
// 7. Send hash to Simplex simplex_mailbox.propose(block_hash).await;}2. Consensus Voting (Simplex)
Section titled “2. Consensus Voting (Simplex)”Validators vote on proposed blocks using their BLS shares from DKG.
BLS Scheme Types
Section titled “BLS Scheme Types”pub type PublicKey = ed25519::PublicKey; // Network identitypub type Scheme = bls12381_threshold::Scheme<PublicKey, MinSig>;pub type Evaluation = <MinSig as Variant>::Public; // G2 elementMinSig Variant (signature minimization):
- Public key: G2 element (~96 bytes)
- Signature: G1 element (~48 bytes)
- Aggregation: G1 point addition
Validator Signing
Section titled “Validator Signing”Each validator produces a partial BLS signature using their DKG share:
// Validator i signs with their sharelet partial_sig = ops::sign_message::<MinSig>( &my_share.secret, // Scalar from DKG Some(FINALIZE_DOMAIN), &proposal_hash,);// Result: G1 elementSignature Aggregation
Section titled “Signature Aggregation”When 2f+1 votes are collected:
// Aggregate via G1 point additionlet aggregate_sig = partial_sigs .iter() .fold(G1Projective::identity(), |acc, sig| acc + sig);
// Result: Single G1 element representing all votes3. Finality Certificate
Section titled “3. Finality Certificate”Structure
Section titled “Structure”pub struct FinalityProof { pub header: BlockHeader, // Finalized block pub epoch: u64, // Consensus epoch pub view: u64, // Round in epoch pub parent_view: u64, // Parent's round pub key_version: u32, // Validator set version pub certificate: FinalityCertificateBytes, // Threshold BLS sig}
pub struct FinalityCertificateBytes { pub bytes: Vec<u8>, // Borsh-encoded SimplexFinalization}Certificate Contents
Section titled “Certificate Contents”The certificate encodes a SimplexFinalization:
SimplexFinalization { proposal: Proposal { round: Round { epoch, view }, parent_view: View, payload: block_hash, // SHA256 digest }, certificate: ThresholdSignature, // Aggregated G1 point}VRF Output Derivation
Section titled “VRF Output Derivation”pub fn vrf_output(&self) -> Hash { let mut hasher = Blake3::new(); hasher.update(&self.certificate.bytes); Hash::from(hasher.finalize())}Properties:
- Unpredictable until
2f+1validators sign - Deterministic given the certificate
- Used for on-chain randomness, leader election
4. Aggregate BLS Public Key
Section titled “4. Aggregate BLS Public Key”pub struct AggregateBlsPublicKey(pub G2);Derivation from DKG
Section titled “Derivation from DKG”The aggregate key is the constant term of the DKG polynomial:
// From DKG outputlet aggregate_key = polynomial.evaluate(0); // G2 element
// Create validator set with aggregate keylet validator_set = ValidatorSet { aggregate_bls_pubkey: AggregateBlsPublicKey(aggregate_key), validators: vec![...],};Validator Set Structure
Section titled “Validator Set Structure”pub struct ValidatorSet { pub aggregate_bls_pubkey: AggregateBlsPublicKey, pub validators: Vec<Validator>,}
pub struct Validator { pub network_pubkey: NetworkPublicKey, // ed25519 pub voting_power: u64, pub address: Address, pub fee_recipient: Address,}5. Validator Set Commitment
Section titled “5. Validator Set Commitment”The validator set is committed via a Merkle-like tree:
pub fn commitment_root(&self) -> Hash { let mut leaves = Vec::with_capacity(self.validators.len() + 1);
// Leaf 0: aggregate BLS key leaves.push(self.aggregate_key_leaf_hash());
// Leaves 1..n: individual validators for i in 0..self.validators.len() { leaves.push(self.validator_leaf_hash(i)?); }
merkle_root(&leaves)}Aggregate Key Leaf
Section titled “Aggregate Key Leaf”pub fn aggregate_key_leaf_hash(&self) -> Hash { let preimage = AggregateKeyLeafPreimage { prefix: b"AGG_KEY_V1", aggregate_bls_pubkey: &self.aggregate_bls_pubkey, };
sha256(&preimage.to_bytes())}6. Persistent Storage
Section titled “6. Persistent Storage”Marshal Finalization Store
Section titled “Marshal Finalization Store”pub struct PersistentFinalizationStore<E> { archive: PrunableArchive<FourCap, E, Digest, Vec<u8>>,}
impl store::Certificates for PersistentFinalizationStore { async fn put( &mut self, height: u64, commitment: Digest, finalization: SimplexFinalization<Scheme, Digest>, ) -> Result<()> { let bytes = finalization.encode(); self.archive.put(height, commitment, bytes).await }}Application Block Finalization
Section titled “Application Block Finalization”async fn handle_finalized_block(&mut self, block: Block) { let height = block.header.height;
// 1. Execute transactions let meta = self.exec.apply_block(&block)?;
// 2. Update txpool self.txpool.on_applied_block(height, &self.exec.state, &block.transactions);
// 3. ATOMIC: Persist block + finalized height self.store.put_block_finalized(&block).await?; self.finalized_height = height;
// 4. Finalize state let root = self.exec.finalize_state(height)?;
// 5. Generate DA chunks let bundle = self.chunk_producer.encode_block(&block); self.pending_chunks.put(block_hash, bundle);
// 6. Update tip self.tip = Some(Arc::new(block));
// 7. Notify DKG self.dkg_finalization_tx.send(height);
// 8. WebSocket notifications self.bus.emit_block(BlockNotification { ... });}Finality Proof Persistence
Section titled “Finality Proof Persistence”async fn handle_tip_update(&mut self, height: u64, commitment: Digest) { // Get finalization from marshal let finalization = marshal_mailbox.get_finalization(height).await?;
// Get block let block = self.store.get_block_by_height(height).await?;
// Create FinalityProof let proof = FinalityProof::from_certificate( block.header.clone(), epoch, view, finalization.proposal.parent.get(), finalization.certificate, );
// Persist self.store.put_finality_proof(&proof).await?; self.latest_finality = Some(proof);}7. Light Client Verification
Section titled “7. Light Client Verification”Light Client Context
Section titled “Light Client Context”pub struct LightClientContext { pub validator_set: ValidatorSet, pub aggregate_key_proof: ValidatorProof, // Merkle proof pub version: u32,}Verification Function
Section titled “Verification Function”pub fn verify_finality_proof( proof: &FinalityProof, ctx: &LightClientContext,) -> Result<(), LightClientError> { // 1. Check validator set commitment let root = ctx.commitment_root(); if root != proof.header.validator_set_id.id { return Err(LightClientError::ValidatorSetIdMismatch); }
// 2. Verify aggregate key membership proof if ctx.aggregate_key_proof.leaf_index != 0 { return Err(LightClientError::InvalidMembershipProof); } if !ctx.aggregate_key_proof.verify(&root) { return Err(LightClientError::InvalidMembershipProof); }
// 3. Verify threshold BLS signature if !proof.verify(&ctx.validator_set.aggregate_bls_pubkey) { return Err(LightClientError::InvalidSignature); }
Ok(())}BLS Signature Verification
Section titled “BLS Signature Verification”pub fn verify(&self, aggregate_bls_pubkey: &AggregateBlsPublicKey) -> bool { // 1. Decode certificate let certificate = self.decode_certificate()?;
// 2. Reconstruct proposal let proposal = Proposal::new( Round::new(Epoch::new(self.epoch), View::new(self.view)), View::new(self.parent_view), self.block_id().into(), );
// 3. Create verifier with aggregate key let scheme = Scheme::certificate_verifier(aggregate_bls_pubkey.0);
// 4. Verify threshold signature scheme.verify_certificate( &mut rng, namespace(), Subject::Finalize { proposal: &proposal }, &certificate, )}8. Validator Set Transitions
Section titled “8. Validator Set Transitions”Announcement
Section titled “Announcement”When the validator set changes, the block header announces it:
pub struct BlockHeader { pub validator_set_id: ValidatorSetId, // Current pub next_validator_set_id: ValidatorSetId, // Next (if different) // ...}Light Client State Machine
Section titled “Light Client State Machine”pub struct LightClientState { pub context: LightClientContext, pub pending_update: Option<PendingUpdate>, pub current_epoch: u64, pub latest_height: u64,}
pub fn process_finality_proof(&mut self, proof: &FinalityProof) -> Result<()> { // Verify against current context verify_finality_proof(proof, &self.context)?;
// Check for validator set change announcement if proof.header.next_validator_set_id != proof.header.validator_set_id { self.pending_update = Some(PendingUpdate { next_validator_set_id: proof.header.next_validator_set_id.clone(), announced_at_epoch: proof.header.epoch, }); }
self.latest_height = proof.header.height; Ok(())}
pub fn apply_validator_set_update( &mut self, new_context: LightClientContext, transition_proof: &FinalityProof,) -> Result<()> { let pending = self.pending_update.as_ref()?;
// Verify new commitment matches announced if new_context.commitment_root() != pending.next_validator_set_id.id { return Err(LightClientError::TransitionCommitmentMismatch); }
// Verify proof with new context verify_finality_proof(transition_proof, &new_context)?;
// Must be at epoch boundary if transition_proof.header.epoch <= pending.announced_at_epoch { return Err(LightClientError::TransitionNotAtEpochBoundary); }
self.context = new_context; self.pending_update = None; Ok(())}9. Sequence Diagram
Section titled “9. Sequence Diagram”BLOCK PROPOSAL │ ├─ Application: handle_propose_request() │ ├─ Select transactions │ ├─ Load parent VRF output │ ├─ Build execution context │ ├─ Compute state root │ └─ Send block hash to Simplex │ ▼CONSENSUS VOTING (Simplex) │ ├─ Validators receive proposal ├─ Each validator signs with BLS share │ └─ partial_sig = sign(share, proposal_hash) ├─ Collect 2f+1 partial signatures └─ Aggregate: threshold_sig = sum(partial_sigs) │ ▼FINALIZATION CERTIFICATE (Marshal) │ ├─ Create SimplexFinalization │ ├─ proposal: (round, parent_view, block_hash) │ └─ certificate: aggregated BLS signature ├─ Encode to bytes └─ Store in PersistentFinalizationStore │ ▼FINALITY PROOF (Application) │ ├─ FinalityProof::from_certificate() │ ├─ header: BlockHeader │ ├─ epoch, view, parent_view │ ├─ key_version │ └─ certificate: encoded threshold sig │ ├─ VRF output = Blake3(certificate.bytes) │ └─ Persist to ChainStore │ ▼POST-FINALIZATION │ ├─ Execute transactions ├─ Update txpool ├─ Persist block (atomic with height) ├─ Finalize state ├─ Generate DA chunks ├─ Notify DKG coordinator └─ WebSocket notifications │ ▼LIGHT CLIENT VERIFICATION │ ├─ 1. Check validator_set_id matches commitment_root │ ├─ 2. Verify aggregate key Merkle proof │ ├─ leaf_index = 0 │ ├─ leaf_hash = aggregate_key_leaf_hash() │ └─ Merkle path leads to root │ └─ 3. Verify threshold BLS signature ├─ Decode certificate ├─ Reconstruct proposal ├─ Create verifier with aggregate key └─ Verify over Subject::Finalize10. Cryptographic Components
Section titled “10. Cryptographic Components”| Component | Type | Size | Purpose |
|---|---|---|---|
| Aggregate BLS Key | G2 | ~96 bytes | Threshold public key |
| Threshold Signature | G1 | ~48 bytes | Aggregated 2f+1 votes |
| Block Hash | SHA256 | 32 bytes | Payload commitment |
| VRF Output | Blake3 | 32 bytes | Verifiable randomness |
| Validator Set Root | SHA256 | 32 bytes | Merkle commitment |
| Partial Signature | G1 | ~48 bytes | Individual validator vote |
11. Safety Properties
Section titled “11. Safety Properties”| Property | Mechanism |
|---|---|
| Threshold Security | Requires 2f+1 of n validators |
| Non-Repudiation | Threshold BLS proves collective commitment |
| Unbiasable Randomness | VRF unpredictable until threshold reached |
| Crash Safety | Atomic block + height persistence |
| Validator Continuity | Epoch boundary transitions verified |
| Light Client Trust | Single aggregate key + Merkle proof sufficient |
Related Documentation
Section titled “Related Documentation”- DKG Flow - How threshold keys are generated
- Sealed Transactions - TLE using threshold sigs
- Light Clients - Light client architecture