Skip to main content

BlockOutputsArePersisted

Trait BlockOutputsArePersisted 

Source
pub trait BlockOutputsArePersisted:
    CommittedBlock
    + StorageAtomicity
    + CorrectValidator { }
Expand description

Lemma (A block’s outputs are persisted before it counts as processed). When a correct validator’s ChainTipState::next_block_height passes a height, the outputs of the block at that height are already in storage: the blobs it publishes, the events it emits, and the certificate itself. A crash before that point costs nothing but repeated work — the tip is what marks a block processed, so the block is handled again on restart and the writes are redone.

Code correspondence.

transitionChainWorkerState::process_confirmed_block
writeswrite_blobs_and_certificate, then write_events, then maybe_write_blob_states, and only then the tip
re-entry guardtip.next_block_height > height, returning BlockOutcome::Skipped

Proof. Three parts.

Ordering. None of these writes is atomic with any other — not even within a call, since write_blobs_and_certificate fans out to one write per storage partition (StorageAtomicity). process_confirmed_block issues the three writes and only afterwards dispatches to execute_contiguous_block (or execute_block_with_checkpoint_restore), which is where tip_state is set and save() runs. The writes are three separate awaited calls, not one batch, so a crash can land between them; what the argument needs is only that all of them precede the tip.

The tip is the guard. On restart the certificate is offered again, and the early return if !in_trust_set && tip.next_block_height > height decides whether the block is skipped. That test reads persisted chain state, which by StorageAtomicity is consistent, so a crash before save() leaves the block unprocessed and every write is reissued.

The repeats are byte-identical. The events come from block.body.events and the blobs from get_required_blobs over the block’s required_blob_ids and created_blobs — all fields of an already-certified block rather than products of execution. Nothing is recomputed, so the argument needs no appeal to DeterministicExecution. ∎

The order is load-bearing, not incidental. The outputs and the tip go to different key spaces of the same backing store, so one batch could in principle span them; none does. The ordering is what stands in for that atomicity, and only one order works. Outputs first costs at most repeated work, because the block is reprocessed. The tip first would be unrecoverable: the guard would classify the block as already processed and return BlockOutcome::Skipped, so the outputs would be missing permanently with nothing left to notice it.

What becomes visible early. Certificates, blobs and events are written to storage shared across chains — the channel by which chains observe each other at all — while the tip and the inboxes are per-chain state no other worker reads (SequentialChainState). Between the two writes, then, a block’s outputs are globally readable while the producing chain has not yet recorded the block locally. That is harmless because certificate.check precedes every one of these writes: what becomes visible early is content a quorum has already certified, and by CommitAgreement no conflicting block can ever be certified at that height. An uncertified block reaches none of these stores.

Preprocessing persists outputs with no tip to record them. In Preprocess mode, or Auto with an unbridgeable gap, preprocess_certified_block updates outboxes and event streams and deliberately does not advance the tip. The outputs of such a block are in storage, but the guard above will not short-circuit a later offer of the same certificate, so the writes are simply redone.

What this does not cover. It places a block’s outputs in the producing validator’s storage. That the resulting bundles reach the recipient chain’s inbox is EffectsSurviveRestart; that the outbox is ever drained is stated nowhere.

Implementors§