395 lines
13 KiB
Rust
Raw Normal View History

2026-05-14 21:19:25 -04:00
use std::{collections::HashMap, path::Path, sync::Arc};
2024-11-25 07:26:16 +02:00
2026-05-14 21:19:25 -04:00
use anyhow::{Context as _, Result};
use common::{
HashType,
block::{Block, BlockMeta, MantleMsgId},
transaction::NSSATransaction,
};
2026-05-14 21:19:25 -04:00
use log::info;
use logos_blockchain_zone_sdk::sequencer::SequencerCheckpoint;
use nssa::V03State;
2026-05-14 21:19:25 -04:00
pub use storage::DbResult;
use storage::sequencer::RocksDBIO;
2024-11-25 07:26:16 +02:00
2026-01-27 10:09:34 -03:00
pub struct SequencerStore {
2026-05-14 21:19:25 -04:00
dbio: Arc<RocksDBIO>,
2025-07-24 08:21:20 -03:00
// TODO: Consider adding the hashmap to the database for faster recovery.
2025-11-18 19:31:03 +03:00
tx_hash_to_block_map: HashMap<HashType, u64>,
genesis_id: u64,
signing_key: nssa::PrivateKey,
2024-11-25 07:26:16 +02:00
}
2026-01-27 10:09:34 -03:00
impl SequencerStore {
2026-05-14 21:19:25 -04:00
/// Open existing database at the given location. Fails if no database is found.
pub fn open_db(location: &Path, signing_key: nssa::PrivateKey) -> DbResult<Self> {
let dbio = Arc::new(RocksDBIO::open(location)?);
let genesis_id = dbio.get_meta_first_block_in_db()?;
let last_id = dbio.latest_block_meta()?.id;
info!("Preparing block cache");
let mut tx_hash_to_block_map = HashMap::new();
for i in genesis_id..=last_id {
let block = dbio
.get_block(i)?
.expect("Block should be present in the database");
tx_hash_to_block_map.extend(block_to_transactions_map(&block));
}
info!(
"Block cache prepared. Total blocks in cache: {}",
tx_hash_to_block_map.len()
);
Ok(Self {
dbio,
tx_hash_to_block_map,
genesis_id,
signing_key,
})
}
2025-11-26 00:27:20 +03:00
/// Starting database at the start of new chain.
2024-11-25 07:26:16 +02:00
/// Creates files if necessary.
///
/// ATTENTION: Will overwrite genesis block.
2026-05-14 21:19:25 -04:00
pub fn create_db_with_genesis(
2025-09-03 10:29:51 +03:00
location: &Path,
2026-03-04 18:42:33 +03:00
genesis_block: &Block,
genesis_msg_id: MantleMsgId,
2026-05-14 21:19:25 -04:00
genesis_state: &V03State,
2025-09-03 10:29:51 +03:00
signing_key: nssa::PrivateKey,
2026-05-14 21:19:25 -04:00
) -> DbResult<Self> {
let dbio = Arc::new(RocksDBIO::create(
location,
genesis_block,
genesis_msg_id,
genesis_state,
)?);
2024-12-05 13:05:58 +02:00
let genesis_id = dbio.get_meta_first_block_in_db()?;
2026-05-14 21:19:25 -04:00
let tx_hash_to_block_map = block_to_transactions_map(genesis_block);
2024-12-05 13:05:58 +02:00
2025-07-22 10:23:52 -03:00
Ok(Self {
dbio,
tx_hash_to_block_map,
2026-03-03 23:21:08 +03:00
genesis_id,
2025-09-03 10:29:51 +03:00
signing_key,
2025-07-22 10:23:52 -03:00
})
2024-11-25 07:26:16 +02:00
}
2026-05-14 21:19:25 -04:00
/// Shared handle to the underlying rocksdb. Used to persist the zone-sdk
/// checkpoint from the sequencer's drive task without needing &mut to the
/// store.
#[must_use]
pub fn dbio(&self) -> Arc<RocksDBIO> {
Arc::clone(&self.dbio)
}
pub fn get_block_at_id(&self, id: u64) -> DbResult<Option<Block>> {
self.dbio.get_block(id)
2024-11-25 07:26:16 +02:00
}
2026-05-14 21:19:25 -04:00
pub fn delete_block_at_id(&mut self, block_id: u64) -> DbResult<()> {
self.dbio.delete_block(block_id)
2025-07-22 10:23:52 -03:00
}
2026-05-14 21:19:25 -04:00
pub fn mark_block_as_finalized(&mut self, block_id: u64) -> DbResult<()> {
self.dbio.mark_block_as_finalized(block_id)
}
/// Returns the transaction corresponding to the given hash, if it exists in the blockchain.
2026-05-14 21:19:25 -04:00
#[must_use]
pub fn get_transaction_by_hash(&self, hash: HashType) -> Option<NSSATransaction> {
let block_id = *self.tx_hash_to_block_map.get(&hash)?;
let block = self
.get_block_at_id(block_id)
.ok()
.flatten()
.expect("Block should be present since the hash is in the map");
for transaction in block.body.transactions {
if transaction.hash() == hash {
return Some(transaction);
2025-07-22 10:23:52 -03:00
}
}
panic!(
"Transaction hash was in the map but transaction was not found in the block. This should never happen."
);
2024-11-25 07:26:16 +02:00
}
2025-11-18 19:31:03 +03:00
2026-05-14 21:19:25 -04:00
pub fn latest_block_meta(&self) -> DbResult<BlockMeta> {
self.dbio.latest_block_meta()
2025-11-18 19:31:03 +03:00
}
2026-05-14 21:19:25 -04:00
#[must_use]
2026-03-09 18:27:56 +03:00
pub const fn genesis_id(&self) -> u64 {
2025-11-18 19:31:03 +03:00
self.genesis_id
}
2026-05-14 21:19:25 -04:00
#[must_use]
2026-03-09 18:27:56 +03:00
pub const fn signing_key(&self) -> &nssa::PrivateKey {
2025-11-18 19:31:03 +03:00
&self.signing_key
}
2026-05-14 21:19:25 -04:00
pub fn get_all_blocks(&self) -> impl Iterator<Item = DbResult<Block>> {
self.dbio.get_all_blocks()
}
2026-01-27 01:20:17 -03:00
pub(crate) fn update(
&mut self,
block: &Block,
msg_id: MantleMsgId,
state: &V03State,
2026-05-14 21:19:25 -04:00
) -> DbResult<()> {
2026-01-31 18:12:59 -03:00
let new_transactions_map = block_to_transactions_map(block);
self.dbio.atomic_update(block, msg_id, state)?;
2026-01-27 10:09:34 -03:00
self.tx_hash_to_block_map.extend(new_transactions_map);
Ok(())
2026-01-27 01:20:17 -03:00
}
2026-01-27 16:03:21 -03:00
2026-05-14 21:19:25 -04:00
pub fn get_nssa_state(&self) -> DbResult<V03State> {
self.dbio.get_nssa_state()
}
pub fn get_zone_checkpoint(&self) -> Result<Option<SequencerCheckpoint>> {
let Some(bytes) = self.dbio.get_zone_sdk_checkpoint_bytes()? else {
return Ok(None);
};
let checkpoint: SequencerCheckpoint = serde_json::from_slice(&bytes)
.context("Failed to deserialize stored zone-sdk checkpoint")?;
Ok(Some(checkpoint))
}
pub fn set_zone_checkpoint(&self, checkpoint: &SequencerCheckpoint) -> Result<()> {
let bytes =
serde_json::to_vec(checkpoint).context("Failed to serialize zone-sdk checkpoint")?;
self.dbio.put_zone_sdk_checkpoint_bytes(&bytes)?;
Ok(())
2026-01-27 16:03:21 -03:00
}
2024-11-25 07:26:16 +02:00
}
2025-07-22 10:23:52 -03:00
2025-10-25 00:30:04 -03:00
pub(crate) fn block_to_transactions_map(block: &Block) -> HashMap<HashType, u64> {
2025-07-22 10:23:52 -03:00
block
2025-09-03 10:29:51 +03:00
.body
2025-07-22 10:23:52 -03:00
.transactions
.iter()
2025-09-03 10:29:51 +03:00
.map(|transaction| (transaction.hash(), block.header.block_id))
2025-07-22 10:23:52 -03:00
.collect()
}
2025-07-22 12:27:42 -03:00
#[cfg(test)]
mod tests {
2026-03-04 18:42:33 +03:00
#![expect(clippy::shadow_unrelated, reason = "We don't care about it in tests")]
2025-09-03 10:29:51 +03:00
use common::{block::HashableBlockData, test_utils::sequencer_sign_key_for_testing};
2025-07-22 12:27:42 -03:00
use tempfile::tempdir;
2025-08-10 00:53:53 -03:00
2025-11-26 00:27:20 +03:00
use super::*;
2025-07-22 12:27:42 -03:00
#[test]
2026-03-04 18:42:33 +03:00
fn get_transaction_by_hash() {
2025-07-22 12:27:42 -03:00
let temp_dir = tempdir().unwrap();
let path = temp_dir.path();
2025-08-28 12:00:04 +03:00
2025-09-03 10:29:51 +03:00
let signing_key = sequencer_sign_key_for_testing();
let genesis_block_hashable_data = HashableBlockData {
block_id: 0,
prev_block_hash: HashType([0; 32]),
2025-09-03 10:29:51 +03:00
timestamp: 0,
transactions: vec![],
};
2025-09-03 10:29:51 +03:00
2026-01-29 15:21:15 -03:00
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
// Start an empty node store
2026-05-14 21:19:25 -04:00
let mut node_store = SequencerStore::create_db_with_genesis(
path,
&genesis_block,
[0; 32],
&testnet_initial_state::initial_state(),
signing_key,
)
.unwrap();
2025-08-05 14:59:20 +03:00
let tx = common::test_utils::produce_dummy_empty_transaction();
2025-08-12 12:18:13 -03:00
let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]);
2025-08-05 14:59:20 +03:00
// Try retrieve a tx that's not in the chain yet.
2025-08-07 15:19:06 -03:00
let retrieved_tx = node_store.get_transaction_by_hash(tx.hash());
2025-07-22 12:27:42 -03:00
assert_eq!(None, retrieved_tx);
// Add the block with the transaction
let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0);
node_store.update(&block, [1; 32], &dummy_state).unwrap();
// Try again
2025-08-07 15:19:06 -03:00
let retrieved_tx = node_store.get_transaction_by_hash(tx.hash());
assert_eq!(Some(tx), retrieved_tx);
2025-07-22 12:27:42 -03:00
}
#[test]
2026-03-04 18:42:33 +03:00
fn latest_block_meta_returns_genesis_meta_initially() {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path();
let signing_key = sequencer_sign_key_for_testing();
let genesis_block_hashable_data = HashableBlockData {
block_id: 0,
prev_block_hash: HashType([0; 32]),
timestamp: 0,
transactions: vec![],
};
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
let genesis_hash = genesis_block.header.hash;
2026-05-14 21:19:25 -04:00
let node_store = SequencerStore::create_db_with_genesis(
path,
&genesis_block,
[0; 32],
&testnet_initial_state::initial_state(),
signing_key,
)
.unwrap();
// Verify that initially the latest block hash equals genesis hash
let latest_meta = node_store.latest_block_meta().unwrap();
assert_eq!(latest_meta.hash, genesis_hash);
assert_eq!(latest_meta.msg_id, [0; 32]);
}
#[test]
2026-03-04 18:42:33 +03:00
fn latest_block_meta_updates_after_new_block() {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path();
let signing_key = sequencer_sign_key_for_testing();
let genesis_block_hashable_data = HashableBlockData {
block_id: 0,
prev_block_hash: HashType([0; 32]),
timestamp: 0,
transactions: vec![],
};
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
2026-05-14 21:19:25 -04:00
let mut node_store = SequencerStore::create_db_with_genesis(
path,
&genesis_block,
[0; 32],
&testnet_initial_state::initial_state(),
signing_key,
)
.unwrap();
// Add a new block
let tx = common::test_utils::produce_dummy_empty_transaction();
2026-03-09 18:27:56 +03:00
let block = common::test_utils::produce_dummy_block(1, None, vec![tx]);
let block_hash = block.header.hash;
let block_msg_id = [1; 32];
let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0);
node_store
.update(&block, block_msg_id, &dummy_state)
.unwrap();
// Verify that the latest block meta now equals the new block's hash and msg_id
let latest_meta = node_store.latest_block_meta().unwrap();
assert_eq!(latest_meta.hash, block_hash);
assert_eq!(latest_meta.msg_id, block_msg_id);
}
#[test]
2026-03-04 18:42:33 +03:00
fn mark_block_finalized() {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path();
let signing_key = sequencer_sign_key_for_testing();
let genesis_block_hashable_data = HashableBlockData {
block_id: 0,
prev_block_hash: HashType([0; 32]),
timestamp: 0,
transactions: vec![],
};
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
2026-05-14 21:19:25 -04:00
let mut node_store = SequencerStore::create_db_with_genesis(
path,
&genesis_block,
[0; 32],
&testnet_initial_state::initial_state(),
signing_key,
)
.unwrap();
// Add a new block with Pending status
let tx = common::test_utils::produce_dummy_empty_transaction();
2026-03-09 18:27:56 +03:00
let block = common::test_utils::produce_dummy_block(1, None, vec![tx]);
let block_id = block.header.block_id;
let dummy_state = V03State::new_with_genesis_accounts(&[], vec![], 0);
node_store.update(&block, [1; 32], &dummy_state).unwrap();
// Verify initial status is Pending
let retrieved_block = node_store.get_block_at_id(block_id).unwrap().unwrap();
assert!(matches!(
retrieved_block.bedrock_status,
common::block::BedrockStatus::Pending
));
// Mark block as finalized
node_store.mark_block_as_finalized(block_id).unwrap();
// Verify status is now Finalized
let finalized_block = node_store.get_block_at_id(block_id).unwrap().unwrap();
assert!(matches!(
finalized_block.bedrock_status,
common::block::BedrockStatus::Finalized
));
}
2026-05-14 21:19:25 -04:00
#[test]
fn open_existing_db_caches_transactions() {
let temp_dir = tempdir().unwrap();
let path = temp_dir.path();
let signing_key = sequencer_sign_key_for_testing();
let genesis_block_hashable_data = HashableBlockData {
block_id: 0,
prev_block_hash: HashType([0; 32]),
timestamp: 0,
transactions: vec![],
};
let genesis_block = genesis_block_hashable_data.into_pending_block(&signing_key, [0; 32]);
let tx = common::test_utils::produce_dummy_empty_transaction();
{
// Create a scope to drop the first store after creating the db
let mut node_store = SequencerStore::create_db_with_genesis(
path,
&genesis_block,
[0; 32],
&testnet_initial_state::initial_state(),
signing_key.clone(),
)
.unwrap();
// Add a new block
let block = common::test_utils::produce_dummy_block(1, None, vec![tx.clone()]);
node_store
.update(
&block,
[1; 32],
&V03State::new_with_genesis_accounts(&[], vec![], 0),
)
.unwrap();
}
// Re-open the store and verify that the transaction is still retrievable (which means it
// was cached correctly)
let node_store = SequencerStore::open_db(path, signing_key).unwrap();
let retrieved_tx = node_store.get_transaction_by_hash(tx.hash());
assert_eq!(Some(tx), retrieved_tx);
}
2025-07-22 12:27:42 -03:00
}