Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions crates/store/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use tokio::sync::oneshot;
use tracing::{info, instrument};

use crate::COMPONENT;
use crate::db::migrations::{bootstrap_database, migrate_database, verify_latest_schema};
use crate::db::migrations::{migrate_database, verify_latest_schema};
use crate::db::models::conv::SqlTypeConvert;
use crate::db::models::queries;
pub use crate::db::models::queries::{
Expand All @@ -51,6 +51,8 @@ fn default_storage_map_entries_limit() -> usize {
}

mod migrations;
#[cfg(test)]
pub(crate) use migrations::bootstrap_database;

#[cfg(test)]
mod tests;
Expand Down Expand Up @@ -193,7 +195,8 @@ impl Db {
err,
)]
pub fn bootstrap(database_filepath: PathBuf, genesis: GenesisBlock) -> anyhow::Result<()> {
bootstrap_database(&database_filepath).context("failed to bootstrap database schema")?;
migrations::bootstrap_database(&database_filepath)
.context("failed to bootstrap database schema")?;

let mut conn: SqliteConnection = diesel::sqlite::SqliteConnection::establish(
database_filepath.to_str().context("database filepath is invalid")?,
Expand Down
17 changes: 17 additions & 0 deletions crates/store/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,21 @@ pub enum StateInitializationError {
tree_root: Word,
block_root: Word,
},
#[error("loaded chain MMR cannot produce peaks for block {block_num}")]
ChainMmrLoadError {
block_num: BlockNumber,
#[source]
source: MmrError,
},
#[error(
"chain MMR commitment ({chain_mmr_commitment:?}) does not match expected chain commitment \
from block {block_num} ({block_header_commitment:?})"
)]
ChainMmrStorageDiverged {
block_num: BlockNumber,
chain_mmr_commitment: Word,
block_header_commitment: Word,
},
#[error(
"account state forest root ({forest_root}) does not match SQLite root \
({database_root}) for account {account_id}, slot {slot_name:?}. Delete the account \
Expand All @@ -136,6 +151,8 @@ pub enum StateInitializationError {
PublicAccountMissingDetails(AccountId),
#[error("failed to convert account to delta: {0}")]
AccountToDeltaConversionFailed(String),
#[error("genesis block missing. The database should be bootstrapped first.")]
GenesisBlockMissing,
}

// ENDPOINT ERRORS
Expand Down
126 changes: 125 additions & 1 deletion crates/store/src/state/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use miden_node_utils::clap::RocksDbOptions;
use miden_protocol::account::{AccountId, AccountStorageHeader, StorageSlotType};
use miden_protocol::block::account_tree::{AccountIdKey, AccountTree};
use miden_protocol::block::nullifier_tree::NullifierTree;
use miden_protocol::block::{BlockNumber, Blockchain};
use miden_protocol::block::{BlockHeader, BlockNumber, Blockchain};
#[cfg(not(feature = "rocksdb"))]
use miden_protocol::crypto::merkle::smt::MemoryStorage;
use miden_protocol::crypto::merkle::smt::{LargeSmt, LargeSmtError, SmtStorage};
Expand Down Expand Up @@ -462,16 +462,45 @@ pub fn load_smt<S: SmtStorage>(storage: S) -> Result<LargeSmt<S>, StateInitializ
/// Loads the blockchain MMR from all block headers in the database.
#[instrument(target = COMPONENT, skip_all)]
pub async fn load_mmr(db: &mut Db) -> Result<Blockchain, StateInitializationError> {
let latest_header = db.select_block_header_by_block_num(None).await?;
let block_commitments = db.select_all_block_header_commitments().await?;

// SAFETY: We assume the loaded MMR is valid and does not have more than u32::MAX entries.
let mmr = Mmr::try_from_iter(block_commitments.into_iter().map(BlockHeaderCommitment::word))
.expect("loaded MMR exceeds maximum allowed size");
let chain_mmr = Blockchain::from_mmr_unchecked(mmr);

verify_chain_mmr_consistency(&chain_mmr, latest_header.as_ref())?;

Ok(chain_mmr)
}

fn verify_chain_mmr_consistency(
chain_mmr: &Blockchain,
latest_header: Option<&BlockHeader>,
) -> Result<(), StateInitializationError> {
let Some(latest_header) = latest_header else {
return Err(StateInitializationError::GenesisBlockMissing);
};
Comment thread
Mirko-von-Leipzig marked this conversation as resolved.

let block_num = latest_header.block_num();
let expected_chain_commitment = latest_header.chain_commitment();
let actual_chain_commitment = chain_mmr
.peaks_at(block_num)
.map_err(|source| StateInitializationError::ChainMmrLoadError { block_num, source })?
.hash_peaks();

if actual_chain_commitment != expected_chain_commitment {
return Err(StateInitializationError::ChainMmrStorageDiverged {
block_num,
chain_mmr_commitment: actual_chain_commitment,
block_header_commitment: expected_chain_commitment,
});
}

Ok(())
}

/// Rebuilds SMT forest with storage map and vault Merkle paths for all public accounts.
#[instrument(target = COMPONENT, skip_all, fields(block.number = %block_num))]
pub async fn rebuild_account_state_forest(
Expand Down Expand Up @@ -645,17 +674,112 @@ fn verify_account_state_forest_record(

#[cfg(test)]
mod tests {
use diesel::{ExpressionMethods, RunQueryDsl};
use miden_protocol::account::{
AccountId,
AccountStorageHeader,
StorageSlotHeader,
StorageSlotName,
StorageSlotType,
};
use miden_protocol::block::BlockHeader;
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey;
use miden_protocol::crypto::merkle::mmr::Mmr;
use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE;
use miden_protocol::utils::serde::Serializable;

use super::*;

fn build_headers(count: u32) -> Vec<BlockHeader> {
let mut mmr = Mmr::new();
let mut headers = Vec::new();

for block_num in 0..count {
let chain_commitment = mmr.peaks().hash_peaks();
let header =
BlockHeader::mock(block_num, Some(chain_commitment), None, &[], Word::default());
mmr.add(header.commitment()).expect("test MMR should accept block commitment");
headers.push(header);
}

headers
}

#[test]
fn chain_mmr_consistency_accepts_valid_loaded_chain() {
let headers = build_headers(5);
let mmr = Mmr::try_from_iter(headers.iter().map(BlockHeader::commitment))
.expect("test MMR should build");
let blockchain = Blockchain::from_mmr_unchecked(mmr);

verify_chain_mmr_consistency(&blockchain, headers.last())
.expect("valid loaded chain MMR should match latest header chain commitment");
}

#[test]
fn chain_mmr_consistency_rejects_corrupted_loaded_chain() {
let headers = build_headers(5);
let mut commitments = headers.iter().map(BlockHeader::commitment).collect::<Vec<_>>();
commitments[2] = Word::from([42, 0, 0, 0u32]);

let mmr = Mmr::try_from_iter(commitments).expect("test MMR should build");
let blockchain = Blockchain::from_mmr_unchecked(mmr);

let error = verify_chain_mmr_consistency(&blockchain, headers.last())
.expect_err("corrupted chain MMR should be rejected");

assert_matches::assert_matches!(
error,
StateInitializationError::ChainMmrStorageDiverged {
block_num,
chain_mmr_commitment,
block_header_commitment,
} if block_num == headers.last().unwrap().block_num()
&& chain_mmr_commitment != block_header_commitment
&& block_header_commitment == headers.last().unwrap().chain_commitment()
);
}

#[tokio::test]
#[miden_node_test_macro::enable_logging]
async fn load_mmr_rejects_chain_mmr_mismatch_from_database() {
let temp_dir = tempfile::tempdir().expect("temp directory should be created");
let db_path = temp_dir.path().join("store.sqlite");
crate::db::bootstrap_database(&db_path).expect("test database should bootstrap");

let headers = build_headers(5);
let signing_key = SigningKey::new();
let mut db = crate::db::Db::load(db_path).await.expect("test database should load");

db.query("insert corrupted block headers", move |conn| {
for header in &headers {
let signature = signing_key.sign(header.commitment());
crate::db::models::queries::insert_block_header(conn, header, &signature)?;
}

diesel::update(crate::db::schema::block_headers::table)
.filter(crate::db::schema::block_headers::block_num.eq(2_i64))
.set(
crate::db::schema::block_headers::commitment
.eq(Word::from([42, 0, 0, 0u32]).to_bytes()),
)
.execute(conn)?;

Ok::<_, DatabaseError>(())
})
.await
.expect("test block headers should be inserted");

let error = load_mmr(&mut db)
.await
.expect_err("startup MMR load should reject inconsistent block headers");

assert_matches::assert_matches!(
error,
StateInitializationError::ChainMmrStorageDiverged { .. }
);
}

#[test]
fn account_state_forest_consistency_detects_storage_map_root_mismatch() {
let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)
Expand Down
Loading