mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-07-09 15:29:34 +00:00
183 lines
6.2 KiB
Rust
183 lines
6.2 KiB
Rust
//! Validating and applying channel L2 blocks to a `V03State`.
|
|
//!
|
|
//! These primitives are shared by the consumers that reconstruct L2 state from
|
|
//! the Bedrock channel.
|
|
|
|
use common::{
|
|
HashType,
|
|
block::Block,
|
|
transaction::{LeeTransaction, clock_invocation},
|
|
};
|
|
use lee::{GENESIS_BLOCK_ID, V03State};
|
|
use lee_core::BlockId;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Why a channel L2 block could not be validated or applied.
|
|
///
|
|
/// Persisted in `RocksDB` (via the Indexer's stall reason), so every variant
|
|
/// must be `Clone + Serialize + Deserialize`.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
|
|
pub enum BlockIngestError {
|
|
#[error("Failed to deserialize L2 block: {0}")]
|
|
/// Here we store the error string that is derived from [`borsh::from_slice`]'s [`Err`].
|
|
Deserialize(String),
|
|
#[error("Unexpected block id: expected {expected}, got {got}")]
|
|
UnexpectedBlockId { expected: BlockId, got: BlockId },
|
|
#[error("Broken chain link: expected prev {expected_prev}, got {got_prev}")]
|
|
BrokenChainLink {
|
|
expected_prev: HashType,
|
|
got_prev: HashType,
|
|
},
|
|
#[error("Block hash mismatch: computed {computed}, header {header}")]
|
|
HashMismatch {
|
|
computed: HashType,
|
|
header: HashType,
|
|
},
|
|
#[error("Block has no transactions")]
|
|
EmptyBlock,
|
|
#[error("Last transaction must be the public clock invocation for the block timestamp")]
|
|
InvalidClockTransaction,
|
|
#[error("Genesis block must contain only public transactions")]
|
|
NonPublicGenesisTransaction,
|
|
#[error("State transition failed at transaction {tx_index}: {reason}")]
|
|
StateTransition {
|
|
/// Index of the failing transaction within the block body.
|
|
tx_index: u64,
|
|
/// Reason string from `lee::Error` to `anyhow::Error` to `{:#}`.
|
|
///
|
|
/// This is required because `lee::Error` is not `Clone + Serialize + Deserialize`, so we
|
|
/// cannot store it directly.
|
|
reason: String,
|
|
},
|
|
}
|
|
|
|
/// The last successfully applied block a candidate must extend.
|
|
pub struct Tip {
|
|
pub block_id: BlockId,
|
|
pub hash: HashType,
|
|
}
|
|
|
|
/// Checks that `block` is the valid continuation of `tip`: hash integrity,
|
|
/// then block-id continuity, then `prev_block_hash` linkage. A `None` tip
|
|
/// (cold store) expects the genesis block.
|
|
pub fn validate_against_tip(tip: Option<&Tip>, block: &Block) -> Result<(), BlockIngestError> {
|
|
let computed = block.recompute_hash();
|
|
if computed != block.header.hash {
|
|
return Err(BlockIngestError::HashMismatch {
|
|
computed,
|
|
header: block.header.hash,
|
|
});
|
|
}
|
|
|
|
match tip {
|
|
None => {
|
|
if block.header.block_id != GENESIS_BLOCK_ID {
|
|
return Err(BlockIngestError::UnexpectedBlockId {
|
|
expected: GENESIS_BLOCK_ID,
|
|
got: block.header.block_id,
|
|
});
|
|
}
|
|
}
|
|
Some(tip) => {
|
|
let expected = tip
|
|
.block_id
|
|
.checked_add(1)
|
|
.expect("block id should not overflow");
|
|
if block.header.block_id != expected {
|
|
return Err(BlockIngestError::UnexpectedBlockId {
|
|
expected,
|
|
got: block.header.block_id,
|
|
});
|
|
}
|
|
if block.header.prev_block_hash != tip.hash {
|
|
return Err(BlockIngestError::BrokenChainLink {
|
|
expected_prev: tip.hash,
|
|
got_prev: block.header.prev_block_hash,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Applies a block's transactions to `state`.
|
|
pub fn apply_block(block: &Block, state: &mut V03State) -> Result<(), BlockIngestError> {
|
|
let (clock_tx, user_txs) = block
|
|
.body
|
|
.transactions
|
|
.split_last()
|
|
.ok_or(BlockIngestError::EmptyBlock)?;
|
|
|
|
let expected_clock = LeeTransaction::Public(clock_invocation(block.header.timestamp));
|
|
if *clock_tx != expected_clock {
|
|
return Err(BlockIngestError::InvalidClockTransaction);
|
|
}
|
|
|
|
let is_genesis = block.header.block_id == GENESIS_BLOCK_ID;
|
|
for (tx_index, transaction) in user_txs.iter().enumerate() {
|
|
let state_transition = |err: anyhow::Error| BlockIngestError::StateTransition {
|
|
tx_index: tx_index.try_into().expect("tx index fits in u64"),
|
|
reason: format!("{err:#}"),
|
|
};
|
|
if is_genesis {
|
|
let LeeTransaction::Public(public_tx) = transaction else {
|
|
return Err(BlockIngestError::NonPublicGenesisTransaction);
|
|
};
|
|
state
|
|
.transition_from_public_transaction(
|
|
public_tx,
|
|
block.header.block_id,
|
|
block.header.timestamp,
|
|
)
|
|
.map_err(|err| state_transition(err.into()))?;
|
|
} else {
|
|
transaction
|
|
.clone()
|
|
.execute_on_state(state, block.header.block_id, block.header.timestamp)
|
|
.map_err(|err| state_transition(err.into()))?;
|
|
}
|
|
}
|
|
|
|
let LeeTransaction::Public(clock_public_tx) = clock_tx else {
|
|
return Err(BlockIngestError::InvalidClockTransaction);
|
|
};
|
|
state
|
|
.transition_from_public_transaction(
|
|
clock_public_tx,
|
|
block.header.block_id,
|
|
block.header.timestamp,
|
|
)
|
|
.map_err(|err| BlockIngestError::StateTransition {
|
|
tx_index: user_txs.len().try_into().expect("tx index fits in u64"),
|
|
reason: format!("{:#}", anyhow::Error::from(err)),
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn serializes_and_round_trips_externally_tagged() {
|
|
let err = BlockIngestError::UnexpectedBlockId {
|
|
expected: 5,
|
|
got: 7,
|
|
};
|
|
let value = serde_json::to_value(&err).expect("serialize");
|
|
assert_eq!(
|
|
value,
|
|
serde_json::json!({ "UnexpectedBlockId": { "expected": 5, "got": 7 } })
|
|
);
|
|
let back: BlockIngestError = serde_json::from_value(value).expect("deserialize");
|
|
assert!(matches!(
|
|
back,
|
|
BlockIngestError::UnexpectedBlockId {
|
|
expected: 5,
|
|
got: 7
|
|
}
|
|
));
|
|
}
|
|
}
|