From 3ebd7839b80176e88937c680393e90b85731368e Mon Sep 17 00:00:00 2001 From: Trey Del Bonis Date: Thu, 9 Jan 2025 17:52:08 -0500 Subject: [PATCH] state: removed `ClientStateWrite`, reworked types, added wrapper for safely mutating client state --- crates/state/src/batch.rs | 1 + crates/state/src/client_state.rs | 152 ++++++++++++++++++++++++--- crates/state/src/epoch.rs | 14 ++- crates/state/src/operation.rs | 170 +++---------------------------- crates/state/src/sync_event.rs | 6 -- 5 files changed, 169 insertions(+), 174 deletions(-) diff --git a/crates/state/src/batch.rs b/crates/state/src/batch.rs index a69b6ab13..30f8df2e3 100644 --- a/crates/state/src/batch.rs +++ b/crates/state/src/batch.rs @@ -241,6 +241,7 @@ impl BatchInfo { self.l1_pow_transition.1, ) } + /// check for whether the l2 block is covered by the checkpoint pub fn includes_l2_block(&self, block_height: u64) -> bool { let (_, last_l2_height) = self.l2_range; diff --git a/crates/state/src/client_state.rs b/crates/state/src/client_state.rs index ce160d603..27eec9cbb 100644 --- a/crates/state/src/client_state.rs +++ b/crates/state/src/client_state.rs @@ -7,12 +7,15 @@ use arbitrary::Arbitrary; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; use strata_primitives::{buf::Buf32, l1::L1BlockCommitment}; +use tracing::*; use crate::{ batch::{BatchInfo, BootstrapState}, epoch::EpochCommitment, id::L2BlockId, l1::{HeaderVerificationState, L1BlockId}, + operation::{ClientUpdateOutput, SyncAction}, + sync_event::SyncEvent, }; /// High level client's state of the network. This is local to the client, not @@ -133,14 +136,6 @@ pub struct SyncState { // TODO remove this pub(super) cur_epoch: u64, - /// Height of last L2 block we've chosen as the current tip. - // TODO remove this - pub(super) tip_slot: u64, - - /// Last L2 block we've chosen as the current tip. - // TODO remove this - pub(super) tip_blkid: L2BlockId, - /// L2 epoch that's been finalized on L1 and proven. pub(super) finalized_epoch: u64, @@ -161,8 +156,6 @@ impl SyncState { pub fn from_genesis_blkid(gblkid: L2BlockId) -> Self { Self { cur_epoch: 0, - tip_slot: 0, - tip_blkid: gblkid, finalized_epoch: 0, finalized_slot: 0, finalized_blkid: gblkid, @@ -172,12 +165,12 @@ impl SyncState { #[deprecated(note = "getting rid of CSM awareness of tip soon (STR-696)")] pub fn tip_height(&self) -> u64 { - self.tip_slot + panic!("csm: tip does not exist anymore"); } #[deprecated(note = "getting rid of CSM awareness of tip soon (STR-696)")] pub fn chain_tip_blkid(&self) -> &L2BlockId { - &self.tip_blkid + panic!("csm: tip does not exist anymore"); } pub fn finalized_epoch(&self) -> u64 { @@ -338,3 +331,138 @@ impl L1Checkpoint { } } } + +/// Wrapper structure for constructing a new client state and tracking emitted +/// actions while processing a sync event. +pub struct ClientStateMut { + state: ClientState, + actions: Vec, +} + +impl ClientStateMut { + /// Constructs a new instance from a base client state. + pub fn new(state: ClientState) -> Self { + Self { + state, + actions: Vec::new(), + } + } + + pub fn state(&self) -> &ClientState { + &self.state + } + + pub fn actions(&self) -> &[SyncAction] { + &self.actions + } + + pub fn into_output(self) -> ClientUpdateOutput { + ClientUpdateOutput::new(self.state, self.actions) + } + + fn sync_state(&self) -> Option<&SyncState> { + self.state.sync_state.as_ref() + } + + // Utility. + + fn expect_sync_state(&self) -> &SyncState { + self.state + .sync_state + .as_ref() + .expect("client_state: missing sync state") + } + + fn expect_sync_state_mut(&mut self) -> &mut SyncState { + self.state + .sync_state + .as_mut() + .expect("client_state: missing sync state") + } + + fn l1_state_mut(&mut self) -> &mut LocalL1State { + self.state.l1_view_mut() + } + + // Semantic mutator functions. + + /// Sets the flag that the chain is now active, which will eventually allow + /// the FCM to start. + pub fn activate_chain(&mut self) { + self.state.chain_active = true; + } + + /// Updates the L1 header verification state + pub fn update_l1_verif_state(&mut self, l1_vs: HeaderVerificationState) { + if self.state.genesis_verification_hash().is_none() { + info!(?l1_vs, "Setting genesis L1 verification state"); + self.state.genesis_l1_verification_state_hash = Some(l1_vs.compute_hash().unwrap()); + } + + self.state.l1_view_mut().header_verification_state = Some(l1_vs); + } + + /// Sets the L1 tip, performing no validation. + pub fn set_l1_tip(&mut self, l1bc: L1BlockCommitment) { + let l1view = self.l1_state_mut(); + l1view.tip_l1_block = l1bc; + } + + /// Rolls back our view of L1 blocks. + pub fn rollback_l1_tip(&mut self, l1_height: u64) { + let l1v = self.l1_state_mut(); + + // Keep pending checkpoints whose l1 height is less than or equal to + // rollback height + l1v.verified_checkpoints + .retain(|ckpt| ckpt.height <= l1_height); + } + + /// Accepts a L1 checkpoint. + pub fn accept_pending_checkpoint(&mut self, ckpt: L1Checkpoint) { + let l1view = self.l1_state_mut(); + l1view.verified_checkpoints.push(ckpt); + } + + /// Finalizes a checkpoint by L1 height. + // TODO convert this to epoch index + pub fn finalized_checkpoint(&mut self, l1_height: u64) { + // TODO verify this is all actually correct, could we split some of this + // out into calling code? + + let l1v = self.l1_state_mut(); + + let finalized_checkpts: Vec<_> = l1v + .verified_checkpoints + .iter() + .take_while(|ckpt| ckpt.height <= l1_height) + .collect(); + + let new_finalized = finalized_checkpts.last().cloned().cloned(); + let total_finalized = finalized_checkpts.len(); + debug!(?new_finalized, ?total_finalized, "Finalized checkpoints"); + + // Remove the finalized from pending and then mark the last one as last_finalized + // checkpoint + l1v.verified_checkpoints.drain(..total_finalized); + + if let Some(ckpt) = new_finalized { + // Check if heights match accordingly + if !l1v + .last_finalized_checkpoint + .as_ref() + .is_none_or(|prev_ckpt| ckpt.batch_info.epoch() == prev_ckpt.batch_info.epoch() + 1) + { + panic!("operation: mismatched indices of pending checkpoint"); + } + + let fin_blockid = *ckpt.batch_info.l2_blockid(); + l1v.last_finalized_checkpoint = Some(ckpt); + + // Update finalized blockid in StateSync + self.expect_sync_state_mut().finalized_blkid = fin_blockid; + } + + // TODO also write a `FinalizeBlock` sync action + } +} diff --git a/crates/state/src/epoch.rs b/crates/state/src/epoch.rs index b24d01272..bafec8307 100644 --- a/crates/state/src/epoch.rs +++ b/crates/state/src/epoch.rs @@ -2,6 +2,7 @@ use arbitrary::Arbitrary; use borsh::{BorshDeserialize, BorshSerialize}; +use serde::{Deserialize, Serialize}; use strata_primitives::{buf::Buf32, l1::L1BlockCommitment}; use crate::id::L2BlockId; @@ -36,7 +37,18 @@ pub struct EpochHeader { /// might be slightly different depending on the context and we'd want to name /// them explicitly. So avoid returning this in RPC endpoints, instead copy the /// fields to an RPC type that's more contextual to avoid misinterpretation. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Arbitrary, BorshDeserialize, BorshSerialize)] +#[derive( + Copy, + Clone, + Debug, + Eq, + PartialEq, + Arbitrary, + BorshDeserialize, + BorshSerialize, + Deserialize, + Serialize, +)] pub struct EpochCommitment { /// Epoch that this refers to. epoch: u64, diff --git a/crates/state/src/operation.rs b/crates/state/src/operation.rs index d01652921..2838a13ef 100644 --- a/crates/state/src/operation.rs +++ b/crates/state/src/operation.rs @@ -10,197 +10,57 @@ use tracing::*; use crate::{ batch::BatchCheckpointWithCommitment, client_state::{ClientState, L1Checkpoint, SyncState}, + epoch::EpochCommitment, id::L2BlockId, l1::{HeaderVerificationState, L1BlockId}, }; -/// Output of a consensus state transition. Both the consensus state writes and -/// sync actions. +/// Output of a consensus state transition. Both the new client state and sync +/// actions. #[derive( Clone, Debug, Eq, PartialEq, Arbitrary, BorshDeserialize, BorshSerialize, Deserialize, Serialize, )] pub struct ClientUpdateOutput { - writes: Vec, + new_state: ClientState, actions: Vec, } impl ClientUpdateOutput { - pub fn new(writes: Vec, actions: Vec) -> Self { - Self { writes, actions } + pub fn new(new_state: ClientState, actions: Vec) -> Self { + Self { new_state, actions } } - pub fn writes(&self) -> &[ClientStateWrite] { - &self.writes + pub fn new_state(&self) -> &ClientState { + &self.new_state } pub fn actions(&self) -> &[SyncAction] { &self.actions } - pub fn into_parts(self) -> (Vec, Vec) { - (self.writes, self.actions) + pub fn into_parts(self) -> (ClientState, Vec) { + (self.new_state, self.actions) } } -/// Describes possible writes to client state that we can make. We use this -/// instead of directly modifying the client state to reduce the volume of data -/// that we have to clone and save to disk with each sync event. -#[derive( - Clone, Debug, Eq, PartialEq, Arbitrary, BorshDeserialize, BorshSerialize, Deserialize, Serialize, -)] -pub enum ClientStateWrite { - /// Completely replace the full state with a new instance. - Replace(Box), - - /// Replace the sync state. - ReplaceSync(Box), - - /// Sets the flag that the chain is now active, kicking off the FCM to - /// start. - ActivateChain, - - /// Accept an L2 block and its height and update tip state. - AcceptL2Block(L2BlockId, u64), - - /// Rolls back checkpoints to whatever was present at this block height. - RollbackL1BlocksTo(u64), - - /// Sets the L1 tip, performing no validation. - SetL1Tip(L1BlockCommitment), - - /// Update the checkpoints - CheckpointsReceived(Vec), - - /// The previously confirmed checkpoint is finalized at given l1 height - CheckpointFinalized(u64), - - /// Updates the L1 header verification state - UpdateVerificationState(HeaderVerificationState), -} - /// Actions the client state machine directs the node to take to update its own /// bookkeeping. These should not be able to fail. #[derive( Clone, Debug, Eq, PartialEq, Arbitrary, BorshDeserialize, BorshSerialize, Deserialize, Serialize, )] pub enum SyncAction { - /// Extends our externally-facing tip to a new block ID. This might trigger - /// a reorg of some unfinalized blocks. We probably won't roll this block - /// back but we haven't seen it proven on-chain yet. This is also where - /// we'd build a new block if it's our turn to. - UpdateTip(L2BlockId), - - /// Marks an L2 blockid as invalid and we won't follow any chain that has - /// it, and will reject it from our peers. - MarkInvalid(L2BlockId), - - /// Finalizes a block, indicating that it won't be reverted. - FinalizeBlock(L2BlockId), - /// Indicates to the worker that it's safe to perform the L2 genesis /// operations and start the chain sync work, using a particular L1 block /// as the genesis lock-in block. - L2Genesis(L1BlockId), + L2Genesis(L1BlockCommitment), + + /// Finalizes an epoch, indicating that it won't be reverted. + FinalizeEpoch(EpochCommitment), /// Indicates to the worker to write the checkpoints to checkpoint db WriteCheckpoints(u64, Vec), + /// Indicates the worker to write the checkpoints to checkpoint db that appear in given L1 /// height FinalizeCheckpoints(u64, Vec), } - -/// Applies client state writes to a target state. -pub fn apply_writes_to_state( - state: &mut ClientState, - writes: impl Iterator, -) { - for w in writes { - use ClientStateWrite::*; - match w { - Replace(cs) => *state = *cs, - - ReplaceSync(nss) => { - state.set_sync_state(*nss); - } - - ActivateChain => { - // This is all this does. Actually setting the finalized tip is - // done by some sync event emitted by the FCM. - state.chain_active = true; - } - - UpdateVerificationState(l1_vs) => { - debug!(?l1_vs, "received HeaderVerificationState"); - if state.genesis_verification_hash().is_none() { - info!(?l1_vs, "Setting genesis L1 verification state"); - state.genesis_l1_verification_state_hash = Some(l1_vs.compute_hash().unwrap()); - } - - state.l1_view_mut().header_verification_state = Some(l1_vs); - } - - RollbackL1BlocksTo(height) => { - let l1v = state.l1_view_mut(); - - // Keep pending checkpoints whose l1 height is less than or equal to rollback height - l1v.verified_checkpoints - .retain(|ckpt| ckpt.height <= height); - } - - AcceptL2Block(blkid, height) => { - // TODO do any other bookkeeping - debug!(%height, %blkid, "received AcceptL2Block"); - let ss = state.expect_sync_mut(); - ss.tip_blkid = blkid; - ss.tip_slot = height; - } - - SetL1Tip(l1blk) => { - let l1v = state.l1_view_mut(); - l1v.tip_l1_block = l1blk; - } - - CheckpointsReceived(checkpts) => { - // Extend the pending checkpoints - state.l1_view_mut().verified_checkpoints.extend(checkpts); - } - - CheckpointFinalized(height) => { - let l1v = state.l1_view_mut(); - - let finalized_checkpts: Vec<_> = l1v - .verified_checkpoints - .iter() - .take_while(|ckpt| ckpt.height <= height) - .collect(); - - let new_finalized = finalized_checkpts.last().cloned().cloned(); - let total_finalized = finalized_checkpts.len(); - debug!(?new_finalized, ?total_finalized, "Finalized checkpoints"); - - // Remove the finalized from pending and then mark the last one as last_finalized - // checkpoint - l1v.verified_checkpoints.drain(..total_finalized); - - if let Some(ckpt) = new_finalized { - // Check if heights match accordingly - if !l1v - .last_finalized_checkpoint - .as_ref() - .is_none_or(|prev_ckpt| { - ckpt.batch_info.epoch() == prev_ckpt.batch_info.epoch() + 1 - }) - { - panic!("operation: mismatched indices of pending checkpoint"); - } - - let fin_blockid = *ckpt.batch_info.l2_blockid(); - l1v.last_finalized_checkpoint = Some(ckpt); - - // Update finalized blockid in StateSync - state.expect_sync_mut().finalized_blkid = fin_blockid; - } - } - } - } -} diff --git a/crates/state/src/sync_event.rs b/crates/state/src/sync_event.rs index 8b2381c4f..44d7ef0c2 100644 --- a/crates/state/src/sync_event.rs +++ b/crates/state/src/sync_event.rs @@ -27,11 +27,6 @@ pub enum SyncEvent { /// We've observed that the `genesis_l1_height` has reached maturity L1BlockGenesis(u64, HeaderVerificationState), - - /// Fork choice manager found a new valid chain tip block. At this point - /// we've already asked the EL to check if it's valid and know we *could* - /// accept it. This is also how we indicate the genesis block. - NewTipBlock(L2BlockId), } impl fmt::Display for SyncEvent { @@ -42,7 +37,6 @@ impl fmt::Display for SyncEvent { // TODO implement this when we determine wwhat useful information we can take from here Self::L1DABatch(h, _ckpts) => f.write_fmt(format_args!("l1da:<$data>@{h}")), Self::L1BlockGenesis(h, _st) => f.write_fmt(format_args!("l1genesis:{h}")), - Self::NewTipBlock(id) => f.write_fmt(format_args!("newtip:{id}")), } } }