From da3e9d6e3a47a7d388603ba2d113b1c1a3f22ef2 Mon Sep 17 00:00:00 2001 From: Mr-kay-cloud2 <100731016+Mr-kay-cloud2@users.noreply.github.com> Date: Tue, 19 May 2026 15:25:27 +0100 Subject: [PATCH 01/10] docs: fix run-node.sh (#2086) --- bin/network-monitor/README.md | 8 +++++--- bin/validator/src/commands/bootstrap.rs | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/bin/network-monitor/README.md b/bin/network-monitor/README.md index 604e054c88..257ab473bb 100644 --- a/bin/network-monitor/README.md +++ b/bin/network-monitor/README.md @@ -21,14 +21,14 @@ The monitor application supports configuration through both command-line argumen miden-network-monitor --help # Common usage examples -miden-network-monitor start --port 8080 --rpc-url http://localhost:50051 +miden-network-monitor start --port 8080 --rpc.listen http://localhost:50051 miden-network-monitor start --remote-prover-urls http://prover1.com:50052,http://prover2.com:50053 miden-network-monitor start --faucet-url http://localhost:8080 --enable-otel ``` **Available Options:** - `--network-name`: Display name of the network shown on the dashboard (default: `Localhost`) -- `--rpc-url`: RPC service URL (default: `http://localhost:50051`) +- `--rpc.listen`: RPC service URL (default: `http://localhost:50051`) - `--remote-prover-urls`: Comma-separated list of remote prover URLs. If omitted or empty, prover tasks are disabled. - `--faucet-url`: Faucet service URL for testing. If omitted, faucet testing is disabled. - `--explorer-url`: Explorer service GraphQL endpoint. If omitted, explorer checks are disabled. @@ -92,7 +92,9 @@ Starts the network monitoring service with the web dashboard. RPC status is alwa miden-network-monitor start # Start with custom configuration -miden-network-monitor start --port 8080 --rpc-url http://localhost:50051 + miden-network-monitor start \ + --port 8080 \ + --rpc.url "http://localhost:50051" # Enable network transaction service (both increment and tracking) with custom account file paths miden-network-monitor start \ diff --git a/bin/validator/src/commands/bootstrap.rs b/bin/validator/src/commands/bootstrap.rs index c0ad8b580b..3c88c049de 100644 --- a/bin/validator/src/commands/bootstrap.rs +++ b/bin/validator/src/commands/bootstrap.rs @@ -29,7 +29,7 @@ pub async fn bootstrap( .transpose()? .unwrap_or_default(); - for directory in [accounts_directory, genesis_block_directory] { + for directory in [accounts_directory, genesis_block_directory, data_directory] { ensure_empty_directory(directory)?; } From bc2fe092ceed7cd394da7c6a2de58d5273e32b70 Mon Sep 17 00:00:00 2001 From: SantiagoPittella Date: Tue, 19 May 2026 18:56:35 -0300 Subject: [PATCH 02/10] only use block streaming --- bin/ntx-builder/src/builder.rs | 306 ++---------------- bin/ntx-builder/src/chain_state.rs | 33 ++ bin/ntx-builder/src/clients/store.rs | 271 +--------------- bin/ntx-builder/src/coordinator.rs | 14 +- .../db/migrations/2026020900000_setup/up.sql | 7 +- bin/ntx-builder/src/db/mod.rs | 45 +-- bin/ntx-builder/src/db/models/conv.rs | 10 + .../src/db/models/queries/chain_state.rs | 16 +- bin/ntx-builder/src/db/models/queries/mod.rs | 4 +- .../src/db/models/queries/notes.rs | 25 ++ .../src/db/models/queries/tests.rs | 11 +- bin/ntx-builder/src/db/schema.rs | 1 + bin/ntx-builder/src/lib.rs | 152 +++------ bin/ntx-builder/src/test_utils.rs | 45 +-- crates/store/src/db/mod.rs | 54 +--- crates/store/src/db/models/mod.rs | 9 - .../store/src/db/models/queries/accounts.rs | 123 ------- crates/store/src/db/models/queries/notes.rs | 115 +------ crates/store/src/db/tests.rs | 89 +---- crates/store/src/errors.rs | 22 -- crates/store/src/server/ntx_builder.rs | 141 +------- crates/store/src/state/mod.rs | 68 +--- proto/proto/internal/store.proto | 85 ----- 23 files changed, 211 insertions(+), 1435 deletions(-) diff --git a/bin/ntx-builder/src/builder.rs b/bin/ntx-builder/src/builder.rs index 98a292e8d7..b61136861d 100644 --- a/bin/ntx-builder/src/builder.rs +++ b/bin/ntx-builder/src/builder.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use anyhow::Context; use futures::Stream; -use miden_node_proto::domain::account::NetworkAccountId; use miden_protocol::block::{BlockNumber, SignedBlock}; use tokio::net::TcpListener; use tokio::sync::mpsc; @@ -13,7 +12,6 @@ use tokio_stream::StreamExt; use crate::NtxBuilderConfig; use crate::actor::{AccountActorContext, ActorRequest}; use crate::chain_state::SharedChainState; -use crate::clients::StoreClient; use crate::clients::store::StoreError; use crate::committed_block::CommittedBlockEffects; use crate::coordinator::Coordinator; @@ -44,8 +42,6 @@ pub struct NetworkTransactionBuilder { config: NtxBuilderConfig, /// Coordinator for managing actor tasks. coordinator: Coordinator, - /// Client for the store gRPC API. - store: StoreClient, /// Database for persistent state. db: Db, /// Shared chain state updated by the event loop and read by actors. @@ -54,10 +50,7 @@ pub struct NetworkTransactionBuilder { actor_context: AccountActorContext, /// Stream of committed blocks from the store. block_stream: BlockStream, - /// The chain tip the catch-up phase must reach before actors are spawned. - catch_up_target: BlockNumber, - /// Highest block number applied to the DB so far. Used during catch-up to decide when to - /// stop draining the stream. + /// Highest block number applied to the DB so far. last_applied_block: BlockNumber, /// Database update requests from account actors. /// @@ -71,24 +64,20 @@ impl NetworkTransactionBuilder { pub(crate) fn new( config: NtxBuilderConfig, coordinator: Coordinator, - store: StoreClient, db: Db, chain_state: Arc, actor_context: AccountActorContext, block_stream: BlockStream, - catch_up_target: BlockNumber, last_applied_block: BlockNumber, actor_request_rx: mpsc::Receiver, ) -> Self { Self { config, coordinator, - store, db, chain_state, actor_context, block_stream, - catch_up_target, last_applied_block, actor_request_rx, } @@ -122,8 +111,19 @@ impl NetworkTransactionBuilder { server.serve(listener).await.context("ntx-builder gRPC server failed") }); - // Catch up to the chain tip before spawning any actors. - self.catch_up().await?; + // Spawn actors for accounts that have unconsumed notes inherited from a previous run. + // Accounts touched by the live stream are spawned reactively via send_targeted. + let pending = self + .db + .accounts_with_pending_notes() + .await + .context("failed to query accounts with pending notes")?; + if !pending.is_empty() { + tracing::info!(count = pending.len(), "spawning actors for inherited pending notes"); + for account_id in pending { + self.coordinator.spawn_actor(account_id, &self.actor_context); + } + } join_set.spawn(self.run_event_loop()); @@ -136,60 +136,8 @@ impl NetworkTransactionBuilder { Ok(()) } - /// Drains the block stream until the synced block reaches the catch-up target. - /// - /// During this phase the coordinator does not spawn any actors: we just apply committed-state - /// effects to the local DB and advance the shared chain state. - async fn catch_up(&mut self) -> anyhow::Result<()> { - let target = self.catch_up_target; - - if self.last_applied_block >= target { - tracing::info!( - current = %self.last_applied_block, - %target, - "ntx-builder already at or past chain tip" - ); - return Ok(()); - } - - tracing::info!( - current = %self.last_applied_block, - %target, - "ntx-builder catching up to chain tip before starting actors" - ); - - while self.last_applied_block < target { - let block = self - .block_stream - .next() - .await - .context("block stream ended during catch-up")? - .context("block stream failed during catch-up")?; - self.apply_committed_block(block).await?; - } - - tracing::info!( - tip = %self.last_applied_block, - "ntx-builder catch-up complete, starting actors" - ); - - Ok(()) - } - /// Runs the main event loop. async fn run_event_loop(mut self) -> anyhow::Result<()> { - // Spawn a background task to load network accounts from the store. - // Accounts are sent through a channel and processed in the main event loop. - let (account_tx, mut account_rx) = - mpsc::channel::(self.config.account_channel_capacity); - let account_loader_store = self.store.clone(); - let mut account_loader_handle = tokio::spawn(async move { - account_loader_store - .stream_network_account_ids(account_tx) - .await - .context("failed to load network accounts from store") - }); - // Main event loop. loop { tokio::select! { @@ -208,75 +156,16 @@ impl NetworkTransactionBuilder { self.handle_committed_block(block).await?; }, - // Handle account batches loaded from the store. - // Once all accounts are loaded, the channel closes and this branch - // becomes inactive (recv returns None and we stop matching). - Some(account_id) = account_rx.recv() => { - self.handle_loaded_account(account_id).await?; - }, // Handle requests from actors. Some(request) = self.actor_request_rx.recv() => { self.handle_actor_request(request).await?; }, - // Handle account loader task completion/failure. - // If the task fails, we abort since the builder would be in a degraded state - // where existing notes against network accounts won't be processed. - result = &mut account_loader_handle => { - result - .context("account loader task panicked") - .flatten()?; - - tracing::info!("account loading from store completed"); - account_loader_handle = tokio::spawn(std::future::pending()); - }, } } } - /// Handles account IDs loaded from the store by syncing state to DB and spawning actors. - #[tracing::instrument(name = "ntx.builder.handle_loaded_account", skip(self, account_id))] - async fn handle_loaded_account( - &mut self, - account_id: NetworkAccountId, - ) -> Result<(), anyhow::Error> { - // Skip accounts already populated by the catch-up phase. - if self - .db - .has_account(account_id) - .await - .context("failed to check for committed account")? - { - self.coordinator.spawn_actor(account_id, &self.actor_context); - return Ok(()); - } - - // Fetch account from store and write to DB. - let account = self - .store - .get_network_account(account_id) - .await - .context("failed to load account from store")? - .context("account should exist in store")?; - - let block_num = self.chain_state.chain_tip_block_number(); - let notes = self - .store - .get_unconsumed_network_notes(account_id, block_num.as_u32()) - .await - .context("failed to load notes from store")?; - - // Write account and notes to DB. - self.db - .sync_account_from_store(account_id, account.clone(), notes.clone()) - .await - .context("failed to sync account to DB")?; - - self.coordinator.spawn_actor(account_id, &self.actor_context); - Ok(()) - } - - /// Handles a committed block from the live stream: applies effects, updates chain state, and - /// notifies (and possibly respawns) affected actors. + /// Handles a committed block from the stream: persists effects together with the new chain + /// MMR, advances in-memory chain state, and notifies (or spawns) affected actors. #[tracing::instrument( name = "ntx.builder.handle_committed_block", skip(self, block), @@ -286,9 +175,16 @@ impl NetworkTransactionBuilder { let header = block.header().clone(); let block_num = header.block_num(); let effects = CommittedBlockEffects::from_signed_block(&block); + + // Compute the chain MMR that will result from advancing to this block, then persist it + // atomically with the block effects so the DB stays consistent across restarts. + let next_mmr = self + .chain_state + .next_chain_mmr(&header, self.config.max_block_count); + let result = self .coordinator - .apply_block(&effects) + .apply_block(&effects, next_mmr) .await .context("failed to apply committed block to DB")?; @@ -301,6 +197,13 @@ impl NetworkTransactionBuilder { for account_id in inactive_targets { self.coordinator.spawn_actor(account_id, &self.actor_context); } + // Spawn actors for newly-observed network accounts whose state changed but didn't receive + // a new note (e.g. a delta-only update). + for (account_id, _details) in &effects.network_account_updates { + if !self.coordinator.has_actor(*account_id) { + self.coordinator.spawn_actor(*account_id, &self.actor_context); + } + } self.coordinator.notify_accounts(&result.accounts_to_notify); // Also notify every active actor so any actor currently waiting on its own submitted @@ -311,20 +214,6 @@ impl NetworkTransactionBuilder { Ok(()) } - /// Applies a committed block during the catch-up phase. Does not notify actors (there are - /// none yet). The in-memory chain state is not touched during catch-up either, since it was - /// initialized to the chain tip we are catching up to. - async fn apply_committed_block(&mut self, block: SignedBlock) -> anyhow::Result<()> { - let block_num = block.header().block_num(); - let effects = CommittedBlockEffects::from_signed_block(&block); - self.coordinator - .apply_block(&effects) - .await - .context("failed to apply committed block during catch-up")?; - self.last_applied_block = block_num; - Ok(()) - } - /// Processes a request from an account actor. async fn handle_actor_request(&mut self, request: ActorRequest) -> Result<(), anyhow::Error> { match request { @@ -346,136 +235,3 @@ impl NetworkTransactionBuilder { } } -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use futures::stream; - use miden_protocol::block::SignedBlock; - use miden_protocol::crypto::merkle::mmr::{Forest, MmrPeaks, PartialMmr}; - use url::Url; - - use super::*; - use crate::NtxBuilderConfig; - use crate::actor::AccountActorContext; - use crate::clients::store::StoreError; - use crate::test_utils::{mock_block_header, mock_signed_block}; - - impl NetworkTransactionBuilder { - /// Test-only accessor for `last_applied_block`. - pub(crate) fn last_applied_block(&self) -> BlockNumber { - self.last_applied_block - } - } - - /// Constructs a `NetworkTransactionBuilder` suitable for testing `catch_up`. Only the fields - /// actually exercised during catch-up (`db`, `coordinator`, `chain_state`, `block_stream`, - /// `catch_up_target`, `last_applied_block`) are populated with real values; everything else - /// uses throwaway placeholders. - async fn builder_for_catch_up_test( - block_stream: BlockStream, - catch_up_target: BlockNumber, - last_applied_block: BlockNumber, - ) -> (NetworkTransactionBuilder, tempfile::TempDir) { - let (db, dir) = Db::test_setup().await; - let url = Url::parse("http://127.0.0.1:1").unwrap(); - let config = NtxBuilderConfig::new( - url.clone(), - url.clone(), - url.clone(), - PathBuf::from("unused.sqlite3"), - ); - let coordinator = Coordinator::new(4, 10, db.clone()); - let store = StoreClient::new(url); - let chain_mmr = PartialMmr::from_peaks(MmrPeaks::new(Forest::new(0), vec![]).unwrap()); - let chain_state = - Arc::new(SharedChainState::new(mock_block_header(0_u32.into()), chain_mmr)); - let actor_context = AccountActorContext::test(&db); - let (_request_tx, actor_request_rx) = mpsc::channel(1); - - let builder = NetworkTransactionBuilder::new( - config, - coordinator, - store, - db, - chain_state, - actor_context, - block_stream, - catch_up_target, - last_applied_block, - actor_request_rx, - ); - (builder, dir) - } - - /// Builds a `BlockStream` from a sequence of `SignedBlock`s. - fn ok_stream(blocks: Vec) -> BlockStream { - Box::pin(stream::iter(blocks.into_iter().map(Ok::<_, StoreError>))) - } - - #[tokio::test] - async fn catch_up_returns_immediately_when_already_at_tip() { - let target = BlockNumber::from(5u32); - // No blocks: if catch_up tried to pull from the stream, it would error. - let stream = ok_stream(vec![]); - - let (mut builder, _dir) = builder_for_catch_up_test(stream, target, target).await; - builder.catch_up().await.expect("catch_up should no-op when already at tip"); - - assert_eq!(builder.last_applied_block(), target); - } - - #[tokio::test] - async fn catch_up_drains_stream_to_target() { - let target = BlockNumber::from(3u32); - let blocks = vec![ - mock_signed_block(BlockNumber::from(1u32), &[], vec![]), - mock_signed_block(BlockNumber::from(2u32), &[], vec![]), - mock_signed_block(BlockNumber::from(3u32), &[], vec![]), - ]; - let stream = ok_stream(blocks); - - let (mut builder, _dir) = - builder_for_catch_up_test(stream, target, BlockNumber::from(0u32)).await; - builder.catch_up().await.expect("catch_up should drain stream up to target"); - - assert_eq!(builder.last_applied_block(), target); - } - - #[tokio::test] - async fn catch_up_errors_when_stream_ends_before_target() { - let target = BlockNumber::from(3u32); - // Stream yields only block 1, then ends. - let stream = ok_stream(vec![mock_signed_block(BlockNumber::from(1u32), &[], vec![])]); - - let (mut builder, _dir) = - builder_for_catch_up_test(stream, target, BlockNumber::from(0u32)).await; - let err = builder.catch_up().await.expect_err("catch_up should fail when stream ends"); - - assert!( - format!("{err:#}").contains("block stream ended during catch-up"), - "unexpected error message: {err:#}" - ); - // The block that did arrive should still have been applied. - assert_eq!(builder.last_applied_block(), BlockNumber::from(1u32)); - } - - #[tokio::test] - async fn catch_up_propagates_stream_error() { - let target = BlockNumber::from(2u32); - // First item is an error; catch_up should surface it without applying anything. - let stream: BlockStream = Box::pin(stream::iter(vec![Err::( - StoreError::MalformedResponse("boom".into()), - )])); - - let (mut builder, _dir) = - builder_for_catch_up_test(stream, target, BlockNumber::from(0u32)).await; - let err = builder.catch_up().await.expect_err("catch_up should propagate stream error"); - - assert!( - format!("{err:#}").contains("block stream failed during catch-up"), - "unexpected error message: {err:#}" - ); - assert_eq!(builder.last_applied_block(), BlockNumber::from(0u32)); - } -} diff --git a/bin/ntx-builder/src/chain_state.rs b/bin/ntx-builder/src/chain_state.rs index 6a67e1554c..87fc731500 100644 --- a/bin/ntx-builder/src/chain_state.rs +++ b/bin/ntx-builder/src/chain_state.rs @@ -41,12 +41,26 @@ impl ChainState { } } + /// Constructs the genesis bootstrap [`ChainState`] from the genesis block header. + /// + /// The chain MMR at the genesis tip is empty: by the convention that the MMR lags one block + /// behind the tip, the MMR for block 0 contains no leaves. + pub(crate) fn bootstrap_genesis(genesis_header: BlockHeader) -> Self { + debug_assert_eq!(genesis_header.block_num(), BlockNumber::GENESIS); + Self::new(genesis_header, PartialMmr::default()) + } + /// Consumes the chain state and returns the chain tip header and the partial blockchain as a /// tuple. pub fn into_parts(self) -> (BlockHeader, Arc) { (self.chain_tip_header, self.chain_mmr) } + /// Returns the chain MMR as a serializable [`PartialMmr`]. + pub(crate) fn mmr_for_persistence(&self) -> PartialMmr { + self.chain_mmr.mmr().clone() + } + /// Updates the chain tip and prunes old blocks from the MMR. fn update_chain_tip(&mut self, tip: BlockHeader, max_block_count: usize) { // Update MMR which lags by one block. @@ -73,6 +87,10 @@ impl SharedChainState { Self(RwLock::new(ChainState::new(chain_tip_header, chain_mmr))) } + pub fn from_state(state: ChainState) -> Self { + Self(RwLock::new(state)) + } + pub(crate) fn chain_tip_block_number(&self) -> BlockNumber { self.0.read().expect("chain state lock poisoned").chain_tip_header.block_num() } @@ -84,6 +102,21 @@ impl SharedChainState { .update_chain_tip(tip, max_block_count); } + /// Computes the [`PartialMmr`] that would result from advancing the chain tip to `next_tip`, + /// without mutating the in-memory chain state. + /// + /// Used by callers that need to persist the next chain state atomically with block effects + /// before committing the in-memory transition via [`Self::update_chain_tip`]. + pub(crate) fn next_chain_mmr( + &self, + next_tip: &BlockHeader, + max_block_count: usize, + ) -> PartialMmr { + let mut snapshot = self.0.read().expect("chain state lock poisoned").clone(); + snapshot.update_chain_tip(next_tip.clone(), max_block_count); + snapshot.mmr_for_persistence() + } + pub(crate) fn get_cloned(&self) -> ChainState { self.0.read().expect("chain state lock poisoned").clone() } diff --git a/bin/ntx-builder/src/clients/store.rs b/bin/ntx-builder/src/clients/store.rs index 47e2411f97..31a6a9b541 100644 --- a/bin/ntx-builder/src/clients/store.rs +++ b/bin/ntx-builder/src/clients/store.rs @@ -1,19 +1,14 @@ use std::collections::BTreeSet; -use std::ops::RangeInclusive; use std::time::Duration; use futures::{Stream, StreamExt}; use miden_node_proto::clients::{Builder, StoreNtxBuilderClient}; use miden_node_proto::decode::ConversionResultExt; -use miden_node_proto::domain::account::{AccountDetails, AccountResponse, NetworkAccountId}; +use miden_node_proto::domain::account::{AccountDetails, AccountResponse}; use miden_node_proto::errors::ConversionError; -use miden_node_proto::generated::rpc::BlockRange; use miden_node_proto::generated::{self as proto}; -use miden_node_proto::try_convert; -use miden_node_utils::tracing::OpenTelemetrySpanExt; use miden_protocol::Word; use miden_protocol::account::{ - Account, AccountCode, AccountId, PartialAccount, @@ -23,13 +18,11 @@ use miden_protocol::account::{ StorageSlotName, }; use miden_protocol::asset::{AssetVaultKey, AssetWitness, PartialVault}; -use miden_protocol::block::{BlockHeader, BlockNumber, SignedBlock}; -use miden_protocol::crypto::merkle::mmr::{Forest, MmrPeaks, PartialMmr}; +use miden_protocol::block::{BlockNumber, SignedBlock}; use miden_protocol::crypto::merkle::smt::SmtProof; use miden_protocol::note::NoteScript; use miden_protocol::transaction::AccountInputs; use miden_protocol::utils::serde::{Deserializable, Serializable}; -use miden_standards::note::AccountTargetNetworkNote; use thiserror::Error; use tracing::{info, instrument}; use url::Url; @@ -113,96 +106,6 @@ impl StoreClient { })) } - /// Returns the block header and MMR peaks at the current chain tip. - #[instrument(target = COMPONENT, name = "store.client.get_latest_blockchain_data_with_retry", skip_all, err)] - pub async fn get_latest_blockchain_data_with_retry( - &self, - ) -> Result, StoreError> { - let mut retry_counter = 0; - loop { - match self.get_latest_blockchain_data().await { - Err(StoreError::GrpcClientError(err)) => { - // Exponential backoff with base 500ms and max 30s. - let backoff = Duration::from_millis(500) - .saturating_mul(1 << retry_counter.min(6)) - .min(Duration::from_secs(30)); - - tracing::warn!( - ?backoff, - %retry_counter, - %err, - "store connection failed while fetching latest blockchain data, retrying" - ); - - retry_counter += 1; - tokio::time::sleep(backoff).await; - }, - result => return result, - } - } - } - - #[instrument(target = COMPONENT, name = "store.client.get_latest_blockchain_data", skip_all, err)] - async fn get_latest_blockchain_data( - &self, - ) -> Result, StoreError> { - let request = tonic::Request::new(proto::blockchain::MaybeBlockNumber::default()); - - let response = self.inner.clone().get_current_blockchain_data(request).await?.into_inner(); - - match response.current_block_header { - // There are new blocks compared to the builder's latest state - Some(block) => { - let peaks: Vec = try_convert(response.current_peaks) - .collect::>() - .context("current_peaks") - .map_err(StoreError::DeserializationError)?; - let header = - BlockHeader::try_from(block).map_err(StoreError::DeserializationError)?; - - let peaks = MmrPeaks::new(Forest::new(header.block_num().as_usize()), peaks) - .map_err(|_| { - StoreError::MalformedResponse( - "returned peaks are not valid for the sent request".into(), - ) - })?; - - let partial_mmr = PartialMmr::from_peaks(peaks); - - Ok(Some((header, partial_mmr))) - }, - // No new blocks were created, return - None => Ok(None), - } - } - - #[instrument(target = COMPONENT, name = "store.client.get_network_account", skip_all, err)] - pub async fn get_network_account( - &self, - account_id: NetworkAccountId, - ) -> Result, StoreError> { - let request = proto::account::AccountId::from(account_id.inner()); - - let store_response = self - .inner - .clone() - .get_network_account_details_by_id(request) - .await? - .into_inner() - .details; - - // we only care about the case where the account returns and is actually a network account, - // which implies details being public, so OK to error otherwise - let account = match store_response.map(|acc| acc.details) { - Some(Some(details)) => Some(Account::read_from_bytes(&details).map_err(|err| { - StoreError::DeserializationError(ConversionError::from(err).context("details")) - })?), - _ => None, - }; - - Ok(account) - } - /// Get the inputs for an account at a given block number from the store. /// /// Retrieves account details from the store. The retrieved details are limited to the account @@ -244,176 +147,6 @@ impl StoreClient { Ok(AccountInputs::new(partial_account, account_response.witness)) } - /// Returns the list of unconsumed network notes for a specific network account up to a - /// specified block. - #[instrument(target = COMPONENT, name = "store.client.get_unconsumed_network_notes", skip_all, err)] - pub async fn get_unconsumed_network_notes( - &self, - network_account_id: NetworkAccountId, - block_num: u32, - ) -> Result, StoreError> { - // Upper bound of each note is ~10KB. Limit page size to ~10MB. - const PAGE_SIZE: u64 = 1024; - - let mut all_notes = Vec::new(); - let mut page_token: Option = None; - - let mut store_client = self.inner.clone(); - loop { - let req = proto::store::UnconsumedNetworkNotesRequest { - page_token, - page_size: PAGE_SIZE, - account_id: Some(network_account_id.inner().into()), - block_num, - }; - let resp = store_client.get_unconsumed_network_notes(req).await?.into_inner(); - - all_notes.reserve(resp.notes.len()); - for note in resp.notes { - all_notes.push( - AccountTargetNetworkNote::try_from(note) - .map_err(StoreError::DeserializationError)?, - ); - } - - match resp.next_token { - Some(token) => page_token = Some(token), - None => break, - } - } - - Ok(all_notes) - } - - /// Streams network account IDs to the provided sender. - /// - /// This method is designed to be run in a background task, sending accounts to the main event - /// loop as they are loaded. This allows the ntx-builder to start processing mempool events - /// without waiting for all accounts to be preloaded. - pub async fn stream_network_account_ids( - &self, - sender: tokio::sync::mpsc::Sender, - ) -> Result<(), StoreError> { - let mut block_range = BlockNumber::GENESIS..=BlockNumber::MAX; - - while let Some(next_start) = self.load_accounts_page(block_range, &sender).await? { - block_range = next_start..=BlockNumber::MAX; - } - - Ok(()) - } - - /// Loads a single page of network accounts and submits them to the sender. - /// - /// Returns the next block number to fetch from, or `None` if the chain tip has been reached. - #[instrument(target = COMPONENT, name = "store.client.load_accounts_page", skip_all, err)] - async fn load_accounts_page( - &self, - block_range: RangeInclusive, - sender: &tokio::sync::mpsc::Sender, - ) -> Result, StoreError> { - let (accounts, pagination_info) = self.fetch_network_account_ids_page(block_range).await?; - - let chain_tip = pagination_info.chain_tip; - let current_height = pagination_info.block_num; - - self.send_accounts_to_channel(accounts, sender).await?; - - if current_height >= chain_tip { - Ok(None) - } else { - Ok(Some(BlockNumber::from(current_height + 1))) - } - } - - #[instrument(target = COMPONENT, name = "store.client.fetch_network_account_ids_page", skip_all, err)] - async fn fetch_network_account_ids_page( - &self, - block_range: std::ops::RangeInclusive, - ) -> Result<(Vec, proto::rpc::PaginationInfo), StoreError> { - self.fetch_network_account_ids_page_inner(block_range) - .await - .inspect_err(|err| tracing::Span::current().set_error(err)) - } - - async fn fetch_network_account_ids_page_inner( - &self, - block_range: std::ops::RangeInclusive, - ) -> Result<(Vec, proto::rpc::PaginationInfo), StoreError> { - let mut retry_counter = 0u32; - - let response = loop { - match self - .inner - .clone() - .get_network_account_ids(Into::::into(block_range.clone())) - .await - { - Ok(response) => break response.into_inner(), - Err(err) => { - // Exponential backoff with base 500ms and max 30s. - let backoff = Duration::from_millis(500) - .saturating_mul(1 << retry_counter.min(6)) - .min(Duration::from_secs(30)); - - tracing::warn!( - ?backoff, - %retry_counter, - %err, - "store connection failed while fetching committed accounts page, retrying" - ); - - retry_counter += 1; - tokio::time::sleep(backoff).await; - }, - } - }; - - let accounts = response - .account_ids - .into_iter() - .map(|account_id| { - let account_id = AccountId::read_from_bytes(&account_id.id).map_err(|err| { - StoreError::DeserializationError( - ConversionError::from(err).context("account_id"), - ) - })?; - NetworkAccountId::try_from(account_id).map_err(|_| { - StoreError::MalformedResponse( - "account id is not a valid network account".into(), - ) - }) - }) - .collect::, StoreError>>()?; - - let pagination_info = response.pagination_info.ok_or(ConversionError::missing_field::< - proto::store::NetworkAccountIdList, - >("pagination_info"))?; - - Ok((accounts, pagination_info)) - } - - #[instrument( - target = COMPONENT, - name = "store.client.send_accounts_to_channel", - skip_all - )] - async fn send_accounts_to_channel( - &self, - accounts: Vec, - sender: &tokio::sync::mpsc::Sender, - ) -> Result<(), StoreError> { - for account in accounts { - // If the receiver is dropped, stop loading. - if sender.send(account).await.is_err() { - tracing::warn!("Account receiver dropped"); - return Ok(()); - } - } - - Ok(()) - } - #[instrument(target = COMPONENT, name = "store.client.get_note_script_by_root", skip_all, err)] pub async fn get_note_script_by_root( &self, diff --git a/bin/ntx-builder/src/coordinator.rs b/bin/ntx-builder/src/coordinator.rs index 4fbc2a470b..4e85614a9f 100644 --- a/bin/ntx-builder/src/coordinator.rs +++ b/bin/ntx-builder/src/coordinator.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use miden_node_db::DatabaseError; use miden_node_proto::domain::account::NetworkAccountId; +use miden_protocol::crypto::merkle::mmr::PartialMmr; use tokio::sync::{Notify, Semaphore}; use tokio::task::JoinSet; @@ -195,6 +196,11 @@ impl Coordinator { tracing::info!(account_id = %account_id, "Created actor for account prefix"); } + /// Returns `true` if an actor is currently registered for the given account. + pub fn has_actor(&self, account_id: NetworkAccountId) -> bool { + self.actor_registry.contains_key(&account_id) + } + /// Notifies specific account actors that state may have changed. /// /// Only actors that are currently active are notified. Each actor will re-evaluate its state @@ -311,13 +317,15 @@ impl Coordinator { /// Applies a committed block's effects to the database. /// - /// This must be called BEFORE sending notifications to actors. Returns an [`ApplyBlockResult`] - /// with the accounts that should be notified. + /// Persists the new chain MMR atomically with the block effects. Must be called BEFORE + /// sending notifications to actors. Returns an [`ApplyBlockResult`] with the accounts that + /// should be notified. pub async fn apply_block( &self, effects: &CommittedBlockEffects, + chain_mmr: PartialMmr, ) -> Result { - let affected_accounts = self.db.apply_committed_block(effects.clone()).await?; + let affected_accounts = self.db.apply_committed_block(effects.clone(), chain_mmr).await?; Ok(ApplyBlockResult { accounts_to_notify: affected_accounts }) } } diff --git a/bin/ntx-builder/src/db/migrations/2026020900000_setup/up.sql b/bin/ntx-builder/src/db/migrations/2026020900000_setup/up.sql index b151cb3345..fb060cf847 100644 --- a/bin/ntx-builder/src/db/migrations/2026020900000_setup/up.sql +++ b/bin/ntx-builder/src/db/migrations/2026020900000_setup/up.sql @@ -1,5 +1,6 @@ --- Singleton row storing the chain tip header. --- The chain MMR is reconstructed on startup from the store and maintained in memory. +-- Singleton row storing the chain tip header and partial MMR. +-- Both are populated by replaying committed blocks; on restart the row is loaded so the +-- subscription can resume at `block_num + 1` without re-fetching anything from the store. CREATE TABLE chain_state ( -- Singleton constraint: only one row allowed. id INTEGER NOT NULL PRIMARY KEY CHECK (id = 0), @@ -7,6 +8,8 @@ CREATE TABLE chain_state ( block_num INTEGER NOT NULL, -- Serialized BlockHeader. block_header BLOB NOT NULL, + -- Serialized PartialMmr representing the chain MMR at this tip. + chain_mmr BLOB NOT NULL, CONSTRAINT chain_state_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF) ); diff --git a/bin/ntx-builder/src/db/mod.rs b/bin/ntx-builder/src/db/mod.rs index 5b8820d61b..c66939a2df 100644 --- a/bin/ntx-builder/src/db/mod.rs +++ b/bin/ntx-builder/src/db/mod.rs @@ -7,6 +7,7 @@ use miden_node_proto::domain::account::NetworkAccountId; use miden_protocol::Word; use miden_protocol::account::Account; use miden_protocol::block::{BlockHeader, BlockNumber}; +use miden_protocol::crypto::merkle::mmr::PartialMmr; use miden_protocol::note::{NoteId, NoteScript, Nullifier}; use miden_standards::note::AccountTargetNetworkNote; use tracing::{info, instrument}; @@ -154,50 +155,36 @@ impl Db { /// Applies a committed block's effects to the database in a single transaction. /// + /// Persists the new chain MMR atomically with the block effects so the persisted state can + /// be loaded directly on restart. + /// /// Returns the list of affected account IDs that should be notified. pub async fn apply_committed_block( &self, effects: CommittedBlockEffects, + chain_mmr: PartialMmr, ) -> Result> { self.inner .transact("apply_committed_block", move |conn| { - queries::apply_committed_block(conn, &effects) + queries::apply_committed_block(conn, &effects, &chain_mmr) }) .await } - /// Reads the singleton chain state row, returning the last synced block number and its header - /// if any block has been applied locally. - pub async fn get_chain_state(&self) -> Result> { - self.inner.query("get_chain_state", queries::select_chain_state).await - } - - /// Inserts or replaces the singleton chain state row. - pub async fn upsert_chain_state( + /// Reads the singleton chain state row, returning the last synced block number, its header, + /// and the persisted chain MMR if any block has been applied locally. + pub async fn get_chain_state( &self, - block_num: BlockNumber, - header: BlockHeader, - ) -> Result<()> { - self.inner - .transact("upsert_chain_state", move |conn| { - queries::upsert_chain_state(conn, block_num, &header) - }) - .await + ) -> Result> { + self.inner.query("get_chain_state", queries::select_chain_state).await } - /// Syncs an account and its notes from the store into the DB. - pub async fn sync_account_from_store( - &self, - account_id: NetworkAccountId, - account: Account, - notes: Vec, - ) -> Result<()> { + /// Returns the set of network account IDs that have at least one unconsumed note in the local + /// DB. Used at startup to spawn actors for the pending-note backlog inherited from a previous + /// run. + pub async fn accounts_with_pending_notes(&self) -> Result> { self.inner - .transact("sync_account_from_store", move |conn| { - queries::upsert_account(conn, account_id, &account)?; - queries::insert_committed_notes(conn, ¬es)?; - Ok(()) - }) + .query("accounts_with_pending_notes", queries::accounts_with_pending_notes) .await } diff --git a/bin/ntx-builder/src/db/models/conv.rs b/bin/ntx-builder/src/db/models/conv.rs index b6842fa87e..b7b6e3b946 100644 --- a/bin/ntx-builder/src/db/models/conv.rs +++ b/bin/ntx-builder/src/db/models/conv.rs @@ -5,6 +5,7 @@ use miden_node_proto::domain::account::NetworkAccountId; use miden_protocol::Word; use miden_protocol::account::{Account, AccountId}; use miden_protocol::block::{BlockHeader, BlockNumber}; +use miden_protocol::crypto::merkle::mmr::PartialMmr; use miden_protocol::note::{NoteId, NoteScript, Nullifier}; use miden_protocol::utils::serde::{Deserializable, Serializable}; @@ -73,3 +74,12 @@ pub fn block_header_from_bytes(bytes: &[u8]) -> Result Vec { + mmr.to_bytes() +} + +pub fn partial_mmr_from_bytes(bytes: &[u8]) -> Result { + PartialMmr::read_from_bytes(bytes) + .map_err(|e| DatabaseError::deserialization("partial mmr", e)) +} diff --git a/bin/ntx-builder/src/db/models/queries/chain_state.rs b/bin/ntx-builder/src/db/models/queries/chain_state.rs index 8f55124e3c..befb6b4918 100644 --- a/bin/ntx-builder/src/db/models/queries/chain_state.rs +++ b/bin/ntx-builder/src/db/models/queries/chain_state.rs @@ -3,6 +3,7 @@ use diesel::prelude::*; use miden_node_db::DatabaseError; use miden_protocol::block::{BlockHeader, BlockNumber}; +use miden_protocol::crypto::merkle::mmr::PartialMmr; use crate::db::models::conv as conversions; use crate::db::schema; @@ -18,6 +19,7 @@ pub struct ChainStateInsert { pub id: i32, pub block_num: i64, pub block_header: Vec, + pub chain_mmr: Vec, } #[derive(Debug, Clone, Queryable, Selectable)] @@ -26,6 +28,7 @@ pub struct ChainStateInsert { pub struct ChainStateRow { pub block_num: i64, pub block_header: Vec, + pub chain_mmr: Vec, } // QUERIES @@ -36,18 +39,20 @@ pub struct ChainStateRow { /// # Raw SQL /// /// ```sql -/// INSERT OR REPLACE INTO chain_state (id, block_num, block_header) -/// VALUES (0, ?1, ?2) +/// INSERT OR REPLACE INTO chain_state (id, block_num, block_header, chain_mmr) +/// VALUES (0, ?1, ?2, ?3) /// ``` pub fn upsert_chain_state( conn: &mut SqliteConnection, block_num: BlockNumber, block_header: &BlockHeader, + chain_mmr: &PartialMmr, ) -> Result<(), DatabaseError> { let row = ChainStateInsert { id: 0, block_num: conversions::block_num_to_i64(block_num), block_header: conversions::block_header_to_bytes(block_header), + chain_mmr: conversions::partial_mmr_to_bytes(chain_mmr), }; diesel::replace_into(schema::chain_state::table).values(&row).execute(conn)?; Ok(()) @@ -58,11 +63,11 @@ pub fn upsert_chain_state( /// # Raw SQL /// /// ```sql -/// SELECT block_num, block_header FROM chain_state WHERE id = 0 +/// SELECT block_num, block_header, chain_mmr FROM chain_state WHERE id = 0 /// ``` pub fn select_chain_state( conn: &mut SqliteConnection, -) -> Result, DatabaseError> { +) -> Result, DatabaseError> { let row: Option = schema::chain_state::table .find(0i32) .select(ChainStateRow::as_select()) @@ -72,7 +77,8 @@ pub fn select_chain_state( row.map(|row| { let block_num = conversions::block_num_from_i64(row.block_num); let header = conversions::block_header_from_bytes(&row.block_header)?; - Ok((block_num, header)) + let mmr = conversions::partial_mmr_from_bytes(&row.chain_mmr)?; + Ok((block_num, header, mmr)) }) .transpose() } diff --git a/bin/ntx-builder/src/db/models/queries/mod.rs b/bin/ntx-builder/src/db/models/queries/mod.rs index 5aedd7e479..3acbb40355 100644 --- a/bin/ntx-builder/src/db/models/queries/mod.rs +++ b/bin/ntx-builder/src/db/models/queries/mod.rs @@ -5,6 +5,7 @@ use std::collections::HashSet; use diesel::prelude::*; use miden_node_db::DatabaseError; use miden_node_proto::domain::account::NetworkAccountId; +use miden_protocol::crypto::merkle::mmr::PartialMmr; use super::account_effect::NetworkAccountEffect; use crate::committed_block::CommittedBlockEffects; @@ -42,6 +43,7 @@ mod tests; pub fn apply_committed_block( conn: &mut SqliteConnection, effects: &CommittedBlockEffects, + chain_mmr: &PartialMmr, ) -> Result, DatabaseError> { let block_num = effects.header.block_num(); let mut affected_accounts: HashSet = HashSet::new(); @@ -105,7 +107,7 @@ pub fn apply_committed_block( } // Update chain state singleton. - upsert_chain_state(conn, block_num, &effects.header)?; + upsert_chain_state(conn, block_num, &effects.header, chain_mmr)?; Ok(affected_accounts.into_iter().collect()) } diff --git a/bin/ntx-builder/src/db/models/queries/notes.rs b/bin/ntx-builder/src/db/models/queries/notes.rs index 15762ae9ec..88df9cd546 100644 --- a/bin/ntx-builder/src/db/models/queries/notes.rs +++ b/bin/ntx-builder/src/db/models/queries/notes.rs @@ -137,6 +137,31 @@ pub fn available_notes( Ok(result) } +/// Returns the set of account IDs that have at least one note still pending consumption +/// (i.e., `committed_at IS NULL`). +/// +/// Used on startup to spawn actors for accounts whose pending-note backlog predates the +/// current block subscription window. +/// +/// # Raw SQL +/// +/// ```sql +/// SELECT DISTINCT account_id FROM notes WHERE committed_at IS NULL +/// ``` +pub fn accounts_with_pending_notes( + conn: &mut SqliteConnection, +) -> Result, DatabaseError> { + let rows: Vec> = schema::notes::table + .filter(schema::notes::committed_at.is_null()) + .select(schema::notes::account_id) + .distinct() + .load(conn)?; + + rows.iter() + .map(|bytes| conversions::network_account_id_from_bytes(bytes)) + .collect() +} + /// Marks notes as failed by incrementing `attempt_count`, setting `last_attempt`, and storing /// the latest error message. /// diff --git a/bin/ntx-builder/src/db/models/queries/tests.rs b/bin/ntx-builder/src/db/models/queries/tests.rs index 47787bacf2..7145ded87d 100644 --- a/bin/ntx-builder/src/db/models/queries/tests.rs +++ b/bin/ntx-builder/src/db/models/queries/tests.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use diesel::prelude::*; use miden_protocol::Word; use miden_protocol::block::BlockNumber; +use miden_protocol::crypto::merkle::mmr::PartialMmr; use super::*; use crate::NoteError; @@ -49,7 +50,7 @@ fn apply_committed_block_inserts_notes_and_advances_chain_state() { network_account_updates: vec![], }; - let affected = apply_committed_block(conn, &effects).unwrap(); + let affected = apply_committed_block(conn, &effects, &PartialMmr::default()).unwrap(); // Note inserted; note's target account is reported as affected. assert_eq!(count_notes(conn), 1); @@ -79,6 +80,7 @@ fn apply_committed_block_marks_nullifiers_consumed() { nullifiers: vec![], network_account_updates: vec![], }, + &PartialMmr::default(), ) .unwrap(); @@ -92,6 +94,7 @@ fn apply_committed_block_marks_nullifiers_consumed() { nullifiers: vec![nullifier], network_account_updates: vec![], }, + &PartialMmr::default(), ) .unwrap(); @@ -120,6 +123,7 @@ fn apply_committed_block_advances_chain_state() { nullifiers: vec![], network_account_updates: vec![], }, + &PartialMmr::default(), ) .unwrap(); apply_committed_block( @@ -130,6 +134,7 @@ fn apply_committed_block_advances_chain_state() { nullifiers: vec![], network_account_updates: vec![], }, + &PartialMmr::default(), ) .unwrap(); @@ -302,12 +307,12 @@ fn upsert_chain_state_updates_singleton() { let block_num_1 = BlockNumber::from(1u32); let header_1 = mock_block_header(block_num_1); - upsert_chain_state(conn, block_num_1, &header_1).unwrap(); + upsert_chain_state(conn, block_num_1, &header_1, &PartialMmr::default()).unwrap(); // Upsert again with higher block. let block_num_2 = BlockNumber::from(2u32); let header_2 = mock_block_header(block_num_2); - upsert_chain_state(conn, block_num_2, &header_2).unwrap(); + upsert_chain_state(conn, block_num_2, &header_2, &PartialMmr::default()).unwrap(); // Should only have one row. let row_count: i64 = schema::chain_state::table.count().get_result(conn).unwrap(); diff --git a/bin/ntx-builder/src/db/schema.rs b/bin/ntx-builder/src/db/schema.rs index f9f17fc826..dc41d61768 100644 --- a/bin/ntx-builder/src/db/schema.rs +++ b/bin/ntx-builder/src/db/schema.rs @@ -12,6 +12,7 @@ diesel::table! { id -> Integer, block_num -> BigInt, block_header -> Binary, + chain_mmr -> Binary, } } diff --git a/bin/ntx-builder/src/lib.rs b/bin/ntx-builder/src/lib.rs index 3fa7c59e25..3d3ada8d16 100644 --- a/bin/ntx-builder/src/lib.rs +++ b/bin/ntx-builder/src/lib.rs @@ -6,17 +6,21 @@ use std::time::Duration; use actor::{AccountActorContext, ActorConfig, GrpcClients, State}; use anyhow::Context; use builder::BlockStream; -use chain_state::SharedChainState; +use chain_state::{ChainState, SharedChainState}; use clients::{BlockProducerClient, StoreClient, ValidatorClient}; use coordinator::Coordinator; use db::Db; +use futures::StreamExt; use miden_node_utils::ErrorReport; use miden_node_utils::lru_cache::LruCache; use miden_protocol::block::BlockNumber; +use miden_protocol::crypto::merkle::mmr::PartialMmr; use miden_remote_prover_client::RemoteTransactionProver; use tokio::sync::mpsc; use url::Url; +use crate::committed_block::CommittedBlockEffects; + pub(crate) type NoteError = Arc; mod actor; @@ -51,9 +55,6 @@ const DEFAULT_MAX_CONCURRENT_TXS: usize = 4; /// Default maximum number of blocks to keep in the chain MMR. const DEFAULT_MAX_BLOCK_COUNT: usize = 4; -/// Default channel capacity for account loading from the store. -const DEFAULT_ACCOUNT_CHANNEL_CAPACITY: usize = 1_000; - /// Default maximum number of attempts to execute a failing note before dropping it. const DEFAULT_MAX_NOTE_ATTEMPTS: usize = 30; @@ -118,9 +119,6 @@ pub struct NtxBuilderConfig { /// Maximum number of blocks to keep in the chain MMR. Older blocks are pruned. pub max_block_count: usize, - /// Channel capacity for loading accounts from the store during startup. - pub account_channel_capacity: usize, - /// Duration after which an idle network account will deactivate. /// /// An account is considered idle once it has no viable notes to consume. @@ -168,7 +166,6 @@ impl NtxBuilderConfig { max_notes_per_tx: DEFAULT_MAX_NOTES_PER_TX, max_note_attempts: DEFAULT_MAX_NOTE_ATTEMPTS, max_block_count: DEFAULT_MAX_BLOCK_COUNT, - account_channel_capacity: DEFAULT_ACCOUNT_CHANNEL_CAPACITY, idle_timeout: DEFAULT_IDLE_TIMEOUT, max_account_crashes: DEFAULT_MAX_ACCOUNT_CRASHES, max_cycles: DEFAULT_MAX_TX_CYCLES, @@ -239,13 +236,6 @@ impl NtxBuilderConfig { self } - /// Sets the account channel capacity for startup loading. - #[must_use] - pub fn with_account_channel_capacity(mut self, capacity: usize) -> Self { - self.account_channel_capacity = capacity; - self - } - /// Sets the idle timeout for actors. /// /// Actors that remain idle (no viable notes) for this duration will be deactivated. @@ -278,16 +268,17 @@ impl NtxBuilderConfig { /// Builds and initializes the network transaction builder. /// - /// This method connects to the store services, determines the catch-up target block, and - /// opens a committed-block subscription. The catch-up phase itself runs inside - /// [`NetworkTransactionBuilder::run`]. + /// Opens the committed-block subscription on the store. On a fresh DB the subscription + /// starts at genesis and the first block is consumed inline to bootstrap the in-memory + /// chain state; on resume, the in-memory chain state is loaded from the persisted MMR and + /// the subscription starts at `persisted_tip + 1`. /// /// # Errors /// /// Returns an error if: /// - The store connection fails /// - The block subscription cannot be opened (after retries) - /// - The store contains no blocks (not bootstrapped) + /// - The genesis bootstrap fails to read the first block from the subscription pub async fn build(self) -> anyhow::Result { // Set up the database (bootstrap + connection pool). let db = Db::setup_with_pool_size( @@ -305,33 +296,18 @@ impl NtxBuilderConfig { let validator = ValidatorClient::new(self.validator_url.clone()); let prover = self.tx_prover_url.clone().map(RemoteTransactionProver::new); - // Fetch the current chain tip + MMR. This is needed as the catch-up target and as the - // initial in-memory chain state used by actors. - let (chain_tip_header, chain_mmr) = store - .get_latest_blockchain_data_with_retry() - .await? - .context("store should contain a latest block")?; - let chain_tip_block_num = chain_tip_header.block_num(); - - // Resume from where we left off. If the DB has no chain state yet, we initialize it - // from the current chain tip. - // Existing DBs resume from their persisted block; the catch-up phase then drains the - // stream until the in-memory chain state reaches the current tip. + // Decide where to start the subscription. On resume we load the persisted chain state; + // on fresh start we begin at genesis and bootstrap inline below. let stored_chain_state = db.get_chain_state().await.context("failed to read chain state")?; - let (block_from, last_applied_block) = - resume_point(stored_chain_state.as_ref().map(|(num, _)| *num), chain_tip_block_num); - - if stored_chain_state.is_none() { - db.upsert_chain_state(chain_tip_block_num, chain_tip_header.clone()) - .await - .context("failed to upsert chain state")?; - } + let block_from = stored_chain_state + .as_ref() + .map_or(BlockNumber::GENESIS, |(num, _, _)| num.child()); tracing::info!( %block_from, - %chain_tip_block_num, + resume = stored_chain_state.is_some(), "ntx-builder opening block subscription" ); @@ -340,11 +316,39 @@ impl NtxBuilderConfig { .await .map_err(|err| anyhow::anyhow!(err)) .context("failed to subscribe to committed blocks")?; - let block_stream: BlockStream = Box::pin(block_stream_inner); + let mut block_stream: BlockStream = Box::pin(block_stream_inner); + + let (chain_state, last_applied_block) = if let Some((block_num, header, mmr)) = + stored_chain_state + { + let cs = Arc::new(SharedChainState::new(header, mmr)); + (cs, block_num) + } else { + // Fresh DB: consume the genesis block from the subscription, apply it with an empty + // chain MMR (the MMR for tip=GENESIS has no leaves by the one-block-lag convention), + // and bootstrap the in-memory chain state. + let genesis = block_stream + .next() + .await + .context("block stream ended before delivering the genesis block")? + .context("block stream failed before delivering the genesis block")?; + let genesis_header = genesis.header().clone(); + anyhow::ensure!( + genesis_header.block_num() == BlockNumber::GENESIS, + "expected genesis block from subscription but got block {}", + genesis_header.block_num() + ); + + let effects = CommittedBlockEffects::from_signed_block(&genesis); + db.apply_committed_block(effects, PartialMmr::default()) + .await + .context("failed to apply genesis block during bootstrap")?; - // Chain state is initialized at the chain tip, actors only start after catch-up, so the - // tip is consistent with the DB by the time they run. - let chain_state = Arc::new(SharedChainState::new(chain_tip_header, chain_mmr)); + let cs = Arc::new(SharedChainState::from_state(ChainState::bootstrap_genesis( + genesis_header, + ))); + (cs, BlockNumber::GENESIS) + }; let (request_tx, actor_request_rx) = mpsc::channel(1); @@ -374,73 +378,13 @@ impl NtxBuilderConfig { Ok(NetworkTransactionBuilder::new( self, coordinator, - store, db, chain_state, actor_context, block_stream, - chain_tip_block_num, last_applied_block, actor_request_rx, )) } } -// HELPERS -// ================================================================================================= - -/// Decides where the ntx-builder should start consuming the block stream from on startup. -/// -/// Returns `(block_from, last_applied_block)`: -/// - `block_from` is the first block number the subscription should yield (inclusive). -/// - `last_applied_block` is the highest block already reflected in the DB. The catch-up phase -/// drains the stream until this reaches the chain tip. -/// -/// If the DB has a persisted chain state, resume from the block after it. Otherwise the DB is -/// fresh and we treat the current chain tip as already applied (the caller is responsible for -/// persisting that state). -fn resume_point(stored: Option, chain_tip: BlockNumber) -> (BlockNumber, BlockNumber) { - match stored { - Some(num) => (num.child(), num), - None => (chain_tip.child(), chain_tip), - } -} - -#[cfg(test)] -mod tests { - use miden_protocol::block::BlockNumber; - - use super::resume_point; - - #[test] - fn resume_point_fresh_db_starts_after_chain_tip() { - let tip = BlockNumber::from(10u32); - - let (block_from, last_applied) = resume_point(None, tip); - - assert_eq!(last_applied, tip); - assert_eq!(block_from, tip.child()); - } - - #[test] - fn resume_point_existing_db_resumes_after_stored_block() { - let tip = BlockNumber::from(10u32); - let stored = BlockNumber::from(7u32); - - let (block_from, last_applied) = resume_point(Some(stored), tip); - - assert_eq!(last_applied, stored); - assert_eq!(block_from, stored.child()); - } - - #[test] - fn resume_point_db_already_at_tip() { - let tip = BlockNumber::from(10u32); - - let (block_from, last_applied) = resume_point(Some(tip), tip); - - assert_eq!(last_applied, tip); - assert_eq!(block_from, tip.child()); - // Catch-up loop terminates immediately because last_applied >= target. - } -} diff --git a/bin/ntx-builder/src/test_utils.rs b/bin/ntx-builder/src/test_utils.rs index 730e9bc673..b66e557fdd 100644 --- a/bin/ntx-builder/src/test_utils.rs +++ b/bin/ntx-builder/src/test_utils.rs @@ -3,14 +3,11 @@ use miden_node_proto::domain::account::NetworkAccountId; use miden_protocol::Word; use miden_protocol::account::{AccountId, AccountStorageMode, AccountType}; -use miden_protocol::block::{BlockBody, BlockNumber, SignedBlock}; -use miden_protocol::note::Nullifier; +use miden_protocol::block::BlockNumber; use miden_protocol::testing::account_id::{ ACCOUNT_ID_REGULAR_NETWORK_ACCOUNT_IMMUTABLE_CODE, AccountIdBuilder, }; -use miden_protocol::testing::random_secret_key::random_secret_key; -use miden_protocol::transaction::{OrderedTransactionHeaders, OutputNote, PublicOutputNote}; use miden_standards::note::{AccountTargetNetworkNote, NetworkAccountTarget, NoteExecutionHint}; use miden_standards::testing::note::NoteBuilder; use rand_chacha::ChaCha20Rng; @@ -56,43 +53,3 @@ pub fn mock_block_header(block_num: BlockNumber) -> miden_protocol::block::Block miden_protocol::block::BlockHeader::mock(block_num, None, None, &[], Word::default()) } -/// Creates a mock [`SignedBlock`] for the given block number containing the provided network notes -/// and nullifiers. -/// -/// The block is built with `SignedBlock::new_unchecked`, so the signature is generated against an -/// independent key (not the validator key in the mock header). Suitable for code paths that only -/// observe the block via [`crate::committed_block::CommittedBlockEffects::from_signed_block`]. -pub fn mock_signed_block( - block_num: BlockNumber, - network_notes: &[AccountTargetNetworkNote], - nullifiers: Vec, -) -> SignedBlock { - let header = mock_block_header(block_num); - - let output_notes: Vec<(usize, OutputNote)> = network_notes - .iter() - .enumerate() - .map(|(idx, note)| { - let public = PublicOutputNote::new(note.as_note().clone()) - .expect("network note should be public"); - (idx, OutputNote::Public(public)) - }) - .collect(); - - let output_note_batches = if output_notes.is_empty() { - Vec::new() - } else { - vec![output_notes] - }; - - let body = BlockBody::new_unchecked( - Vec::new(), - output_note_batches, - nullifiers, - OrderedTransactionHeaders::new_unchecked(Vec::new()), - ); - - let signature = random_secret_key().sign(header.commitment()); - - SignedBlock::new_unchecked(header, body, signature) -} diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index cf32b85bcf..6e1632d04f 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -40,7 +40,7 @@ pub use crate::db::models::queries::{ PublicAccountStateRootsPage, }; use crate::db::models::queries::{BlockHeaderCommitment, StorageMapValuesPage}; -use crate::db::models::{Page, queries}; +use crate::db::models::queries; use crate::errors::{DatabaseError, NoteSyncError}; use crate::genesis::GenesisBlock; @@ -458,42 +458,6 @@ impl Db { .await } - /// Loads public account details for a network account by its full account ID. - #[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)] - pub async fn select_network_account_by_id( - &self, - account_id: AccountId, - ) -> Result> { - self.transact("Get network account by id", move |conn| { - queries::select_network_account_by_id(conn, account_id) - }) - .await - } - - /// Returns network account IDs within the specified block range (based on account creation - /// block). - /// - /// The function may return fewer accounts than exist in the range if the result would exceed - /// `MAX_RESPONSE_PAYLOAD_BYTES / AccountId::SERIALIZED_SIZE` rows. In this case, the result is - /// truncated at a block boundary to ensure all accounts from included blocks are returned. - /// - /// # Returns - /// - /// A tuple containing: - /// - A vector of network account IDs. - /// - The last block number that was fully included in the result. When truncated, this will be - /// less than the requested range end. - #[instrument(level = "debug", target = COMPONENT, skip_all, ret(level = "debug"), err)] - pub async fn select_all_network_account_ids( - &self, - block_range: RangeInclusive, - ) -> Result<(Vec, BlockNumber)> { - self.transact("Get all network account IDs", move |conn| { - queries::select_all_network_account_ids(conn, block_range) - }) - .await - } - /// Queries the account code by its commitment hash. /// /// Returns `None` if no code exists with that commitment. @@ -721,22 +685,6 @@ impl Db { }) } - /// Loads the network notes for an account that are unconsumed by a specified block number. - /// Pagination is used to limit the number of notes returned. - pub(crate) async fn select_unconsumed_network_notes( - &self, - account_id: AccountId, - block_num: BlockNumber, - page: Page, - ) -> Result<(Vec, Page)> { - self.transact("unconsumed network notes for account", move |conn| { - models::queries::select_unconsumed_network_notes_by_account_id( - conn, account_id, block_num, page, - ) - }) - .await - } - pub async fn get_account_vault_sync( &self, account_id: AccountId, diff --git a/crates/store/src/db/models/mod.rs b/crates/store/src/db/models/mod.rs index 09d4bf92fc..e968480845 100644 --- a/crates/store/src/db/models/mod.rs +++ b/crates/store/src/db/models/mod.rs @@ -13,8 +13,6 @@ //! The first step in debugging should always be using the fully qualified //! calling syntext when dealing with diesel. -use std::num::NonZeroUsize; - use crate::errors::DatabaseError; pub(crate) mod conv; @@ -23,10 +21,3 @@ pub mod queries; pub(crate) mod utils; pub(crate) use utils::*; - -/// The page token and size to query from the DB. -#[derive(Debug, Copy, Clone)] -pub struct Page { - pub token: Option, - pub size: NonZeroUsize, -} diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index 3edc318216..68668c2fb1 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -217,50 +217,6 @@ pub(crate) fn select_full_account( Ok(Account::new(account_id, vault, storage, code, nonce, None)?) } -/// Select the latest account info for a network account by its full account ID. -/// -/// # Returns -/// -/// The latest account info, `None` if the account was not found, or an error. -/// -/// # Raw SQL -/// -/// ```sql -/// SELECT -/// accounts.account_id, -/// accounts.account_commitment, -/// accounts.block_num -/// FROM -/// accounts -/// WHERE -/// account_id = ?1 -/// AND network_account_type = 1 -/// AND is_latest = 1 -/// ``` -pub(crate) fn select_network_account_by_id( - conn: &mut SqliteConnection, - account_id: AccountId, -) -> Result, DatabaseError> { - let maybe_summary = SelectDsl::select(schema::accounts::table, AccountSummaryRaw::as_select()) - .filter(schema::accounts::account_id.eq(account_id.to_bytes())) - .filter(schema::accounts::network_account_type.eq(NetworkAccountType::Network.to_raw_sql())) - .filter(schema::accounts::is_latest.eq(true)) - .get_result::(conn) - .optional() - .map_err(DatabaseError::Diesel)?; - - match maybe_summary { - None => Ok(None), - Some(raw) => { - let summary: AccountSummary = raw.try_into()?; - let account_id = summary.account_id; - // Backfill account details from database - let details = select_full_account(conn, account_id).ok(); - Ok(Some(AccountInfo { summary, details })) - }, - } -} - /// Page of account commitments returned by [`select_account_commitments_paged`]. #[derive(Debug)] pub struct AccountCommitmentsPage { @@ -635,85 +591,6 @@ pub(crate) fn select_all_accounts( Ok(account_infos) } -/// Returns network account IDs within the specified block range (based on account creation -/// block). -/// -/// The function may return fewer accounts than exist in the range if the result would exceed -/// `MAX_RESPONSE_PAYLOAD_BYTES / AccountId::SERIALIZED_SIZE` rows. In this case, the result is -/// truncated at a block boundary to ensure all accounts from included blocks are returned. -/// -/// # Returns -/// -/// A tuple containing: -/// - A vector of network account IDs. -/// - The last block number that was fully included in the result. When truncated, this will be less -/// than the requested range end. -pub(crate) fn select_all_network_account_ids( - conn: &mut SqliteConnection, - block_range: RangeInclusive, -) -> Result<(Vec, BlockNumber), DatabaseError> { - const ROW_OVERHEAD_BYTES: usize = AccountId::SERIALIZED_SIZE; - const MAX_ROWS: usize = MAX_RESPONSE_PAYLOAD_BYTES / ROW_OVERHEAD_BYTES; - - const _: () = assert!( - MAX_ROWS > miden_protocol::MAX_ACCOUNTS_PER_BLOCK, - "Block pagination limit must exceed maximum block capacity to uphold assumed logic invariant" - ); - - if block_range.is_empty() { - return Err(DatabaseError::InvalidBlockRange { - from: *block_range.start(), - to: *block_range.end(), - }); - } - - let account_ids_raw: Vec<(Vec, i64)> = Box::new( - QueryDsl::select( - schema::accounts::table - .filter( - schema::accounts::network_account_type - .eq(NetworkAccountType::Network.to_raw_sql()), - ) - .filter(schema::accounts::is_latest.eq(true)), - (schema::accounts::account_id, schema::accounts::created_at_block), - ) - .filter( - schema::accounts::block_num - .between(block_range.start().to_raw_sql(), block_range.end().to_raw_sql()), - ) - .order(schema::accounts::created_at_block.asc()) - .limit(i64::try_from(MAX_ROWS + 1).expect("limit fits within i64")), - ) - .load::<(Vec, i64)>(conn)?; - - if account_ids_raw.len() > MAX_ROWS { - // SAFETY: We just checked that len > MAX_ROWS, so the vec is not empty. - let last_created_at_block = account_ids_raw.last().expect("vec is not empty").1; - - let account_ids = account_ids_raw - .into_iter() - .take_while(|(_, created_at_block)| *created_at_block != last_created_at_block) - .map(|(id_bytes, _)| { - AccountId::read_from_bytes(&id_bytes).map_err(DatabaseError::DeserializationError) - }) - .collect::, DatabaseError>>()?; - - let last_block_included = - BlockNumber::from_raw_sql(last_created_at_block.saturating_sub(1))?; - - Ok((account_ids, last_block_included)) - } else { - let account_ids = account_ids_raw - .into_iter() - .map(|(id_bytes, _)| { - AccountId::read_from_bytes(&id_bytes).map_err(DatabaseError::DeserializationError) - }) - .collect::, DatabaseError>>()?; - - Ok((account_ids, *block_range.end())) - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct StorageMapValue { pub block_num: BlockNumber, diff --git a/crates/store/src/db/models/queries/notes.rs b/crates/store/src/db/models/queries/notes.rs index 4fccfdd463..6dbe87e61f 100644 --- a/crates/store/src/db/models/queries/notes.rs +++ b/crates/store/src/db/models/queries/notes.rs @@ -7,7 +7,6 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::ops::RangeInclusive; use diesel::prelude::{ - BoolExpressionMethods, ExpressionMethods, Insertable, QueryDsl, @@ -61,7 +60,7 @@ use crate::db::models::conv::{ }; use crate::db::models::queries::select_block_header_by_block_num; use crate::db::models::{serialize_vec, vec_raw_try_into}; -use crate::db::{DatabaseError, NoteRecord, NoteSyncRecord, NoteSyncUpdate, Page, schema}; +use crate::db::{DatabaseError, NoteRecord, NoteSyncRecord, NoteSyncUpdate, schema}; use crate::errors::NoteSyncError; /// Estimated byte size of a [`NoteSyncUpdate`] excluding its notes. @@ -442,118 +441,6 @@ pub(crate) fn select_note_script_by_root( .map_err(Into::into) } -/// Returns a paginated batch of network notes for an account that are unconsumed by a specified -/// block number. -/// -/// Notes that are created or consumed after the specified block are excluded from the result. -/// -/// # Returns -/// -/// A set of unconsumed network notes with maximum length of `size` and the page to get -/// the next set. -/// -/// # Raw SQL -/// -/// Attention: uses the _implicit_ column `rowid`, which requires to use a few raw SQL nugget -/// statements. -/// -/// ```sql -/// SELECT -/// notes.committed_at, -/// notes.batch_index, -/// notes.note_index, -/// notes.note_id, -/// notes.note_type, -/// notes.sender, -/// notes.tag, -/// notes.attachment, -/// notes.assets, -/// notes.storage, -/// notes.serial_num, -/// notes.inclusion_path, -/// note_scripts.script, -/// notes.rowid -/// FROM notes -/// LEFT JOIN note_scripts ON notes.script_root = note_scripts.script_root -/// WHERE -/// network_note_type = 1 AND target_account_id = ?1 AND -/// committed_at <= ?2 AND -/// (consumed_at IS NULL OR consumed_at > ?2) AND notes.rowid >= ?3 -/// ORDER BY notes.rowid ASC -/// LIMIT ?4 -/// ``` -#[expect(clippy::cast_sign_loss, reason = "row_id is a positive integer")] -pub(crate) fn select_unconsumed_network_notes_by_account_id( - conn: &mut SqliteConnection, - account_id: AccountId, - block_num: BlockNumber, - mut page: Page, -) -> Result<(Vec, Page), DatabaseError> { - let rowid_sel = diesel::dsl::sql::("notes.rowid"); - let rowid_sel_ge = - diesel::dsl::sql::("notes.rowid >= ") - .bind::(page.token.unwrap_or_default() as i64); - - #[expect( - clippy::items_after_statements, - reason = "It's only relevant for a single call function" - )] - type RawLoadedTuple = ( - NoteRecordRawRow, - Option>, // script - i64, // rowid (from sql::("notes.rowid")) - ); - - #[expect( - clippy::items_after_statements, - reason = "It's only relevant for a single call function" - )] - fn split_into_raw_note_record_and_implicit_row_id( - tuple: RawLoadedTuple, - ) -> (NoteRecordWithScriptRawJoined, i64) { - let (note, script, row) = tuple; - let combined = NoteRecordWithScriptRawJoined::from((note, script)); - (combined, row) - } - - let raw = SelectDsl::select( - schema::notes::table.left_join( - schema::note_scripts::table - .on(schema::notes::script_root.eq(schema::note_scripts::script_root.nullable())), - ), - ( - NoteRecordRawRow::as_select(), - schema::note_scripts::script.nullable(), - rowid_sel.clone(), - ), - ) - .filter(schema::notes::network_note_type.eq(i32::from(NetworkNoteType::SingleTarget))) - .filter(schema::notes::target_account_id.eq(Some(account_id.to_bytes()))) - .filter(schema::notes::committed_at.le(block_num.to_raw_sql())) - .filter( - schema::notes::consumed_at - .is_null() - .or(schema::notes::consumed_at.gt(block_num.to_raw_sql())), - ) - .filter(rowid_sel_ge) - .order(rowid_sel.asc()) - .limit(page.size.get() as i64 + 1) - .load::(conn)?; - - let mut notes = Vec::with_capacity(page.size.into()); - for raw_item in raw { - let (raw_item, row_id) = split_into_raw_note_record_and_implicit_row_id(raw_item); - page.token = None; - if notes.len() == page.size.get() { - page.token = Some(row_id as u64); - break; - } - notes.push(TryInto::::try_into(raw_item)?); - } - - Ok((notes, page)) -} - /// Loads the data necessary for a note sync across all matching blocks in the given range. /// /// Returns one [`NoteSyncUpdate`] per block that contains at least one note matching the diff --git a/crates/store/src/db/tests.rs b/crates/store/src/db/tests.rs index 25948a47a7..5b1456f9ee 100644 --- a/crates/store/src/db/tests.rs +++ b/crates/store/src/db/tests.rs @@ -1,4 +1,3 @@ -use std::num::NonZeroUsize; use std::sync::{Arc, Mutex}; use assert_matches::assert_matches; @@ -82,7 +81,7 @@ use super::{AccountInfo, NoteRecord, NoteSyncRecord, NullifierInfo, TransactionR use crate::account_state_forest::HISTORICAL_BLOCK_RETENTION; use crate::db::migrations::apply_migrations; use crate::db::models::queries::{StorageMapValue, insert_account_storage_map_value}; -use crate::db::models::{Page, queries, utils}; +use crate::db::models::{queries, utils}; use crate::errors::DatabaseError; fn create_db() -> SqliteConnection { @@ -339,92 +338,6 @@ fn make_account_and_note( .unwrap() } -#[test] -#[miden_node_test_macro::enable_logging] -fn sql_unconsumed_network_notes() { - let mut conn = create_db(); - - // Create account. - let account_note = - make_account_and_note(&mut conn, 0.into(), [1u8; 32], AccountStorageMode::Network); - - // Create 2 blocks. - create_block(&mut conn, 0.into()); - create_block(&mut conn, 1.into()); - - // Create a NetworkAccountTarget attachment for the network account - let target = NetworkAccountTarget::new(account_note.0, NoteExecutionHint::Always) - .expect("NetworkAccountTarget creation should succeed for network account"); - let attachment: NoteAttachment = target.into(); - - // Create an unconsumed note in each block. - let notes = Vec::from_iter((0..2).map(|i: u32| { - let attachments = NoteAttachments::from(attachment.clone()); - let metadata = NoteMetadata::new( - PartialNoteMetadata::new(account_note.0, NoteType::Public), - &attachments, - ); - let note = NoteRecord { - block_num: 0.into(), // Created on same block. - note_index: BlockNoteIndex::new(0, i as usize).unwrap(), - note_id: num_to_word(i.into()), - note_commitment: num_to_word(i.into()), - metadata, - details: None, - attachments, - inclusion_path: SparseMerklePath::default(), - }; - (note, Some(num_to_nullifier(i.into()))) - })); - queries::insert_scripts(&mut conn, notes.iter().map(|(note, _)| note)).unwrap(); - queries::insert_notes(&mut conn, ¬es).unwrap(); - - // Both notes are unconsumed, query should return both notes on both blocks. - (0..2).for_each(|i: u32| { - let (result, _) = queries::select_unconsumed_network_notes_by_account_id( - &mut conn, - account_note.0, - i.into(), - Page { - token: None, - size: NonZeroUsize::new(10).unwrap(), - }, - ) - .unwrap(); - assert_eq!(result.len(), 2); - }); - - // Consume the 2nd note on the 2nd block. - queries::insert_nullifiers_for_block(&mut conn, &[notes[1].1.unwrap()], 1.into()).unwrap(); - - // Query against first block should return both notes. - let (result, _) = queries::select_unconsumed_network_notes_by_account_id( - &mut conn, - account_note.0, - 0.into(), - Page { - token: None, - size: NonZeroUsize::new(10).unwrap(), - }, - ) - .unwrap(); - assert_eq!(result.len(), 2); - - // Query against second block should return only first note. - let (result, _) = queries::select_unconsumed_network_notes_by_account_id( - &mut conn, - account_note.0, - 1.into(), - Page { - token: None, - size: NonZeroUsize::new(10).unwrap(), - }, - ) - .unwrap(); - assert_eq!(result.len(), 1); - assert_eq!(result[0].note_id, num_to_word(0)); -} - #[test] #[miden_node_test_macro::enable_logging] fn sql_select_accounts() { diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index fb938d0897..76ac965bb1 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -324,14 +324,6 @@ impl From for NoteSyncError { } } -#[derive(Error, Debug)] -pub enum GetCurrentBlockchainDataError { - #[error("failed to retrieve block header")] - ErrorRetrievingBlockHeader(#[source] DatabaseError), - #[error("failed to instantiate MMR peaks")] - InvalidPeaks(MmrError), -} - #[derive(Error, Debug)] pub enum GetBatchInputsError { #[error("failed to select note inclusion proofs")] @@ -399,20 +391,6 @@ pub enum SyncAccountStorageMapsError { AccountNotPublic(AccountId), } -// GET NETWORK ACCOUNT IDS -// ================================================================================================ - -#[derive(Debug, Error, GrpcError)] -pub enum GetNetworkAccountIdsError { - #[error("database error")] - #[grpc(internal)] - DatabaseError(#[from] DatabaseError), - #[error("invalid block range")] - InvalidBlockRange(#[from] InvalidBlockRange), - #[error("malformed nullifier prefix")] - DeserializationFailed(#[from] ConversionError), -} - // GET BLOCK BY NUMBER ERRORS // ================================================================================================ diff --git a/crates/store/src/server/ntx_builder.rs b/crates/store/src/server/ntx_builder.rs index badf1ea119..5ede1869b0 100644 --- a/crates/store/src/server/ntx_builder.rs +++ b/crates/store/src/server/ntx_builder.rs @@ -1,29 +1,17 @@ use std::collections::BTreeSet; -use std::num::{NonZero, TryFromIntError}; use miden_crypto::merkle::smt::SmtProof; -use miden_node_proto::decode::{read_account_id, read_block_range, read_root}; -use miden_node_proto::domain::account::AccountInfo; +use miden_node_proto::decode::{read_account_id, read_root}; use miden_node_proto::errors::ConversionError; use miden_node_proto::generated as proto; -use miden_node_proto::generated::rpc::BlockRange; use miden_node_proto::generated::store::{BlockSubscriptionRequest, ntx_builder_server}; -use miden_node_utils::ErrorReport; use miden_protocol::account::{StorageMapKey, StorageSlotName}; use miden_protocol::asset::AssetVaultKey; -use miden_protocol::block::BlockNumber; -use miden_protocol::note::Note; use tonic::{Request, Response, Status}; use tracing::debug; use crate::COMPONENT; -use crate::db::models::Page; -use crate::errors::{ - GetAccountError, - GetNetworkAccountIdsError, - GetNoteScriptByRootError, - GetWitnessesError, -}; +use crate::errors::{GetAccountError, GetNoteScriptByRootError, GetWitnessesError}; use crate::server::api::{StoreApi, internal_error, invalid_argument}; use crate::server::replica::BlockSubscriptionStream; use crate::state::Finality; @@ -45,131 +33,6 @@ impl ntx_builder_server::NtxBuilder for StoreApi { self.block_subscription_inner(request) } - /// Returns the chain tip's header and MMR peaks corresponding to that header. - /// If there are N blocks, the peaks will represent the MMR at block `N - 1`. - /// - /// This returns all the blockchain-related information needed for executing transactions - /// without authenticating notes. - async fn get_current_blockchain_data( - &self, - request: Request, - ) -> Result, Status> { - let block_num = request.into_inner().block_num.map(BlockNumber::from); - - let response = match self - .state - .get_current_blockchain_data(block_num) - .await - .map_err(internal_error)? - { - Some((header, peaks)) => proto::store::CurrentBlockchainData { - current_peaks: peaks.peaks().iter().map(Into::into).collect(), - current_block_header: Some(header.into()), - }, - None => proto::store::CurrentBlockchainData { - current_peaks: vec![], - current_block_header: None, - }, - }; - - Ok(Response::new(response)) - } - - async fn get_network_account_details_by_id( - &self, - request: Request, - ) -> Result, Status> { - let account_id = - read_account_id::(Some(request.into_inner()))?; - - let account_info: Option = - self.state.get_network_account_details_by_id(account_id).await?; - - Ok(Response::new(proto::store::MaybeAccountDetails { - details: account_info.map(|acc| (&acc).into()), - })) - } - - async fn get_unconsumed_network_notes( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - let block_num = BlockNumber::from(request.block_num); - let account_id = read_account_id::( - request.account_id, - )?; - - let state = self.state.clone(); - - let size = - NonZero::try_from(request.page_size as usize).map_err(|err: TryFromIntError| { - invalid_argument(err.as_report_context("invalid page_size")) - })?; - let page = Page { token: request.page_token, size }; - // TODO: no need to get the whole NoteRecord here, a NetworkNote wrapper should be created - // instead - let (notes, next_page) = state - .get_unconsumed_network_notes_for_account(account_id, block_num, page) - .await - .map_err(internal_error)?; - - let mut network_notes = Vec::with_capacity(notes.len()); - for note in notes { - // SAFETY: Network notes are filtered in the database, so they should have details; - // otherwise the state would be corrupted - let (assets, recipient) = note.details.unwrap().into_parts(); - let partial_metadata = *note.metadata.partial_metadata(); - let note = - Note::with_attachments(assets, partial_metadata, recipient, note.attachments); - network_notes.push(note.into()); - } - - Ok(Response::new(proto::store::UnconsumedNetworkNotes { - notes: network_notes, - next_token: next_page.token, - })) - } - - /// Returns network account IDs within the specified block range (based on account creation - /// block). - /// - /// The function may return fewer accounts than exist in the range if the result would exceed - /// `MAX_RESPONSE_PAYLOAD_BYTES / AccountId::SERIALIZED_SIZE` rows. In this case, the result is - /// truncated at a block boundary to ensure all accounts from included blocks are returned. - /// - /// The response includes pagination info with the last block number that was fully included. - async fn get_network_account_ids( - &self, - request: Request, - ) -> Result, Status> { - let request = request.into_inner(); - - let block_range = - read_block_range::(Some(request), "GetNetworkAccountIds")? - .into_inclusive_range::()?; - - let (account_ids, mut last_block_included) = - self.state.get_all_network_accounts(block_range).await.map_err(internal_error)?; - - let account_ids = Vec::from_iter(account_ids.into_iter().map(Into::into)); - - let mut chain_tip = self.state.chain_tip(Finality::Committed).await; - if last_block_included > chain_tip { - last_block_included = chain_tip; - } - - chain_tip = self.state.chain_tip(Finality::Committed).await; - - Ok(Response::new(proto::store::NetworkAccountIdList { - account_ids, - pagination_info: Some(proto::rpc::PaginationInfo { - chain_tip: chain_tip.as_u32(), - block_num: last_block_included.as_u32(), - }), - })) - } - async fn get_account( &self, request: Request, diff --git a/crates/store/src/state/mod.rs b/crates/store/src/state/mod.rs index 7d8bfcc1c0..27f91ea029 100644 --- a/crates/store/src/state/mod.rs +++ b/crates/store/src/state/mod.rs @@ -5,7 +5,6 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::num::NonZeroUsize; -use std::ops::RangeInclusive; use std::path::Path; use std::sync::Arc; @@ -31,7 +30,7 @@ use miden_protocol::asset::{AssetVaultKey, AssetWitness}; use miden_protocol::block::account_tree::AccountWitness; use miden_protocol::block::nullifier_tree::{NullifierTree, NullifierWitness}; use miden_protocol::block::{BlockHeader, BlockInputs, BlockNumber, Blockchain}; -use miden_protocol::crypto::merkle::mmr::{MmrPeaks, MmrProof, PartialMmr}; +use miden_protocol::crypto::merkle::mmr::{MmrProof, PartialMmr}; use miden_protocol::crypto::merkle::smt::{LargeSmt, SmtStorage}; use miden_protocol::note::{NoteId, NoteScript, Nullifier}; use miden_protocol::transaction::PartialBlockchain; @@ -46,7 +45,6 @@ use crate::account_state_forest::{ }; use crate::accounts::AccountTreeWithHistory; use crate::blocks::BlockStore; -use crate::db::models::Page; use crate::db::{Db, NoteRecord, NullifierInfo}; use crate::errors::{ ApplyBlockError, @@ -55,7 +53,6 @@ use crate::errors::{ GetBatchInputsError, GetBlockHeaderError, GetBlockInputsError, - GetCurrentBlockchainDataError, StateInitializationError, }; use crate::proven_tip::ProvenTipWriter; @@ -420,35 +417,6 @@ impl State { self.db.select_notes_by_id(note_ids).await } - /// If the input block number is the current chain tip, `None` is returned. - /// Otherwise, gets the current chain tip's block header with its corresponding MMR peaks. - pub async fn get_current_blockchain_data( - &self, - block_num: Option, - ) -> Result, GetCurrentBlockchainDataError> { - if let Some(number) = block_num - && number == self.chain_tip(Finality::Committed).await - { - return Ok(None); - } - - // SAFETY: `select_block_header_by_block_num` will always return `Some(chain_tip_header)` - // when `None` is passed - let block_header: BlockHeader = self - .db - .select_block_header_by_block_num(None) - .await - .map_err(GetCurrentBlockchainDataError::ErrorRetrievingBlockHeader)? - .unwrap(); - - let blockchain = &self.inner.read().await.blockchain; - let peaks = blockchain - .peaks_at(block_header.block_num()) - .map_err(GetCurrentBlockchainDataError::InvalidPeaks)?; - - Ok(Some((block_header, peaks))) - } - /// Fetches the inputs for a transaction batch from the database. /// /// ## Inputs @@ -759,29 +727,6 @@ impl State { self.db.select_account(id).await } - /// Returns details for public (on-chain) network accounts by full account ID. - pub async fn get_network_account_details_by_id( - &self, - account_id: AccountId, - ) -> Result, DatabaseError> { - self.db.select_network_account_by_id(account_id).await - } - - /// Returns network account IDs within the specified block range (based on account creation - /// block). - /// - /// The function may return fewer accounts than exist in the range if the result would exceed - /// `MAX_RESPONSE_PAYLOAD_BYTES / AccountId::SERIALIZED_SIZE` rows. In this case, the result is - /// truncated at a block boundary to ensure all accounts from included blocks are returned. - /// - /// The response includes the last block number that was fully included in the result. - pub async fn get_all_network_accounts( - &self, - block_range: RangeInclusive, - ) -> Result<(Vec, BlockNumber), DatabaseError> { - self.db.select_all_network_account_ids(block_range).await - } - /// Returns an account witness and optionally account details at a specific block. /// /// The witness is a Merkle proof of inclusion in the account tree, proving the account's @@ -1093,17 +1038,6 @@ impl State { self.block_store.load_proof(block_num).await.map_err(Into::into) } - /// Returns the network notes for an account that are unconsumed by a specified block number, - /// along with the next pagination token. - pub async fn get_unconsumed_network_notes_for_account( - &self, - account_id: AccountId, - block_num: BlockNumber, - page: Page, - ) -> Result<(Vec, Page), DatabaseError> { - self.db.select_unconsumed_network_notes(account_id, block_num, page).await - } - /// Returns the script for a note by its root. pub async fn get_note_script_by_root( &self, diff --git a/proto/proto/internal/store.proto b/proto/proto/internal/store.proto index 6c3bdde72d..a06e458efe 100644 --- a/proto/proto/internal/store.proto +++ b/proto/proto/internal/store.proto @@ -294,20 +294,6 @@ service NtxBuilder { // single client. rpc BlockSubscription(BlockSubscriptionRequest) returns (stream SignedBlock) {} - // Returns a paginated list of unconsumed network notes. - rpc GetUnconsumedNetworkNotes(UnconsumedNetworkNotesRequest) returns (UnconsumedNetworkNotes) {} - - // Returns the block header at the chain tip, as well as the MMR peaks corresponding to this - // header for executing network transactions. If the block number is not provided, the latest - // header and peaks will be retrieved. - rpc GetCurrentBlockchainData(blockchain.MaybeBlockNumber) returns (CurrentBlockchainData) {} - - // Returns the latest state of a network account with the specified account ID. - rpc GetNetworkAccountDetailsById(account.AccountId) returns (MaybeAccountDetails) {} - - // Returns a list of all network account ids. - rpc GetNetworkAccountIds(rpc.BlockRange) returns (NetworkAccountIdList) {} - // Returns the latest details of the specified account. rpc GetAccount(rpc.AccountRequest) returns (rpc.AccountResponse) {} @@ -321,77 +307,6 @@ service NtxBuilder { rpc GetStorageMapWitness(StorageMapWitnessRequest) returns (StorageMapWitnessResponse) {} } -// GET NETWORK ACCOUNT DETAILS BY ID -// ================================================================================================ - -// Represents the result of getting network account details by ID. -message MaybeAccountDetails { - // Account details. - optional account.AccountDetails details = 1; -} - -// GET UNCONSUMED NETWORK NOTES -// ================================================================================================ - -// Returns a paginated list of unconsumed network notes for an account. -// -// Notes created or consumed after the specified block are excluded from the result. -message UnconsumedNetworkNotesRequest { - // This should be null on the first call, and set to the response token until the response token - // is null, at which point all data has been fetched. - // - // Note that this token is only valid if used with the same parameters. - optional uint64 page_token = 1; - - // Number of notes to retrieve per page. - uint64 page_size = 2; - - // The full account ID to filter notes by. - account.AccountId account_id = 3; - - // The block number to filter the returned notes by. - // - // Notes that are created or consumed after this block are excluded from the result. - fixed32 block_num = 4; -} - -// Represents the result of getting the unconsumed network notes. -message UnconsumedNetworkNotes { - // An opaque pagination token. - // - // Use this in your next request to get the next - // set of data. - // - // Will be null once there is no more data remaining. - optional uint64 next_token = 1; - - // The list of unconsumed network notes. - repeated note.NetworkNote notes = 2; -} - -// GET NETWORK ACCOUNTS -// ================================================================================================ - -// Represents the result of getting the network account ids. -message NetworkAccountIdList { - // Pagination information. - rpc.PaginationInfo pagination_info = 1; - - // The list of network account ids. - repeated account.AccountId account_ids = 2; -} - -// GET CURRENT BLOCKCHAIN DATA -// ================================================================================================ - -// Current blockchain data based on the requested block number. -message CurrentBlockchainData { - // Commitments that represent the current state according to the MMR. - repeated primitives.Digest current_peaks = 1; - // Current block header. - optional blockchain.BlockHeader current_block_header = 2; -} - // GET VAULT ASSET WITNESSES // ================================================================================================ From 21bf0c19f01a2668471a5808e441640f9e0bccab Mon Sep 17 00:00:00 2001 From: Mirko <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Wed, 20 May 2026 07:50:04 +0200 Subject: [PATCH 03/10] feat: xtask for reflowing comments (#2099) --- .cargo/config.toml | 3 + .github/workflows/ci.yml | 7 + Cargo.lock | 49 ++ Cargo.toml | 1 + Makefile | 6 +- bin/genesis/src/main.rs | 21 +- bin/network-monitor/src/counter.rs | 18 +- bin/network-monitor/src/deploy/mod.rs | 13 +- bin/network-monitor/src/explorer.rs | 8 +- bin/network-monitor/src/remote_prover.rs | 18 +- bin/network-monitor/src/service.rs | 10 +- .../src/view/cards/explorer.rs | 4 +- bin/network-monitor/src/view/cards/faucet.rs | 4 +- .../src/view/cards/remote_prover.rs | 4 +- bin/network-monitor/src/view/helpers.rs | 4 +- bin/node/src/commands/block_producer.rs | 4 +- bin/node/src/commands/rpc.rs | 4 +- bin/ntx-builder/src/actor/execute.rs | 3 +- bin/ntx-builder/src/actor/mod.rs | 28 +- bin/ntx-builder/src/builder.rs | 24 +- bin/ntx-builder/src/chain_state.rs | 4 +- bin/ntx-builder/src/commands/mod.rs | 4 +- bin/ntx-builder/src/coordinator.rs | 18 +- bin/ntx-builder/src/db/models/queries/mod.rs | 14 +- .../src/db/models/queries/notes.rs | 4 +- bin/ntx-builder/src/lib.rs | 16 +- bin/remote-prover/src/server/mod.rs | 3 +- bin/remote-prover/src/server/prove.rs | 5 +- bin/remote-prover/src/server/tests.rs | 8 +- bin/stress-test/src/main.rs | 12 +- bin/stress-test/src/seeding/mod.rs | 12 +- bin/stress-test/src/store/metrics.rs | 8 +- bin/stress-test/src/store/mod.rs | 6 +- bin/validator/src/block_validation/mod.rs | 3 +- bin/validator/src/commands/bootstrap.rs | 4 +- bin/validator/src/server/sign_block.rs | 4 +- bin/validator/src/server/tests.rs | 32 +- bin/validator/src/signers/kms.rs | 7 +- bin/validator/src/tx_validation/data_store.rs | 4 +- .../block-producer/src/batch_builder/mod.rs | 5 +- .../block-producer/src/block_builder/mod.rs | 16 +- .../block-producer/src/domain/transaction.rs | 4 +- crates/block-producer/src/errors.rs | 3 +- crates/block-producer/src/mempool/budget.rs | 4 +- .../block-producer/src/mempool/graph/batch.rs | 4 +- .../block-producer/src/mempool/graph/node.rs | 16 +- .../src/mempool/graph/transaction.rs | 6 +- .../src/mempool/subscription.rs | 4 +- crates/block-producer/src/mempool/tests.rs | 10 +- .../src/mempool/tests/add_transaction.rs | 4 +- crates/block-producer/src/server/mod.rs | 4 +- crates/block-producer/src/server/tests.rs | 12 +- .../block-producer/src/test_utils/account.rs | 4 +- crates/block-producer/src/test_utils/mod.rs | 4 +- crates/proto/build.rs | 10 +- crates/proto/src/clients/mod.rs | 4 +- crates/proto/src/decode/mod.rs | 4 +- crates/proto/src/domain/account.rs | 27 +- crates/proto/src/domain/note.rs | 8 +- crates/remote-prover-client/src/lib.rs | 11 +- .../src/remote_prover/batch_prover.rs | 22 +- .../src/remote_prover/block_prover.rs | 10 +- crates/rocksdb-cxx-linkage-fix/src/lib.rs | 3 +- crates/rpc/src/server/accept.rs | 20 +- crates/rpc/src/server/api.rs | 4 +- crates/rpc/src/server/mod.rs | 6 +- crates/rpc/src/tests.rs | 21 +- crates/store/benches/account_tree.rs | 8 +- crates/store/build.rs | 19 +- crates/store/src/account_state_forest/mod.rs | 8 +- .../store/src/account_state_forest/tests.rs | 6 +- crates/store/src/accounts/mod.rs | 22 +- crates/store/src/accounts/tests.rs | 3 +- crates/store/src/db/mod.rs | 12 +- .../store/src/db/models/queries/accounts.rs | 39 +- .../src/db/models/queries/accounts/delta.rs | 12 +- .../src/db/models/queries/accounts/tests.rs | 20 +- .../src/db/models/queries/block_headers.rs | 4 +- crates/store/src/db/models/queries/notes.rs | 8 +- .../src/db/models/queries/transactions.rs | 18 +- crates/store/src/db/models/utils.rs | 4 +- crates/store/src/db/schema.rs | 4 +- crates/store/src/db/tests.rs | 79 ++- crates/store/src/errors.rs | 4 +- crates/store/src/genesis/config/mod.rs | 12 +- crates/store/src/genesis/mod.rs | 4 +- crates/store/src/server/block_producer.rs | 7 +- crates/store/src/server/mod.rs | 4 +- crates/store/src/server/proof_scheduler.rs | 4 +- crates/store/src/server/replica_sync.rs | 4 +- crates/store/src/server/rpc_api.rs | 4 +- crates/store/src/state/apply_block.rs | 31 +- crates/store/src/state/loader.rs | 25 +- crates/store/src/state/mod.rs | 37 +- crates/store/src/state/sync_state.rs | 7 +- crates/utils/src/clap.rs | 8 +- crates/utils/src/fifo_cache.rs | 8 +- crates/utils/src/limiter.rs | 7 +- crates/utils/src/logging.rs | 12 +- crates/utils/src/lru_cache.rs | 10 +- rustfmt.toml | 12 +- xtask/Cargo.toml | 18 + xtask/src/comment_reflow.rs | 501 ++++++++++++++++++ xtask/src/main.rs | 122 +++++ 104 files changed, 1221 insertions(+), 537 deletions(-) create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/comment_reflow.rs create mode 100644 xtask/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index fd6fa0925a..dfd2ca161a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,6 @@ +[alias] +xtask = "run --locked --package xtask --" + [target.wasm32-unknown-unknown] rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff4d4c0d8b..8693e23ca5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -258,8 +258,15 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false + - name: Rustup + run: rustup toolchain install --no-self-update - name: Rustup +nightly run: rustup toolchain install --no-self-update nightly --component rustfmt + - uses: WarpBuilds/rust-cache@9d0cc3090d9c87de74ea67617b246e978735b1a1 # v2.9.1 + with: + shared-key: ${{ github.job }} + prefix-key: ${{ env.CACHE_PREFIX }} + save-if: ${{ env.SAVE_CACHE }} - name: Fmt run: make format-check diff --git a/Cargo.lock b/Cargo.lock index e04755e2fb..76f46af9d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5679,6 +5679,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "indexmap", "itoa", "memchr", "serde", @@ -5930,6 +5931,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + [[package]] name = "strength_reduce" version = "0.2.4" @@ -6715,6 +6722,36 @@ dependencies = [ "strength_reduce", ] +[[package]] +name = "tree-sitter" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887bd495d0582c5e3e0d8ece2233666169fa56a9644d172fc22ad179ab2d0538" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" + +[[package]] +name = "tree-sitter-rust" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439e577dbe07423ec2582ac62c7531120dbfccfa6e5f92406f93dd271a120e45" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -7539,6 +7576,18 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "xtask" +version = "0.15.0" +dependencies = [ + "anyhow", + "clap", + "fs-err", + "toml 1.1.2+spec-1.1.0", + "tree-sitter", + "tree-sitter-rust", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index f0c9c8f2e5..89b2b89b8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "crates/test-macro", "crates/utils", "proto", + "xtask", ] resolver = "2" diff --git a/Makefile b/Makefile index 2628151c20..cadf025d02 100644 --- a/Makefile +++ b/Makefile @@ -26,12 +26,14 @@ fix: ## Runs Fix with configs .PHONY: format -format: ## Runs Format using nightly toolchain +format: ## Runs rustfmt and comment reflow + cargo xtask fmt-comments --write cargo +nightly fmt --all .PHONY: format-check -format-check: ## Runs Format using nightly toolchain but only in check mode +format-check: ## Checks rustfmt and comment reflow + cargo xtask fmt-comments --check cargo +nightly fmt --all --check diff --git a/bin/genesis/src/main.rs b/bin/genesis/src/main.rs index 4244ee6b13..9ce02cf5f9 100644 --- a/bin/genesis/src/main.rs +++ b/bin/genesis/src/main.rs @@ -23,8 +23,8 @@ use rand::Rng; use rand_chacha::ChaCha20Rng; use rand_chacha::rand_core::SeedableRng; -/// Generate canonical Miden genesis accounts (bridge, bridge admin, GER manager) -/// and a genesis.toml configuration file. +/// Generate canonical Miden genesis accounts (bridge, bridge admin, GER manager) and a genesis.toml +/// configuration file. #[derive(Parser)] #[command(name = "miden-genesis")] struct Cli { @@ -32,13 +32,13 @@ struct Cli { #[arg(long, default_value = "./genesis")] output_dir: PathBuf, - /// Hex-encoded Falcon512 public key for the bridge admin account. - /// If omitted, a new keypair is generated and the secret key is included in the .mac file. + /// Hex-encoded Falcon512 public key for the bridge admin account. If omitted, a new keypair is + /// generated and the secret key is included in the .mac file. #[arg(long, value_name = "HEX", requires = "ger_manager_public_key")] bridge_admin_public_key: Option, - /// Hex-encoded Falcon512 public key for the GER manager account. - /// If omitted, a new keypair is generated and the secret key is included in the .mac file. + /// Hex-encoded Falcon512 public key for the GER manager account. If omitted, a new keypair is + /// generated and the secret key is included in the .mac file. #[arg(long, value_name = "HEX", requires = "bridge_admin_public_key")] ger_manager_public_key: Option, } @@ -97,9 +97,8 @@ fn run( let bridge_seed = Word::from(bridge_seed.map(Felt::new)); let bridge = create_bridge_account(bridge_seed, bridge_admin_id, ger_manager_id); - // Bump bridge nonce to 1 (required for genesis accounts). - // File-loaded accounts via [[account]] in genesis.toml are included as-is, - // so we must set nonce=1 before writing the .mac file. + // Bump bridge nonce to 1 (required for genesis accounts). File-loaded accounts via [[account]] + // in genesis.toml are included as-is, so we must set nonce=1 before writing the .mac file. let bridge = bump_nonce_to_one(bridge).context("failed to bump bridge account nonce")?; // Write .mac files. @@ -203,8 +202,8 @@ mod tests { use super::*; - /// Parses the generated genesis.toml, builds a genesis block, and asserts the bridge account - /// is included with nonce=1. + /// Parses the generated genesis.toml, builds a genesis block, and asserts the bridge account is + /// included with nonce=1. fn assert_valid_genesis_block(dir: &Path) { let bridge_id = AccountFile::read(dir.join("bridge.mac")).unwrap().account.id(); diff --git a/bin/network-monitor/src/counter.rs b/bin/network-monitor/src/counter.rs index 6bc589b278..2d829235ba 100644 --- a/bin/network-monitor/src/counter.rs +++ b/bin/network-monitor/src/counter.rs @@ -156,8 +156,8 @@ impl IncrementService { }) } - /// Applies a successful increment: updates the wallet nonce, bumps counters, and returns - /// the next expected counter value. + /// Applies a successful increment: updates the wallet nonce, bumps counters, and returns the + /// next expected counter value. fn handle_increment_success(&mut self, final_account: &AccountHeader, tx_id: String) -> u64 { let updated_wallet = Account::new( self.tx.wallet_account.id(), @@ -407,9 +407,9 @@ impl CounterTrackingService { }) } - /// The increment service regenerates accounts on persistent failure and rewrites the - /// counter account file. If the file's account ID has changed, switch to the new account - /// and reset tracking state. + /// The increment service regenerates accounts on persistent failure and rewrites the counter + /// account file. If the file's account ID has changed, switch to the new account and reset + /// tracking state. async fn reload_counter_account_if_changed(&mut self) { let reloaded = match load_counter_account(&self.config.counter_filepath) { Ok(account) => account, @@ -523,8 +523,8 @@ impl Service for CounterTrackingService { } fn interval(&self) -> Duration { - // Tracking polls twice per increment cadence so it catches freshly-incremented values - // soon after submission. + // Tracking polls twice per increment cadence so it catches freshly-incremented values soon + // after submission. self.config.counter_increment_interval / 2 } @@ -542,8 +542,8 @@ impl Service for CounterTrackingService { // SETUP // ================================================================================================ -/// Load wallet + counter accounts, fetch the genesis block header, and build the data store -/// and increment script needed to produce network notes. +/// Load wallet + counter accounts, fetch the genesis block header, and build the data store and +/// increment script needed to produce network notes. async fn setup_increment_task( config: MonitorConfig, rpc_client: &mut RpcClient, diff --git a/bin/network-monitor/src/deploy/mod.rs b/bin/network-monitor/src/deploy/mod.rs index cb99b24d5e..06963c1e55 100644 --- a/bin/network-monitor/src/deploy/mod.rs +++ b/bin/network-monitor/src/deploy/mod.rs @@ -38,15 +38,14 @@ use crate::deploy::wallet::{create_wallet_account, save_wallet_account}; pub mod counter; pub mod wallet; -/// Create an RPC client configured with the correct genesis metadata in the -/// `Accept` header so that write RPCs such as `SubmitProvenTx` are -/// accepted by the node. +/// Create an RPC client configured with the correct genesis metadata in the `Accept` header so that +/// write RPCs such as `SubmitProvenTx` are accepted by the node. pub async fn create_genesis_aware_rpc_client( rpc_url: &Url, timeout: Duration, ) -> Result { - // First, create a temporary client without genesis metadata to discover the - // genesis block header and its commitment. + // First, create a temporary client without genesis metadata to discover the genesis block + // header and its commitment. let mut rpc: RpcClient = Builder::new(rpc_url.clone()) .with_tls() .context("Failed to configure TLS for RPC client")? @@ -78,8 +77,8 @@ pub async fn create_genesis_aware_rpc_client( let genesis_commitment = genesis_header.commitment(); let genesis = genesis_commitment.to_hex(); - // Rebuild the client, this time including the required genesis metadata so that - // write RPCs like SubmitProvenTx are accepted by the node. + // Rebuild the client, this time including the required genesis metadata so that write RPCs like + // SubmitProvenTx are accepted by the node. let rpc_client = Builder::new(rpc_url.clone()) .with_tls() .context("Failed to configure TLS for RPC client")? diff --git a/bin/network-monitor/src/explorer.rs b/bin/network-monitor/src/explorer.rs index cc0d2439a3..d0fcd8f9d8 100644 --- a/bin/network-monitor/src/explorer.rs +++ b/bin/network-monitor/src/explorer.rs @@ -244,8 +244,8 @@ mod tests { #[test] fn truncate_json_multibyte_chars_are_handled() { - // Each 'é' is 2 bytes in UTF-8. Build a string whose serialized JSON form - // exceeds 60 characters, ensuring truncation lands on a char boundary. + // Each 'é' is 2 bytes in UTF-8. Build a string whose serialized JSON form exceeds 60 + // characters, ensuring truncation lands on a char boundary. let multibyte_string = "é".repeat(80); let value = json!(multibyte_string); // Should not panic and should still truncate correctly. @@ -255,8 +255,8 @@ mod tests { #[test] fn truncate_json_exactly_60_chars_is_not_truncated() { - // Build a JSON string whose serialized form is exactly 60 characters. - // json!("x".repeat(58)) serializes as `"xxx...xxx"` (58 chars + 2 quotes = 60). + // Build a JSON string whose serialized form is exactly 60 characters. json!("x".repeat(58)) + // serializes as `"xxx...xxx"` (58 chars + 2 quotes = 60). let value = json!("x".repeat(58)); let result = truncate_json(&value); assert_eq!(result.chars().count(), 60); diff --git a/bin/network-monitor/src/remote_prover.rs b/bin/network-monitor/src/remote_prover.rs index 3370d63076..7857928a7c 100644 --- a/bin/network-monitor/src/remote_prover.rs +++ b/bin/network-monitor/src/remote_prover.rs @@ -96,8 +96,8 @@ pub struct ProbeSnapshot { // PROVER STATUS SERVICE // ================================================================================================ -/// Parameters captured at construction time for spawning the probe task lazily, the first -/// time the status service observes the prover reporting [`ProofType::Transaction`]. +/// Parameters captured at construction time for spawning the probe task lazily, the first time the +/// status service observes the prover reporting [`ProofType::Transaction`]. struct ProbeSpawner { client: RemoteProverClient, payload: proto::remote_prover::ProofRequest, @@ -106,9 +106,9 @@ struct ProbeSpawner { name: String, } -/// Polls the remote prover's proxy status endpoint and publishes the combined -/// [`ServiceStatus`] (status + latest probe outcome). Spawns the probe task the first -/// time the prover reports Transaction type. +/// Polls the remote prover's proxy status endpoint and publishes the combined [`ServiceStatus`] +/// (status + latest probe outcome). Spawns the probe task the first time the prover reports +/// Transaction type. pub struct ProverStatusService { name: String, url: String, @@ -154,8 +154,8 @@ impl ProverStatusService { } } - /// Spawns the probe task if the prover has just been observed to support Transaction - /// proofs and we haven't spawned it yet. No-op in all other cases. + /// Spawns the probe task if the prover has just been observed to support Transaction proofs and + /// we haven't spawned it yet. No-op in all other cases. fn maybe_spawn_probe(&mut self) { let Some(status) = &self.last_status else { return }; if !matches!(status.supported_proof_type, ProofType::Transaction) { @@ -267,8 +267,8 @@ impl Service for ProverStatusService { // ================================================================================================ /// Runs proof-test probes on the configured interval. The task is spawned by -/// [`ProverStatusService::maybe_spawn_probe`] only after the prover has been observed to -/// support Transaction proofs. +/// [`ProverStatusService::maybe_spawn_probe`] only after the prover has been observed to support +/// Transaction proofs. #[instrument( parent = None, target = COMPONENT, diff --git a/bin/network-monitor/src/service.rs b/bin/network-monitor/src/service.rs index fdbfa4c01e..4d54e30b3f 100644 --- a/bin/network-monitor/src/service.rs +++ b/bin/network-monitor/src/service.rs @@ -17,8 +17,8 @@ use url::Url; use crate::service_status::ServiceStatus; -/// Build a lazily-connected gRPC client using the network monitor's standard settings -/// (TLS enabled, no metadata, no OTEL propagation). +/// Build a lazily-connected gRPC client using the network monitor's standard settings (TLS enabled, +/// no metadata, no OTEL propagation). pub fn build_tls_client(url: Url, timeout: Duration) -> C { ClientBuilder::new(url) .with_tls() @@ -44,9 +44,9 @@ pub trait Service: Send + 'static { /// Runs a single check iteration. fn check(&mut self) -> impl std::future::Future + Send; - /// Full service lifecycle. The default implementation loops on [`Self::interval`] ticks, - /// calls [`Self::check`], and publishes the result. Returns when the channel has no - /// receivers (clean shutdown). Services with custom scheduling override this. + /// Full service lifecycle. The default implementation loops on [`Self::interval`] ticks, calls + /// [`Self::check`], and publishes the result. Returns when the channel has no receivers (clean + /// shutdown). Services with custom scheduling override this. fn run( mut self, tx: watch::Sender, diff --git a/bin/network-monitor/src/view/cards/explorer.rs b/bin/network-monitor/src/view/cards/explorer.rs index eae5544a1c..51051488a9 100644 --- a/bin/network-monitor/src/view/cards/explorer.rs +++ b/bin/network-monitor/src/view/cards/explorer.rs @@ -1,5 +1,5 @@ -//! Renders the explorer card. Compares the explorer's tip against the RPC's tip (passed in by -//! the dispatcher) and surfaces a warning banner past `EXPLORER_LAG_TOLERANCE` blocks of drift. +//! Renders the explorer card. Compares the explorer's tip against the RPC's tip (passed in by the +//! dispatcher) and surfaces a warning banner past `EXPLORER_LAG_TOLERANCE` blocks of drift. use maud::{Markup, html}; diff --git a/bin/network-monitor/src/view/cards/faucet.rs b/bin/network-monitor/src/view/cards/faucet.rs index bd80177db2..24494ab119 100644 --- a/bin/network-monitor/src/view/cards/faucet.rs +++ b/bin/network-monitor/src/view/cards/faucet.rs @@ -1,5 +1,5 @@ -//! Renders the faucet card: HTTP test outcome plus the metadata block (token id, supply, -//! decimals, …) when the faucet exposed it. +//! Renders the faucet card: HTTP test outcome plus the metadata block (token id, supply, decimals, +//! …) when the faucet exposed it. use maud::{Markup, html}; diff --git a/bin/network-monitor/src/view/cards/remote_prover.rs b/bin/network-monitor/src/view/cards/remote_prover.rs index 7e0534a406..bbc4aaedee 100644 --- a/bin/network-monitor/src/view/cards/remote_prover.rs +++ b/bin/network-monitor/src/view/cards/remote_prover.rs @@ -1,5 +1,5 @@ -//! Renders the remote-prover card: proxy info, worker list, and last proof-generation test -//! outcome. Embeds `data-grpc-url` for `/remote_prover.ProxyStatusApi/Status` browser probes. +//! Renders the remote-prover card: proxy info, worker list, and last proof-generation test outcome. +//! Embeds `data-grpc-url` for `/remote_prover.ProxyStatusApi/Status` browser probes. use maud::{Markup, html}; diff --git a/bin/network-monitor/src/view/helpers.rs b/bin/network-monitor/src/view/helpers.rs index 671fc27ff5..204807bf8e 100644 --- a/bin/network-monitor/src/view/helpers.rs +++ b/bin/network-monitor/src/view/helpers.rs @@ -19,8 +19,8 @@ pub(super) fn metric_row(label: &str, value: &str) -> Markup { } } -/// Inline copy-to-clipboard button. `label` appears in the tooltip ("Copy full {label}"); -/// `value` is the literal text written to the clipboard via the JS helper. +/// Inline copy-to-clipboard button. `label` appears in the tooltip ("Copy full {label}"); `value` +/// is the literal text written to the clipboard via the JS helper. pub(super) fn copy_button(value: &str, label: &str) -> Markup { let onclick = format!("copyToClipboard({}, event)", json_string(value)); html! { diff --git a/bin/node/src/commands/block_producer.rs b/bin/node/src/commands/block_producer.rs index b921868231..b1056a39d9 100644 --- a/bin/node/src/commands/block_producer.rs +++ b/bin/node/src/commands/block_producer.rs @@ -192,8 +192,8 @@ pub struct BlockProducerConfig { )] pub batch_interval: Duration, - /// The remote batch prover's gRPC url. If unset, will default to running a prover - /// in-process which is expensive. + /// The remote batch prover's gRPC url. If unset, will default to running a prover in-process + /// which is expensive. #[arg(long = "batch-prover.url", env = ENV_BATCH_PROVER_URL, value_name = "URL")] pub batch_prover_url: Option, diff --git a/bin/node/src/commands/rpc.rs b/bin/node/src/commands/rpc.rs index 3e055cfff0..968dc2c5ba 100644 --- a/bin/node/src/commands/rpc.rs +++ b/bin/node/src/commands/rpc.rs @@ -25,8 +25,8 @@ pub enum RpcCommand { #[arg(long = "store.url", env = ENV_STORE_URL, value_name = "URL")] store_url: Url, - /// The block-producer's gRPC url. If unset, will run the RPC in read-only mode, - /// i.e. without a block-producer. + /// The block-producer's gRPC url. If unset, will run the RPC in read-only mode, i.e. + /// without a block-producer. #[arg(long = "block-producer.url", env = ENV_BLOCK_PRODUCER_URL, value_name = "URL")] block_producer_url: Option, diff --git a/bin/ntx-builder/src/actor/execute.rs b/bin/ntx-builder/src/actor/execute.rs index d58ec5b00d..c2152dab89 100644 --- a/bin/ntx-builder/src/actor/execute.rs +++ b/bin/ntx-builder/src/actor/execute.rs @@ -400,8 +400,7 @@ struct NtxDataStore { script_cache: LruCache, /// Local database for persistent note script. db: Db, - /// Scripts fetched from the remote store during execution, to be persisted by the - /// coordinator. + /// Scripts fetched from the remote store during execution, to be persisted by the coordinator. fetched_scripts: Arc>>, /// Mapping of storage map roots to storage slot names observed during various calls. /// diff --git a/bin/ntx-builder/src/actor/mod.rs b/bin/ntx-builder/src/actor/mod.rs index 4609bebecd..f53815f484 100644 --- a/bin/ntx-builder/src/actor/mod.rs +++ b/bin/ntx-builder/src/actor/mod.rs @@ -29,10 +29,10 @@ use crate::db::Db; /// A request sent from an account actor to the coordinator via a shared mpsc channel. pub enum ActorRequest { - /// One or more notes failed during transaction execution and should have their attempt - /// counters incremented. The actor waits for the coordinator to acknowledge the DB write via - /// the oneshot channel, preventing race conditions where the actor could re-select the same - /// notes before the failure is persisted. + /// One or more notes failed during transaction execution and should have their attempt counters + /// incremented. The actor waits for the coordinator to acknowledge the DB write via the oneshot + /// channel, preventing race conditions where the actor could re-select the same notes before + /// the failure is persisted. NotesFailed { failed_notes: Vec<(Nullifier, NoteError)>, block_num: BlockNumber, @@ -54,8 +54,8 @@ pub struct GrpcClients { pub block_producer: BlockProducerClient, /// Client for interacting with the validator. pub validator: ValidatorClient, - /// Client for remote transaction proving. If `None`, transactions will be proven locally, - /// which is undesirable due to the performance impact. + /// Client for remote transaction proving. If `None`, transactions will be proven locally, which + /// is undesirable due to the performance impact. pub prover: Option, } @@ -193,8 +193,8 @@ pub struct AccountActor { state: State, /// Per-actor configuration knobs. config: ActorConfig, - /// Notification signal from the coordinator indicating that DB state relevant to this actor - /// may have changed. The actor re-evaluates its state from the DB on each notification. + /// Notification signal from the coordinator indicating that DB state relevant to this actor may + /// have changed. The actor re-evaluates its state from the DB on each notification. notify: Arc, /// Channel for sending requests to the coordinator. request: mpsc::Sender, @@ -226,8 +226,8 @@ impl AccountActor { pub async fn run(self, semaphore: Arc) -> anyhow::Result<()> { let account_id = self.account_id; - // Wait for the account to be committed to the DB. For newly created accounts, - // the creation transaction must be committed before we start processing notes. + // Wait for the account to be committed to the DB. For newly created accounts, the creation + // transaction must be committed before we start processing notes. if !self.wait_for_committed_account(account_id).await? { return Ok(()); } @@ -258,8 +258,8 @@ impl AccountActor { ActorMode::NotesAvailable => semaphore.acquire().boxed(), }; - // Idle timeout timer: only ticks when in NoViableNotes mode. - // Mode changes cause the next loop iteration to create a fresh sleep or pending. + // Idle timeout timer: only ticks when in NoViableNotes mode. Mode changes cause the + // next loop iteration to create a fresh sleep or pending. let idle_timeout_sleep = match mode { ActorMode::NoViableNotes => tokio::time::sleep(self.config.idle_timeout).boxed(), _ => std::future::pending().boxed(), @@ -456,8 +456,8 @@ impl AccountActor { "network transaction failed", ); - // For `AllNotesFailed`, use the per-note errors which contain the - // specific reason each note failed (e.g. consumability check details). + // For `AllNotesFailed`, use the per-note errors which contain the specific reason + // each note failed (e.g. consumability check details). let failed_notes: Vec<_> = match err { execute::NtxError::AllNotesFailed(per_note) => log_failed_notes(per_note), other => { diff --git a/bin/ntx-builder/src/builder.rs b/bin/ntx-builder/src/builder.rs index f2df31a69d..a051249601 100644 --- a/bin/ntx-builder/src/builder.rs +++ b/bin/ntx-builder/src/builder.rs @@ -115,8 +115,8 @@ impl NetworkTransactionBuilder { join_set.spawn(self.run_event_loop()); - // Wait for either the event loop or the gRPC server to complete. - // Any completion is treated as fatal. + // Wait for either the event loop or the gRPC server to complete. Any completion is treated + // as fatal. if let Some(result) = join_set.join_next().await { result.context("ntx-builder task panicked")??; } @@ -126,8 +126,8 @@ impl NetworkTransactionBuilder { /// Runs the main event loop. async fn run_event_loop(mut self) -> anyhow::Result<()> { - // Spawn a background task to load network accounts from the store. - // Accounts are sent through a channel and processed in the main event loop. + // Spawn a background task to load network accounts from the store. Accounts are sent + // through a channel and processed in the main event loop. let (account_tx, mut account_rx) = mpsc::channel::(self.config.account_channel_capacity); let account_loader_store = self.store.clone(); @@ -156,9 +156,9 @@ impl NetworkTransactionBuilder { self.handle_mempool_event(event).await?; }, - // Handle account batches loaded from the store. - // Once all accounts are loaded, the channel closes and this branch - // becomes inactive (recv returns None and we stop matching). + // Handle account batches loaded from the store. Once all accounts are loaded, the + // channel closes and this branch becomes inactive (recv returns None and we stop + // matching). Some(account_id) = account_rx.recv() => { self.handle_loaded_account(account_id).await?; }, @@ -166,9 +166,9 @@ impl NetworkTransactionBuilder { Some(request) = self.actor_request_rx.recv() => { self.handle_actor_request(request).await?; }, - // Handle account loader task completion/failure. - // If the task fails, we abort since the builder would be in a degraded state - // where existing notes against network accounts won't be processed. + // Handle account loader task completion/failure. If the task fails, we abort since + // the builder would be in a degraded state where existing notes against network + // accounts won't be processed. result = &mut account_loader_handle => { result .context("account loader task panicked") @@ -250,8 +250,8 @@ impl NetworkTransactionBuilder { self.coordinator.notify_accounts(&result.accounts_to_notify); Ok(()) }, - // Notify affected actors (reverted account actors will self-cancel when they - // detect their account has been removed from the DB). + // Notify affected actors (reverted account actors will self-cancel when they detect + // their account has been removed from the DB). MempoolEvent::TransactionsReverted(_) => { // Write event effects to DB first. let result = self diff --git a/bin/ntx-builder/src/chain_state.rs b/bin/ntx-builder/src/chain_state.rs index 12d5b79c57..b59549ffec 100644 --- a/bin/ntx-builder/src/chain_state.rs +++ b/bin/ntx-builder/src/chain_state.rs @@ -49,8 +49,8 @@ impl ChainState { /// Updates the chain tip and prunes old blocks from the MMR. fn update_chain_tip(&mut self, tip: BlockHeader, max_block_count: usize) { - // Skip blocks already reflected in the chain state. A `BlockCommitted` event may arrive - // for a block whose state was already loaded from the store during startup: the mempool + // Skip blocks already reflected in the chain state. A `BlockCommitted` event may arrive for + // a block whose state was already loaded from the store during startup: the mempool // subscription is established first and then the chain tip is fetched, so any block // committed in that window produces an event for state we have already ingested. if tip.block_num() <= self.chain_tip_header.block_num() { diff --git a/bin/ntx-builder/src/commands/mod.rs b/bin/ntx-builder/src/commands/mod.rs index e309419caa..a883b81a8b 100644 --- a/bin/ntx-builder/src/commands/mod.rs +++ b/bin/ntx-builder/src/commands/mod.rs @@ -45,8 +45,8 @@ pub enum NtxBuilderCommand { #[arg(long = "validator.url", env = ENV_VALIDATOR_URL, value_name = "URL")] validator_url: Url, - /// The remote transaction prover's gRPC url. If unset, will default to running a - /// prover in-process which is expensive. + /// The remote transaction prover's gRPC url. If unset, will default to running a prover + /// in-process which is expensive. #[arg(long = "tx-prover.url", env = ENV_TX_PROVER_URL, value_name = "URL")] tx_prover_url: Option, diff --git a/bin/ntx-builder/src/coordinator.rs b/bin/ntx-builder/src/coordinator.rs index 7fdb6f0d5d..d4243cc979 100644 --- a/bin/ntx-builder/src/coordinator.rs +++ b/bin/ntx-builder/src/coordinator.rs @@ -26,8 +26,8 @@ pub struct WriteEventResult { /// Handle to an account actor spawned by the coordinator. #[derive(Clone)] struct ActorHandle { - /// [`Notify`] shared with the actor. The coordinator calls [`Notify::notify_one`] when DB - /// state relevant to the actor may have changed, the actor awaits [`Notify::notified`] and + /// [`Notify`] shared with the actor. The coordinator calls [`Notify::notify_one`] when DB state + /// relevant to the actor may have changed, the actor awaits [`Notify::notified`] and /// re-evaluates its state on wake-up. notify: Arc, } @@ -37,8 +37,8 @@ impl ActorHandle { Self { notify } } - /// Signals the actor that DB state may have changed. Notifications coalesce when one is - /// already pending. + /// Signals the actor that DB state may have changed. Notifications coalesce when one is already + /// pending. fn notify(&self) { self.notify.notify_one(); } @@ -137,8 +137,8 @@ pub struct Coordinator { } impl Coordinator { - /// Creates a new coordinator with the specified maximum number of inflight transactions - /// and the crash threshold for account deactivation. + /// Creates a new coordinator with the specified maximum number of inflight transactions and the + /// crash threshold for account deactivation. pub fn new(max_inflight_transactions: usize, max_account_crashes: usize, db: Db) -> Self { Self { actor_registry: HashMap::new(), @@ -224,9 +224,9 @@ impl Coordinator { let actor_result = self.actor_join_set.join_next().await; match actor_result { Some(Ok((account_id, Ok(())))) => { - // Actor shut down intentionally (idle timeout or account removed). - // Remove from registry and check if a notification arrived just as it shut - // down. If so, the caller should respawn it. + // Actor shut down intentionally (idle timeout or account removed). Remove from + // registry and check if a notification arrived just as it shut down. If so, the + // caller should respawn it. let should_respawn = self .actor_registry .remove(&account_id) diff --git a/bin/ntx-builder/src/db/models/queries/mod.rs b/bin/ntx-builder/src/db/models/queries/mod.rs index 1bf74a66ba..f2f97eaf3e 100644 --- a/bin/ntx-builder/src/db/models/queries/mod.rs +++ b/bin/ntx-builder/src/db/models/queries/mod.rs @@ -140,9 +140,9 @@ pub fn add_transaction( } } - // Insert notes with created_by = tx_id. - // Uses INSERT OR IGNORE to make this idempotent if the same event is delivered twice - // (the nullifier PK would otherwise cause a constraint violation). + // Insert notes with created_by = tx_id. Uses INSERT OR IGNORE to make this idempotent if the + // same event is delivered twice (the nullifier PK would otherwise cause a constraint + // violation). for note in notes { let insert = NoteInsert { nullifier: conversions::nullifier_to_bytes(¬e.as_note().nullifier()), @@ -218,8 +218,8 @@ pub fn commit_block( for tx_id in tx_ids { let tx_id_bytes = conversions::transaction_id_to_bytes(tx_id); - // Promote inflight account rows: delete old committed, set transaction_id = NULL. - // Find accounts that have an inflight row for this tx. + // Promote inflight account rows: delete old committed, set transaction_id = NULL. Find + // accounts that have an inflight row for this tx. let inflight_account_ids: Vec> = schema::accounts::table .filter(schema::accounts::transaction_id.eq(&tx_id_bytes)) .select(schema::accounts::account_id) @@ -236,8 +236,8 @@ pub fn commit_block( ) .execute(conn)?; - // Promote the inflight row to committed (set transaction_id = NULL). - // Only promote the row for this specific tx. + // Promote the inflight row to committed (set transaction_id = NULL). Only promote the + // row for this specific tx. diesel::update( schema::accounts::table .filter(schema::accounts::account_id.eq(account_id_bytes)) diff --git a/bin/ntx-builder/src/db/models/queries/notes.rs b/bin/ntx-builder/src/db/models/queries/notes.rs index 384994c74f..bb846d7a5e 100644 --- a/bin/ntx-builder/src/db/models/queries/notes.rs +++ b/bin/ntx-builder/src/db/models/queries/notes.rs @@ -120,8 +120,8 @@ pub fn available_notes( ) -> Result, DatabaseError> { let account_id_bytes = conversions::network_account_id_to_bytes(account_id); - // Get unconsumed, uncommitted notes for this account that haven't exceeded the max - // attempt count. + // Get unconsumed, uncommitted notes for this account that haven't exceeded the max attempt + // count. let rows: Vec = schema::notes::table .filter(schema::notes::account_id.eq(&account_id_bytes)) .filter(schema::notes::consumed_by.is_null()) diff --git a/bin/ntx-builder/src/lib.rs b/bin/ntx-builder/src/lib.rs index ac4d770c04..ec6cb147c9 100644 --- a/bin/ntx-builder/src/lib.rs +++ b/bin/ntx-builder/src/lib.rs @@ -92,19 +92,19 @@ pub struct NtxBuilderConfig { /// Address of the remote transaction prover. If `None`, transactions will be proven locally. pub tx_prover_url: Option, - /// Size of the LRU cache for note scripts. Scripts are fetched from the store and cached - /// to avoid repeated gRPC calls. + /// Size of the LRU cache for note scripts. Scripts are fetched from the store and cached to + /// avoid repeated gRPC calls. pub script_cache_size: NonZeroUsize, - /// Maximum number of network transactions which should be in progress concurrently across - /// all account actors. + /// Maximum number of network transactions which should be in progress concurrently across all + /// account actors. pub max_concurrent_txs: usize, /// Maximum number of network notes a single transaction is allowed to consume. pub max_notes_per_tx: NonZeroUsize, - /// Maximum number of attempts to execute a failing note before dropping it. - /// Notes use exponential backoff between attempts. + /// Maximum number of attempts to execute a failing note before dropping it. Notes use + /// exponential backoff between attempts. pub max_note_attempts: usize, /// Maximum number of blocks to keep in the chain MMR. Older blocks are pruned. @@ -285,8 +285,8 @@ impl NtxBuilderConfig { let validator = ValidatorClient::new(self.validator_url.clone()); let prover = self.tx_prover_url.clone().map(RemoteTransactionProver::new); - // Subscribe to mempool first to ensure we don't miss any events. The subscription - // replays all inflight transactions, so the subscriber's state is fully reconstructed. + // Subscribe to mempool first to ensure we don't miss any events. The subscription replays + // all inflight transactions, so the subscriber's state is fully reconstructed. let subscription = block_producer .subscribe_to_mempool_with_retry() .await diff --git a/bin/remote-prover/src/server/mod.rs b/bin/remote-prover/src/server/mod.rs index 0915b3587c..087820a194 100644 --- a/bin/remote-prover/src/server/mod.rs +++ b/bin/remote-prover/src/server/mod.rs @@ -34,8 +34,7 @@ pub struct Server { /// The proof type that the prover will be handling. #[arg(long, value_enum, env = "MIDEN_PROVER_KIND")] kind: ProofKind, - /// Maximum time allowed for a proof request to complete. Once exceeded, the request is - /// aborted. + /// Maximum time allowed for a proof request to complete. Once exceeded, the request is aborted. #[arg(long, default_value = "60s", env = "MIDEN_PROVER_TIMEOUT", value_parser = humantime::parse_duration)] timeout: std::time::Duration, /// Maximum number of concurrent proof requests that the prover will allow. diff --git a/bin/remote-prover/src/server/prove.rs b/bin/remote-prover/src/server/prove.rs index 66fea10bd7..6617957e98 100644 --- a/bin/remote-prover/src/server/prove.rs +++ b/bin/remote-prover/src/server/prove.rs @@ -30,9 +30,8 @@ impl grpc::server::remote_prover_api::Prove for ProverService { } fn decode(request: grpc::remote_prover::ProofRequest) -> tonic::Result { - // Check that the proof type is supported. - // Protobuf enums return a default value if the enum is set to an unknown value. - // This round trip checks that the value is valid. + // Check that the proof type is supported. Protobuf enums return a default value if the enum + // is set to an unknown value. This round trip checks that the value is valid. if request.proof_type() as i32 != request.proof_type { return Err(tonic::Status::invalid_argument("unknown proof_type value")); } diff --git a/bin/remote-prover/src/server/tests.rs b/bin/remote-prover/src/server/tests.rs index a675b72667..360dbf45d0 100644 --- a/bin/remote-prover/src/server/tests.rs +++ b/bin/remote-prover/src/server/tests.rs @@ -204,8 +204,8 @@ async fn legacy_behaviour_with_capacity_1() { let (first, second) = tokio::join!(a, b); - // We cannot know which got served and which got rejected. - // We can only assert that one of them is Ok and the other is Err. + // We cannot know which got served and which got rejected. We can only assert that one of them + // is Ok and the other is Err. assert!(first.is_ok() || second.is_ok()); assert!(first.is_err() || second.is_err()); // We also expect that the error is a resource exhaustion error. @@ -240,8 +240,8 @@ async fn capacity_is_respected() { let (first, second, third) = tokio::join!(a, b, c); - // We cannot know which got served and which got rejected. - // We can only assert that two succeeded and one failed. + // We cannot know which got served and which got rejected. We can only assert that two succeeded + // and one failed. let mut expected = [true, true, false]; let mut result = [first.is_ok(), second.is_ok(), third.is_ok()]; expected.sort_unstable(); diff --git a/bin/stress-test/src/main.rs b/bin/stress-test/src/main.rs index b8bbed3652..79c466c9fc 100644 --- a/bin/stress-test/src/main.rs +++ b/bin/stress-test/src/main.rs @@ -27,8 +27,8 @@ pub enum Command { /// Create and store blocks into the store. Create a given number of accounts, where each /// account consumes a note created from a faucet. SeedStore { - /// Directory in which to store the database and raw block data. If the directory contains - /// a database dump file, it will be replaced. + /// Directory in which to store the database and raw block data. If the directory contains a + /// database dump file, it will be replaced. #[arg(short, long, value_name = "DATA_DIRECTORY")] data_directory: PathBuf, @@ -36,8 +36,8 @@ pub enum Command { #[arg(short, long, value_name = "NUM_ACCOUNTS")] num_accounts: usize, - /// Percentage of accounts that will be created as public accounts. The rest will be - /// private accounts. + /// Percentage of accounts that will be created as public accounts. The rest will be private + /// accounts. #[arg(short, long, value_name = "PUBLIC_ACCOUNTS_PERCENTAGE", default_value = "0")] public_accounts_percentage: u8, @@ -68,8 +68,8 @@ pub enum Command { #[arg(short, long, value_name = "ITERATIONS", default_value = "10000")] iterations: usize, - /// Concurrency level of the sync request. Represents the number of request that - /// can be sent in parallel. + /// Concurrency level of the sync request. Represents the number of request that can be sent + /// in parallel. #[arg(short, long, value_name = "CONCURRENCY", default_value = "1")] concurrency: usize, }, diff --git a/bin/stress-test/src/seeding/mod.rs b/bin/stress-test/src/seeding/mod.rs index 9f6d78c0d6..bed3d5d34c 100644 --- a/bin/stress-test/src/seeding/mod.rs +++ b/bin/stress-test/src/seeding/mod.rs @@ -181,10 +181,10 @@ async fn generate_blocks( asset_faucet_ids: Vec, ) -> SeedingMetrics { // Each block is composed of [`BATCHES_PER_BLOCK`] batches, and each batch is composed of - // [`TRANSACTIONS_PER_BATCH`] txs. The first note of the block is always a send assets tx - // from the faucet to (BATCHES_PER_BLOCK * TRANSACTIONS_PER_BATCH) - 1 accounts. The rest of - // the notes are consume note txs from the (BATCHES_PER_BLOCK * TRANSACTIONS_PER_BATCH) - 1 - // accounts that were minted in the previous block. + // [`TRANSACTIONS_PER_BATCH`] txs. The first note of the block is always a send assets tx from + // the faucet to (BATCHES_PER_BLOCK * TRANSACTIONS_PER_BATCH) - 1 accounts. The rest of the + // notes are consume note txs from the (BATCHES_PER_BLOCK * TRANSACTIONS_PER_BATCH) - 1 accounts + // that were minted in the previous block. let mut metrics = SeedingMetrics::new(data_directory.database_path()); let mut account_ids = vec![]; @@ -744,8 +744,8 @@ fn create_existing_account_delta( AccountDelta::new(account.id(), storage_delta, vault_delta, ONE).unwrap() } -/// Creates a transaction from the faucet that creates the given output notes. -/// Updates the faucet account to increase the issuance slot and it's nonce. +/// Creates a transaction from the faucet that creates the given output notes. Updates the faucet +/// account to increase the issuance slot and it's nonce. fn create_emit_note_tx( block_ref: &BlockHeader, faucet: &mut Account, diff --git a/bin/stress-test/src/store/metrics.rs b/bin/stress-test/src/store/metrics.rs index b56f362643..518c452b0d 100644 --- a/bin/stress-test/src/store/metrics.rs +++ b/bin/stress-test/src/store/metrics.rs @@ -1,7 +1,7 @@ use std::time::Duration; -/// Prints a summary of the benchmark results, including the average and various percentile -/// request latencies to help diagnose performance outliers. +/// Prints a summary of the benchmark results, including the average and various percentile request +/// latencies to help diagnose performance outliers. pub fn print_summary(timers_accumulator: &[Duration]) { let avg_time = timers_accumulator.iter().sum::() / timers_accumulator.len() as u32; println!("Average request latency: {avg_time:?}"); @@ -27,8 +27,8 @@ fn compute_percentile(times: &[Duration], percentile: f64) -> Duration { let mut sorted_times = times.to_vec(); sorted_times.sort_unstable(); - // Calculate the index for the given percentile - // For P99.9 with 10000 samples: index = (99.9 / 100.0) * 10000 = 9990 + // Calculate the index for the given percentile For P99.9 with 10000 samples: index = (99.9 / + // 100.0) * 10000 = 9990 let index = (percentile / 100.0 * sorted_times.len() as f64).round() as usize; // Ensure index is within bounds diff --git a/bin/stress-test/src/store/mod.rs b/bin/stress-test/src/store/mod.rs index 63f928ad6c..7dfef90b35 100644 --- a/bin/stress-test/src/store/mod.rs +++ b/bin/stress-test/src/store/mod.rs @@ -257,9 +257,9 @@ pub async fn bench_sync_notes(data_directory: PathBuf, iterations: usize, concur print_summary(&timers_accumulator); } -/// Sends a single `sync_notes` request to the store and returns the elapsed time. -/// The note tags are generated from the account ids, so the request will contain a note tag for -/// each account id, with a block number of 0. +/// Sends a single `sync_notes` request to the store and returns the elapsed time. The note tags are +/// generated from the account ids, so the request will contain a note tag for each account id, with +/// a block number of 0. pub async fn sync_notes( api_client: &mut RpcClient>, account_ids: Vec, diff --git a/bin/validator/src/block_validation/mod.rs b/bin/validator/src/block_validation/mod.rs index 0a1b4052a3..602cbfa066 100644 --- a/bin/validator/src/block_validation/mod.rs +++ b/bin/validator/src/block_validation/mod.rs @@ -86,8 +86,7 @@ pub async fn validate_block( .map_err(BlockValidationError::DatabaseError)? .ok_or(BlockValidationError::NoPrevBlockHeader)? } else { - // Proposed block is a new block. - // Block number must be sequential. + // Proposed block is a new block. Block number must be sequential. let expected_block_num = chain_tip.block_num().child(); if proposed_header.block_num() != expected_block_num { return Err(BlockValidationError::BlockNumberMismatch { diff --git a/bin/validator/src/commands/bootstrap.rs b/bin/validator/src/commands/bootstrap.rs index 3c88c049de..dd1aacd501 100644 --- a/bin/validator/src/commands/bootstrap.rs +++ b/bin/validator/src/commands/bootstrap.rs @@ -45,8 +45,8 @@ pub async fn bootstrap( .await } -/// Builds the genesis state, writes account secret files, signs the genesis block, writes it -/// to disk, and initializes the validator's database with the genesis block as the chain tip. +/// Builds the genesis state, writes account secret files, signs the genesis block, writes it to +/// disk, and initializes the validator's database with the genesis block as the chain tip. async fn build_and_write_genesis( config: GenesisConfig, signer: ValidatorSigner, diff --git a/bin/validator/src/server/sign_block.rs b/bin/validator/src/server/sign_block.rs index a9f288b60a..88ffcaaae5 100644 --- a/bin/validator/src/server/sign_block.rs +++ b/bin/validator/src/server/sign_block.rs @@ -28,8 +28,8 @@ impl grpc::server::validator_api::SignBlock for ValidatorServer { } async fn handle(&self, proposed_block: Self::Input) -> tonic::Result { - // Serialize sign_block requests to prevent race conditions between loading the - // chain tip and persisting the validated block header. + // Serialize sign_block requests to prevent race conditions between loading the chain tip + // and persisting the validated block header. let _permit = self.sign_block_semaphore.acquire().await.map_err(|err| { tonic::Status::internal(format!("sign_block semaphore closed: {err}")) })?; diff --git a/bin/validator/src/server/tests.rs b/bin/validator/src/server/tests.rs index 3aed9df2ef..6a95feca61 100644 --- a/bin/validator/src/server/tests.rs +++ b/bin/validator/src/server/tests.rs @@ -16,8 +16,8 @@ use crate::db::{load, load_chain_tip, upsert_block_header}; // TEST HELPERS // ================================================================================================ -/// Test harness that wraps a [`ValidatorServer`] and tracks the chain MMR state needed to -/// construct valid [`ProposedBlock`]s. +/// Test harness that wraps a [`ValidatorServer`] and tracks the chain MMR state needed to construct +/// valid [`ProposedBlock`]s. struct TestValidator { server: ValidatorServer, chain: PartialBlockchain, @@ -101,8 +101,8 @@ impl TestValidator { } } -/// Builds an empty [`ProposedBlock`] that extends the given parent block header using the -/// provided partial blockchain state. +/// Builds an empty [`ProposedBlock`] that extends the given parent block header using the provided +/// partial blockchain state. fn empty_block(parent_header: &BlockHeader, chain: &PartialBlockchain) -> ProposedBlock { let block_inputs = BlockInputs::new( parent_header.clone(), @@ -133,15 +133,15 @@ async fn chain_tip_plus_one_succeeds() { async fn chain_tip_replacement_succeeds() { let mut tv = TestValidator::new().await; - // The genesis block can never be replaced, so we advance the chain - // to block 1, which we can then replace. + // The genesis block can never be replaced, so we advance the chain to block 1, which we can + // then replace. let genesis_header = tv.chain_tip.clone(); let chain_at_genesis = tv.chain.clone(); tv.apply_empty_block().await; let original_header = tv.chain_tip.clone(); - // Submit a different block at the same height (block 1), which is a replacement. - // Use an explicit timestamp far in the future to ensure the replacement block differs. + // Submit a different block at the same height (block 1), which is a replacement. Use an + // explicit timestamp far in the future to ensure the replacement block differs. let block_inputs = BlockInputs::new( genesis_header.clone(), chain_at_genesis.clone(), @@ -215,8 +215,8 @@ async fn chain_tip_minus_one_rejected() { tv.apply_empty_block().await; tv.apply_empty_block().await; - // Try to submit a block at height 1 (chain tip - 1). This is neither a replacement - // (which would need to match tip height 2) nor the next block (which would be 3). + // Try to submit a block at height 1 (chain tip - 1). This is neither a replacement (which would + // need to match tip height 2) nor the next block (which would be 3). let stale_block = empty_block(&genesis_header, &chain_at_genesis); let result = tv.call_sign_block(&stale_block).await; @@ -234,8 +234,8 @@ async fn chain_tip_minus_one_rejected() { async fn commitment_mismatch_rejected() { let tv = TestValidator::new().await; - // Build a valid ProposedBlock on a *different* genesis so its prev_block_commitment - // won't match the validator's actual chain tip. + // Build a valid ProposedBlock on a *different* genesis so its prev_block_commitment won't match + // the validator's actual chain tip. let other_genesis_signer = random_secret_key(); let other_genesis_state = GenesisState::new(vec![], test_fee_params(), 1, 1, other_genesis_signer.public_key()); @@ -313,8 +313,8 @@ async fn unknown_transactions_rejected() { let tv = TestValidator::new().await; let genesis_header = tv.chain_tip.clone(); - // Build a dummy transaction header with a transaction ID that has NOT been - // submitted through `submit_proven_transaction`. + // Build a dummy transaction header with a transaction ID that has NOT been submitted through + // `submit_proven_transaction`. let account_id = ACCOUNT_ID_SENDER.try_into().unwrap(); let fee = FungibleAsset::new(test_fee_params().fee_faucet_id(), 0).unwrap(); let tx_header = TransactionHeader::new( @@ -399,8 +399,8 @@ async fn new_block_after_replacement_with_stale_commitment_rejected() { ); tv.call_sign_block(&replacement).await.unwrap(); - // Now try to submit block 2 built on top of the *original* block 1. - // Its prev_block_commitment points to the old block 1, not the replacement. + // Now try to submit block 2 built on top of the *original* block 1. Its prev_block_commitment + // points to the old block 1, not the replacement. let stale_block_2 = empty_block(&original_block_1_header, &chain_after_block_1); let result = tv.call_sign_block(&stale_block_2).await; diff --git a/bin/validator/src/signers/kms.rs b/bin/validator/src/signers/kms.rs index f9a5f47b2d..35093dcf27 100644 --- a/bin/validator/src/signers/kms.rs +++ b/bin/validator/src/signers/kms.rs @@ -76,10 +76,9 @@ impl KmsSigner { pub async fn sign(&self, commitment: Word) -> Result { // The Validator produces Ethereum-style ECDSA (secp256k1) signatures over Keccak-256 - // digests. AWS KMS does not support SHA-3 hashing for ECDSA keys - // (ECC_SECG_P256K1 being the corresponding AWS key-spec), so we pre-hash the - // message and pass MessageType::Digest. KMS signs the provided 32-byte digest - // verbatim. + // digests. AWS KMS does not support SHA-3 hashing for ECDSA keys (ECC_SECG_P256K1 being the + // corresponding AWS key-spec), so we pre-hash the message and pass MessageType::Digest. KMS + // signs the provided 32-byte digest verbatim. let msg = commitment.to_bytes(); let digest = Keccak256::hash(&msg); diff --git a/bin/validator/src/tx_validation/data_store.rs b/bin/validator/src/tx_validation/data_store.rs index 9b97421076..bdaa14f5a6 100644 --- a/bin/validator/src/tx_validation/data_store.rs +++ b/bin/validator/src/tx_validation/data_store.rs @@ -1,5 +1,5 @@ -/// NOTE: This module contains logic that will eventually be moved to the Validator component -/// when it is added to this repository. +/// NOTE: This module contains logic that will eventually be moved to the Validator component when +/// it is added to this repository. use std::collections::BTreeSet; use miden_protocol::Word; diff --git a/crates/block-producer/src/batch_builder/mod.rs b/crates/block-producer/src/batch_builder/mod.rs index 86f7e49d5e..c93fadf4ca 100644 --- a/crates/block-producer/src/batch_builder/mod.rs +++ b/crates/block-producer/src/batch_builder/mod.rs @@ -177,8 +177,9 @@ impl BatchJob { .inspect_ok(TelemetryInjectorExt::inject_telemetry) .and_then(|proposed| self.prove_batch(proposed)) - // Failure must be injected before the final pipeline stage i.e. before commit is called. The system cannot - // handle errors after it considers the process complete (which makes sense). + // Failure must be injected before the final pipeline stage i.e. before commit is + // called. The system cannot handle errors after it considers the process complete + // (which makes sense). .and_then(|x| self.inject_failure(x)) .and_then(|proven_batch| async { self.commit_batch(proven_batch).await; Ok(()) }) // Handle errors by propagating the error to the root span and rolling back the batch. diff --git a/crates/block-producer/src/block_builder/mod.rs b/crates/block-producer/src/block_builder/mod.rs index 3d1613c6cd..08ebd38f5d 100644 --- a/crates/block-producer/src/block_builder/mod.rs +++ b/crates/block-producer/src/block_builder/mod.rs @@ -160,10 +160,10 @@ impl BlockBuilder { let unauthenticated_notes_iter = batch_iter.clone().flat_map(|batch| { // Note: .cloned() shouldn't be necessary but not having it produces an odd lifetime // error in BlockProducer::serve. Not sure if there's a better fix. Error: - // implementation of `FnOnce` is not general enough - // closure with signature `fn(&InputNoteCommitment) -> miden_protocol::note::NoteId` - // must implement `FnOnce<(&InputNoteCommitment,)>` ...but it actually - // implements `FnOnce<(&InputNoteCommitment,)>` + // implementation of `FnOnce` is not general enough closure with signature + // `fn(&InputNoteCommitment) -> miden_protocol::note::NoteId` must implement + // `FnOnce<(&InputNoteCommitment,)>` ...but it actually implements + // `FnOnce<(&InputNoteCommitment,)>` batch .input_notes() .iter() @@ -240,16 +240,16 @@ impl BlockBuilder { .map_err(|err| BuildBlockError::other(format!("task join error: {err}")))? .map_err(BuildBlockError::ProposeBlockFailed)?; - // Verify the signature against the built block to ensure that - // the validator has provided a valid signature for the relevant block. + // Verify the signature against the built block to ensure that the validator has provided a + // valid signature for the relevant block. if !signature.verify(header.commitment(), header.validator_key()) { return Err(BuildBlockError::InvalidSignature); } let (ordered_batches, ..) = proposed_block.into_parts(); // SAFETY: The header, body, and signature are known to correspond to each other because the - // header and body are derived from the proposed block and the signature is verified - // against the corresponding commitment. + // header and body are derived from the proposed block and the signature is verified against + // the corresponding commitment. let signed_block = SignedBlock::new_unchecked(header, body, signature); Ok((ordered_batches, signed_block)) } diff --git a/crates/block-producer/src/domain/transaction.rs b/crates/block-producer/src/domain/transaction.rs index e9580f3c72..4292db804c 100644 --- a/crates/block-producer/src/domain/transaction.rs +++ b/crates/block-producer/src/domain/transaction.rs @@ -112,8 +112,8 @@ impl AuthenticatedTransaction { (self.inner.ref_block_num(), self.inner.ref_block_commitment()) } - /// Note commitments which were unauthenticated in the transaction __and__ which were - /// not authenticated by the store inputs. + /// Note commitments which were unauthenticated in the transaction __and__ which were not + /// authenticated by the store inputs. pub fn unauthenticated_note_commitments(&self) -> impl Iterator + '_ { self.inner .unauthenticated_notes() diff --git a/crates/block-producer/src/errors.rs b/crates/block-producer/src/errors.rs index b008f30b06..d32550d9c1 100644 --- a/crates/block-producer/src/errors.rs +++ b/crates/block-producer/src/errors.rs @@ -160,8 +160,7 @@ pub enum BuildBlockError { } impl BuildBlockError { - /// Creates a custom error using the [`BuildBlockError::Other`] variant from an - /// error message. + /// Creates a custom error using the [`BuildBlockError::Other`] variant from an error message. pub fn other(message: impl Into) -> Self { let message: String = message.into(); Self::Other { error_msg: message.into(), source: None } diff --git a/crates/block-producer/src/mempool/budget.rs b/crates/block-producer/src/mempool/budget.rs index a4c9c2167d..eed2f19adb 100644 --- a/crates/block-producer/src/mempool/budget.rs +++ b/crates/block-producer/src/mempool/budget.rs @@ -60,8 +60,8 @@ impl BatchBudget { /// otherwise returns [`BudgetStatus::WithinScope`] and subtracts the resources from the budget. #[must_use] pub(crate) fn check_then_subtract(&mut self, tx: &AuthenticatedTransaction) -> BudgetStatus { - // This type assertion reminds us to update the account check if we ever support - // multiple account updates per tx. + // This type assertion reminds us to update the account check if we ever support multiple + // account updates per tx. pub(crate) const ACCOUNT_UPDATES_PER_TX: usize = 1; let _: miden_protocol::account::AccountId = tx.account_update().account_id(); diff --git a/crates/block-producer/src/mempool/graph/batch.rs b/crates/block-producer/src/mempool/graph/batch.rs index a3200de168..03a3c276f3 100644 --- a/crates/block-producer/src/mempool/graph/batch.rs +++ b/crates/block-producer/src/mempool/graph/batch.rs @@ -111,8 +111,8 @@ impl BatchGraph { reverted } - /// Marks the given batch as proven, making it available for selection in a block - /// once it becomes a root. + /// Marks the given batch as proven, making it available for selection in a block once it + /// becomes a root. pub fn submit_proof(&mut self, proof: Arc) { if self.inner.contains(&proof.id()) { self.proven.insert(proof.id(), proof); diff --git a/crates/block-producer/src/mempool/graph/node.rs b/crates/block-producer/src/mempool/graph/node.rs index ac036612c4..ae0d67905d 100644 --- a/crates/block-producer/src/mempool/graph/node.rs +++ b/crates/block-producer/src/mempool/graph/node.rs @@ -12,16 +12,16 @@ pub trait GraphNode { fn id(&self) -> Self::Id; - /// All [`Nullifier`]s created by this node, **including** nullifiers for erased notes. This - /// may not be strictly necessary but it removes having to worry about reverting batches and - /// blocks with erased notes -- since these would otherwise have different state impact than - /// the transactions within them. - fn nullifiers(&self) -> Box + '_>; - - /// All output notes created by this node, **including** erased notes. This may not - /// be strictly necessary but it removes having to worry about reverting batches and blocks + /// All [`Nullifier`]s created by this node, **including** nullifiers for erased notes. This may + /// not be strictly necessary but it removes having to worry about reverting batches and blocks /// with erased notes -- since these would otherwise have different state impact than the /// transactions within them. + fn nullifiers(&self) -> Box + '_>; + + /// All output notes created by this node, **including** erased notes. This may not be strictly + /// necessary but it removes having to worry about reverting batches and blocks with erased + /// notes -- since these would otherwise have different state impact than the transactions + /// within them. fn output_notes(&self) -> Box + '_>; /// Input notes which were not authenticated against any committed block thus far. diff --git a/crates/block-producer/src/mempool/graph/transaction.rs b/crates/block-producer/src/mempool/graph/transaction.rs index 219e87a372..fe2a5570d3 100644 --- a/crates/block-producer/src/mempool/graph/transaction.rs +++ b/crates/block-producer/src/mempool/graph/transaction.rs @@ -253,9 +253,9 @@ impl TransactionGraph { self.failures.remove(&tx.id()); // Note that this is a pretty rough shod approach. We just dump the entire batch of - // transactions in, which will result in at least the current - // transaction being duplicated in `to_revert`. This isn't a concern - // though since we skip already processed transactions at the top of the loop. + // transactions in, which will result in at least the current transaction being + // duplicated in `to_revert`. This isn't a concern though since we skip already + // processed transactions at the top of the loop. if let Some(batch) = self.txs_user_batch.remove(&tx.id()) { if let Some(batch) = self.user_batch_txs.remove(&batch) { to_revert.extend(batch); diff --git a/crates/block-producer/src/mempool/subscription.rs b/crates/block-producer/src/mempool/subscription.rs index 01eaa3fccb..d38ea0fe0f 100644 --- a/crates/block-producer/src/mempool/subscription.rs +++ b/crates/block-producer/src/mempool/subscription.rs @@ -165,8 +165,8 @@ struct InflightTransactions { /// A reverse lookup index is maintained in `index`. txs: BTreeMap, - /// A reverse lookup index for `txs` which allows for efficient removal of - /// committed or reverted events. + /// A reverse lookup index for `txs` which allows for efficient removal of committed or reverted + /// events. index: BTreeMap, } diff --git a/crates/block-producer/src/mempool/tests.rs b/crates/block-producer/src/mempool/tests.rs index 946891207d..e1b38b0108 100644 --- a/crates/block-producer/src/mempool/tests.rs +++ b/crates/block-producer/src/mempool/tests.rs @@ -63,9 +63,9 @@ async fn add_transaction_traces_are_correct() { #[test] fn children_of_failed_batches_are_ignored() { - // Batches are proved concurrently. This makes it possible for a child job to complete after - // the parent has been reverted (and therefore reverting the child job). Such a child job - // should be ignored. + // Batches are proved concurrently. This makes it possible for a child job to complete after the + // parent has been reverted (and therefore reverting the child job). Such a child job should be + // ignored. let txs = MockProvenTxBuilder::sequential(); let (mut uut, _) = Mempool::for_tests(); @@ -354,8 +354,8 @@ fn pass_through_txs_on_an_empty_account() { )); itertools::assert_equal(batch.account_updates(), expected); - // Ensure the batch contains a,b and final. Final should also be the last tx since its order - // is required. + // Ensure the batch contains a,b and final. Final should also be the last tx since its order is + // required. assert!(batch.transactions().contains(&tx_pass_through_a)); assert!(batch.transactions().contains(&tx_pass_through_b)); assert_eq!(batch.transactions().last().unwrap(), &tx_final); diff --git a/crates/block-producer/src/mempool/tests/add_transaction.rs b/crates/block-producer/src/mempool/tests/add_transaction.rs index 6abe596572..ae70c5f612 100644 --- a/crates/block-producer/src/mempool/tests/add_transaction.rs +++ b/crates/block-producer/src/mempool/tests/add_transaction.rs @@ -53,8 +53,8 @@ fn valid_with_state_from_multiple_parents() { } } -/// Ensures that transactions that expire before or within the expiration slack of the chain tip -/// are rejected. +/// Ensures that transactions that expire before or within the expiration slack of the chain tip are +/// rejected. mod tx_expiration { use super::*; diff --git a/crates/block-producer/src/server/mod.rs b/crates/block-producer/src/server/mod.rs index 54f11ba93d..853875b780 100644 --- a/crates/block-producer/src/server/mod.rs +++ b/crates/block-producer/src/server/mod.rs @@ -214,8 +214,8 @@ struct BlockProducerRpcServer { store: StoreClient, - /// Cached mempool statistics that are updated periodically to avoid locking the mempool - /// for each status request. + /// Cached mempool statistics that are updated periodically to avoid locking the mempool for + /// each status request. cached_mempool_stats: Arc>, } diff --git a/crates/block-producer/src/server/tests.rs b/crates/block-producer/src/server/tests.rs index 4857efd248..9360d047bd 100644 --- a/crates/block-producer/src/server/tests.rs +++ b/crates/block-producer/src/server/tests.rs @@ -59,8 +59,8 @@ async fn block_producer_startup_is_robust_to_network_failures() { .unwrap(); }); - // start the block producer BEFORE the store is available - // this tests the exponential backoff behavior + // start the block producer BEFORE the store is available this tests the exponential backoff + // behavior let store_url = Url::parse(&format!("http://{store_addr}")).expect("Failed to parse store URL"); let validator_url = Url::parse(&format!("http://{validator_addr}")).expect("Failed to parse validator URL"); @@ -82,8 +82,8 @@ async fn block_producer_startup_is_robust_to_network_failures() { .unwrap(); }); - // test: connecting to the block producer should fail because the store is not yet started - // (and therefore the block producer is not yet listening) + // test: connecting to the block producer should fail because the store is not yet started (and + // therefore the block producer is not yet listening) let block_producer_endpoint = Endpoint::try_from(format!("http://{block_producer_addr}")).expect("valid url"); let block_producer_client = @@ -97,8 +97,8 @@ async fn block_producer_startup_is_robust_to_network_failures() { let data_directory = tempfile::tempdir().expect("tempdir should be created"); let store_runtime = start_store(store_addr, data_directory.path()).await; - // wait for the block producer's exponential backoff to connect to the store - // use a retry loop since CI environments may be slower + // wait for the block producer's exponential backoff to connect to the store use a retry loop + // since CI environments may be slower let block_producer_client = { let mut attempts = 0; loop { diff --git a/crates/block-producer/src/test_utils/account.rs b/crates/block-producer/src/test_utils/account.rs index 09b2d5f675..dcd6da86a7 100644 --- a/crates/block-producer/src/test_utils/account.rs +++ b/crates/block-producer/src/test_utils/account.rs @@ -54,8 +54,8 @@ impl MockPrivateAccount { } impl From for MockPrivateAccount { - /// Each index gives rise to a different account ID - /// Passing index 0 signifies that it's a new account + /// Each index gives rise to a different account ID Passing index 0 signifies that it's a new + /// account fn from(index: u32) -> Self { let mut lock = MOCK_ACCOUNTS.lock().expect("Poisoned mutex"); if let Some(&(account_id, init_state)) = lock.get(&index) { diff --git a/crates/block-producer/src/test_utils/mod.rs b/crates/block-producer/src/test_utils/mod.rs index aea1ca35ae..d43db74335 100644 --- a/crates/block-producer/src/test_utils/mod.rs +++ b/crates/block-producer/src/test_utils/mod.rs @@ -23,8 +23,8 @@ pub mod note; pub struct Random(RandomCoin); impl Random { - /// Creates a [Random] with a random seed. This seed is logged - /// so that it is known for test failures. + /// Creates a [Random] with a random seed. This seed is logged so that it is known for test + /// failures. pub fn with_random_seed() -> Self { let seed: [u32; 4] = rand::random(); diff --git a/crates/proto/build.rs b/crates/proto/build.rs index fa69a59045..2cadf011c8 100644 --- a/crates/proto/build.rs +++ b/crates/proto/build.rs @@ -56,8 +56,8 @@ fn main() -> miette::Result<()> { Ok(()) } -/// Generates protobuf bindings from the given file descriptor set and stores them in the -/// given destination directory. +/// Generates protobuf bindings from the given file descriptor set and stores them in the given +/// destination directory. fn generate_bindings(file_descriptors: FileDescriptorSet, dst_dir: &Path) -> miette::Result<()> { let mut prost_config = tonic_prost_build::Config::new(); prost_config.skip_debug(["AccountId", "Digest"]); @@ -81,9 +81,9 @@ fn rustfmt_generated(dir: &Path) -> miette::Result<()> { } // Just ignore output and exit status. The `rustfmt` binary is part of the Rust toolchain even - // if the `rustfmt` component is not installed, and it will print a warning and exit with - // status code 1. We don't actually care about formatting in this case, so we can just ignore - // the error. + // if the `rustfmt` component is not installed, and it will print a warning and exit with status + // code 1. We don't actually care about formatting in this case, so we can just ignore the + // error. let _output = Command::new("rustfmt") .args(["--edition", "2024"]) .args(&rs_files) diff --git a/crates/proto/src/clients/mod.rs b/crates/proto/src/clients/mod.rs index 66f11a5268..d171eb091d 100644 --- a/crates/proto/src/clients/mod.rs +++ b/crates/proto/src/clients/mod.rs @@ -403,8 +403,8 @@ impl Builder { } impl Builder { - /// Create a new strict builder from a gRPC endpoint URL such as - /// `http://localhost:8080` or `https://api.example.com:443`. + /// Create a new strict builder from a gRPC endpoint URL such as `http://localhost:8080` or + /// `https://api.example.com:443`. pub fn new(url: Url) -> Builder { let endpoint = Endpoint::from_shared(String::from(url)) .expect("Url type always results in valid endpoint"); diff --git a/crates/proto/src/decode/mod.rs b/crates/proto/src/decode/mod.rs index a67ab309c6..bf52f1fada 100644 --- a/crates/proto/src/decode/mod.rs +++ b/crates/proto/src/decode/mod.rs @@ -39,8 +39,8 @@ impl Default for GrpcStructDecoder { } impl GrpcStructDecoder { - /// Decode a required optional field: checks for `None`, converts via `TryInto`, and adds - /// field context on error. + /// Decode a required optional field: checks for `None`, converts via `TryInto`, and adds field + /// context on error. pub fn decode_field( &self, name: &'static str, diff --git a/crates/proto/src/domain/account.rs b/crates/proto/src/domain/account.rs index 158e34febe..33e31b8526 100644 --- a/crates/proto/src/domain/account.rs +++ b/crates/proto/src/domain/account.rs @@ -334,8 +334,8 @@ impl From for proto::account::AccountStorageHeader { /// to use the `SyncAccountVault` endpoint instead. #[derive(Debug, Clone, PartialEq, Eq)] pub enum AccountVaultDetails { - /// The vault has too many assets to return inline. - /// Clients must use `SyncAccountVault` endpoint instead. + /// The vault has too many assets to return inline. Clients must use `SyncAccountVault` endpoint + /// instead. LimitExceeded, /// The assets in the vault (up to `MAX_RETURN_ENTRIES`). @@ -343,8 +343,8 @@ pub enum AccountVaultDetails { } impl AccountVaultDetails { - /// Maximum number of vault entries that can be returned in a single response. - /// Accounts with more assets will have `LimitExceeded` variant. + /// Maximum number of vault entries that can be returned in a single response. Accounts with + /// more assets will have `LimitExceeded` variant. pub const MAX_RETURN_ENTRIES: usize = 1000; pub fn new(vault: &AssetVault) -> Self { @@ -419,16 +419,16 @@ pub struct AccountStorageMapDetails { /// instead. #[derive(Debug, Clone, PartialEq, Eq)] pub enum StorageMapEntries { - /// The map has too many entries to return inline. - /// Clients must use `SyncAccountStorageMaps` endpoint instead. + /// The map has too many entries to return inline. Clients must use `SyncAccountStorageMaps` + /// endpoint instead. LimitExceeded, - /// All storage map entries (key-value pairs) without proofs. - /// Used when all entries are requested for small maps. + /// All storage map entries (key-value pairs) without proofs. Used when all entries are + /// requested for small maps. AllEntries(Vec<(StorageMapKey, Word)>), - /// Specific entries with their SMT proofs for client-side verification. - /// Used when specific keys are requested from the storage map. + /// Specific entries with their SMT proofs for client-side verification. Used when specific keys + /// are requested from the storage map. EntriesWithProofs(Vec), } @@ -926,8 +926,8 @@ impl TryFrom fo let account_commitment = decode!(decoder, from.account_commitment)?; - // If the commitment is equal to `Word::empty()`, it signifies that this is a new - // account which is not yet present in the Store. + // If the commitment is equal to `Word::empty()`, it signifies that this is a new account + // which is not yet present in the Store. let account_commitment = if account_commitment == Word::empty() { None } else { @@ -1039,8 +1039,7 @@ impl From for AccountId { } impl From for u32 { - /// Returns the 30-bit prefix of the network account ID. - /// This is used for note tag matching. + /// Returns the 30-bit prefix of the network account ID. This is used for note tag matching. fn from(value: NetworkAccountId) -> Self { value.prefix() } diff --git a/crates/proto/src/domain/note.rs b/crates/proto/src/domain/note.rs index 5184bc1b0d..5d8a51f0cf 100644 --- a/crates/proto/src/domain/note.rs +++ b/crates/proto/src/domain/note.rs @@ -307,8 +307,8 @@ impl TryFrom for NoteScript { /// Decodes the `(sender, note_type, tag)` triple from a proto `NoteMetadata` into a /// [`PartialNoteMetadata`]. The attachment-related fields on the proto are ignored — when full -/// attachments are also transmitted, the receiver derives the canonical headers and commitment -/// from those instead. +/// attachments are also transmitted, the receiver derives the canonical headers and commitment from +/// those instead. fn partial_note_metadata_from_proto( value: proto::note::NoteMetadata, ) -> Result { @@ -322,8 +322,8 @@ fn partial_note_metadata_from_proto( Ok(PartialNoteMetadata::new(sender, note_type).with_tag(tag)) } -/// Decodes a serialized [`NoteAttachments`] payload. Empty bytes are treated as an empty -/// collection so that proto3's default value round-trips cleanly. +/// Decodes a serialized [`NoteAttachments`] payload. Empty bytes are treated as an empty collection +/// so that proto3's default value round-trips cleanly. fn decode_attachments(bytes: &[u8]) -> Result { if bytes.is_empty() { Ok(NoteAttachments::empty()) diff --git a/crates/remote-prover-client/src/lib.rs b/crates/remote-prover-client/src/lib.rs index a319793d9d..a30ba35edd 100644 --- a/crates/remote-prover-client/src/lib.rs +++ b/crates/remote-prover-client/src/lib.rs @@ -39,8 +39,7 @@ pub enum RemoteProverClientError { #[error("{error_msg}")] Other { error_msg: Box, - // thiserror will return this when calling `Error::source` on - // `RemoteProverClientError`. + // thiserror will return this when calling `Error::source` on `RemoteProverClientError`. source: Option>, }, } @@ -52,15 +51,15 @@ impl From for String { } impl RemoteProverClientError { - /// Creates a custom error using the [`RemoteProverClientError::Other`] variant from an - /// error message. + /// Creates a custom error using the [`RemoteProverClientError::Other`] variant from an error + /// message. pub fn other(message: impl Into) -> Self { let message: String = message.into(); Self::Other { error_msg: message.into(), source: None } } - /// Creates a custom error using the [`RemoteProverClientError::Other`] variant from an - /// error message and a source error. + /// Creates a custom error using the [`RemoteProverClientError::Other`] variant from an error + /// message and a source error. pub fn other_with_source( message: impl Into, source: impl CoreError + Send + Sync + 'static, diff --git a/crates/remote-prover-client/src/remote_prover/batch_prover.rs b/crates/remote-prover-client/src/remote_prover/batch_prover.rs index c5c100f3e1..9514fdeb10 100644 --- a/crates/remote-prover-client/src/remote_prover/batch_prover.rs +++ b/crates/remote-prover-client/src/remote_prover/batch_prover.rs @@ -40,8 +40,8 @@ pub struct RemoteBatchProver { } impl RemoteBatchProver { - /// Creates a new [`RemoteBatchProver`] with the specified gRPC server endpoint. The - /// endpoint should be in the format `{protocol}://{hostname}:{port}`. + /// Creates a new [`RemoteBatchProver`] with the specified gRPC server endpoint. The endpoint + /// should be in the format `{protocol}://{hostname}:{port}`. pub fn new(endpoint: impl Into) -> Self { RemoteBatchProver { endpoint: endpoint.into(), @@ -65,9 +65,9 @@ impl RemoteBatchProver { self } - /// Establishes a connection to the remote batch prover server. The connection is - /// maintained for the lifetime of the prover. If the connection is already established, this - /// method does nothing. + /// Establishes a connection to the remote batch prover server. The connection is maintained for + /// the lifetime of the prover. If the connection is already established, this method does + /// nothing. async fn connect(&self) -> Result<(), RemoteProverClientError> { let mut client = self.client.lock().await; if client.is_some() { @@ -165,8 +165,8 @@ impl RemoteBatchProver { ))); } - // Because we checked the length matches we can zip the iterators up. - // We expect the transactions to be in the same order. + // Because we checked the length matches we can zip the iterators up. We expect the + // transactions to be in the same order. for (proposed_header, proven_header) in proposed_txs.into_iter().zip(proven_batch.transactions().as_slice()) { @@ -204,8 +204,8 @@ impl RemoteBatchProver { ))); } - // Because we checked the length matches we can zip the iterators up. - // We expect the nullifiers to be in the same order. + // Because we checked the length matches we can zip the iterators up. We expect the + // nullifiers to be in the same order. for (proposed_nullifier, input_note_commitment) in proposed_header.nullifiers().zip(proven_header.input_notes().iter()) { @@ -225,8 +225,8 @@ impl RemoteBatchProver { ))); } - // Because we checked the length matches we can zip the iterators up. - // We expect the note IDs to be in the same order. + // Because we checked the length matches we can zip the iterators up. We expect the note + // IDs to be in the same order. for (proposed_note_id, header_note) in proposed_header .output_notes() .iter() diff --git a/crates/remote-prover-client/src/remote_prover/block_prover.rs b/crates/remote-prover-client/src/remote_prover/block_prover.rs index e59d605d74..ee45aef67e 100644 --- a/crates/remote-prover-client/src/remote_prover/block_prover.rs +++ b/crates/remote-prover-client/src/remote_prover/block_prover.rs @@ -36,8 +36,8 @@ pub struct RemoteBlockProver { } impl RemoteBlockProver { - /// Creates a new [`RemoteBlockProver`] with the specified gRPC server endpoint. The - /// endpoint should be in the format `{protocol}://{hostname}:{port}`. + /// Creates a new [`RemoteBlockProver`] with the specified gRPC server endpoint. The endpoint + /// should be in the format `{protocol}://{hostname}:{port}`. pub fn new(endpoint: impl Into) -> Self { RemoteBlockProver { endpoint: endpoint.into(), @@ -61,9 +61,9 @@ impl RemoteBlockProver { self } - /// Establishes a connection to the remote block prover server. The connection is - /// maintained for the lifetime of the prover. If the connection is already established, this - /// method does nothing. + /// Establishes a connection to the remote block prover server. The connection is maintained for + /// the lifetime of the prover. If the connection is already established, this method does + /// nothing. async fn connect(&self) -> Result<(), RemoteProverClientError> { let mut client = self.client.lock().await; if client.is_some() { diff --git a/crates/rocksdb-cxx-linkage-fix/src/lib.rs b/crates/rocksdb-cxx-linkage-fix/src/lib.rs index 35bc05d004..2cc5d1ed2b 100644 --- a/crates/rocksdb-cxx-linkage-fix/src/lib.rs +++ b/crates/rocksdb-cxx-linkage-fix/src/lib.rs @@ -16,7 +16,8 @@ pub fn configure() { } fn should_compile() -> bool { - // in sync with + // in sync with + // if let Ok(v) = env::var("ROCKSDB_COMPILE") { if v.to_lowercase() == "true" || v == "1" { return true; diff --git a/crates/rpc/src/server/accept.rs b/crates/rpc/src/server/accept.rs index 6cf68a4b84..d156fed292 100644 --- a/crates/rpc/src/server/accept.rs +++ b/crates/rpc/src/server/accept.rs @@ -41,9 +41,9 @@ pub enum GenesisNegotiation { #[derive(Clone)] pub struct AcceptHeaderLayer { supported_versions: VersionReq, - /// The pre-release label (e.g. `"alpha"` from `"alpha.3"`), or `None` for stable versions. - /// Only the label is stored so that different pre-release numbers are accepted - /// (e.g. a server at `alpha.3` accepts clients at `alpha.1`). + /// The pre-release label (e.g. `"alpha"` from `"alpha.3"`), or `None` for stable versions. Only + /// the label is stored so that different pre-release numbers are accepted (e.g. a server at + /// `alpha.3` accepts clients at `alpha.1`). expected_pre_label: Option, /// The patch version of the server. Used to enforce exact patch matching for pre-release /// versions (patch flexibility only applies to stable versions). @@ -107,9 +107,9 @@ impl AcceptHeaderLayer { } } -/// Extracts the label portion of a semver pre-release identifier, stripping any trailing -/// numeric segment. For example, `"alpha.3"` returns `Some("alpha")` and `"rc.1"` returns -/// `Some("rc")`. Returns `None` for empty (stable) pre-release identifiers. +/// Extracts the label portion of a semver pre-release identifier, stripping any trailing numeric +/// segment. For example, `"alpha.3"` returns `Some("alpha")` and `"rc.1"` returns `Some("rc")`. +/// Returns `None` for empty (stable) pre-release identifiers. fn pre_release_label(pre: &semver::Prerelease) -> Option { if pre.is_empty() { return None; @@ -137,8 +137,8 @@ impl AcceptHeaderLayer { const GENESIS: Name<'static> = Name::new_unchecked("genesis"); const GRPC: Name<'static> = Name::new_unchecked("grpc"); - /// Parses the `Accept` header's contents, searching for any media type compatible with our - /// RPC version and genesis commitment, controlling whether `genesis` is optional or mandatory. + /// Parses the `Accept` header's contents, searching for any media type compatible with our RPC + /// version and genesis commitment, controlling whether `genesis` is optional or mandatory. fn negotiate( &self, accept: &str, @@ -218,8 +218,8 @@ impl AcceptHeaderLayer { if pre_release_label(&version.pre) != self.expected_pre_label { continue; } - // Pre-release versions must also match the patch version exactly - // (patch flexibility only applies to stable versions). + // Pre-release versions must also match the patch version exactly (patch flexibility + // only applies to stable versions). if self.expected_pre_label.is_some() && version.patch != self.expected_patch { continue; } diff --git a/crates/rpc/src/server/api.rs b/crates/rpc/src/server/api.rs index 8ceac6b511..ed2d5b7b2f 100644 --- a/crates/rpc/src/server/api.rs +++ b/crates/rpc/src/server/api.rs @@ -519,8 +519,8 @@ impl api_server::Api for RpcService { block_producer.clone().submit_proven_tx(request).await } - /// Deserializes the batch, strips MAST decorators from full output note scripts, rebuilds - /// the batch, then forwards it to the block producer. + /// Deserializes the batch, strips MAST decorators from full output note scripts, rebuilds the + /// batch, then forwards it to the block producer. async fn submit_proven_tx_batch( &self, request: tonic::Request, diff --git a/crates/rpc/src/server/mod.rs b/crates/rpc/src/server/mod.rs index 21ca958e57..6a8c3acc98 100644 --- a/crates/rpc/src/server/mod.rs +++ b/crates/rpc/src/server/mod.rs @@ -95,9 +95,9 @@ impl Rpc { .layer(GrpcWebLayer::new()) .layer(grpc::rate_limit_concurrent_connections(self.grpc_options)) .layer(grpc::rate_limit_per_ip(self.grpc_options)?) - // Note: must come after the CORS layer, as otherwise accept rejections - // do _not_ get CORS headers applied, masking the accept error in - // web-clients (which would experience CORS rejection). + // Note: must come after the CORS layer, as otherwise accept rejections do _not_ get + // CORS headers applied, masking the accept error in web-clients (which would experience + // CORS rejection). .layer( AcceptHeaderLayer::new(&rpc_version, genesis.commitment()) .with_genesis_enforced_method("SubmitProvenTx") diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index 4a1cb390e6..362980f81b 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -45,8 +45,8 @@ use url::Url; use crate::Rpc; -/// Byte offset of the account delta commitment in serialized `ProvenTransaction`. -/// Layout: `AccountId` (15) + `initial_commitment` (32) + `final_commitment` (32) = 79 +/// Byte offset of the account delta commitment in serialized `ProvenTransaction`. Layout: +/// `AccountId` (15) + `initial_commitment` (32) + `final_commitment` (32) = 79 const DELTA_COMMITMENT_BYTE_OFFSET: usize = 15 + 32 + 32; const REQUEST_TIMEOUT: Duration = Duration::from_secs(5); @@ -217,8 +217,8 @@ async fn rpc_server_rejects_requests_with_accept_header_invalid_version() { #[tokio::test] async fn rpc_startup_is_robust_to_network_failures() { - // This test starts the store and RPC components and verifies that they successfully - // connect to each other on startup and that they reconnect after the store is restarted. + // This test starts the store and RPC components and verifies that they successfully connect to + // each other on startup and that they reconnect after the store is restarted. // Start the RPC. let (mut rpc_client, _, store_listener) = start_rpc().await; @@ -473,8 +473,8 @@ async fn connect_rpc(url: Url, local_address: Option) -> RpcClient { RpcClient::with_interceptor(channel, interceptor) } -/// Binds a socket on an available port, runs the RPC server on it, and -/// returns a client to talk to the server, along with the socket address. +/// Binds a socket on an available port, runs the RPC server on it, and returns a client to talk to +/// the server, along with the socket address. async fn start_rpc() -> (RpcClient, std::net::SocketAddr, TcpListener) { start_rpc_with_options(GrpcOptionsExternal::test()).await } @@ -544,9 +544,8 @@ async fn start_store(store_listener: TcpListener) -> (Runtime, TempDir, Word, So .expect("Failed to bind store ntx-builder gRPC endpoint"); let block_producer_listener = TcpListener::bind("127.0.0.1:0").await.expect("store should bind a port"); - // In order to later kill the store, we need to spawn a new runtime and run the store on - // it. That allows us to kill all the tasks spawned by the store when we - // kill the runtime. + // In order to later kill the store, we need to spawn a new runtime and run the store on it. + // That allows us to kill all the tasks spawned by the store when we kill the runtime. let store_runtime = runtime::Builder::new_multi_thread().enable_time().enable_io().build().unwrap(); store_runtime.spawn(async move { @@ -656,8 +655,8 @@ async fn get_limits_endpoint() { QueryParamNoteTagLimit::LIMIT ); - // SyncAccountVault and SyncAccountStorageMaps accept a singular account_id, - // not a repeated list, so they do not have list parameter limits. + // SyncAccountVault and SyncAccountStorageMaps accept a singular account_id, not a repeated + // list, so they do not have list parameter limits. assert!( !limits.endpoints.contains_key("SyncAccountVault"), "SyncAccountVault should not have list parameter limits" diff --git a/crates/store/benches/account_tree.rs b/crates/store/benches/account_tree.rs index 005a3780ec..2231e5f47a 100644 --- a/crates/store/benches/account_tree.rs +++ b/crates/store/benches/account_tree.rs @@ -124,8 +124,8 @@ fn setup_account_tree_with_history( // VANILLA ACCOUNTTREE BENCHMARKS // ================================================================================================ -/// Benchmarks vanilla `AccountTree` open (query) operations. -/// This provides a baseline for comparison with historical access operations. +/// Benchmarks vanilla `AccountTree` open (query) operations. This provides a baseline for +/// comparison with historical access operations. fn bench_vanilla_access(c: &mut Criterion) { let mut group = c.benchmark_group("account_tree_vanilla_access"); let base_path = default_storage_path(); @@ -146,8 +146,8 @@ fn bench_vanilla_access(c: &mut Criterion) { group.finish(); } -/// Benchmarks vanilla `AccountTree` insertion (mutation) performance. -/// This provides a baseline for comparison with history-tracking insertion. +/// Benchmarks vanilla `AccountTree` insertion (mutation) performance. This provides a baseline for +/// comparison with history-tracking insertion. fn bench_vanilla_insertion(c: &mut Criterion) { let mut group = c.benchmark_group("account_tree_insertion"); let base_path = default_storage_path(); diff --git a/crates/store/build.rs b/crates/store/build.rs index dfd6ee5c69..09bf745d32 100644 --- a/crates/store/build.rs +++ b/crates/store/build.rs @@ -44,14 +44,13 @@ fn generate_agglayer_sample_accounts() { // Create the directory if it doesn't exist fs_err::create_dir_all(&samples_dir).expect("Failed to create samples directory"); - // Use deterministic seeds for reproducible builds. - // WARNING: DO NOT USE THESE IN PRODUCTION + // Use deterministic seeds for reproducible builds. WARNING: DO NOT USE THESE IN PRODUCTION let bridge_seed: Word = Word::new([Felt::new(1u64); 4]); let eth_faucet_seed: Word = Word::new([Felt::new(2u64); 4]); let usdc_faucet_seed: Word = Word::new([Felt::new(3u64); 4]); - // Create bridge admin and GER manager as proper wallet accounts. - // WARNING: DO NOT USE THESE IN PRODUCTION + // Create bridge admin and GER manager as proper wallet accounts. WARNING: DO NOT USE THESE IN + // PRODUCTION let bridge_admin_key = SecretKey::with_rng(&mut RandomCoin::new(Word::new([Felt::new(4u64); 4]))); let ger_manager_key = @@ -80,19 +79,19 @@ fn generate_agglayer_sample_accounts() { let bridge_admin_id = bridge_admin.id(); let ger_manager_id = ger_manager.id(); - // Create the bridge account first (faucets need to reference it) - // Use "existing" variant so accounts have nonce > 0 (required for genesis) + // Create the bridge account first (faucets need to reference it) Use "existing" variant so + // accounts have nonce > 0 (required for genesis) let bridge_account = create_existing_bridge_account(bridge_seed, bridge_admin_id, ger_manager_id); let bridge_account_id = bridge_account.id(); - // Placeholder Ethereum addresses for sample faucets. - // WARNING: DO NOT USE THESE ADDRESSES IN PRODUCTION + // Placeholder Ethereum addresses for sample faucets. WARNING: DO NOT USE THESE ADDRESSES IN + // PRODUCTION let eth_origin_address = EthAddress::new([1u8; 20]); let usdc_origin_address = EthAddress::new([2u8; 20]); - // Create AggLayer faucets using "existing" variant - // ETH: 8 decimals (protocol max is 12), max supply of 1 billion tokens + // Create AggLayer faucets using "existing" variant ETH: 8 decimals (protocol max is 12), max + // supply of 1 billion tokens let eth_faucet = create_existing_agglayer_faucet( eth_faucet_seed, "ETH", diff --git a/crates/store/src/account_state_forest/mod.rs b/crates/store/src/account_state_forest/mod.rs index 26513dc9cd..fdd63f682c 100644 --- a/crates/store/src/account_state_forest/mod.rs +++ b/crates/store/src/account_state_forest/mod.rs @@ -88,8 +88,8 @@ pub enum AccountStorageMapResult { /// Container for forest-related state that needs to be updated atomically. pub(crate) struct AccountStateForest { - /// `LargeSmtForest` for efficient account storage reconstruction. - /// Populated during block import with storage and vault SMTs. + /// `LargeSmtForest` for efficient account storage reconstruction. Populated during block import + /// with storage and vault SMTs. forest: LargeSmtForest, /// Reverse lookup from hashed SMT storage keys to raw storage map keys. @@ -551,8 +551,8 @@ impl AccountStateForest { self.forest.latest_root(lineage).unwrap_or_else(empty_smt_root) } - /// Inserts asset vault data into the forest for the specified account. Assumes that asset - /// vault for this account does not yet exist in the forest. + /// Inserts asset vault data into the forest for the specified account. Assumes that asset vault + /// for this account does not yet exist in the forest. fn insert_account_vault( &mut self, block_num: BlockNumber, diff --git a/crates/store/src/account_state_forest/tests.rs b/crates/store/src/account_state_forest/tests.rs index fb252f09d6..eb63f56fdd 100644 --- a/crates/store/src/account_state_forest/tests.rs +++ b/crates/store/src/account_state_forest/tests.rs @@ -1073,9 +1073,9 @@ fn prune_preserves_entries_within_retention_window() { assert!(forest.get_vault_root(account_id, BlockNumber::from(100)).is_some()); } -/// Two accounts start with identical vault roots (same asset amount). When one account changes -/// in the next block, verify the unchanged account's vault root still works for lookups and -/// witness generation. +/// Two accounts start with identical vault roots (same asset amount). When one account changes in +/// the next block, verify the unchanged account's vault root still works for lookups and witness +/// generation. #[test] fn shared_vault_root_retained_when_one_account_changes() { let mut forest = AccountStateForest::new(); diff --git a/crates/store/src/accounts/mod.rs b/crates/store/src/accounts/mod.rs index 31c7ed9c6a..c99488df0d 100644 --- a/crates/store/src/accounts/mod.rs +++ b/crates/store/src/accounts/mod.rs @@ -88,9 +88,8 @@ impl HistoricalOverlay { match mutation { NodeMutation::Addition(inner_node) => (*node_index, inner_node.hash()), NodeMutation::Removal => { - // Store the actual empty subtree root for this depth - // depth() is 1-indexed from leaf, so we use it directly for - // EmptySubtreeRoots + // Store the actual empty subtree root for this depth depth() is 1-indexed + // from leaf, so we use it directly for EmptySubtreeRoots let empty_root = *EmptySubtreeRoots::entry(SMT_DEPTH, node_index.depth()); (*node_index, empty_root) }, @@ -266,9 +265,9 @@ impl AccountTreeWithHistory { let leaf_index = NodeIndex::from(leaf.index()); - // Apply reversion overlays to reconstruct historical state. - // We reverse the overlay iteration (newest to oldest) to walk backwards in time - // from the latest state to the target block. + // Apply reversion overlays to reconstruct historical state. We reverse the overlay + // iteration (newest to oldest) to walk backwards in time from the latest state to the + // target block. let (path, leaf) = Self::apply_reversion_overlays( self.overlays.range(block_target..).rev().map(|(_, overlay)| overlay), path_nodes, @@ -319,10 +318,9 @@ impl AccountTreeWithHistory { .expect("proof_indices should not include root") as usize; - // Apply reversion mutation if this node was modified. - // It's sound since `proof_indices()`` returns siblings on the path from leaf to - // root, hence the height is always less than `SMT_DEPTH`, the leaf - // and root are not included. + // Apply reversion mutation if this node was modified. It's sound since + // `proof_indices()`` returns siblings on the path from leaf to root, hence the + // height is always less than `SMT_DEPTH`, the leaf and root are not included. if let Some(hash) = overlay.node_mutations.get(&sibling) { path_nodes[height] = *hash; } @@ -338,8 +336,8 @@ impl AccountTreeWithHistory { } } - // Build the Merkle path directly from the reconstructed nodes - // No need for build_dense_path since all nodes have actual values (not sentinels) + // Build the Merkle path directly from the reconstructed nodes No need for build_dense_path + // since all nodes have actual values (not sentinels) let dense: Vec = path_nodes.iter().rev().copied().collect(); let path = MerklePath::new(dense); let path = SparseMerklePath::try_from(path).ok()?; diff --git a/crates/store/src/accounts/tests.rs b/crates/store/src/accounts/tests.rs index 68cf0e1988..7ecc8a8628 100644 --- a/crates/store/src/accounts/tests.rs +++ b/crates/store/src/accounts/tests.rs @@ -351,8 +351,7 @@ mod account_tree_with_history_tests { .collect(); hist.compute_and_apply_mutations(updates3).unwrap(); - // Verify states at different blocks - // Check genesis - first 50 accounts exist, others don't + // Verify states at different blocks Check genesis - first 50 accounts exist, others don't for i in 0..50 { let witness = hist.open_at(account_ids[i], BlockNumber::GENESIS).unwrap(); assert_eq!(witness.state_commitment(), Word::from([i as u32, 0, 0, 0])); diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index cf32b85bcf..965ef439d9 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -142,8 +142,8 @@ impl PartialEq<(Nullifier, BlockNumber)> for NullifierInfo { pub struct TransactionRecord { pub block_num: BlockNumber, pub header: TransactionHeader, - /// Inclusion proofs for committed output notes. Notes in `header.output_notes()` without - /// a corresponding proof here were erased (created and consumed within the same batch). + /// Inclusion proofs for committed output notes. Notes in `header.output_notes()` without a + /// corresponding proof here were erased (created and consumed within the same batch). pub output_note_proofs: Vec, } @@ -586,8 +586,8 @@ impl Db { self.transact("apply block", move |conn| -> Result<()> { models::queries::apply_block(conn, &signed_block, ¬es)?; - // XXX FIXME TODO free floating mutex MUST NOT exist - // it doesn't bind it properly to the data locked! + // XXX FIXME TODO free floating mutex MUST NOT exist it doesn't bind it properly to the + // data locked! { let _span = tracing::info_span!(target: COMPONENT, "acquire_write_lock").entered(); if allow_acquire.send(()).is_err() { @@ -665,8 +665,8 @@ impl Db { values.extend(page.values); let mut last_block_included = page.last_block_included; - // If the first page returned no values, the block at block_range_start has more - // entries than the limit allows (e.g. genesis accounts with large storage maps). + // If the first page returned no values, the block at block_range_start has more entries + // than the limit allows (e.g. genesis accounts with large storage maps). if values.is_empty() && last_block_included == block_range_start { return Ok(AccountStorageMapDetails::limit_exceeded(slot_name)); } diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index 3edc318216..53dacbeab3 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -145,8 +145,8 @@ pub(crate) fn select_account( let summary: AccountSummary = raw.try_into()?; - // Backfill account details from database - // For private accounts, we don't store full details in the database + // Backfill account details from database For private accounts, we don't store full details in + // the database let details = if account_id.has_public_state() { Some(select_full_account(conn, account_id)?) } else { @@ -348,8 +348,7 @@ pub struct PublicAccountStateRoots { pub storage_header: AccountStorageHeader, } -/// Page of public account state roots returned by -/// [`select_public_account_state_roots_paged`]. +/// Page of public account state roots returned by [`select_public_account_state_roots_paged`]. #[derive(Debug)] pub struct PublicAccountStateRootsPage { /// The public account state roots in this page. @@ -539,8 +538,8 @@ pub(crate) fn select_account_vault_assets( block_range: RangeInclusive, ) -> Result<(BlockNumber, Vec), DatabaseError> { use schema::account_vault_assets as t; - // TODO: These limits should be given by the protocol. - // See miden-protocol/issues/1770 for more details + // TODO: These limits should be given by the protocol. See miden-protocol/issues/1770 for more + // details const ROW_OVERHEAD_BYTES: usize = 2 * size_of::() + size_of::(); // key + asset + block_num const MAX_ROWS: usize = MAX_RESPONSE_PAYLOAD_BYTES / ROW_OVERHEAD_BYTES; @@ -567,8 +566,8 @@ pub(crate) fn select_account_vault_assets( .limit(i64::try_from(MAX_ROWS + 1).expect("should fit within i64")) .load::<(i64, Vec, Option>)>(conn)?; - // If we got more rows than the limit, the last block may be incomplete so we - // drop it entirely and derive last_block_included from the remaining rows. + // If we got more rows than the limit, the last block may be incomplete so we drop it entirely + // and derive last_block_included from the remaining rows. let (last_block_included, values) = if let Some(&(last_block_num, ..)) = raw.last() && raw.len() > MAX_ROWS { @@ -814,8 +813,8 @@ pub(crate) fn select_account_storage_map_values_paged( .limit(i64::try_from(limit + 1).expect("limit fits within i64")) .load(conn)?; - // If we got more rows than the limit, the last block may be incomplete so we - // drop it entirely and derive last_block_included from the remaining rows. + // If we got more rows than the limit, the last block may be incomplete so we drop it entirely + // and derive last_block_included from the remaining rows. let (last_block_included, values) = if let Some(&(last_block_num, ..)) = raw.last() && raw.len() > limit { @@ -1169,13 +1168,13 @@ fn prepare_partial_account_update( account_id: AccountId, delta: &miden_protocol::account::delta::AccountDelta, ) -> Result<(AccountStateForInsert, PendingStorageInserts, PendingAssetInserts), DatabaseError> { - // Build the minimal account state needed for partial delta application. - // Only load the storage map entries and vault balances that will receive updates. - // The next line fetches the header, which will always change unless the delta is empty. + // Build the minimal account state needed for partial delta application. Only load the storage + // map entries and vault balances that will receive updates. The next line fetches the header, + // which will always change unless the delta is empty. let state_headers = select_minimal_account_state_headers(conn, account_id)?; - // --- Process asset updates. --------------------------------- - // Only query balances for faucet_ids that are being updated. + // --- Process asset updates. --------------------------------- Only query balances for + // faucet_ids that are being updated. let faucet_ids = Vec::from_iter(delta.vault().fungible().iter().map(|(vault_key, _)| vault_key.faucet_id())); let prev_balances = select_vault_balances_by_faucet_ids(conn, account_id, &faucet_ids)?; @@ -1244,8 +1243,7 @@ fn prepare_partial_account_update( vault.root() }; - // --- Compute updated account state for the accounts row. --- - // Apply nonce delta. + // --- Compute updated account state for the accounts row. --- Apply nonce delta. let new_nonce_value = state_headers .nonce .as_canonical_u64() @@ -1399,8 +1397,7 @@ pub(crate) fn upsert_accounts( .set(&account_value) .execute(conn)?; - // insert pending storage map entries - // TODO consider batching + // insert pending storage map entries TODO consider batching for (acc_id, slot_name, key, value) in pending_storage_inserts { insert_account_storage_map_value(conn, acc_id, block_num, slot_name, key, value)?; } @@ -1555,8 +1552,8 @@ pub(crate) struct AccountStorageMapRowInsert { // ================================================================================================ /// Number of historical blocks to retain for vault assets, storage map values, and account codes. -/// Entries older than `chain_tip - HISTORICAL_BLOCK_RETENTION` will be deleted, -/// except for entries marked with `is_latest=true` which are always retained. +/// Entries older than `chain_tip - HISTORICAL_BLOCK_RETENTION` will be deleted, except for entries +/// marked with `is_latest=true` which are always retained. pub const HISTORICAL_BLOCK_RETENTION: u32 = 50; /// Clean up old entries for all accounts, deleting entries older than the retention window. diff --git a/crates/store/src/db/models/queries/accounts/delta.rs b/crates/store/src/db/models/queries/accounts/delta.rs index e4f7042b93..88e5f8d68b 100644 --- a/crates/store/src/db/models/queries/accounts/delta.rs +++ b/crates/store/src/db/models/queries/accounts/delta.rs @@ -46,8 +46,8 @@ struct AccountStateDeltaRow { storage_header: Option>, } -/// Data needed for applying a delta update to an existing account. -/// Fetches only the minimal data required, avoiding loading full code and storage. +/// Data needed for applying a delta update to an existing account. Fetches only the minimal data +/// required, avoiding loading full code and storage. #[derive(Debug, Clone)] pub(super) struct AccountStateHeadersForDelta { pub nonce: Felt, @@ -55,8 +55,8 @@ pub(super) struct AccountStateHeadersForDelta { pub storage_header: AccountStorageHeader, } -/// Minimal account state computed from a partial delta update. -/// Contains only the fields needed for the accounts table row insert. +/// Minimal account state computed from a partial delta update. Contains only the fields needed for +/// the accounts table row insert. #[derive(Debug, Clone)] pub(super) struct PartialAccountState { pub nonce: Felt, @@ -65,8 +65,8 @@ pub(super) struct PartialAccountState { pub vault_root: Word, } -/// Represents the account state to be inserted, either from a full account -/// or from a partial delta update. +/// Represents the account state to be inserted, either from a full account or from a partial delta +/// update. pub(super) enum AccountStateForInsert { /// Private account - no public state stored Private, diff --git a/crates/store/src/db/models/queries/accounts/tests.rs b/crates/store/src/db/models/queries/accounts/tests.rs index e5c8d4dbb7..eee9f69795 100644 --- a/crates/store/src/db/models/queries/accounts/tests.rs +++ b/crates/store/src/db/models/queries/accounts/tests.rs @@ -677,8 +677,8 @@ fn test_upsert_accounts_with_multiple_storage_slots() { "Expected 5 storage slots (3 component + 2 auth)" ); - // The storage commitment matching proves that all values are correctly preserved. - // We don't check individual slot values by index since slot ordering may vary. + // The storage commitment matching proves that all values are correctly preserved. We don't + // check individual slot values by index since slot ordering may vary. } #[test] @@ -1087,8 +1087,8 @@ fn test_select_account_vault_at_block_exponential_updates() { } } -/// Tests that deleted vault assets (asset = None) are correctly excluded from results, -/// and that the deduplication handles deletion entries properly. +/// Tests that deleted vault assets (asset = None) are correctly excluded from results, and that the +/// deduplication handles deletion entries properly. #[test] fn test_select_account_vault_at_block_with_deletion() { use assert_matches::assert_matches; @@ -1274,8 +1274,8 @@ fn test_prune_account_code_retains_latest_after_code_change() { assert_eq!(count_account_codes(&mut conn), 2, "both codes must exist before pruning"); - // Advance past retention window and prune. - // cutoff = block_prunable - RETENTION = 2*RETENTION+1 - RETENTION = RETENTION+1 = block_code_b + // Advance past retention window and prune. cutoff = block_prunable - RETENTION = 2*RETENTION+1 + // - RETENTION = RETENTION+1 = block_code_b let (_, _, codes_deleted) = prune_history(&mut conn, block_prunable).expect("prune_history failed"); @@ -1299,8 +1299,8 @@ fn test_prune_account_code_retains_latest_after_code_change() { ); } -/// Prune test 3: code A → code B → code A; after the retention window, code B must be pruned -/// but code A must be retained because it is still the latest. +/// Prune test 3: code A → code B → code A; after the retention window, code B must be pruned but +/// code A must be retained because it is still the latest. #[test] fn test_prune_account_code_retains_revisited_code() { let mut conn = setup_test_db(); @@ -1354,8 +1354,8 @@ fn test_prune_account_code_retains_revisited_code() { let (_, _, codes_deleted) = prune_history(&mut conn, block_prunable).expect("prune_history failed"); - // Code B is no longer referenced by any account row within the retention window → pruned. - // Code A is still referenced by the block_code_a_again accounts row (within cutoff) → retained. + // Code B is no longer referenced by any account row within the retention window → pruned. Code + // A is still referenced by the block_code_a_again accounts row (within cutoff) → retained. assert_eq!(codes_deleted, 1, "exactly one code (B) must be pruned"); assert!(account_code_exists(&mut conn, code_commitment_a), "code A must be retained"); assert!(!account_code_exists(&mut conn, code_commitment_b), "code B must be pruned"); diff --git a/crates/store/src/db/models/queries/block_headers.rs b/crates/store/src/db/models/queries/block_headers.rs index 6b72462c73..f9a793860d 100644 --- a/crates/store/src/db/models/queries/block_headers.rs +++ b/crates/store/src/db/models/queries/block_headers.rs @@ -51,8 +51,8 @@ pub(crate) fn select_block_header_by_block_num( sel.filter(schema::block_headers::block_num.eq(block_num.to_raw_sql())) .get_result::(conn) .optional()? - // invariant: only one block exists with the given block header, so the length is - // always zero or one + // invariant: only one block exists with the given block header, so the length is always + // zero or one } else { sel.order(schema::block_headers::block_num.desc()) .limit(1) diff --git a/crates/store/src/db/models/queries/notes.rs b/crates/store/src/db/models/queries/notes.rs index 4fccfdd463..20777011fe 100644 --- a/crates/store/src/db/models/queries/notes.rs +++ b/crates/store/src/db/models/queries/notes.rs @@ -632,10 +632,8 @@ pub struct NoteDetailsRawRow { pub serial_num: Option>, } -// Note: One cannot use `#[diesel(embed)]` to structure -// this, it will yield a significant amount of errors -// when used with join and debugging is painful to put it -// mildly. +// Note: One cannot use `#[diesel(embed)]` to structure this, it will yield a significant amount of +// errors when used with join and debugging is painful to put it mildly. #[derive(Debug, Clone, PartialEq, Queryable)] pub struct NoteRecordWithScriptRawJoined { pub committed_at: i64, @@ -721,7 +719,7 @@ impl TryInto for NoteRecordWithScriptRawJoined { assets, storage, serial_num, - //details ^^^, + // details ^^^, inclusion_path, script, .. diff --git a/crates/store/src/db/models/queries/transactions.rs b/crates/store/src/db/models/queries/transactions.rs index c6c6b6ade7..4551219068 100644 --- a/crates/store/src/db/models/queries/transactions.rs +++ b/crates/store/src/db/models/queries/transactions.rs @@ -219,8 +219,8 @@ pub fn select_transactions_records( let desired_account_ids = serialize_vec(account_ids); - // Read transactions in chunks to prevent loading excessive data and to stop - // as soon as we approach the size limit + // Read transactions in chunks to prevent loading excessive data and to stop as soon as we + // approach the size limit let mut all_transactions = Vec::new(); let mut total_size = 0i64; let mut last_block_num: Option = None; @@ -276,8 +276,8 @@ pub fn select_transactions_records( } } - // Ensure block consistency: remove the last block if it's incomplete - // (we may have stopped loading mid-block due to size constraints) + // Ensure block consistency: remove the last block if it's incomplete (we may have stopped + // loading mid-block due to size constraints) if total_size >= max_payload_bytes { // SAFETY: We're guaranteed to have at least one transaction since total_size > 0 let last_block_num = last_block_num.expect( @@ -291,9 +291,9 @@ pub fn select_transactions_records( .collect(), )?; - // SAFETY: block_num came from the database and was previously validated. - // Subtraction is safe under the assumption that genesis block (where it could fail) does - // not have any transactions. + // SAFETY: block_num came from the database and was previously validated. Subtraction is + // safe under the assumption that genesis block (where it could fail) does not have any + // transactions. let last_included_block = BlockNumber::from_raw_sql(last_block_num.saturating_sub(1))?; Ok((last_included_block, filtered_transactions)) } else { @@ -328,8 +328,8 @@ fn with_output_note_proofs( .zip(tx_output_notes) .map(|(raw, output_notes)| { let transaction_id = TransactionId::read_from_bytes(&raw.transaction_id)?; - // Collect inclusion proofs for committed output notes. Notes not found in - // the `notes` table were erased (created and consumed in the same batch). + // Collect inclusion proofs for committed output notes. Notes not found in the `notes` + // table were erased (created and consumed in the same batch). let output_note_proofs = output_notes .iter() .filter_map(|note| output_notes_by_id.get(¬e.id()).cloned()) diff --git a/crates/store/src/db/models/utils.rs b/crates/store/src/db/models/utils.rs index 80dc1e9197..1415ee29eb 100644 --- a/crates/store/src/db/models/utils.rs +++ b/crates/store/src/db/models/utils.rs @@ -4,8 +4,8 @@ use miden_protocol::utils::serde::Serializable; use crate::errors::DatabaseError; -/// Utility to convert an iterable container of containing `R`-typed values -/// to a `Vec` and bail at the first failing conversion +/// Utility to convert an iterable container of containing `R`-typed values to a `Vec` and bail +/// at the first failing conversion pub(crate) fn vec_raw_try_into>( raw: impl IntoIterator, ) -> std::result::Result, >::Error> { diff --git a/crates/store/src/db/schema.rs b/crates/store/src/db/schema.rs index 60660afcbf..09e3621383 100644 --- a/crates/store/src/db/schema.rs +++ b/crates/store/src/db/schema.rs @@ -107,8 +107,8 @@ diesel::table! { diesel::joinable!(accounts -> account_codes (code_commitment)); diesel::joinable!(accounts -> block_headers (block_num)); // Note: Cannot use diesel::joinable! with accounts table due to composite primary key -// diesel::joinable!(notes -> accounts (sender)); -// diesel::joinable!(transactions -> accounts (account_id)); +// diesel::joinable!(notes -> accounts (sender)); diesel::joinable!(transactions -> accounts +// (account_id)); diesel::joinable!(notes -> block_headers (committed_at)); diesel::joinable!(notes -> note_scripts (script_root)); diesel::joinable!(nullifiers -> block_headers (block_num)); diff --git a/crates/store/src/db/tests.rs b/crates/store/src/db/tests.rs index 25948a47a7..5a8b4df197 100644 --- a/crates/store/src/db/tests.rs +++ b/crates/store/src/db/tests.rs @@ -908,8 +908,8 @@ fn notes() { assert_eq!(note_1.details, None); } -/// Creates notes across 3 blocks, then calls `get_note_sync_multi` once and verifies -/// all 3 blocks' notes are returned in a single query, ordered by block number. +/// Creates notes across 3 blocks, then calls `get_note_sync_multi` once and verifies all 3 blocks' +/// notes are returned in a single query, ordered by block number. #[test] #[miden_node_test_macro::enable_logging] fn note_sync_across_multiple_blocks() { @@ -1233,8 +1233,8 @@ fn select_storage_map_sync_values() { .unwrap(); } - // Insert data across multiple blocks using individual inserts - // Block 1: key1 -> value1, key2 -> value2 + // Insert data across multiple blocks using individual inserts Block 1: key1 -> value1, key2 -> + // value2 queries::insert_account_storage_map_value( &mut conn, account_id, @@ -1418,8 +1418,8 @@ fn select_storage_map_sync_values_paginates_until_last_block() { assert_eq!(page.values.len(), 1, "should include block 1 only"); } -/// Tests that `select_account_storage_map_values_paged` does not panic when all entries -/// exceed the limit and are in genesis block (block 0). Previously, this caused +/// Tests that `select_account_storage_map_values_paged` does not panic when all entries exceed the +/// limit and are in genesis block (block 0). Previously, this caused /// `last_block_num.saturating_sub(1) = -1` which failed `BlockNumber::from_raw_sql`. #[test] fn select_storage_map_sync_values_all_entries_in_genesis_block() { @@ -1446,9 +1446,9 @@ fn select_storage_map_sync_values_all_entries_in_genesis_block() { .unwrap(); } - // Query with limit=1 so that raw.len() (3) > limit (1), triggering the - // pagination branch. All entries are in block 0, so take_while produces - // nothing and last_block_num.saturating_sub(1) = -1. + // Query with limit=1 so that raw.len() (3) > limit (1), triggering the pagination branch. All + // entries are in block 0, so take_while produces nothing and last_block_num.saturating_sub(1) = + // -1. let result = queries::select_account_storage_map_values_paged( &mut conn, account_id, @@ -1456,8 +1456,8 @@ fn select_storage_map_sync_values_all_entries_in_genesis_block() { 1, ); - // Should not error - should return a valid page (possibly with empty values - // indicating no progress, which the caller interprets as limit_exceeded) + // Should not error - should return a valid page (possibly with empty values indicating no + // progress, which the caller interprets as limit_exceeded) let page = result.expect("should not return an internal error for genesis block entries"); // The page should indicate no progress was made (stuck at genesis) assert!( @@ -1466,9 +1466,9 @@ fn select_storage_map_sync_values_all_entries_in_genesis_block() { ); } -/// Tests that single-block overflow works for non-genesis blocks too. -/// All entries are in block 5 and exceed the limit. The function should -/// signal no progress rather than returning incorrect data. +/// Tests that single-block overflow works for non-genesis blocks too. All entries are in block 5 +/// and exceed the limit. The function should signal no progress rather than returning incorrect +/// data. #[test] fn select_storage_map_sync_values_all_entries_in_single_non_genesis_block() { let mut conn = create_db(); @@ -1502,8 +1502,8 @@ fn select_storage_map_sync_values_all_entries_in_single_non_genesis_block() { assert_eq!(page.last_block_included, block5, "should signal no progress at block 5"); } -/// Tests that normal multi-block pagination still works correctly: -/// entries in blocks 1, 2, 3 with limit causing block 3 to be dropped. +/// Tests that normal multi-block pagination still works correctly: entries in blocks 1, 2, 3 with +/// limit causing block 3 to be dropped. #[test] fn select_storage_map_sync_values_multi_block_pagination() { let mut conn = create_db(); @@ -1633,10 +1633,10 @@ async fn reconstruct_storage_map_from_db_pages_until_latest() { }); } -/// Tests that `reconstruct_storage_map_from_db` returns `LimitExceeded` when the first -/// block in the range has more entries than the limit allows. Previously this returned -/// `AllEntries([])` because the pagination loop exited immediately (`last_block_included` == -/// `block_num`) without checking that no values were actually returned. +/// Tests that `reconstruct_storage_map_from_db` returns `LimitExceeded` when the first block in the +/// range has more entries than the limit allows. Previously this returned `AllEntries([])` because +/// the pagination loop exited immediately (`last_block_included` == `block_num`) without checking +/// that no values were actually returned. #[tokio::test] #[miden_node_test_macro::enable_logging] async fn reconstruct_storage_map_from_db_returns_limit_exceeded_for_single_block_overflow() { @@ -1674,8 +1674,8 @@ async fn reconstruct_storage_map_from_db_returns_limit_exceeded_for_single_block .await .unwrap(); - // Use limit=1 so that 3 entries in a single block exceed the limit. - // block_range_start is block5 (the first block with data), and the target is also block5. + // Use limit=1 so that 3 entries in a single block exceed the limit. block_range_start is block5 + // (the first block with data), and the target is also block5. let details = db .reconstruct_storage_map_from_db(account_id, slot_name.clone(), block5, Some(1)) .await @@ -2123,8 +2123,8 @@ async fn genesis_with_account_assets_and_storage() { crate::db::Db::bootstrap(":memory:".into(), genesis_block).unwrap(); } -/// Verifies genesis block with multiple accounts of different types. -/// Tests realistic genesis scenario with basic accounts, assets, and storage. +/// Verifies genesis block with multiple accounts of different types. Tests realistic genesis +/// scenario with basic accounts, assets, and storage. #[tokio::test] #[miden_node_test_macro::enable_logging] async fn genesis_with_multiple_accounts() { @@ -3027,8 +3027,8 @@ fn test_prune_history() { "block_tip storage map value should be retained" ); - // Test that is_latest=true entries are never deleted, even if old - // Insert an old entry marked as latest + // Test that is_latest=true entries are never deleted, even if old Insert an old entry marked as + // latest let faucet_4 = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3).unwrap(); let asset_old = Asset::Fungible(FungibleAsset::new(faucet_4, 9999).unwrap()); let vault_key_old_latest = asset_old.vault_key(); @@ -3041,8 +3041,8 @@ fn test_prune_history() { ) .unwrap(); - // This entry at block 0 is marked as is_latest=true by insert_account_vault_asset - // Run cleanup again + // This entry at block 0 is marked as is_latest=true by insert_account_vault_asset Run cleanup + // again let (vault_deleted_2, ..) = queries::prune_history(conn, block_tip).unwrap(); // The old latest entry should not be deleted (vault_deleted_2 should be 0) @@ -3455,8 +3455,8 @@ fn account_state_forest_retains_latest_after_100_blocks_and_pruning() { let initial_storage_map_root = forest.get_storage_map_root(account_id, &slot_map, block_1).unwrap(); - // Blocks 2-100: Do nothing (no updates to this account) - // Simulate other activity by just advancing to block 100 + // Blocks 2-100: Do nothing (no updates to this account) Simulate other activity by just + // advancing to block 100 let block_100 = BlockNumber::from(100); @@ -3481,8 +3481,8 @@ fn account_state_forest_retains_latest_after_100_blocks_and_pruning() { let witness = forest.get_storage_map_witness(account_id, &slot_map, block_100, key1); assert!(witness.is_ok()); - // Now add an update at block 51 (within retention window) to test that old entries - // get pruned when newer entries exist + // Now add an update at block 51 (within retention window) to test that old entries get pruned + // when newer entries exist let block_51 = BlockNumber::from(51); // Update with new values @@ -3800,12 +3800,12 @@ fn account_state_forest_preserves_most_recent_storage_value_slot() { forest.update_account(block_1, &delta_1).unwrap(); - // Note: Value slots don't have roots in AccountStateForest - they're just part of the - // account storage header. The AccountStateForest only tracks map slots. - // So there's nothing to verify for value slots in the forest. + // Note: Value slots don't have roots in AccountStateForest - they're just part of the account + // storage header. The AccountStateForest only tracks map slots. So there's nothing to verify + // for value slots in the forest. - // This test documents that value slots are NOT tracked in AccountStateForest - // (they don't need to be, since their digest is 1:1 with the value) + // This test documents that value slots are NOT tracked in AccountStateForest (they don't need + // to be, since their digest is 1:1 with the value) // Advance 100 blocks without any updates let block_100 = BlockNumber::from(100); @@ -3900,9 +3900,8 @@ fn account_state_forest_preserves_mixed_slots_independently() { // Prune at block 100 let total_roots_removed = forest.prune(block_100); - // Vault: block 1 is most recent, should NOT be pruned - // Map A: block 1 is old (block 51 is newer), SHOULD be pruned - // Map B: block 1 is most recent, should NOT be pruned + // Vault: block 1 is most recent, should NOT be pruned Map A: block 1 is old (block 51 is + // newer), SHOULD be pruned Map B: block 1 is most recent, should NOT be pruned assert_eq!( total_roots_removed, 0, "Vault root from block 1 should NOT be pruned (most recent)" diff --git a/crates/store/src/errors.rs b/crates/store/src/errors.rs index fb938d0897..05b277e775 100644 --- a/crates/store/src/errors.rs +++ b/crates/store/src/errors.rs @@ -595,8 +595,8 @@ mod compile_tests { StateInitializationError, }; - /// Ensure all enum variants remain compat with the desired - /// trait bounds. Otherwise one gets very unwieldy errors. + /// Ensure all enum variants remain compat with the desired trait bounds. Otherwise one gets + /// very unwieldy errors. #[expect(dead_code)] fn assumed_trait_bounds_upheld() { fn ensure_is_error(_phony: PhantomData) diff --git a/crates/store/src/genesis/config/mod.rs b/crates/store/src/genesis/config/mod.rs index 391c059e24..782bfe39c6 100644 --- a/crates/store/src/genesis/config/mod.rs +++ b/crates/store/src/genesis/config/mod.rs @@ -207,8 +207,8 @@ impl GenesisConfig { faucet_account.id(), secret_key, )); - // Do _not_ collect the account, only after we know all wallet assets - // we know the remaining supply in the faucets. + // Do _not_ collect the account, only after we know all wallet assets we know the + // remaining supply in the faucets. } let fee_parameters = @@ -280,8 +280,8 @@ impl GenesisConfig { // Apply all fungible faucet adjustments to the respective faucet for (symbol, mut faucet_account) in faucet_accounts { let faucet_id = faucet_account.id(); - // If there is no account using the asset, we use an empty delta to set the - // nonce to `ONE`. + // If there is no account using the asset, we use an empty delta to set the nonce to + // `ONE`. let total_issuance = faucet_issuance.get(&faucet_id).copied().unwrap_or_default(); let mut storage_delta = AccountStorageDelta::default(); @@ -577,8 +577,8 @@ impl AccountSecrets { // HELPERS // ================================================================================================ -/// Process wallet assets and return them as a fungible asset delta. -/// Track the negative adjustments for the respective faucets. +/// Process wallet assets and return them as a fungible asset delta. Track the negative adjustments +/// for the respective faucets. fn prepare_fungible_asset_update( assets: impl IntoIterator, faucets: &IndexMap, diff --git a/crates/store/src/genesis/mod.rs b/crates/store/src/genesis/mod.rs index 33759ebd27..9de141eade 100644 --- a/crates/store/src/genesis/mod.rs +++ b/crates/store/src/genesis/mod.rs @@ -33,8 +33,8 @@ pub struct GenesisState { pub validator_key: PublicKey, } -/// A type-safety wrapper ensuring that genesis block data can only be created from -/// [`GenesisState`] or validated from a [`SignedBlock`] via [`GenesisBlock::try_from`]. +/// A type-safety wrapper ensuring that genesis block data can only be created from [`GenesisState`] +/// or validated from a [`SignedBlock`] via [`GenesisBlock::try_from`]. pub struct GenesisBlock(SignedBlock); /// A genesis block with all data except the validator signature. diff --git a/crates/store/src/server/block_producer.rs b/crates/store/src/server/block_producer.rs index c19ddfe7fe..7f971b8ecf 100644 --- a/crates/store/src/server/block_producer.rs +++ b/crates/store/src/server/block_producer.rs @@ -30,8 +30,8 @@ use crate::state::Finality; // BLOCK PRODUCER API // ================================================================================================ -/// Extends [`StoreApi`] with the proof-scheduler notification channel, which is only required -/// by the `BlockProducer` gRPC service. Not used in replica mode. +/// Extends [`StoreApi`] with the proof-scheduler notification channel, which is only required by +/// the `BlockProducer` gRPC service. Not used in replica mode. #[derive(Clone)] pub(super) struct BlockProducerApi { pub(super) inner: StoreApi, @@ -119,8 +119,7 @@ impl block_producer_server::BlockProducer for BlockProducerApi { async move { let signed_block = SignedBlock::new(header, body, signature) .map_err(|err| Status::new(tonic::Code::Internal, err.as_report()))?; - // Note: This is an internal endpoint, so its safe to expose the full error - // report. + // Note: This is an internal endpoint, so its safe to expose the full error report. this.inner .state .apply_block(signed_block) diff --git a/crates/store/src/server/mod.rs b/crates/store/src/server/mod.rs index 5c01b75e51..616f0de309 100644 --- a/crates/store/src/server/mod.rs +++ b/crates/store/src/server/mod.rs @@ -398,8 +398,8 @@ impl Store { Ok(join_set) } - /// Spawns a background task that periodically records the on-disk size of every store data - /// path as `OTel` span attributes. + /// Spawns a background task that periodically records the on-disk size of every store data path + /// as `OTel` span attributes. fn spawn_disk_monitor(data_directory: PathBuf) -> tokio::task::JoinHandle<()> { tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_mins(5)); diff --git a/crates/store/src/server/proof_scheduler.rs b/crates/store/src/server/proof_scheduler.rs index 7fc0b69e7e..6b6fc9668a 100644 --- a/crates/store/src/server/proof_scheduler.rs +++ b/crates/store/src/server/proof_scheduler.rs @@ -165,8 +165,8 @@ async fn run( let (block_num, proof_bytes) = proving_result?; pending.insert(block_num, proof_bytes); - // Drain completed proofs in ascending order so the proven tip advances - // without gaps. + // Drain completed proofs in ascending order so the proven tip advances without + // gaps. let mut next = proven_tip.read().child(); while let Some(proof_bytes) = pending.remove(&next) { block_store.commit_proof(next, &proof_bytes).await?; diff --git a/crates/store/src/server/replica_sync.rs b/crates/store/src/server/replica_sync.rs index 0bb0553443..1f8ee54ba1 100644 --- a/crates/store/src/server/replica_sync.rs +++ b/crates/store/src/server/replica_sync.rs @@ -38,8 +38,8 @@ pub(crate) trait ReplicaSync: Sized + Send + Sync + 'static { /// Returns the upstream store URL to connect to. fn upstream_url(&self) -> &Url; - /// Subscribes to the upstream stream via `client` and processes events until the stream ends - /// or an error occurs. + /// Subscribes to the upstream stream via `client` and processes events until the stream ends or + /// an error occurs. async fn subscribe(&self, client: StoreReplicaClient) -> anyhow::Result<()>; /// Opens a connection to [`upstream_url`](Self::upstream_url) and calls diff --git a/crates/store/src/server/rpc_api.rs b/crates/store/src/server/rpc_api.rs index cea9065dc0..99158a1ece 100644 --- a/crates/store/src/server/rpc_api.rs +++ b/crates/store/src/server/rpc_api.rs @@ -400,8 +400,8 @@ impl rpc_server::Rpc for StoreApi { .await .map_err(SyncTransactionsError::from)?; - // Convert database TransactionRecords directly to proto TransactionRecords. - // All data needed for the proto TransactionHeader is stored in the transactions table. + // Convert database TransactionRecords directly to proto TransactionRecords. All data needed + // for the proto TransactionHeader is stored in the transactions table. let transactions: Vec<_> = transaction_records_db .into_iter() .map(crate::db::TransactionRecord::into_proto) diff --git a/crates/store/src/state/apply_block.rs b/crates/store/src/state/apply_block.rs index 8550efdf2f..2cf12a1b10 100644 --- a/crates/store/src/state/apply_block.rs +++ b/crates/store/src/state/apply_block.rs @@ -80,8 +80,8 @@ impl State { // Signals the write lock has been acquired, and the transaction can be committed. let (inform_acquire_done, acquire_done) = oneshot::channel::<()>(); - // Extract public account updates with deltas before block is moved into async task. - // Private accounts are filtered out since they don't expose their state changes. + // Extract public account updates with deltas before block is moved into async task. Private + // accounts are filtered out since they don't expose their state changes. let account_deltas = Vec::from_iter(body.updated_accounts().iter().filter_map( |update| match update.details() { @@ -90,18 +90,16 @@ impl State { }, )); - // The DB and in-memory state updates need to be synchronized and are partially - // overlapping. Namely, the DB transaction only proceeds after this task acquires the - // in-memory write lock. This requires the DB update to run concurrently, so a new task is - // spawned. + // The DB and in-memory state updates need to be synchronized and are partially overlapping. + // Namely, the DB transaction only proceeds after this task acquires the in-memory write + // lock. This requires the DB update to run concurrently, so a new task is spawned. let db = Arc::clone(&self.db); let db_update_task = tokio::spawn( async move { db.apply_block(allow_acquire, acquire_done, signed_block, notes).await } .in_current_span(), ); - // Wait for the message from the DB update task, that we ready to commit the DB - // transaction. + // Wait for the message from the DB update task, that we ready to commit the DB transaction. acquired_allowed .instrument(info_span!(target: COMPONENT, "await_db_readiness")) .await @@ -112,25 +110,24 @@ impl State { self.with_inner_write_blocking(|inner| { // We need to check that neither the nullifier tree nor the account tree have changed - // while we were waiting for the DB preparation task to complete. If either of them - // did change, we do not proceed with in-memory and database updates, since it may - // lead to an inconsistent state. + // while we were waiting for the DB preparation task to complete. If either of them did + // change, we do not proceed with in-memory and database updates, since it may lead to + // an inconsistent state. if inner.nullifier_tree.root() != nullifier_tree_old_root || inner.account_tree.root_latest() != account_tree_old_root { return Err(ApplyBlockError::ConcurrentWrite); } - // Notify the DB update task that the write lock has been acquired, so it can commit - // the DB transaction. + // Notify the DB update task that the write lock has been acquired, so it can commit the + // DB transaction. inform_acquire_done .send(()) .map_err(|_| ApplyBlockError::DbUpdateTaskFailed("Receiver was dropped".into()))?; - // TODO: shutdown #91 - // Await for successful commit of the DB transaction. If the commit fails, we mustn't - // change in-memory state, so we return a block applying error and don't proceed with - // in-memory updates. + // TODO: shutdown #91 Await for successful commit of the DB transaction. If the commit + // fails, we mustn't change in-memory state, so we return a block applying error and + // don't proceed with in-memory updates. tokio::runtime::Handle::current() .block_on(db_update_task)? .map_err(|err| ApplyBlockError::DbUpdateTaskFailed(err.as_report()))?; diff --git a/crates/store/src/state/loader.rs b/crates/store/src/state/loader.rs index 4400121f38..72df4fff34 100644 --- a/crates/store/src/state/loader.rs +++ b/crates/store/src/state/loader.rs @@ -50,16 +50,16 @@ pub const NULLIFIER_TREE_STORAGE_DIR: &str = "nullifiertree"; /// Directory name for the account state forest storage within the data directory. pub const ACCOUNT_STATE_FOREST_STORAGE_DIR: &str = "accountstateforest"; -/// Page size for loading account commitments from the database during tree rebuilding. -/// This limits memory usage when rebuilding trees with millions of accounts. +/// Page size for loading account commitments from the database during tree rebuilding. This limits +/// memory usage when rebuilding trees with millions of accounts. const ACCOUNT_COMMITMENTS_PAGE_SIZE: NonZeroUsize = NonZeroUsize::new(10_000).unwrap(); -/// Page size for loading nullifiers from the database during tree rebuilding. -/// This limits memory usage when rebuilding trees with millions of nullifiers. +/// Page size for loading nullifiers from the database during tree rebuilding. This limits memory +/// usage when rebuilding trees with millions of nullifiers. const NULLIFIERS_PAGE_SIZE: NonZeroUsize = NonZeroUsize::new(10_000).unwrap(); -/// Page size for loading public account IDs from the database during forest rebuilding. -/// This limits memory usage when rebuilding with millions of public accounts. +/// Page size for loading public account IDs from the database during forest rebuilding. This limits +/// memory usage when rebuilding with millions of public accounts. const PUBLIC_ACCOUNT_IDS_PAGE_SIZE: NonZeroUsize = NonZeroUsize::new(1_000).unwrap(); // STORAGE TYPE ALIAS @@ -212,9 +212,9 @@ impl TreeStorageLoader for MemoryStorage { AccountTree::new(smt).map_err(StateInitializationError::FailedToCreateAccountsTree) } - // TODO: Make the loading methodology for account and nullifier trees consistent. - // Currently we use `NullifierTree::new_unchecked()` for nullifiers but `AccountTree::new()` - // for accounts. Consider using `NullifierTree::with_storage_from_entries()` for consistency. + // TODO: Make the loading methodology for account and nullifier trees consistent. Currently we + // use `NullifierTree::new_unchecked()` for nullifiers but `AccountTree::new()` for accounts. + // Consider using `NullifierTree::with_storage_from_entries()` for consistency. #[instrument(target = COMPONENT, skip_all)] async fn load_nullifier_tree( self, @@ -464,8 +464,7 @@ pub fn load_smt(storage: S) -> Result, StateInitializ pub async fn load_mmr(db: &mut Db) -> Result { 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. + // SAFETY: We assume the loaded MMR is valid and does not have more than u32::MAX entries. let chain_mmr = Blockchain::from_mmr_unchecked(Mmr::from( block_commitments.iter().copied().map(BlockHeaderCommitment::word), )); @@ -493,8 +492,8 @@ pub async fn rebuild_account_state_forest( // Process each account in this page for account_id in page.account_ids { - // TODO: Loading the full account from the database is inefficient and will need to - // go away. + // TODO: Loading the full account from the database is inefficient and will need to go + // away. let account_info = db.select_account(account_id).await?; let account = account_info .details diff --git a/crates/store/src/state/mod.rs b/crates/store/src/state/mod.rs index 7d8bfcc1c0..2bdb907f82 100644 --- a/crates/store/src/state/mod.rs +++ b/crates/store/src/state/mod.rs @@ -163,8 +163,7 @@ pub struct State { /// Request termination of the process due to a fatal internal state error. termination_ask: tokio::sync::mpsc::Sender, - /// The latest proven-in-sequence block number, updated by the proof scheduler or - /// `apply_proof`. + /// The latest proven-in-sequence block number, updated by the proof scheduler or `apply_proof`. proven_tip: ProvenTipWriter, /// Watch sender fired after each block is committed. Replicas subscribe via @@ -175,8 +174,8 @@ pub struct State { /// block that has been evicted, it falls back to loading from the block store. pub(crate) block_cache: BlockCache, - /// FIFO cache of recent block proofs for replica subscriptions. When a subscriber needs a - /// proof that has been evicted, it falls back to loading from the block store. + /// FIFO cache of recent block proofs for replica subscriptions. When a subscriber needs a proof + /// that has been evicted, it falls back to loading from the block store. pub(crate) proof_cache: ProofCache, } @@ -254,9 +253,9 @@ impl State { TreeStorage::create(data_path, &nullifier_storage_config, NULLIFIER_TREE_STORAGE_DIR)?; let nullifier_tree = nullifier_storage.load_nullifier_tree(&mut db).await?; - // Verify that tree roots match the expected roots from the database. - // This catches any divergence between persistent storage and the database caused by - // corruption or incomplete shutdown. + // Verify that tree roots match the expected roots from the database. This catches any + // divergence between persistent storage and the database caused by corruption or incomplete + // shutdown. verify_tree_consistency(account_tree.root(), nullifier_tree.root(), &mut db).await?; let account_tree = AccountTreeWithHistory::new(account_tree, latest_block_num); @@ -420,8 +419,8 @@ impl State { self.db.select_notes_by_id(note_ids).await } - /// If the input block number is the current chain tip, `None` is returned. - /// Otherwise, gets the current chain tip's block header with its corresponding MMR peaks. + /// If the input block number is the current chain tip, `None` is returned. Otherwise, gets the + /// current chain tip's block header with its corresponding MMR peaks. pub async fn get_current_blockchain_data( &self, block_num: Option, @@ -476,9 +475,9 @@ impl State { return Err(GetBatchInputsError::TransactionBlockReferencesEmpty); } - // First we grab note inclusion proofs for the known notes. These proofs only - // prove that the note was included in a given block. We then also need to prove that - // each of those blocks is included in the chain. + // First we grab note inclusion proofs for the known notes. These proofs only prove that the + // note was included in a given block. We then also need to prove that each of those blocks + // is included in the chain. let note_proofs = self .db .select_note_inclusion_proofs(unauthenticated_note_commitments) @@ -494,8 +493,8 @@ impl State { let mut blocks: BTreeSet = tx_reference_blocks; blocks.extend(note_blocks); - // Scoped block to automatically drop the read lock guard as soon as we're done. - // We also avoid accessing the db in the block as this would delay dropping the guard. + // Scoped block to automatically drop the read lock guard as soon as we're done. We also + // avoid accessing the db in the block as this would delay dropping the guard. let (batch_reference_block, partial_mmr) = { let inner_state = self.inner.read().await; @@ -576,9 +575,9 @@ impl State { unauthenticated_note_commitments: BTreeSet, reference_blocks: BTreeSet, ) -> Result { - // Get the note inclusion proofs from the DB. - // We do this first so we have to acquire the lock to the state just once. There we need the - // reference blocks of the note proofs to get their authentication paths in the chain MMR. + // Get the note inclusion proofs from the DB. We do this first so we have to acquire the + // lock to the state just once. There we need the reference blocks of the note proofs to get + // their authentication paths in the chain MMR. let unauthenticated_note_proofs = self .db .select_note_inclusion_proofs(unauthenticated_note_commitments) @@ -604,8 +603,8 @@ impl State { .await .map_err(GetBlockInputsError::SelectBlockHeaderError)?; - // Find and remove the latest block as we must not add it to the chain MMR, since it is - // not yet in the chain. + // Find and remove the latest block as we must not add it to the chain MMR, since it is not + // yet in the chain. let latest_block_header_index = headers .iter() .enumerate() diff --git a/crates/store/src/state/sync_state.rs b/crates/store/src/state/sync_state.rs index b978c17faa..dc890571a4 100644 --- a/crates/store/src/state/sync_state.rs +++ b/crates/store/src/state/sync_state.rs @@ -93,10 +93,9 @@ impl State { block_range: RangeInclusive, ) -> Result<(Vec<(NoteSyncUpdate, MmrProof)>, BlockNumber), NoteSyncError> { let block_end = *block_range.end(); - // The MMR at forest N contains proofs for blocks 0..N-1, so we use block_end + 1 to - // include the proof for block_end. - // SAFETY: it is ensured that block_end <= chain_tip, and the blockchain MMR always has - // at least chain_tip + 1 leaves. + // The MMR at forest N contains proofs for blocks 0..N-1, so we use block_end + 1 to include + // the proof for block_end. SAFETY: it is ensured that block_end <= chain_tip, and the + // blockchain MMR always has at least chain_tip + 1 leaves. let mmr_checkpoint = block_end + 1; let note_syncs = self.db.get_note_sync_multi(block_range, note_tags.into()).await?; diff --git a/crates/utils/src/clap.rs b/crates/utils/src/clap.rs index ffd0a96be0..d8fa718a05 100644 --- a/crates/utils/src/clap.rs +++ b/crates/utils/src/clap.rs @@ -15,8 +15,8 @@ const DEFAULT_REPLENISH_N_PER_SECOND_PER_IP: NonZeroU64 = NonZeroU64::new(16).un const DEFAULT_BURST_SIZE: NonZeroU32 = NonZeroU32::new(128).unwrap(); const DEFAULT_MAX_CONCURRENT_CONNECTIONS: u64 = 1_000; -// Formats a Duration into a human-readable string for display in clap help text -// and yields a &'static str by _leaking_ the string deliberately. +// Formats a Duration into a human-readable string for display in clap help text and yields a +// &'static str by _leaking_ the string deliberately. pub fn duration_to_human_readable_string(duration: Duration) -> &'static str { Box::new(humantime::format_duration(duration).to_string()).leak() } @@ -80,8 +80,8 @@ pub struct GrpcOptionsExternal { )] pub max_connection_age: Duration, - /// Number of connections to be served before the "API tokens" need to be replenished - /// per IP address. + /// Number of connections to be served before the "API tokens" need to be replenished per IP + /// address. #[arg( long = "grpc.burst_size", default_value_t = DEFAULT_BURST_SIZE, diff --git a/crates/utils/src/fifo_cache.rs b/crates/utils/src/fifo_cache.rs index 148f06be9f..df10793d11 100644 --- a/crates/utils/src/fifo_cache.rs +++ b/crates/utils/src/fifo_cache.rs @@ -87,10 +87,10 @@ mod tests { #[test] fn overwrite_key_evicts_on_next_push() { - // Pushing the same key twice leaves a ghost entry in the eviction queue. - // The ghost is a no-op when it surfaces as the oldest entry: the key is - // already absent from the map so map.remove() does nothing. The important - // invariant is that no *other* key is spuriously evicted. + // Pushing the same key twice leaves a ghost entry in the eviction queue. The ghost is a + // no-op when it surfaces as the oldest entry: the key is already absent from the map so + // map.remove() does nothing. The important invariant is that no *other* key is spuriously + // evicted. let c = cache(2); c.push(1, "a"); c.push(1, "b"); // eviction queue: [1, 1], map: {1: "b"} diff --git a/crates/utils/src/limiter.rs b/crates/utils/src/limiter.rs index 98d28d7134..c68b9cf53f 100644 --- a/crates/utils/src/limiter.rs +++ b/crates/utils/src/limiter.rs @@ -22,8 +22,8 @@ pub struct QueryLimitError { limit: usize, } -/// Checks limits against the desired query parameters, per query parameter and -/// bails if they exceed a defined value. +/// Checks limits against the desired query parameters, per query parameter and bails if they exceed +/// a defined value. pub trait QueryParamLimiter { /// Name of the parameter to mention in the error. const PARAM_NAME: &'static str; @@ -42,8 +42,7 @@ pub trait QueryParamLimiter { } } -/// Maximum payload size (in bytes) for paginated responses returned by the -/// store. +/// Maximum payload size (in bytes) for paginated responses returned by the store. pub const MAX_RESPONSE_PAYLOAD_BYTES: usize = 4 * 1024 * 1024; /// Used for the following RPC endpoints: diff --git a/crates/utils/src/logging.rs b/crates/utils/src/logging.rs index e8164f6e52..8e38ac7928 100644 --- a/crates/utils/src/logging.rs +++ b/crates/utils/src/logging.rs @@ -67,8 +67,8 @@ pub fn setup_tracing(otel: OpenTelemetry) -> anyhow::Result> { let tracer_provider = if otel.is_enabled() { let provider = init_tracer_provider()?; - // Store the provider globally so the panic hook can flush it. - // SdkTracerProvider is internally reference-counted, so cloning is cheap. + // Store the provider globally so the panic hook can flush it. SdkTracerProvider is + // internally reference-counted, so cloning is cheap. TRACER_PROVIDER .set(provider.clone()) .expect("setup_tracing should only be called once"); @@ -86,8 +86,8 @@ pub fn setup_tracing(otel: OpenTelemetry) -> anyhow::Result> { .with(otel_layer.with_filter(env_or_default_filter())); tracing::subscriber::set_global_default(subscriber).map_err(Into::::into)?; - // Register panic hook now that tracing is initialized. - // This chains with the default panic hook to preserve backtrace printing. + // Register panic hook now that tracing is initialized. This chains with the default panic hook + // to preserve backtrace printing. let default_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { tracing::error!(panic = true, info = %info, "panic"); @@ -97,8 +97,8 @@ pub fn setup_tracing(otel: OpenTelemetry) -> anyhow::Result> { let wrapped = anyhow::Error::msg(info_str); tracing::Span::current().set_error(wrapped.as_ref()); - // Flush traces before the program terminates. - // This ensures the panic trace is exported even though the OtelGuard won't be dropped. + // Flush traces before the program terminates. This ensures the panic trace is exported even + // though the OtelGuard won't be dropped. if let Some(provider) = TRACER_PROVIDER.get() { if let Err(err) = provider.force_flush() { eprintln!("Failed to flush traces on panic: {err:?}"); diff --git a/crates/utils/src/lru_cache.rs b/crates/utils/src/lru_cache.rs index f0e73f829b..42e79f1c57 100644 --- a/crates/utils/src/lru_cache.rs +++ b/crates/utils/src/lru_cache.rs @@ -5,8 +5,8 @@ use std::sync::{Arc, Mutex, MutexGuard}; use lru::LruCache as InnerCache; use tracing::instrument; -/// A newtype wrapper around an LRU cache. Ensures that the cache lock is not held across -/// await points. +/// A newtype wrapper around an LRU cache. Ensures that the cache lock is not held across await +/// points. #[derive(Clone)] pub struct LruCache(Arc>>); @@ -54,9 +54,9 @@ where #[instrument(name = "lru.lock", skip_all)] fn lock(&self) -> MutexGuard<'_, InnerCache> { - // SAFETY: The mutex is only held for the duration of the get/put operation - // where panics are possible only if we're running out of memory, in which - // case the entire process is likely to be unstable anyway. + // SAFETY: The mutex is only held for the duration of the get/put operation where panics are + // possible only if we're running out of memory, in which case the entire process is likely + // to be unstable anyway. self.0.lock().expect("LRU cache mutex poisoned") } } diff --git a/rustfmt.toml b/rustfmt.toml index 4ee447c23b..5a94ff1cac 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,7 +1,16 @@ +# We have a custom `cargo xtask fmt-comments` which performs the comment formatting. +# +# The custom xtask also supports reflowing short comment lines, unlike rustftm which +# only supports truncation. +# +# The custom xtask reads `comment_width` from here for consistency and we explicitly +# disable rustfmt's comment wrapping to give the xtask control. +comment_width = 100 +wrap_comments = false + array_width = 80 attr_fn_like_width = 80 chain_width = 80 -comment_width = 100 condense_wildcard_suffixes = true edition = "2024" fn_call_width = 80 @@ -16,4 +25,3 @@ struct_lit_width = 40 struct_variant_width = 40 use_field_init_shorthand = true use_try_shorthand = true -wrap_comments = true diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000000..55c7e153e8 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,18 @@ +[package] +edition.workspace = true +license.workspace = true +name = "xtask" +publish = false +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + +[dependencies] +anyhow = { workspace = true } +clap = { features = ["derive"], workspace = true } +fs-err = { workspace = true } +toml = { workspace = true } +tree-sitter = "0.26" +tree-sitter-rust = "0.24" diff --git a/xtask/src/comment_reflow.rs b/xtask/src/comment_reflow.rs new file mode 100644 index 0000000000..ea780bebc8 --- /dev/null +++ b/xtask/src/comment_reflow.rs @@ -0,0 +1,501 @@ +//! Conservative Rust line-comment reflow support for the repository formatter. +//! +//! The formatter is intentionally narrower than rustfmt's comment handling. It only rewrites +//! contiguous full-line comment blocks that look like plain prose, and it skips comments whose +//! layout may carry meaning in Markdown, code examples, or hand-aligned text. +//! +//! The CLI includes regular `//` comments by default. Callers can still use [`Config`] to restrict +//! reflowing to rustdoc comments (`///` and `//!`) when they need a narrower pass. + +use std::ffi::OsStr; +use std::ops::Range; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result}; +use tree_sitter::{Node, Parser}; + +/// Comment reflow settings. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Config { + /// Target total line width, including indentation and comment marker. + pub width: usize, + + /// Whether regular `//` comments should be reflowed in addition to rustdoc comments. + pub include_normal_comments: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum Prefix { + InnerDoc, + OuterDoc, + Normal, +} + +impl Prefix { + fn parse(comment: &str, include_normal_comments: bool) -> Option<(Self, &str)> { + if let Some(rest) = comment.strip_prefix("//!") { + return Some((Self::InnerDoc, rest)); + } + + // Four-slash comments are usually deliberate non-rustdoc comments and should not be + // normalized into `///`-style prose. + if comment.starts_with("////") { + return None; + } + + if let Some(rest) = comment.strip_prefix("///") { + return Some((Self::OuterDoc, rest)); + } + + if include_normal_comments { + return comment.strip_prefix("//").map(|rest| (Self::Normal, rest)); + } + + None + } + + const fn marker(self) -> &'static str { + match self { + Self::InnerDoc => "//!", + Self::OuterDoc => "///", + Self::Normal => "//", + } + } +} + +/// A full-line comment with enough source position data to replace its original line. +#[derive(Debug, Eq, PartialEq)] +struct CommentLine { + line_idx: usize, + line_start: usize, + line_end: usize, + indent: String, + prefix: Prefix, + content: String, +} + +/// A byte-range rewrite to be applied to the original source. +#[derive(Debug, Eq, PartialEq)] +struct Replacement { + range: Range, + text: String, +} + +/// Reflow all safe comment blocks in a Rust source string. +/// +/// The source is parsed with tree-sitter so that only syntactic line comments are considered. This +/// avoids touching comment-looking text inside strings, macros, or other token contexts where regex +/// matching would be unsafe. +pub fn reflow_source(source: &str, config: Config) -> Result { + let mut parser = Parser::new(); + let language = tree_sitter::Language::from(tree_sitter_rust::LANGUAGE); + parser.set_language(&language).context("loading tree-sitter Rust grammar")?; + + let tree = parser.parse(source, None).context("tree-sitter returned no parse tree")?; + let line_starts = line_starts(source); + let comment_lines = + comment_lines(source, tree.root_node(), &line_starts, config.include_normal_comments); + let replacements = replacements(&comment_lines, config.width); + + Ok(apply_replacements(source, &replacements)) +} + +/// Collect Rust source files from explicit files, directories, or the repository root. +/// +/// Directory traversal skips common generated or dependency directories so the default repository +/// run stays scoped to checked-in source. +pub fn rust_files(paths: &[PathBuf]) -> Result> { + let mut files = Vec::new(); + + if paths.is_empty() { + collect_rust_files(Path::new("."), &mut files)?; + } else { + for path in paths { + collect_rust_files(path, &mut files) + .with_context(|| format!("walking {}", path.display()))?; + } + } + + files.sort(); + files.dedup(); + Ok(files) +} + +fn collect_rust_files(path: &Path, files: &mut Vec) -> Result<()> { + let metadata = + fs_err::symlink_metadata(path).with_context(|| format!("reading {}", path.display()))?; + + if metadata.is_file() { + if path.extension() == Some(OsStr::new("rs")) { + files.push(path.to_path_buf()); + } + return Ok(()); + } + + if !metadata.is_dir() || should_skip_dir(path) { + return Ok(()); + } + + for entry in fs_err::read_dir(path)? { + collect_rust_files(&entry?.path(), files)?; + } + + Ok(()) +} + +fn should_skip_dir(path: &Path) -> bool { + path.file_name() + .and_then(OsStr::to_str) + .is_some_and(|name| matches!(name, ".git" | "target" | "node_modules")) +} + +fn line_starts(source: &str) -> Vec { + let mut starts = vec![0]; + + for (idx, byte) in source.bytes().enumerate() { + if byte == b'\n' { + starts.push(idx + 1); + } + } + + starts +} + +fn comment_lines( + source: &str, + root: Node<'_>, + line_starts: &[usize], + include_normal_comments: bool, +) -> Vec { + let mut ranges = Vec::new(); + collect_line_comment_ranges(root, &mut ranges); + ranges.sort_by_key(|range| range.start); + + ranges + .into_iter() + .filter_map(|range| comment_line(source, range, line_starts, include_normal_comments)) + .collect() +} + +fn collect_line_comment_ranges(node: Node<'_>, ranges: &mut Vec>) { + if node.kind() == "line_comment" { + ranges.push(node.byte_range()); + } + + let mut cursor = node.walk(); + for child in node.children(&mut cursor) { + collect_line_comment_ranges(child, ranges); + } +} + +fn comment_line( + source: &str, + range: Range, + line_starts: &[usize], + include_normal_comments: bool, +) -> Option { + let line_idx = line_starts.partition_point(|start| *start <= range.start) - 1; + let line_start = *line_starts.get(line_idx)?; + let line_end = line_end(source, line_idx, line_starts); + + // Reflow only full-line comments. Trailing comments are often attached to nearby code and can + // be semantically or stylistically different from prose blocks. + if !source[line_start..range.start].chars().all(char::is_whitespace) { + return None; + } + + let comment = &source[range.start..range.end.min(line_end)]; + let (prefix, content) = Prefix::parse(comment, include_normal_comments)?; + + Some(CommentLine { + line_idx, + line_start, + line_end, + indent: source[line_start..range.start].to_owned(), + prefix, + content: content.to_owned(), + }) +} + +fn line_end(source: &str, line_idx: usize, line_starts: &[usize]) -> usize { + let line_start = line_starts[line_idx]; + let next_line_start = line_starts.get(line_idx + 1).copied().unwrap_or(source.len()); + let mut end = next_line_start; + + if end > line_start && source.as_bytes()[end - 1] == b'\n' { + end -= 1; + } + + if end > line_start && source.as_bytes()[end - 1] == b'\r' { + end -= 1; + } + + end +} + +fn replacements(comment_lines: &[CommentLine], width: usize) -> Vec { + let mut replacements = Vec::new(); + let mut block_start = 0; + + while block_start < comment_lines.len() { + let mut block_end = block_start + 1; + + // Only adjacent lines with the same indentation and marker are joined into one paragraph. + // Blank lines, indentation changes, and prefix changes are treated as paragraph boundaries. + while block_end < comment_lines.len() + && same_block(&comment_lines[block_end - 1], &comment_lines[block_end]) + { + block_end += 1; + } + + if let Some(replacement) = reflow_block(&comment_lines[block_start..block_end], width) { + replacements.push(replacement); + } + + block_start = block_end; + } + + replacements +} + +fn same_block(previous: &CommentLine, current: &CommentLine) -> bool { + previous.line_idx + 1 == current.line_idx + && previous.indent == current.indent + && previous.prefix == current.prefix +} + +fn reflow_block(block: &[CommentLine], width: usize) -> Option { + let first = block.first()?; + let last = block.last()?; + let line_prefix = format!("{}{}", first.indent, first.prefix.marker()); + let text_width = width.checked_sub(line_prefix.chars().count() + 1)?; + + if text_width == 0 { + return None; + } + + let mut words = Vec::new(); + for line in block { + let text = safe_comment_text(&line.content)?; + for word in text.split_whitespace() { + // Do not split long words or URLs. If one word cannot fit, leave the block untouched. + if word.chars().count() > text_width { + return None; + } + words.push(word); + } + } + + if words.is_empty() { + return None; + } + + let text = wrap_words(&line_prefix, &words, text_width); + let range = first.line_start..last.line_end; + + Some(Replacement { range, text }) +} + +fn safe_comment_text(content: &str) -> Option<&str> { + let text = content.strip_prefix(' ').unwrap_or(content).trim_end(); + + if text.starts_with(char::is_whitespace) || has_intentional_spacing(text) { + return None; + } + + let trimmed = text.trim(); + if trimmed.is_empty() || trimmed.len() != text.len() { + return None; + } + + if is_markdown_sensitive(trimmed) { + return None; + } + + if is_repeated_separator(trimmed) { + return None; + } + + Some(trimmed) +} + +fn has_intentional_spacing(text: &str) -> bool { + // Multiple spaces and tabs often mean alignment, ASCII diagrams, or manually spaced examples. + text.contains('\t') || text.as_bytes().windows(2).any(|window| window == b" ") +} + +fn is_markdown_sensitive(trimmed: &str) -> bool { + // Markdown structures can change meaning when lines are merged or rewrapped. + trimmed.starts_with("```") + || trimmed.starts_with("~~~") + || trimmed.starts_with('#') + || trimmed.starts_with('>') + || is_list_item(trimmed) + || is_table_row(trimmed) + || is_url_only(trimmed) + || is_link_reference(trimmed) +} + +fn is_repeated_separator(trimmed: &str) -> bool { + let mut chars = trimmed.chars(); + let Some(separator) = chars.next() else { + return false; + }; + + // Section dividers like `// =====` and `// -----` are intentional layout, not prose. + trimmed.len() >= 3 && separator.is_ascii_punctuation() && chars.all(|char| char == separator) +} + +fn is_list_item(trimmed: &str) -> bool { + if trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ") { + return true; + } + + let marker_len = trimmed.bytes().take_while(u8::is_ascii_digit).count(); + marker_len > 0 + && trimmed[marker_len..] + .strip_prefix(['.', ')']) + .is_some_and(|rest| rest.starts_with(char::is_whitespace)) +} + +fn is_table_row(trimmed: &str) -> bool { + trimmed.starts_with('|') || trimmed.ends_with('|') || trimmed.contains(" | ") +} + +fn is_url_only(trimmed: &str) -> bool { + let without_brackets = trimmed + .strip_prefix('<') + .and_then(|value| value.strip_suffix('>')) + .unwrap_or(trimmed); + + (without_brackets.starts_with("http://") || without_brackets.starts_with("https://")) + && !without_brackets.chars().any(char::is_whitespace) +} + +fn is_link_reference(trimmed: &str) -> bool { + trimmed.starts_with('[') && trimmed.contains("]:") +} + +fn wrap_words(line_prefix: &str, words: &[&str], text_width: usize) -> String { + let mut lines = Vec::new(); + let mut current = String::new(); + + for word in words { + let word_len = word.chars().count(); + let current_len = current.chars().count(); + + if current.is_empty() { + current.push_str(word); + } else if current_len + 1 + word_len <= text_width { + current.push(' '); + current.push_str(word); + } else { + lines.push(format!("{line_prefix} {current}")); + current.clear(); + current.push_str(word); + } + } + + if !current.is_empty() { + lines.push(format!("{line_prefix} {current}")); + } + + lines.join("\n") +} + +fn apply_replacements(source: &str, replacements: &[Replacement]) -> String { + if replacements.is_empty() { + return source.to_owned(); + } + + let mut output = String::with_capacity(source.len()); + let mut cursor = 0; + + for replacement in replacements { + output.push_str(&source[cursor..replacement.range.start]); + output.push_str(&replacement.text); + cursor = replacement.range.end; + } + + output.push_str(&source[cursor..]); + output +} + +#[cfg(test)] +mod tests { + use super::{Config, reflow_source}; + + fn reflow(source: &str, width: usize) -> String { + reflow_source(source, Config { width, include_normal_comments: false }).unwrap() + } + + #[test] + fn reflows_outer_doc_comment_blocks() { + let source = r"/// This is a doc comment that should be wrapped into a few short lines by +/// the formatter. +fn main() {} +"; + + let expected = r"/// This is a doc comment that +/// should be wrapped into a +/// few short lines by the +/// formatter. +fn main() {} +"; + + assert_eq!(reflow(source, 30), expected); + } + + #[test] + fn does_not_reflow_trailing_comments() { + let source = r"fn main() { + let _value = 1; /// this trailing doc comment is ignored by design +} +"; + + assert_eq!(reflow(source, 40), source); + } + + #[test] + fn skips_markdown_lists() { + let source = r"/// - first item that should stay exactly where it is +/// - second item that should stay exactly where it is +fn main() {} +"; + + assert_eq!(reflow(source, 30), source); + } + + #[test] + fn can_reflow_regular_comments_when_enabled() { + let source = r"// A normal comment can also be wrapped when the caller explicitly asks for all comments. +fn main() {} +"; + + let reflowed = + reflow_source(source, Config { width: 45, include_normal_comments: true }).unwrap(); + + assert_eq!( + reflowed, + r"// A normal comment can also be wrapped when +// the caller explicitly asks for all +// comments. +fn main() {} +" + ); + } + + #[test] + fn skips_repeated_separator_comments() { + let source = r"// ================================================================================================ +// SECTION HEADER +// ================================================================================================ +fn main() {} +"; + + let reflowed = + reflow_source(source, Config { width: 60, include_normal_comments: true }).unwrap(); + + assert_eq!(reflowed, source); + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000000..4495a4e911 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,122 @@ +mod comment_reflow; + +use std::io::ErrorKind; +use std::path::PathBuf; + +use anyhow::{Context, Result, bail, ensure}; +use clap::{Args, Parser, Subcommand}; + +#[derive(Debug, Parser)] +#[command(about = "Repository maintenance tasks", name = "xtask")] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Reflow safe Rust line-comment blocks. + FmtComments(FmtCommentsArgs), +} + +#[derive(Debug, Args)] +struct FmtCommentsArgs { + /// Rewrite files in place. + #[arg(long, conflicts_with = "check")] + write: bool, + + /// Check whether files are already reflowed. + #[arg(long)] + check: bool, + + /// Reflow only `///` and `//!` rustdoc comments. + #[arg(long)] + doc_comments_only: bool, + + /// Target total line width. Defaults to rustfmt.toml's `comment_width`, or 100. + #[arg(long)] + width: Option, + + /// Files or directories to process. Defaults to the repository tree. + paths: Vec, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.command { + Command::FmtComments(args) => run_fmt_comments(&args), + } +} + +fn run_fmt_comments(args: &FmtCommentsArgs) -> Result<()> { + let width = comment_width(args.width)?; + let paths = comment_reflow::rust_files(&args.paths).context("collecting Rust files")?; + let config = comment_reflow::Config { + width, + include_normal_comments: !args.doc_comments_only, + }; + + let mut changed = Vec::new(); + + for path in paths { + let source = + fs_err::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?; + let reflowed = comment_reflow::reflow_source(&source, config) + .with_context(|| format!("reflowing {}", path.display()))?; + + if reflowed != source { + if args.write { + fs_err::write(&path, reflowed) + .with_context(|| format!("writing {}", path.display()))?; + } + changed.push(path); + } + } + + if changed.is_empty() { + return Ok(()); + } + + if args.write { + eprintln!("reflowed comments in {} file(s)", changed.len()); + return Ok(()); + } + + for path in &changed { + eprintln!("comments need reflow: {}", path.display()); + } + + let mode = if args.check { "--check" } else { "default check" }; + bail!( + "{} file(s) need comment reflow ({mode}); run `cargo xtask fmt-comments --write`", + changed.len() + ); +} + +fn comment_width(explicit: Option) -> Result { + if let Some(width) = explicit { + return validate_width(width); + } + + let source = match fs_err::read_to_string("rustfmt.toml") { + Ok(source) => source, + Err(err) if err.kind() == ErrorKind::NotFound => return Ok(100), + Err(err) => return Err(err).context("reading rustfmt.toml"), + }; + + let config = + toml::from_str::(&source).context("parsing rustfmt.toml for comment_width")?; + let width = config + .get("comment_width") + .and_then(toml::Value::as_integer) + .and_then(|width| usize::try_from(width).ok()) + .unwrap_or(100); + + validate_width(width) +} + +fn validate_width(width: usize) -> Result { + ensure!(width >= 20, "comment width must be at least 20 columns"); + Ok(width) +} From 871cec91f7d88e69480cc49d156eedefd0795550 Mon Sep 17 00:00:00 2001 From: Mirko <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Wed, 20 May 2026 10:47:49 +0200 Subject: [PATCH 04/10] feat: SQLite migration framework (#2093) --- Cargo.lock | 41 +++ Cargo.toml | 2 + bin/network-monitor/Cargo.toml | 2 +- crates/db/Cargo.toml | 6 + crates/db/src/lib.rs | 1 + crates/db/src/migration/build_script.rs | 445 ++++++++++++++++++++++++ crates/db/src/migration/builder.rs | 145 ++++++++ crates/db/src/migration/entry.rs | 115 ++++++ crates/db/src/migration/migrator.rs | 377 ++++++++++++++++++++ crates/db/src/migration/mod.rs | 28 ++ crates/db/src/migration/schema.rs | 231 ++++++++++++ 11 files changed, 1392 insertions(+), 1 deletion(-) create mode 100644 crates/db/src/migration/build_script.rs create mode 100644 crates/db/src/migration/builder.rs create mode 100644 crates/db/src/migration/entry.rs create mode 100644 crates/db/src/migration/migrator.rs create mode 100644 crates/db/src/migration/mod.rs create mode 100644 crates/db/src/migration/schema.rs diff --git a/Cargo.lock b/Cargo.lock index 76f46af9d1..e6818092a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1681,6 +1681,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.4.1" @@ -2077,6 +2089,15 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -3277,11 +3298,17 @@ dependencies = [ name = "miden-node-db" version = "0.15.0" dependencies = [ + "anyhow", + "build-rs", + "codegen", "deadpool", "deadpool-diesel", "deadpool-sync", "diesel", + "fs-err", "miden-protocol", + "rusqlite", + "sha2", "thiserror 2.0.18", "tracing", ] @@ -5348,6 +5375,20 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.27" diff --git a/Cargo.toml b/Cargo.toml index 89b2b89b8a..91c51bc386 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,8 +108,10 @@ rand_chacha = { default-features = false, version = "0.9" } rayon = { version = "1.10" } reqwest = { version = "0.13" } rstest = { version = "0.26" } +rusqlite = { features = ["bundled"], version = "0.37" } serde = { features = ["derive"], version = "1" } serial_test = { version = "3.2" } +sha2 = { version = "0.10" } syn = { version = "2.0" } tempfile = { version = "3.12" } thiserror = { default-features = false, version = "2.0" } diff --git a/bin/network-monitor/Cargo.toml b/bin/network-monitor/Cargo.toml index 7889ac9bad..45944f0c4e 100644 --- a/bin/network-monitor/Cargo.toml +++ b/bin/network-monitor/Cargo.toml @@ -32,7 +32,7 @@ rand_chacha = { workspace = true } reqwest = { features = ["json", "query"], workspace = true } serde = { workspace = true } serde_json = { version = "1.0" } -sha2 = { version = "0.10" } +sha2 = { workspace = true } time = { features = ["formatting", "macros"], version = "0.3" } tokio = { features = ["full"], workspace = true } tonic = { features = ["codegen", "tls-native-roots", "transport"], workspace = true } diff --git a/crates/db/Cargo.toml b/crates/db/Cargo.toml index 2a42af4305..70ed47fc1b 100644 --- a/crates/db/Cargo.toml +++ b/crates/db/Cargo.toml @@ -14,10 +14,16 @@ version.workspace = true workspace = true [dependencies] +anyhow = { workspace = true } +build-rs = { workspace = true } +codegen = { workspace = true } deadpool = { default-features = false, workspace = true } deadpool-diesel = { features = ["sqlite"], workspace = true } deadpool-sync = { default-features = false, workspace = true } diesel = { features = ["sqlite"], workspace = true } +fs-err = { workspace = true } miden-protocol = { workspace = true } +rusqlite = { workspace = true } +sha2 = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } diff --git a/crates/db/src/lib.rs b/crates/db/src/lib.rs index f57e0cb553..d1a3b81b37 100644 --- a/crates/db/src/lib.rs +++ b/crates/db/src/lib.rs @@ -1,6 +1,7 @@ mod conv; mod errors; mod manager; +pub mod migration; use std::num::NonZeroUsize; use std::path::Path; diff --git a/crates/db/src/migration/build_script.rs b/crates/db/src/migration/build_script.rs new file mode 100644 index 0000000000..8ac01f5a39 --- /dev/null +++ b/crates/db/src/migration/build_script.rs @@ -0,0 +1,445 @@ +use std::collections::HashSet; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +use anyhow::{Context, Result, ensure}; +use codegen::{Function, Scope}; +use fs_err as fs; + +use super::Migrator; + +pub const GENERATED_MIGRATOR_FILE: &str = "db_migrator.rs"; +pub const CODE_MIGRATION_FILE: &str = "migration.rs"; + +impl Migrator { + /// Generates Rust source for a migrator from a migration directory. + /// + /// Call this from a `build.rs`, then include the generated file in the crate: + /// + /// ```ignore + /// // build.rs + /// fn main() -> Result<(), Box> { + /// miden_node_db::migration::Migrator::generate("migrations")?; + /// Ok(()) + /// } + /// + /// // src/lib.rs + /// include!(concat!(env!("OUT_DIR"), "/db_migrator.rs")); + /// + /// #[cfg(test)] + /// mod tests { + /// use miden_node_db::migration::SchemaHash; + /// + /// const EXPECTED_SCHEMA_HASHES: [SchemaHash; 3] = [ + /// SchemaHash::from_hex( + /// "1111111111111111111111111111111111111111111111111111111111111111", + /// ), + /// SchemaHash::from_hex( + /// "2222222222222222222222222222222222222222222222222222222222222222", + /// ), + /// SchemaHash::from_hex( + /// "3333333333333333333333333333333333333333333333333333333333333333", + /// ), + /// ]; + /// + /// #[test] + /// fn migration_schema_hashes_are_stable() -> anyhow::Result<()> { + /// let migrator = super::migrator()?; + /// + /// assert_eq!(migrator.schema_hashes(), &EXPECTED_SCHEMA_HASHES); + /// Ok(()) + /// } + /// } + /// ``` + /// + /// The expected layout is: + /// + /// ```text + /// migrations/ + /// retired/ + /// 001_initial.sql + /// 002_indexes.sql + /// code/ + /// 003_backfill/ + /// migration.rs + /// ``` + /// + /// Retired migrations are loaded from lexicographically sorted `.sql` files in `retired`; + /// the migration name is the file stem. Code migrations are loaded from lexicographically + /// sorted folders in `code`; the migration name is the folder name. Each code folder must + /// contain `migration.rs` and that file must expose a `pub fn migrate(...)` matching + /// [`super::CodeMigrationFn`]. + /// + /// The `retired` directory contains SQL retained for fresh database initialization after the + /// corresponding code migrations no longer need to be supported. Relative migration paths are + /// resolved from the package manifest directory, i.e. the crate root. + pub fn generate(migration_dir: impl AsRef) -> Result { + let migration_dir = migration_dir_path(migration_dir.as_ref()); + build_rs::output::rerun_if_changed(&migration_dir); + + let out_path = build_rs::input::out_dir().join(GENERATED_MIGRATOR_FILE); + let migrations = discover_migrations(&migration_dir)?; + fs::write( + &out_path, + render_migrator(&migrations.retired_migrations, &migrations.code_migrations)?, + ) + .with_context(|| format!("failed to write generated migrator to {}", out_path.display()))?; + Ok(out_path) + } +} + +fn migration_dir_path(migration_dir: &Path) -> PathBuf { + if migration_dir.is_absolute() { + migration_dir.to_path_buf() + } else { + build_rs::input::cargo_manifest_dir().join(migration_dir) + } +} + +#[derive(Debug)] +struct DiscoveredMigrations { + retired_migrations: Vec, + code_migrations: Vec, +} + +#[derive(Debug)] +struct SqlMigration { + name: String, + path: PathBuf, +} + +#[derive(Debug)] +struct CodeMigration { + name: String, + module_ident: String, + path: PathBuf, +} + +fn discover_migrations(migration_dir: &Path) -> Result { + ensure!( + migration_dir.is_dir(), + "migration path is not a directory: {}", + migration_dir.display() + ); + + let retired_migrations = discover_retired_migrations(migration_dir)?; + let code_migrations = discover_code_migrations(migration_dir)?; + ensure!( + !retired_migrations.is_empty() || !code_migrations.is_empty(), + "migration directory contains no migrations: {}", + migration_dir.display() + ); + + Ok(DiscoveredMigrations { retired_migrations, code_migrations }) +} + +fn discover_retired_migrations(migration_dir: &Path) -> Result> { + let retired_dir = migration_dir.join("retired"); + if !retired_dir.exists() { + return Ok(Vec::new()); + } + + ensure!( + retired_dir.is_dir(), + "retired migration path is not a directory: {}", + retired_dir.display() + ); + + let mut migrations = Vec::new(); + for entry in read_dir_sorted(&retired_dir)? { + let path = entry.path(); + ensure!(path.is_file(), "retired migration entry is not a file: {}", path.display()); + ensure!( + path.extension() == Some(OsStr::new("sql")), + "retired migration file must use .sql extension: {}", + path.display() + ); + + migrations.push(SqlMigration { + name: file_stem(&path)?, + path: absolute_path(&path)?, + }); + } + + Ok(migrations) +} + +fn discover_code_migrations(migration_dir: &Path) -> Result> { + let code_dir = migration_dir.join("code"); + if !code_dir.exists() { + return Ok(Vec::new()); + } + + ensure!( + code_dir.is_dir(), + "code migration path is not a directory: {}", + code_dir.display() + ); + + // Folder names are converted into Rust module identifiers lossy, e.g. `001-backfill` and + // `001_backfill` both become `migration_001_backfill`. To prevent this, we track seen + // identifiers and reject any collisions. + let mut seen_idents = HashSet::new(); + let mut migrations = Vec::new(); + for entry in read_dir_sorted(&code_dir)? { + let path = entry.path(); + ensure!(path.is_dir(), "code migration entry is not a directory: {}", path.display()); + + let name = file_name(&path)?; + let module_ident = module_ident(&name)?; + ensure!( + seen_idents.insert(module_ident.clone()), + "code migration module identifier collision for migration {name:?}" + ); + + let migration_rs = path.join(CODE_MIGRATION_FILE); + ensure!( + migration_rs.is_file(), + "code migration {} is missing {}", + path.display(), + CODE_MIGRATION_FILE + ); + + migrations.push(CodeMigration { + name, + module_ident, + path: absolute_path(&migration_rs)?, + }); + } + + Ok(migrations) +} + +/// Renders the Rust source written by [`Migrator::generate`]. +/// +/// For one retired migration named `001_initial` and one code migration named `002_backfill`, +/// the generated file has this shape: +/// +/// ```ignore +/// #[path = "/path/to/migrations/code/002_backfill/migration.rs"] +/// mod migration_002_backfill; +/// +/// pub fn migrator() -> ::anyhow::Result<::miden_node_db::migration::Migrator> { +/// ::miden_node_db::migration::Migrator::builder()? +/// .push_retired("001_initial", include_str!("/path/to/migrations/retired/001_initial.sql"))? +/// .push_code("002_backfill", migration_002_backfill::migrate)? +/// .build() +/// } +/// ``` +fn render_migrator( + retired_migrations: &[SqlMigration], + code_migrations: &[CodeMigration], +) -> Result { + let mut scope = Scope::new(); + + for migration in code_migrations { + let path = format!("{:?}", rust_path(&migration.path)?); + scope.raw(format!("#[path = {path}]\nmod {};", migration.module_ident)); + } + + let mut function = Function::new("migrator"); + function.vis("pub"); + function.ret("::anyhow::Result<::miden_node_db::migration::Migrator>"); + function.line("::miden_node_db::migration::Migrator::builder()?"); + + for migration in retired_migrations { + let name = format!("{:?}", migration.name); + let path = format!("{:?}", rust_path(&migration.path)?); + function.line(format!(" .push_retired({name}, include_str!({path}))?")); + } + + for migration in code_migrations { + let name = format!("{:?}", migration.name); + function.line(format!(" .push_code({name}, {}::migrate)?", migration.module_ident)); + } + + function.line(" .build()"); + scope.push_fn(function); + + let mut source = scope.to_string(); + source.push('\n'); + Ok(source) +} + +fn read_dir_sorted(dir: &Path) -> Result> { + let mut entries = fs::read_dir(dir) + .with_context(|| format!("failed to read migration directory {}", dir.display()))? + .collect::, _>>() + .with_context(|| { + format!("failed to read migration directory entry in {}", dir.display()) + })?; + entries.sort_by_key(fs::DirEntry::file_name); + Ok(entries) +} + +fn absolute_path(path: &Path) -> Result { + fs::canonicalize(path) + .with_context(|| format!("failed to canonicalize migration path {}", path.display())) +} + +fn file_name(path: &Path) -> Result { + path.file_name() + .and_then(OsStr::to_str) + .map(str::to_owned) + .with_context(|| format!("migration path has invalid UTF-8 name: {}", path.display())) +} + +fn file_stem(path: &Path) -> Result { + path.file_stem().and_then(OsStr::to_str).map(str::to_owned).with_context(|| { + format!("migration file has invalid UTF-8 stem or no stem: {}", path.display()) + }) +} + +/// Converts a migration folder name into a Rust module identifier. +/// +/// The generated identifier is prefixed with `migration_`, ASCII alphanumeric characters are +/// lowercased, and every other character is replaced with `_`. For example, +/// `001--Backfill-Accounts` becomes `migration_001__backfill_accounts`. +fn module_ident(name: &str) -> Result { + ensure!( + name.chars().any(|ch| ch.is_ascii_alphanumeric()), + "migration name {name:?} cannot be converted to a Rust module identifier" + ); + + let ident = name + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() { + ch.to_ascii_lowercase() + } else { + '_' + } + }) + .collect::(); + + Ok(format!("migration_{ident}")) +} + +fn rust_path(path: &Path) -> Result<&str> { + path.to_str() + .with_context(|| format!("migration path is not valid UTF-8: {}", path.display())) +} + +#[cfg(test)] +mod tests { + use std::env; + + use super::*; + + #[test] + fn renders_migrations_in_lexicographic_order() -> Result<()> { + let root = unique_temp_dir("renders_migrations_in_lexicographic_order")?; + fs::create_dir_all(root.join("retired"))?; + fs::create_dir_all(root.join("code").join("003_backfill"))?; + fs::write(root.join("retired").join("002_indexes.sql"), "CREATE INDEX idx ON t(id);")?; + fs::write(root.join("retired").join("001_init.sql"), "CREATE TABLE t (id INTEGER);")?; + fs::write( + root.join("code").join("003_backfill").join(CODE_MIGRATION_FILE), + "pub fn migrate(_: &rusqlite::Transaction<'_>) -> anyhow::Result<()> { Ok(()) }", + )?; + + let retired = discover_retired_migrations(&root)?; + let code = discover_code_migrations(&root)?; + let rendered = render_migrator(&retired, &code)?; + + let init = rendered.find("\"001_init\"").expect("init migration is rendered"); + let indexes = rendered.find("\"002_indexes\"").expect("index migration is rendered"); + let backfill = rendered.find("\"003_backfill\"").expect("code migration is rendered"); + + assert!(init < indexes); + assert!(indexes < backfill); + assert!(rendered.contains("include_str!(")); + assert!(rendered.contains(".push_retired(")); + assert!(!rendered.contains(".push_base(")); + assert!(rendered.contains("migration_003_backfill::migrate")); + assert!(rendered.contains(".build()\n}\n")); + assert!(!rendered.contains("Ok(migrator)")); + + fs::remove_dir_all(root)?; + Ok(()) + } + + #[test] + fn rejects_empty_migration_directory() -> Result<()> { + let root = unique_temp_dir("rejects_empty_migration_directory")?; + + let err = discover_migrations(&root).expect_err("empty migration directory should fail"); + + assert!(err.to_string().contains("contains no migrations")); + fs::remove_dir_all(root)?; + Ok(()) + } + + #[test] + fn rejects_invalid_retired_migration_entries() -> Result<()> { + let root = unique_temp_dir("rejects_invalid_retired_migration_entries")?; + fs::create_dir_all(root.join("retired"))?; + fs::write(root.join("retired").join("001_init.txt"), "CREATE TABLE t (id INTEGER);")?; + + let err = + discover_retired_migrations(&root).expect_err("invalid retired entry should fail"); + + assert!(err.to_string().contains("must use .sql extension")); + fs::remove_dir_all(root)?; + Ok(()) + } + + #[test] + fn rejects_code_migration_missing_rust_file() -> Result<()> { + let root = unique_temp_dir("rejects_code_migration_missing_rust_file")?; + fs::create_dir_all(root.join("code").join("001_backfill"))?; + + let err = discover_code_migrations(&root).expect_err("missing migration.rs should fail"); + + assert!(err.to_string().contains("is missing migration.rs")); + fs::remove_dir_all(root)?; + Ok(()) + } + + #[test] + fn rejects_code_migration_module_identifier_collisions() -> Result<()> { + let root = unique_temp_dir("rejects_code_migration_module_identifier_collisions")?; + fs::create_dir_all(root.join("code").join("001-backfill"))?; + fs::create_dir_all(root.join("code").join("001_backfill"))?; + fs::write( + root.join("code").join("001-backfill").join(CODE_MIGRATION_FILE), + "pub fn migrate(_: &rusqlite::Transaction<'_>) -> anyhow::Result<()> { Ok(()) }", + )?; + fs::write( + root.join("code").join("001_backfill").join(CODE_MIGRATION_FILE), + "pub fn migrate(_: &rusqlite::Transaction<'_>) -> anyhow::Result<()> { Ok(()) }", + )?; + + let err = discover_code_migrations(&root).expect_err("module collision should fail"); + + assert!(err.to_string().contains("module identifier collision")); + fs::remove_dir_all(root)?; + Ok(()) + } + + #[test] + fn module_ident_preserves_repeated_separators() -> Result<()> { + assert_eq!(module_ident("001--backfill")?, "migration_001__backfill"); + Ok(()) + } + + #[test] + fn migration_dir_path_resolves_relative_paths_from_manifest_dir() { + assert_eq!( + migration_dir_path(Path::new("migrations")), + build_rs::input::cargo_manifest_dir().join("migrations") + ); + + let absolute = env::temp_dir().join("miden-node-db-absolute-migrations"); + assert_eq!(migration_dir_path(&absolute), absolute); + } + + fn unique_temp_dir(name: &str) -> Result { + let dir = env::temp_dir().join(format!("miden-node-db-{name}-{}", std::process::id())); + if dir.exists() { + fs::remove_dir_all(&dir)?; + } + fs::create_dir_all(&dir)?; + Ok(dir) + } +} diff --git a/crates/db/src/migration/builder.rs b/crates/db/src/migration/builder.rs new file mode 100644 index 0000000000..d4afe66a81 --- /dev/null +++ b/crates/db/src/migration/builder.rs @@ -0,0 +1,145 @@ +use anyhow::{Context, Result}; +use rusqlite::Connection; + +use super::entry::{CodeMigration, CodeMigrationFn, SqlMigration, apply_migration}; +use super::{Migrator, SchemaHash}; + +/// Builds a [`Migrator`] while computing expected schema hashes on an in-memory database. +/// +/// ```ignore +/// use miden_node_db::migration::{Migrator, SchemaHash}; +/// use rusqlite::Transaction; +/// +/// fn add_item_height(tx: &Transaction<'_>) -> anyhow::Result<()> { +/// tx.execute_batch("ALTER TABLE items ADD COLUMN height INTEGER;")?; +/// Ok(()) +/// } +/// +/// fn migrator() -> anyhow::Result { +/// Migrator::builder()? +/// .push_retired( +/// "001_create_items", +/// "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);", +/// )? +/// .push_code("002_add_item_height", add_item_height)? +/// .build() +/// } +/// +/// const EXPECTED_SCHEMA_HASHES: [SchemaHash; 2] = [ +/// SchemaHash::from_hex( +/// "1111111111111111111111111111111111111111111111111111111111111111", +/// ), +/// SchemaHash::from_hex( +/// "2222222222222222222222222222222222222222222222222222222222222222", +/// ), +/// ]; +/// +/// #[test] +/// fn migration_schema_hashes_are_stable() -> anyhow::Result<()> { +/// let migrator = migrator()?; +/// +/// assert_eq!(migrator.schema_hashes(), &EXPECTED_SCHEMA_HASHES); +/// Ok(()) +/// } +/// ``` +pub struct MigratorBuilder { + /// Connection to an in-memory SQLite database used to verify the migrations as they are added. + reference: Connection, + /// Migrator being built. + migrator: Migrator, +} + +impl MigratorBuilder { + pub(super) fn new() -> Result { + let reference = Connection::open_in_memory() + .context("failed to create in-memory migration database")?; + + Ok(Self { reference, migrator: Migrator::empty() }) + } + + /// Adds a pure SQL retired migration. + /// + /// Retired migrations initialize fresh databases from SQL that replaces old code migrations. + /// They must be pushed before any code migration. + pub fn push_retired(mut self, name: &'static str, sql: &'static str) -> Result { + let version = self.migrator.next_version(); + let migration = SqlMigration::new(name, sql); + let hash: SchemaHash = apply_migration(&mut self.reference, version, &migration) + .with_context(|| format!("failed to apply retired migration {version} \"{name}\""))?; + + self.migrator.push_retired_unchecked(migration, hash); + Ok(self) + } + + /// Adds a Rust migration function. + pub fn push_code(mut self, name: &'static str, apply: CodeMigrationFn) -> Result { + let version = self.migrator.next_version(); + let migration = CodeMigration::new(name, apply); + let hash: SchemaHash = apply_migration(&mut self.reference, version, &migration) + .with_context(|| format!("failed to apply code migration {version} \"{name}\""))?; + + self.migrator.push_code_unchecked(migration, hash); + Ok(self) + } + + /// Returns a migrator containing all migrations and their expected schema hashes. + pub fn build(self) -> Result { + self.migrator.validate()?; + Ok(self.migrator) + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use rusqlite::{Connection, Transaction}; + + use super::super::{Migrator, SchemaHash}; + + fn add_item_height(tx: &Transaction<'_>) -> Result<()> { + tx.execute_batch("ALTER TABLE items ADD COLUMN height INTEGER;")?; + Ok(()) + } + + fn create_items_table(tx: &Transaction<'_>) -> Result<()> { + tx.execute_batch("CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);")?; + Ok(()) + } + + #[test] + fn empty_builder_returns_error() -> Result<()> { + let err = Migrator::builder()?.build().expect_err("empty builder should fail"); + assert!(err.to_string().contains("cannot build migrator without migrations")); + Ok(()) + } + + #[test] + #[should_panic(expected = "cannot add retired migration after code migrations have started")] + fn panics_when_adding_retired_after_code() { + let _builder = Migrator::builder() + .expect("builder should be created") + .push_code("create items", create_items_table) + .expect("code migration should be added") + .push_retired("add notes", "CREATE TABLE notes (id INTEGER PRIMARY KEY);"); + } + + #[test] + fn exposes_schema_hashes() -> Result<()> { + let reference = Connection::open_in_memory()?; + reference.execute_batch("CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);")?; + let retired_hash = SchemaHash::new(&reference)?; + reference.execute_batch("ALTER TABLE items ADD COLUMN height INTEGER;")?; + let final_hash = SchemaHash::new(&reference)?; + + let migrator = Migrator::builder()? + .push_retired( + "create items", + "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);", + )? + .push_code("add item height", add_item_height)? + .build()?; + + assert_eq!(migrator.schema_hashes(), &[retired_hash, final_hash]); + Ok(()) + } +} diff --git a/crates/db/src/migration/entry.rs b/crates/db/src/migration/entry.rs new file mode 100644 index 0000000000..7e0206c071 --- /dev/null +++ b/crates/db/src/migration/entry.rs @@ -0,0 +1,115 @@ +use std::fmt; + +use anyhow::{Context, Result, ensure}; +use rusqlite::{Connection, Transaction}; + +use super::schema::{self, SchemaHash}; + +/// A migration entry that can be executed inside a SQLite transaction. +pub(super) trait MigrationEntry { + /// Returns the migration name used in diagnostics. + fn name(&self) -> &'static str; + + /// Executes the migration body inside `tx`. + fn execute_migration(&self, tx: &Transaction<'_>) -> Result<()>; +} + +/// A pure SQL migration. +pub(super) struct SqlMigration { + name: &'static str, + sql: &'static str, +} + +impl SqlMigration { + pub(super) fn new(name: &'static str, sql: &'static str) -> Self { + Self { name, sql } + } +} + +impl MigrationEntry for SqlMigration { + fn name(&self) -> &'static str { + self.name + } + + fn execute_migration(&self, tx: &Transaction<'_>) -> Result<()> { + tx.execute_batch(self.sql).map_err(Into::into) + } +} + +impl fmt::Debug for SqlMigration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SqlMigration").field("name", &self.name).finish_non_exhaustive() + } +} + +/// A Rust migration function. +pub(super) struct CodeMigration { + name: &'static str, + apply: CodeMigrationFn, +} + +impl CodeMigration { + pub(super) fn new(name: &'static str, apply: CodeMigrationFn) -> Self { + Self { name, apply } + } +} + +impl MigrationEntry for CodeMigration { + fn name(&self) -> &'static str { + self.name + } + + fn execute_migration(&self, tx: &Transaction<'_>) -> Result<()> { + (self.apply)(tx) + } +} + +impl fmt::Debug for CodeMigration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CodeMigration") + .field("name", &self.name) + .finish_non_exhaustive() + } +} + +/// Applies `migration`, sets `user_version`, commits, and returns the resulting schema hash. +pub(super) fn apply_migration( + conn: &mut Connection, + version: usize, + migration: &impl MigrationEntry, +) -> Result { + apply_migration_transaction(conn, version, migration, Ok::) +} + +/// Applies `migration`, verifies the resulting schema hash, sets `user_version`, and commits. +pub(super) fn apply_migration_and_verify_schema( + conn: &mut Connection, + version: usize, + migration: &impl MigrationEntry, + expected: SchemaHash, +) -> Result<()> { + apply_migration_transaction(conn, version, migration, |actual| { + ensure!(actual == expected, "schema hash mismatch: expected {expected}, got {actual}"); + Ok(()) + }) +} + +fn apply_migration_transaction( + conn: &mut Connection, + version: usize, + migration: &impl MigrationEntry, + verify_hash: impl FnOnce(SchemaHash) -> Result, +) -> Result { + let tx = conn.transaction().context("failed to start transaction")?; + + migration.execute_migration(&tx).context("failed to execute migration")?; + let schema_hash = SchemaHash::new(&tx).context("failed to compute schema hash")?; + let result = verify_hash(schema_hash)?; + schema::set_version(&tx, version).context("failed to update user_version")?; + tx.commit().context("failed to commit transaction")?; + + Ok(result) +} + +/// A Rust migration function executed inside a SQLite transaction. +pub type CodeMigrationFn = for<'conn> fn(&Transaction<'conn>) -> Result<()>; diff --git a/crates/db/src/migration/migrator.rs b/crates/db/src/migration/migrator.rs new file mode 100644 index 0000000000..13e691fe06 --- /dev/null +++ b/crates/db/src/migration/migrator.rs @@ -0,0 +1,377 @@ +use anyhow::{Context, Result, bail, ensure}; +use rusqlite::Connection; + +use super::entry::{ + CodeMigration, + MigrationEntry, + SqlMigration, + apply_migration_and_verify_schema, +}; +use super::{MigratorBuilder, SchemaHash, schema}; + +/// Applies versioned database migrations. +/// +/// A migrator is built from two ordered migration sets: retired SQL migrations followed by code +/// migrations. Retired migrations are pure SQL snapshots of older migrations whose schema we +/// retain, but whose Rust migration code we no longer want to support. Because that old migration +/// path is intentionally unsupported, retired migrations are only applied when creating a new +/// database whose `PRAGMA user_version` is zero. Existing databases are never allowed to run only +/// part of the retired SQL set; once a database has a non-zero version, it must already be at or +/// beyond the end of the retired migrations. +/// +/// Code migrations run after the retired SQL set. For existing databases, the migrator reads +/// `user_version`, verifies that the current schema hash matches the expected hash for that +/// version, and then applies only the missing code migrations. Each migration runs in its own +/// transaction and commits only after the resulting schema hash matches the hash computed by the +/// builder. +/// +/// Construct a migrator with [`Migrator::builder`] by pushing retired migrations first and code +/// migrations second, or call [`Migrator::generate`] from a `build.rs` to generate that builder +/// chain from a migration directory. Callers should snapshot [`Migrator::schema_hashes`] in tests +/// so accidental schema changes are caught, especially when replacing a code migration with +/// equivalent retired SQL. +#[derive(Debug)] +pub struct Migrator { + retired_migrations: Vec, + code_migrations: Vec, + expected_schema_hashes: Vec, +} + +impl Migrator { + /// Creates an empty migrator used as the builder's backing storage. + /// + /// Empty migrators are not exposed to callers; [`MigratorBuilder::build`] validates that at + /// least one migration was pushed before returning a migrator. + pub(super) fn empty() -> Self { + Self { + retired_migrations: Vec::new(), + code_migrations: Vec::new(), + expected_schema_hashes: Vec::new(), + } + } + + /// Creates a migration builder backed by an in-memory SQLite database. + pub fn builder() -> Result { + MigratorBuilder::new() + } + + /// Returns the version number that will be assigned to the next migration. + /// + /// Versions are one-based and follow insertion order across retired migrations first and then + /// code migrations. + pub(super) fn next_version(&self) -> usize { + self.expected_schema_hashes.len() + 1 + } + + /// Adds a retired SQL migration and the schema hash expected after it runs. + /// + /// This is used by [`MigratorBuilder`] after it has already applied the migration to its + /// in-memory reference database. The caller must ensure `schema_hash` is the hash of that + /// reference database after this migration. Retired migrations must be added before any code + /// migration. + pub(super) fn push_retired_unchecked( + &mut self, + migration: SqlMigration, + schema_hash: SchemaHash, + ) { + assert!( + self.code_migrations.is_empty(), + "cannot add retired migration after code migrations have started" + ); + self.retired_migrations.push(migration); + self.expected_schema_hashes.push(schema_hash); + } + + /// Adds a code migration and the schema hash expected after it runs. + /// + /// This is used by [`MigratorBuilder`] after it has already applied the migration to its + /// in-memory reference database. The caller must ensure `schema_hash` is the hash of that + /// reference database after this migration. + pub(super) fn push_code_unchecked( + &mut self, + migration: CodeMigration, + schema_hash: SchemaHash, + ) { + self.code_migrations.push(migration); + self.expected_schema_hashes.push(schema_hash); + } + + /// Validates invariants that must hold before a migrator can be returned to callers. + /// + /// A migrator must contain at least one migration and must have exactly one expected schema + /// hash for each migration. + pub(super) fn validate(&self) -> Result<()> { + let migration_count = self.retired_migrations.len() + self.code_migrations.len(); + ensure!( + !self.expected_schema_hashes.is_empty(), + "cannot build migrator without migrations" + ); + ensure!( + self.expected_schema_hashes.len() == migration_count, + "migrator schema hash count {} must match migration count {migration_count}", + self.expected_schema_hashes.len() + ); + Ok(()) + } + + /// Returns the schema hashes expected after each migration. + /// + /// Callers can use these hashes in tests when retiring a code migration into SQL: the + /// replacement SQL should produce the same hash at the same migration index. + pub fn schema_hashes(&self) -> &[SchemaHash] { + &self.expected_schema_hashes + } + + /// Applies missing migrations to `conn`. + /// + /// New databases, where `PRAGMA user_version` is zero, receive all retired migrations followed + /// by all code migrations. Existing databases must already be past the retired migration range; + /// only missing code migrations are applied. Every migration runs in its own transaction, + /// updates `user_version`, and commits only after the resulting schema hash matches the + /// expected hash. + pub fn migrate(&self, conn: &mut Connection) -> Result<()> { + let current_version = self.version_check(conn)?; + let retired_versions = self.retired_migrations.len(); + + let mut applied_version = current_version; + if applied_version == 0 { + for (idx, migration) in self.retired_migrations.iter().enumerate() { + let version = idx + 1; + self.apply_migration(conn, version, migration)?; + applied_version = version; + } + } + + let code_start = applied_version.saturating_sub(retired_versions); + for (idx, migration) in self.code_migrations.iter().enumerate().skip(code_start) { + let version = retired_versions + idx + 1; + self.apply_migration(conn, version, migration)?; + } + + Ok(()) + } + + /// Reads and validates the database version before any missing migrations are applied. + /// + /// This rejects databases newer than the migrator, databases inside the retired migration + /// range, and databases whose current schema hash does not match the expected hash for their + /// current version. + fn version_check(&self, conn: &Connection) -> Result { + let current_version = + schema::get_version(conn).context("failed to read database version")?; + let total_versions = self.expected_schema_hashes.len(); + + ensure!( + current_version <= total_versions, + "database version {current_version} is newer than migrator version {total_versions}" + ); + + let retired_versions = self.retired_migrations.len(); + if current_version > 0 && current_version < retired_versions { + let name = self.migration_name(current_version).unwrap_or(""); + bail!( + "database version {current_version} \"{name}\" is inside the retired migration \ + range; retired migrations can only initialize new databases" + ); + } + + if current_version > 0 { + self.verify_current_schema(conn, current_version)?; + } + + Ok(current_version) + } + + /// Applies one migration transaction and verifies its resulting schema hash. + fn apply_migration( + &self, + conn: &mut Connection, + version: usize, + migration: &impl MigrationEntry, + ) -> Result<()> { + let name = migration.name(); + let expected = self.expected_schema_hashes[version - 1]; + apply_migration_and_verify_schema(conn, version, migration, expected) + .with_context(|| format!("failed to apply migration {version} \"{name}\"")) + } + + /// Verifies that an existing database still matches the schema hash for its `user_version`. + fn verify_current_schema(&self, conn: &Connection, version: usize) -> Result<()> { + let name = self.migration_name(version).unwrap_or(""); + let expected = self.expected_schema_hashes[version - 1]; + let actual = SchemaHash::new(conn).with_context(|| { + format!("failed to compute schema hash at database version {version} \"{name}\"") + })?; + + ensure!( + actual == expected, + "schema hash mismatch at database version {version} \"{name}\": expected {expected}, \ + got {actual}" + ); + Ok(()) + } + + /// Returns the migration name for a one-based migration version. + fn migration_name(&self, version: usize) -> Option<&'static str> { + if version == 0 { + return None; + } + + if version <= self.retired_migrations.len() { + return Some(self.retired_migrations[version - 1].name()); + } + + self.code_migrations + .get(version - self.retired_migrations.len() - 1) + .map(MigrationEntry::name) + } +} + +#[cfg(test)] +mod tests { + use anyhow::Result; + use rusqlite::{Connection, Transaction}; + + use super::super::{Migrator, schema}; + + fn add_items_index(tx: &Transaction<'_>) -> Result<()> { + tx.execute_batch("CREATE INDEX idx_items_value ON items(value);")?; + Ok(()) + } + + fn add_item_height(tx: &Transaction<'_>) -> Result<()> { + tx.execute_batch("ALTER TABLE items ADD COLUMN height INTEGER;")?; + Ok(()) + } + + fn create_extra_table_when_items_exist(tx: &Transaction<'_>) -> Result<()> { + let item_count: i64 = tx.query_row("SELECT COUNT(*) FROM items", [], |row| row.get(0))?; + if item_count > 0 { + tx.execute_batch("CREATE TABLE unexpected (id INTEGER PRIMARY KEY);")?; + } + Ok(()) + } + + fn create_items_table(tx: &Transaction<'_>) -> Result<()> { + tx.execute_batch("CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);")?; + Ok(()) + } + + fn object_exists(conn: &Connection, name: &str) -> Result { + let exists = conn.query_row( + "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE name = ?1)", + [name], + |row| row.get::<_, bool>(0), + )?; + Ok(exists) + } + + #[test] + fn migrates_new_database_through_retired_and_code() -> Result<()> { + let migrator = Migrator::builder()? + .push_retired( + "create items", + "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);", + )? + .push_code("add item height", add_item_height)? + .build()?; + + let mut conn = Connection::open_in_memory()?; + migrator.migrate(&mut conn)?; + + assert_eq!(schema::get_version(&conn)?, 2); + conn.execute("INSERT INTO items (id, value, height) VALUES (1, 'a', 10)", [])?; + Ok(()) + } + + #[test] + fn migrates_new_database_with_code_only_migration() -> Result<()> { + let migrator = + Migrator::builder()?.push_code("create items", create_items_table)?.build()?; + + let mut conn = Connection::open_in_memory()?; + migrator.migrate(&mut conn)?; + + assert_eq!(schema::get_version(&conn)?, 1); + conn.execute("INSERT INTO items (id, value) VALUES (1, 'a')", [])?; + Ok(()) + } + + #[test] + fn applies_missing_code_migrations_to_existing_database() -> Result<()> { + let migrator = Migrator::builder()? + .push_retired( + "create items", + "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);", + )? + .push_code("index item values", add_items_index)? + .build()?; + + let mut conn = Connection::open_in_memory()?; + conn.execute_batch( + "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT); + PRAGMA user_version = 1;", + )?; + + migrator.migrate(&mut conn)?; + + assert_eq!(schema::get_version(&conn)?, 2); + assert!(object_exists(&conn, "idx_items_value")?); + Ok(()) + } + + #[test] + fn rejects_existing_database_inside_retired_migration_range() -> Result<()> { + let migrator = Migrator::builder()? + .push_retired("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")? + .push_retired("create notes", "CREATE TABLE notes (id INTEGER PRIMARY KEY);")? + .build()?; + + let mut conn = Connection::open_in_memory()?; + conn.execute_batch( + "CREATE TABLE items (id INTEGER PRIMARY KEY); + PRAGMA user_version = 1;", + )?; + + let err = migrator.migrate(&mut conn).expect_err("migration should fail"); + assert!(err.to_string().contains("inside the retired migration range")); + Ok(()) + } + + #[test] + fn verifies_current_schema_before_applying_missing_migrations() -> Result<()> { + let migrator = Migrator::builder()? + .push_retired("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")? + .build()?; + + let mut conn = Connection::open_in_memory()?; + migrator.migrate(&mut conn)?; + conn.execute_batch("CREATE TABLE tampered (id INTEGER PRIMARY KEY);")?; + + let err = migrator.migrate(&mut conn).expect_err("migration should fail"); + assert!(err.to_string().contains("schema hash mismatch at database version 1")); + Ok(()) + } + + #[test] + fn rolls_back_code_migration_when_schema_hash_mismatches() -> Result<()> { + let migrator = Migrator::builder()? + .push_retired("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")? + .push_code("conditionally create extra", create_extra_table_when_items_exist)? + .build()?; + + let mut conn = Connection::open_in_memory()?; + conn.execute_batch( + "CREATE TABLE items (id INTEGER PRIMARY KEY); + INSERT INTO items (id) VALUES (1); + PRAGMA user_version = 1;", + )?; + + let err = migrator.migrate(&mut conn).expect_err("migration should fail"); + assert!(err.to_string().contains("failed to apply migration 2")); + assert!(err.chain().any(|cause| cause.to_string().contains("schema hash mismatch"))); + assert_eq!(schema::get_version(&conn)?, 1); + assert!(!object_exists(&conn, "unexpected")?); + Ok(()) + } +} diff --git a/crates/db/src/migration/mod.rs b/crates/db/src/migration/mod.rs new file mode 100644 index 0000000000..b41dc0d17c --- /dev/null +++ b/crates/db/src/migration/mod.rs @@ -0,0 +1,28 @@ +//! Provides a framework for SQLite migrations. +//! +//! Migrations are built as an ordered [`Migrator`] with two phases. Retired migrations are retained +//! as pure SQL and are only used to initialize fresh databases. Code migrations are Rust functions +//! that run after the retired SQL set and remain supported for existing databases. This lets old +//! code migrations eventually be converted into retired SQL once their upgrade path no longer needs +//! to be supported. +//! +//! The database version is stored in SQLite's `PRAGMA user_version`. Each migration also has an +//! expected [`SchemaHash`] computed by applying migrations to an in-memory reference database +//! during builder construction. Runtime migration commits only after the resulting schema hash +//! matches the expected hash. +//! +//! Build migrators manually with [`Migrator::builder`], or generate one from a migration directory +//! with [`Migrator::generate`] in a `build.rs`. Callers should snapshot [`Migrator::schema_hashes`] +//! in tests to catch accidental schema drift and to prove that retired SQL still produces the same +//! schema as the code migration it replaced. + +mod build_script; +mod builder; +mod entry; +mod migrator; +mod schema; + +pub use builder::MigratorBuilder; +pub use entry::CodeMigrationFn; +pub use migrator::Migrator; +pub use schema::SchemaHash; diff --git a/crates/db/src/migration/schema.rs b/crates/db/src/migration/schema.rs new file mode 100644 index 0000000000..dba736b8b6 --- /dev/null +++ b/crates/db/src/migration/schema.rs @@ -0,0 +1,231 @@ +use std::fmt; + +use anyhow::{Context, Result, ensure}; +use rusqlite::Connection; +use sha2::{Digest, Sha256}; + +/// A schema fingerprint computed from ordered entries in `sqlite_schema`. +/// +/// The hash includes each non-internal schema object's type, name, table name, and normalized SQL. +/// Normalization trims trailing semicolons and collapses whitespace, then entries are ordered by +/// object type, name, and table name before hashing. This makes the hash stable across object +/// creation order while still detecting changes to tables, indexes, views, triggers, constraints, +/// and object names. +/// +/// This is a drift-detection fingerprint, not a semantic SQLite schema model. It does not parse SQL +/// or understand equivalent SQL forms. For example, two semantically equivalent declarations can +/// hash differently if SQLite stores their SQL text differently, while behavior not represented in +/// `sqlite_schema.sql` is outside the hash. SQLite-internal objects whose names start with +/// `sqlite_` are intentionally ignored. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct SchemaHash([u8; 32]); + +impl SchemaHash { + /// Parses a schema hash from its hex representation. + /// + /// Expects exactly 64 hex characters and panics otherwise. + pub const fn from_hex(hex: &str) -> Self { + assert!(hex.len() == 64, "schema hash must be 64 hex characters"); + + let mut hash = [0_u8; 32]; + let bytes = hex.as_bytes(); + let mut idx = 0; + while idx < 32 { + let high = hex_digit(bytes[idx * 2]); + let low = hex_digit(bytes[idx * 2 + 1]); + hash[idx] = (high << 4) | low; + idx += 1; + } + + Self(hash) + } + + /// Computes the hash for the database schema. + /// + /// See [`SchemaHash`] for what is included and the limits of this fingerprint. + pub fn new(conn: &Connection) -> Result { + let mut stmt = conn + .prepare( + "SELECT type, name, tbl_name, sql FROM sqlite_schema \ + WHERE sql IS NOT NULL \ + AND name NOT LIKE 'sqlite_%' \ + ORDER BY type, name, tbl_name", + ) + .context("failed to prepare sqlite_schema query")?; + + let rows = stmt + .query_map([], |row| { + Ok(SchemaEntry { + object_type: row.get(0)?, + name: row.get(1)?, + table_name: row.get(2)?, + sql: normalize_sql(&row.get::<_, String>(3)?), + }) + }) + .context("failed to query sqlite_schema rows")?; + + let schema_entries = rows + .collect::>>() + .context("failed to read sqlite_schema rows")?; + + let mut hasher = Sha256::new(); + hash_field(&mut hasher, "schema-hash-v1"); + for entry in schema_entries { + hash_field(&mut hasher, &entry.object_type); + hash_field(&mut hasher, &entry.name); + hash_field(&mut hasher, &entry.table_name); + hash_field(&mut hasher, &entry.sql); + } + + let digest = hasher.finalize(); + let mut hash = [0_u8; 32]; + hash.copy_from_slice(&digest); + Ok(Self(hash)) + } +} + +struct SchemaEntry { + object_type: String, + name: String, + table_name: String, + sql: String, +} + +impl fmt::Display for SchemaHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.0 { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +fn normalize_sql(sql: &str) -> String { + sql.trim_end() + .trim_end_matches(';') + .split_whitespace() + .collect::>() + .join(" ") +} + +fn hash_field(hasher: &mut Sha256, field: &str) { + hasher.update(field.len().to_le_bytes()); + hasher.update(field.as_bytes()); +} + +const fn hex_digit(byte: u8) -> u8 { + match byte { + b'0'..=b'9' => byte - b'0', + b'a'..=b'f' => byte - b'a' + 10, + b'A'..=b'F' => byte - b'A' + 10, + _ => panic!("invalid schema hash hex digit"), + } +} + +pub fn get_version(conn: &Connection) -> Result { + let version: i64 = conn.query_row("PRAGMA user_version", [], |row| row.get(0))?; + ensure!(version >= 0, "database user_version is negative: {version}"); + usize::try_from(version).context("database user_version does not fit into usize") +} + +pub fn set_version(conn: &Connection, version: usize) -> Result<()> { + let version = version_to_user_version(version)?; + conn.execute_batch(&format!("PRAGMA user_version = {version};"))?; + Ok(()) +} + +fn version_to_user_version(version: usize) -> Result { + i32::try_from(version).with_context(|| { + format!("migration version {version} exceeds SQLite user_version i32 range") + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn schema_hash_round_trips_as_hex() { + const HASH: SchemaHash = SchemaHash::from_hex( + "abababababababababababababababababababababababababababababababab", + ); + + assert_eq!(HASH.to_string(), "ab".repeat(32)); + } + + #[test] + fn schema_hash_is_stable_across_creation_order() -> Result<()> { + let left = Connection::open_in_memory()?; + left.execute_batch( + "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT); + CREATE TABLE notes (id INTEGER PRIMARY KEY, item_id INTEGER); + CREATE INDEX idx_notes_item_id ON notes(item_id);", + )?; + + let right = Connection::open_in_memory()?; + right.execute_batch( + "CREATE TABLE notes (id INTEGER PRIMARY KEY, item_id INTEGER); + CREATE INDEX idx_notes_item_id ON notes(item_id); + CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);", + )?; + + assert_eq!(SchemaHash::new(&left)?, SchemaHash::new(&right)?); + Ok(()) + } + + #[test] + fn schema_hash_changes_for_object_identity() -> Result<()> { + let left = Connection::open_in_memory()?; + left.execute_batch("CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);")?; + + let right = Connection::open_in_memory()?; + right.execute_batch("CREATE TABLE entries (id INTEGER PRIMARY KEY, value TEXT);")?; + + assert_ne!(SchemaHash::new(&left)?, SchemaHash::new(&right)?); + Ok(()) + } + + #[test] + fn schema_hash_changes_for_views_triggers_indexes_and_constraints() -> Result<()> { + let base = Connection::open_in_memory()?; + base.execute_batch("CREATE TABLE items (id INTEGER PRIMARY KEY, value INTEGER);")?; + let base_hash = SchemaHash::new(&base)?; + + let with_index = Connection::open_in_memory()?; + with_index.execute_batch( + "CREATE TABLE items (id INTEGER PRIMARY KEY, value INTEGER); + CREATE INDEX idx_items_value ON items(value);", + )?; + assert_ne!(base_hash, SchemaHash::new(&with_index)?); + + let with_view = Connection::open_in_memory()?; + with_view.execute_batch( + "CREATE TABLE items (id INTEGER PRIMARY KEY, value INTEGER); + CREATE VIEW item_values AS SELECT value FROM items;", + )?; + assert_ne!(base_hash, SchemaHash::new(&with_view)?); + + let with_trigger = Connection::open_in_memory()?; + with_trigger.execute_batch( + "CREATE TABLE items (id INTEGER PRIMARY KEY, value INTEGER); + CREATE TRIGGER items_positive_value + BEFORE INSERT ON items + WHEN NEW.value < 0 + BEGIN + SELECT RAISE(ABORT, 'negative value'); + END;", + )?; + assert_ne!(base_hash, SchemaHash::new(&with_trigger)?); + + let with_constraints = Connection::open_in_memory()?; + with_constraints.execute_batch( + "CREATE TABLE items ( + id INTEGER PRIMARY KEY, + value INTEGER UNIQUE CHECK (value > 0) + );", + )?; + assert_ne!(base_hash, SchemaHash::new(&with_constraints)?); + + Ok(()) + } +} From f85d060cb9d72527e6068f9ebf480022d3da70c0 Mon Sep 17 00:00:00 2001 From: KOVACS Krisztian Date: Thu, 21 May 2026 07:22:19 +0200 Subject: [PATCH 05/10] fix(tests): don't clean up RocksDB directory before store shutdown (#2107) --- crates/block-producer/src/server/tests.rs | 52 +++-- crates/rpc/src/tests.rs | 273 +++++++++++----------- 2 files changed, 169 insertions(+), 156 deletions(-) diff --git a/crates/block-producer/src/server/tests.rs b/crates/block-producer/src/server/tests.rs index 9360d047bd..46559429db 100644 --- a/crates/block-producer/src/server/tests.rs +++ b/crates/block-producer/src/server/tests.rs @@ -5,7 +5,6 @@ use miden_node_proto::generated::block_producer::api_client as block_producer_cl use miden_node_store::{DEFAULT_MAX_CONCURRENT_PROOFS, GenesisState, Store, StoreMode}; use miden_node_utils::clap::{GrpcOptionsInternal, StorageOptions}; use miden_node_utils::fee::test_fee_params; -use miden_node_utils::spawn::spawn_blocking_in_current_span; use miden_protocol::testing::random_secret_key::random_secret_key; use miden_validator::{Validator, ValidatorSigner}; use tokio::net::TcpListener; @@ -16,6 +15,26 @@ use url::Url; use crate::{BlockProducer, DEFAULT_MAX_BATCHES_PER_BLOCK, DEFAULT_MAX_TXS_PER_BATCH}; +/// A wrapper around the store runtime and data directory. +/// +/// Guarantees that the store runtime is shut down _before_ the data directory is dropped and thus removed. +struct TestStore { + runtime: Option, + _data_directory: tempfile::TempDir, +} + +impl Drop for TestStore { + fn drop(&mut self) { + if let Some(runtime) = self.runtime.take() { + std::thread::spawn(move || { + runtime.shutdown_timeout(Duration::from_millis(500)); + }) + .join() + .expect("store runtime shutdown thread should complete"); + } + } +} + /// Tests that the block producer starts up correctly even when the store is not initially /// available. The block producer should retry with exponential backoff until the store becomes /// available, then start serving requests. @@ -94,8 +113,7 @@ async fn block_producer_startup_is_robust_to_network_failures() { ); // start the store - let data_directory = tempfile::tempdir().expect("tempdir should be created"); - let store_runtime = start_store(store_addr, data_directory.path()).await; + let _store = start_store(store_addr).await; // wait for the block producer's exponential backoff to connect to the store use a retry loop // since CI environments may be slower @@ -122,25 +140,20 @@ async fn block_producer_startup_is_robust_to_network_failures() { // verify the response contains expected data let status = response.unwrap().into_inner(); assert_eq!(status.status, "connected"); - - // Shutdown the store before data_directory is dropped to allow the database to flush properly - shutdown_store(store_runtime).await; } /// Starts the store with a fresh genesis state and returns the runtime handle. -async fn start_store( - store_addr: std::net::SocketAddr, - data_directory: &std::path::Path, -) -> runtime::Runtime { +async fn start_store(store_addr: std::net::SocketAddr) -> TestStore { + let data_directory = tempfile::tempdir().expect("tempdir should be created"); let signer = random_secret_key(); let genesis_state = GenesisState::new(vec![], test_fee_params(), 1, 1, signer.public_key()); let genesis_block = genesis_state .clone() .into_block(&signer) .expect("genesis block should be created"); - Store::bootstrap(genesis_block, data_directory).expect("store should bootstrap"); + Store::bootstrap(genesis_block, data_directory.path()).expect("store should bootstrap"); - let dir = data_directory.to_path_buf(); + let dir = data_directory.path().to_path_buf(); let rpc_listener = TcpListener::bind("127.0.0.1:0").await.expect("store should bind the RPC port"); let ntx_builder_listener = TcpListener::bind("127.0.0.1:0") @@ -171,17 +184,10 @@ async fn start_store( .await .expect("store should start serving"); }); - store_runtime -} - -/// Shuts down the store runtime properly to allow the database to flush before the temp directory -/// is deleted. -async fn shutdown_store(store_runtime: runtime::Runtime) { - spawn_blocking_in_current_span(move || { - store_runtime.shutdown_timeout(Duration::from_millis(500)); - }) - .await - .expect("shutdown should complete"); + TestStore { + runtime: Some(store_runtime), + _data_directory: data_directory, + } } /// Sends a status request to the block producer to verify connectivity. diff --git a/crates/rpc/src/tests.rs b/crates/rpc/src/tests.rs index 362980f81b..cf47b5d356 100644 --- a/crates/rpc/src/tests.rs +++ b/crates/rpc/src/tests.rs @@ -45,6 +45,125 @@ use url::Url; use crate::Rpc; +/// A wrapper around the store runtime and data directory. +/// +/// Guarantees that the store runtime is shut down _before_ the data directory is dropped and thus removed. +struct TestStore { + runtime: Option, + data_directory: Option, + genesis_commitment: Word, + store_addr: SocketAddr, +} + +impl TestStore { + fn genesis_commitment(&self) -> Word { + self.genesis_commitment + } + + fn store_addr(&self) -> SocketAddr { + self.store_addr + } + + async fn shutdown(mut self) -> TempDir { + if let Some(runtime) = self.runtime.take() { + shutdown_store_runtime(runtime).await; + } + self.data_directory.take().expect("data_directory should be set") + } + + async fn start(store_listener: TcpListener) -> Self { + let data_directory = tempfile::tempdir().expect("tempdir should be created"); + let genesis_commitment = Self::bootstrap(data_directory.path()); + Self::start_without_bootstrap(data_directory, genesis_commitment, store_listener).await + } + + fn bootstrap(path: &std::path::Path) -> Word { + let config = GenesisConfig::default(); + let signer = SecretKey::new(); + let (genesis_state, _) = config.into_state(signer.public_key()).unwrap(); + let genesis_block = genesis_state + .clone() + .into_block(&signer) + .expect("genesis block should be created"); + let genesis_commitment = genesis_block.inner().header().commitment(); + + Store::bootstrap(genesis_block, path).expect("store should bootstrap"); + + genesis_commitment + } + + async fn start_without_bootstrap( + data_directory: TempDir, + genesis_commitment: Word, + store_listener: TcpListener, + ) -> Self { + let dir = data_directory.path().to_path_buf(); + let store_addr = + store_listener.local_addr().expect("store listener should get a local address"); + let rpc_listener = store_listener; + let ntx_builder_listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind store ntx-builder gRPC endpoint"); + let block_producer_listener = + TcpListener::bind("127.0.0.1:0").await.expect("store should bind a port"); + + // In order to later kill the store, we need to spawn a new runtime and run the store on it. + // That allows us to kill all the tasks spawned by the store when we kill the runtime. + let store_runtime = + runtime::Builder::new_multi_thread().enable_time().enable_io().build().unwrap(); + store_runtime.spawn(async move { + Store { + rpc_listener, + mode: StoreMode::BlockProducer { + block_producer_listener, + ntx_builder_listener, + block_prover_url: None, + max_concurrent_proofs: DEFAULT_MAX_CONCURRENT_PROOFS, + }, + data_directory: dir, + database_options: miden_node_store::DatabaseOptions::default(), + grpc_options: GrpcOptionsInternal::test(), + storage_options: StorageOptions::default(), + } + .serve() + .await + .expect("store should start serving"); + }); + + Self { + runtime: Some(store_runtime), + data_directory: Some(data_directory), + genesis_commitment, + store_addr, + } + } +} + +impl Drop for TestStore { + fn drop(&mut self) { + if let Some(runtime) = self.runtime.take() { + shutdown_store_runtime_blocking(runtime); + } + } +} + +/// Shuts down the store runtime properly to allow `RocksDB` to flush before the temp directory is +/// deleted. +async fn shutdown_store_runtime(store_runtime: Runtime) { + spawn_blocking_in_current_span(move || shutdown_store_runtime_blocking(store_runtime)) + .await + .expect("shutdown should complete"); +} + +fn shutdown_store_runtime_blocking(store_runtime: Runtime) { + std::thread::spawn(move || { + store_runtime.shutdown_timeout(Duration::from_secs(3)); + std::thread::sleep(Duration::from_millis(200)); + }) + .join() + .expect("store runtime shutdown thread should complete"); +} + /// Byte offset of the account delta commitment in serialized `ProvenTransaction`. Layout: /// `AccountId` (15) + `initial_commitment` (32) + `final_commitment` (32) = 79 const DELTA_COMMITMENT_BYTE_OFFSET: usize = 15 + 32 + 32; @@ -107,7 +226,7 @@ fn build_test_proven_tx( async fn rpc_server_accepts_requests_without_accept_header() { // Start the RPC. let (_, rpc_addr, store_listener) = start_rpc().await; - let (store_runtime, _data_directory, _genesis, _store_addr) = start_store(store_listener).await; + let _store = TestStore::start(store_listener).await; // Override the client so that the ACCEPT header is not set. let mut rpc_client = { @@ -125,9 +244,6 @@ async fn rpc_server_accepts_requests_without_accept_header() { // Assert that the server did not reject our request. assert!(response.is_ok()); - - // Shutdown to avoid runtime drop error. - shutdown_store(store_runtime).await; } #[tokio::test] @@ -138,7 +254,7 @@ async fn rpc_rate_limits_per_ip() { ..GrpcOptionsExternal::test() }; let (_, rpc_addr, store_listener) = start_rpc_with_options(grpc_options).await; - let (store_runtime, data_directory, _genesis, _store_addr) = start_store(store_listener).await; + let _store = TestStore::start(store_listener).await; let url = rpc_addr.to_string(); let url = Url::parse(format!("http://{}", &url).as_str()).unwrap(); @@ -159,25 +275,19 @@ async fn rpc_rate_limits_per_ip() { last_error.is_some_and(|code| code == tonic::Code::ResourceExhausted), "expected rate limit error but got: {last_error:?}" ); - - shutdown_store(store_runtime).await; - drop(data_directory); } #[tokio::test] async fn rpc_server_accepts_requests_with_accept_header() { // Start the RPC. let (mut rpc_client, _, store_listener) = start_rpc().await; - let (store_runtime, _data_directory, _genesis, _store_addr) = start_store(store_listener).await; + let _store = TestStore::start(store_listener).await; // Send any request to the RPC. let response = send_request(&mut rpc_client).await; // Assert the server does not reject our request on the basis of missing accept header. assert!(response.is_ok()); - - // Shutdown to avoid runtime drop error. - shutdown_store(store_runtime).await; } #[tokio::test] @@ -185,8 +295,7 @@ async fn rpc_server_rejects_requests_with_accept_header_invalid_version() { for version in ["1.9.0", "0.8.1", "0.8.0", "0.999.0", "99.0.0"] { // Start the RPC. let (_, rpc_addr, store_listener) = start_rpc().await; - let (store_runtime, _data_directory, _genesis, _store_addr) = - start_store(store_listener).await; + let _store = TestStore::start(store_listener).await; // Recreate the RPC client with an invalid version. let url = rpc_addr.to_string(); @@ -209,9 +318,6 @@ async fn rpc_server_rejects_requests_with_accept_header_invalid_version() { assert!(response.is_err()); assert_eq!(response.as_ref().err().unwrap().code(), tonic::Code::InvalidArgument); assert!(response.as_ref().err().unwrap().message().contains("server does not support"),); - - // Shutdown to avoid runtime drop error. - shutdown_store(store_runtime).await; } } @@ -228,31 +334,33 @@ async fn rpc_startup_is_robust_to_network_failures() { assert!(response.is_err()); // Start the store. - let (store_runtime, data_directory, _genesis, store_addr) = start_store(store_listener).await; + let store = TestStore::start(store_listener).await; // Test: send request against RPC api and should succeed let response = send_request_until_success(&mut rpc_client).await; assert!(response.unwrap().into_inner().block_header.is_some()); // Test: shutdown the store and should fail - shutdown_store(store_runtime).await; + let store_addr = store.store_addr(); + let genesis_commitment = store.genesis_commitment(); + let data_directory = store.shutdown().await; let response = send_request(&mut rpc_client).await; assert!(response.is_err()); // Test: restart the store and request should succeed - let store_runtime = restart_store(store_addr, data_directory.path()).await; + let store_listener = TcpListener::bind(store_addr).await.expect("Failed to bind store"); + let _store = + TestStore::start_without_bootstrap(data_directory, genesis_commitment, store_listener) + .await; let response = send_request_until_success(&mut rpc_client).await; assert_eq!(response.unwrap().into_inner().block_header.unwrap().block_num, 0); - - // Shutdown the store before data_directory is dropped to allow RocksDB to flush properly - shutdown_store(store_runtime).await; } #[tokio::test] async fn rpc_server_has_web_support() { // Start server let (_, rpc_addr, store_listener) = start_rpc().await; - let (store_runtime, _data_directory, _genesis, _store_addr) = start_store(store_listener).await; + let _store = TestStore::start(store_listener).await; // Send a status request let client = reqwest::Client::new(); @@ -288,14 +396,14 @@ async fn rpc_server_has_web_support() { assert!(headers.get("access-control-allow-credentials").is_some()); assert!(headers.get("access-control-expose-headers").is_some()); assert!(headers.get("vary").is_some()); - shutdown_store(store_runtime).await; } #[tokio::test] async fn rpc_server_rejects_proven_transactions_with_invalid_commitment() { // Start the RPC. let (_, rpc_addr, store_listener) = start_rpc().await; - let (store_runtime, _data_directory, genesis, _store_addr) = start_store(store_listener).await; + let store = TestStore::start(store_listener).await; + let genesis = store.genesis_commitment(); // Wait for the store to be ready before sending requests. tokio::time::sleep(Duration::from_millis(100)).await; @@ -340,16 +448,14 @@ async fn rpc_server_rejects_proven_transactions_with_invalid_commitment() { err.contains("failed to validate account delta in transaction account update"), "expected error message to contain delta commitment error but got: {err}" ); - - // Shutdown to avoid runtime drop error. - shutdown_store(store_runtime).await; } #[tokio::test] async fn rpc_server_rejects_proven_transactions_with_invalid_reference_block() { // Start the RPC. let (_, rpc_addr, store_listener) = start_rpc().await; - let (store_runtime, _data_directory, genesis, _store_addr) = start_store(store_listener).await; + let store = TestStore::start(store_listener).await; + let genesis = store.genesis_commitment(); // Wait for the store to be ready before sending requests. tokio::time::sleep(Duration::from_millis(100)).await; @@ -385,16 +491,14 @@ async fn rpc_server_rejects_proven_transactions_with_invalid_reference_block() { err.contains("does not match the chain's commitment of"), "expected error message to contain reference block error but got: {err}" ); - - // Shutdown to avoid runtime drop error. - shutdown_store(store_runtime).await; } #[tokio::test] async fn rpc_server_rejects_tx_submissions_without_genesis() { // Start the RPC. let (_, rpc_addr, store_listener) = start_rpc().await; - let (store_runtime, _data_directory, genesis, _store_addr) = start_store(store_listener).await; + let store = TestStore::start(store_listener).await; + let genesis = store.genesis_commitment(); // Override the client so that the ACCEPT header is not set. let mut rpc_client = @@ -427,9 +531,6 @@ async fn rpc_server_rejects_tx_submissions_without_genesis() { ), "expected error message to reference incompatible content media types but got: {err:?}" ); - - // Shutdown to avoid runtime drop error. - shutdown_store(store_runtime).await; } /// Sends an arbitrary / irrelevant request to the RPC. @@ -522,100 +623,11 @@ async fn start_rpc_with_options( (rpc_client, rpc_addr, store_listener) } -async fn start_store(store_listener: TcpListener) -> (Runtime, TempDir, Word, SocketAddr) { - // Start the store. - let data_directory = tempfile::tempdir().expect("tempdir should be created"); - - let config = GenesisConfig::default(); - let signer = SecretKey::new(); - let (genesis_state, _) = config.into_state(signer.public_key()).unwrap(); - let genesis_block = genesis_state - .clone() - .into_block(&signer) - .expect("genesis block should be created"); - let genesis_commitment = genesis_block.inner().header().commitment(); - Store::bootstrap(genesis_block, data_directory.path()).expect("store should bootstrap"); - let dir = data_directory.path().to_path_buf(); - let store_addr = - store_listener.local_addr().expect("store listener should get a local address"); - let rpc_listener = store_listener; - let ntx_builder_listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("Failed to bind store ntx-builder gRPC endpoint"); - let block_producer_listener = - TcpListener::bind("127.0.0.1:0").await.expect("store should bind a port"); - // In order to later kill the store, we need to spawn a new runtime and run the store on it. - // That allows us to kill all the tasks spawned by the store when we kill the runtime. - let store_runtime = - runtime::Builder::new_multi_thread().enable_time().enable_io().build().unwrap(); - store_runtime.spawn(async move { - Store { - rpc_listener, - mode: StoreMode::BlockProducer { - block_producer_listener, - ntx_builder_listener, - block_prover_url: None, - max_concurrent_proofs: DEFAULT_MAX_CONCURRENT_PROOFS, - }, - data_directory: dir, - database_options: miden_node_store::DatabaseOptions::default(), - grpc_options: GrpcOptionsInternal::test(), - storage_options: StorageOptions::default(), - } - .serve() - .await - .expect("store should start serving"); - }); - (store_runtime, data_directory, genesis_commitment, store_addr) -} - -/// Shuts down the store runtime properly to allow `RocksDB` to flush before the temp directory is -/// deleted. -async fn shutdown_store(store_runtime: Runtime) { - spawn_blocking_in_current_span(move || store_runtime.shutdown_timeout(Duration::from_secs(3))) - .await - .expect("shutdown should complete"); - // Give RocksDB time to release its lock file after the runtime shutdown - tokio::time::sleep(Duration::from_millis(200)).await; -} - -/// Restarts a store using an existing data directory. Returns the runtime handle for shutdown. -async fn restart_store(store_addr: SocketAddr, data_directory: &std::path::Path) -> Runtime { - let rpc_listener = TcpListener::bind(store_addr).await.expect("Failed to bind store"); - let ntx_builder_listener = TcpListener::bind("127.0.0.1:0") - .await - .expect("Failed to bind store ntx-builder gRPC endpoint"); - let block_producer_listener = - TcpListener::bind("127.0.0.1:0").await.expect("store should bind a port"); - let dir = data_directory.to_path_buf(); - let store_runtime = - runtime::Builder::new_multi_thread().enable_time().enable_io().build().unwrap(); - store_runtime.spawn(async move { - Store { - rpc_listener, - mode: StoreMode::BlockProducer { - block_producer_listener, - ntx_builder_listener, - block_prover_url: None, - max_concurrent_proofs: DEFAULT_MAX_CONCURRENT_PROOFS, - }, - data_directory: dir, - database_options: miden_node_store::DatabaseOptions::default(), - grpc_options: GrpcOptionsInternal::test(), - storage_options: StorageOptions::default(), - } - .serve() - .await - .expect("store should start serving"); - }); - store_runtime -} - #[tokio::test] async fn get_limits_endpoint() { // Start the RPC and store let (mut rpc_client, _rpc_addr, store_listener) = start_rpc().await; - let (store_runtime, _data_directory, _genesis, _store_addr) = start_store(store_listener).await; + let _store = TestStore::start(store_listener).await; // Call the get_limits endpoint let response = rpc_client.get_limits(()).await.expect("get_limits should succeed"); @@ -675,15 +687,12 @@ async fn get_limits_endpoint() { QueryParamNoteIdLimit::PARAM_NAME, QueryParamNoteIdLimit::LIMIT ); - - // Shutdown to avoid runtime drop error. - shutdown_store(store_runtime).await; } #[tokio::test] async fn sync_chain_mmr_returns_delta() { let (mut rpc_client, _rpc_addr, store_listener) = start_rpc().await; - let (store_runtime, _data_directory, _genesis, _store_addr) = start_store(store_listener).await; + let _store = TestStore::start(store_listener).await; let request = proto::rpc::SyncChainMmrRequest { current_client_block_height: 0, @@ -695,8 +704,6 @@ async fn sync_chain_mmr_returns_delta() { let mmr_delta = response.mmr_delta.expect("mmr_delta should exist"); assert_eq!(mmr_delta.forest, 0); assert!(mmr_delta.data.is_empty()); - - shutdown_store(store_runtime).await; } #[test] From f31cb55f1f7f7a21b0a3a5eda3ea5959ae481f3c Mon Sep 17 00:00:00 2001 From: Mirko <48352201+Mirko-von-Leipzig@users.noreply.github.com> Date: Thu, 21 May 2026 09:38:24 +0200 Subject: [PATCH 06/10] feat: port `diesel` migrations to our own framework (#2104) --- .github/workflows/ci.yml | 24 ++ Cargo.lock | 83 +----- Cargo.toml | 1 - bin/ntx-builder/Cargo.toml | 4 +- bin/ntx-builder/build.rs | 8 +- bin/ntx-builder/src/db/migrations.rs | 82 +++-- .../up.sql => 001_initial.sql} | 8 +- .../migrations/2026020900000_setup/down.sql | 1 - bin/ntx-builder/src/db/mod.rs | 12 +- bin/ntx-builder/src/db/schema_hash.rs | 191 ------------ bin/validator/Cargo.toml | 4 +- bin/validator/build.rs | 9 +- bin/validator/src/db/migrations.rs | 78 ++++- .../up.sql => 001_initial.sql} | 6 +- .../migrations/2025062000000_setup/down.sql | 0 bin/validator/src/db/mod.rs | 4 +- bin/validator/src/db/schema.rs | 20 +- crates/db/src/errors.rs | 9 + crates/db/src/migration/build_script.rs | 282 ++++++++++++------ crates/db/src/migration/builder.rs | 47 +-- crates/db/src/migration/entry.rs | 41 +++ crates/db/src/migration/migrator.rs | 207 ++++++++----- crates/db/src/migration/mod.rs | 10 +- crates/store/Cargo.toml | 6 +- crates/store/build.rs | 10 +- crates/store/src/db/migrations.rs | 106 +++++-- .../up.sql => 001_initial.sql} | 58 ++-- .../migrations/2025062000000_setup/down.sql | 0 .../down.sql | 1 - .../up.sql | 13 - .../down.sql | 5 - .../up.sql | 12 - .../down.sql | 6 - .../up.sql | 16 - crates/store/src/db/mod.rs | 36 +-- .../store/src/db/models/queries/accounts.rs | 22 +- .../db/models/queries/accounts/delta/tests.rs | 11 +- .../src/db/models/queries/accounts/tests.rs | 18 +- crates/store/src/db/schema.rs | 28 +- crates/store/src/db/schema_hash.rs | 198 ------------ crates/store/src/db/tests.rs | 23 +- 41 files changed, 772 insertions(+), 928 deletions(-) rename bin/ntx-builder/src/db/migrations/{2026020900000_setup/up.sql => 001_initial.sql} (95%) delete mode 100644 bin/ntx-builder/src/db/migrations/2026020900000_setup/down.sql delete mode 100644 bin/ntx-builder/src/db/schema_hash.rs rename bin/validator/src/db/migrations/{2025062000000_setup/up.sql => 001_initial.sql} (83%) delete mode 100644 bin/validator/src/db/migrations/2025062000000_setup/down.sql rename crates/store/src/db/migrations/{2025062000000_setup/up.sql => 001_initial.sql} (76%) delete mode 100644 crates/store/src/db/migrations/2025062000000_setup/down.sql delete mode 100644 crates/store/src/db/migrations/2026042800000_add_idx_transactions_sync/down.sql delete mode 100644 crates/store/src/db/migrations/2026042800000_add_idx_transactions_sync/up.sql delete mode 100644 crates/store/src/db/migrations/2026051200000_remove_proving_inputs/down.sql delete mode 100644 crates/store/src/db/migrations/2026051200000_remove_proving_inputs/up.sql delete mode 100644 crates/store/src/db/migrations/2026051800000_add_idx_accounts_latest_code_commitment/down.sql delete mode 100644 crates/store/src/db/migrations/2026051800000_add_idx_accounts_latest_code_commitment/up.sql delete mode 100644 crates/store/src/db/schema_hash.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8693e23ca5..98ecdf1973 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,6 +139,30 @@ jobs: - name: Doc tests run: cargo test --doc --workspace --all-features + # Temporary Diesel schema drift checks. Delete this job once Diesel is removed. + diesel-schema: + name: diesel schema + runs-on: warp-ubuntu-latest-x64-8x + timeout-minutes: 30 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - name: Rustup + run: rustup toolchain install --no-self-update + - uses: WarpBuilds/rust-cache@9d0cc3090d9c87de74ea67617b246e978735b1a1 # v2.9.1 + with: + shared-key: ${{ github.job }} + prefix-key: ${{ env.CACHE_PREFIX }} + save-if: ${{ env.SAVE_CACHE }} + - uses: taiki-e/install-action@055f5df8c3f65ea01cd41e9dc855becd88953486 # v2.75.18 + with: + tool: diesel_cli@2.3.9 + - name: Check Diesel schemas + run: | + cargo test --locked --workspace --all-features \ + diesel_schema_is_in_sync_with_migrations -- --ignored + doc: runs-on: warp-ubuntu-latest-x64-8x steps: diff --git a/Cargo.lock b/Cargo.lock index e6818092a4..34eab17397 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1468,17 +1468,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "diesel_migrations" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "745fd255645f0f1135f9ec55c7b00e0882192af9683ab4731e4bba3da82b8f9c" -dependencies = [ - "diesel", - "migrations_internals", - "migrations_macros", -] - [[package]] name = "diesel_table_macro_syntax" version = "0.3.0" @@ -3407,7 +3396,6 @@ dependencies = [ "deadpool", "deadpool-diesel", "diesel", - "diesel_migrations", "fs-err", "hex", "indexmap", @@ -3433,7 +3421,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-stream", - "toml 1.1.2+spec-1.1.0", + "toml", "tonic", "tonic-reflection", "tower-http", @@ -3510,7 +3498,6 @@ dependencies = [ "build-rs", "clap", "diesel", - "diesel_migrations", "futures", "humantime", "libsqlite3-sys", @@ -3582,7 +3569,7 @@ dependencies = [ "serde", "serde-untagged", "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "toml", ] [[package]] @@ -3610,7 +3597,7 @@ dependencies = [ "semver 1.0.28", "serde", "thiserror 2.0.18", - "toml 1.1.2+spec-1.1.0", + "toml", "walkdir", ] @@ -3807,7 +3794,6 @@ dependencies = [ "build-rs", "clap", "diesel", - "diesel_migrations", "fs-err", "hex", "miden-node-db", @@ -3886,27 +3872,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "migrations_internals" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d" -dependencies = [ - "serde", - "toml 0.9.12+spec-1.1.0", -] - -[[package]] -name = "migrations_macros" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36fc5ac76be324cfd2d3f2cf0fdf5d5d3c4f14ed8aaebadb09e304ba42282703" -dependencies = [ - "migrations_internals", - "proc-macro2", - "quote", -] - [[package]] name = "mime" version = "0.3.17" @@ -6368,19 +6333,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "serde_core", - "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "winnow 0.7.15", -] - [[package]] name = "toml" version = "1.1.2+spec-1.1.0" @@ -6390,19 +6342,10 @@ dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 1.1.1+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", - "winnow 1.0.1", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", + "winnow", ] [[package]] @@ -6421,9 +6364,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", - "toml_datetime 1.1.1+spec-1.1.0", + "toml_datetime", "toml_parser", - "winnow 1.0.1", + "winnow", ] [[package]] @@ -6432,7 +6375,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.1", + "winnow", ] [[package]] @@ -6812,7 +6755,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml 1.1.2+spec-1.1.0", + "toml", ] [[package]] @@ -7492,12 +7435,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" - [[package]] name = "winnow" version = "1.0.1" @@ -7624,7 +7561,7 @@ dependencies = [ "anyhow", "clap", "fs-err", - "toml 1.1.2+spec-1.1.0", + "toml", "tree-sitter", "tree-sitter-rust", ] diff --git a/Cargo.toml b/Cargo.toml index 91c51bc386..4e90f07a16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,6 @@ deadpool = { default-features = false, version = "0.12" } deadpool-diesel = { version = "0.6" } deadpool-sync = { default-features = false, version = "0.1" } diesel = { version = "2.3" } -diesel_migrations = { version = "2.3" } fs-err = { version = "3" } futures = { version = "0.3" } hex = { version = "0.4" } diff --git a/bin/ntx-builder/Cargo.toml b/bin/ntx-builder/Cargo.toml index 2c7d7dde2c..67106ffc19 100644 --- a/bin/ntx-builder/Cargo.toml +++ b/bin/ntx-builder/Cargo.toml @@ -21,7 +21,6 @@ doctest = false anyhow = { workspace = true } clap = { features = ["env", "string"], workspace = true } diesel = { features = ["numeric", "sqlite"], workspace = true } -diesel_migrations = { features = ["sqlite"], workspace = true } futures = { workspace = true } humantime = { workspace = true } libsqlite3-sys = { workspace = true } @@ -43,7 +42,8 @@ tracing = { workspace = true } url = { workspace = true } [build-dependencies] -build-rs = { workspace = true } +build-rs = { workspace = true } +miden-node-db = { workspace = true } [dev-dependencies] miden-node-utils = { features = ["testing"], workspace = true } diff --git a/bin/ntx-builder/build.rs b/bin/ntx-builder/build.rs index 78883c50c1..922ff977c8 100644 --- a/bin/ntx-builder/build.rs +++ b/bin/ntx-builder/build.rs @@ -1,11 +1,9 @@ -// This build.rs is required to trigger the `diesel_migrations::embed_migrations!` proc-macro in -// `src/db/migrations.rs` to include the latest version of the migrations into the binary, see -// . +fn main() -> Result<(), Box> { + miden_node_db::migration::Migrator::generate("src/db/migrations")?; -fn main() { - build_rs::output::rerun_if_changed("src/db/migrations"); // If we do one re-write, the default rules are disabled, // hence we need to trigger explicitly on `Cargo.toml`. // build_rs::output::rerun_if_changed("Cargo.toml"); + Ok(()) } diff --git a/bin/ntx-builder/src/db/migrations.rs b/bin/ntx-builder/src/db/migrations.rs index f3955cb2ad..5e4b7c5691 100644 --- a/bin/ntx-builder/src/db/migrations.rs +++ b/bin/ntx-builder/src/db/migrations.rs @@ -1,29 +1,73 @@ -use diesel::SqliteConnection; -use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; +use std::path::Path; + use miden_node_db::DatabaseError; use tracing::instrument; use crate::COMPONENT; -use crate::db::schema_hash::verify_schema; -// The rebuild is automatically triggered by `build.rs` as described in -// . -pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/db/migrations"); +include!(concat!(env!("OUT_DIR"), "/db_migrator.rs")); #[instrument(level = "debug", target = COMPONENT, skip_all, err)] -pub fn apply_migrations(conn: &mut SqliteConnection) -> Result<(), DatabaseError> { - let migrations = conn.pending_migrations(MIGRATIONS).expect("In memory migrations never fail"); - tracing::info!(target: COMPONENT, migrations = migrations.len(), "Applying pending migrations"); - - let Err(e) = conn.run_pending_migrations(MIGRATIONS) else { - // Migrations applied successfully, verify schema hash. - verify_schema(conn)?; - return Ok(()); - }; - tracing::warn!(target: COMPONENT, "Failed to apply migration: {e:?}"); - // Something went wrong; revert the last migration. - conn.revert_last_migration(MIGRATIONS) - .expect("Duality is maintained by the developer"); +pub fn apply_migrations(database_filepath: &Path) -> Result<(), DatabaseError> { + let migrator = migrator().map_err(DatabaseError::migration)?; + tracing::info!( + target: COMPONENT, + migration_count = migrator.schema_hashes().len(), + "Applying database migrations" + ); + migrator.migrate(database_filepath).map_err(DatabaseError::migration)?; Ok(()) } + +#[cfg(test)] +mod tests { + use std::process::Command; + + use anyhow::{Context, Result, ensure}; + use miden_node_db::migration::SchemaHash; + + use super::*; + + const EXPECTED_SCHEMA_HASHES: [SchemaHash; 1] = [SchemaHash::from_hex( + "c6434bc6a142cd96dd4072bea641546d99788b1495cb0e52c2d98b9138f9c30d", + )]; + + #[test] + fn migration_schema_hashes_are_stable() -> Result<()> { + let migrator = migrator()?; + + assert_eq!(migrator.schema_hashes(), &EXPECTED_SCHEMA_HASHES); + Ok(()) + } + + #[test] + #[ignore = "requires diesel CLI; CI runs this in the diesel-schema job"] + fn diesel_schema_is_in_sync_with_migrations() -> Result<()> { + let temp_dir = tempfile::tempdir()?; + let database_filepath = temp_dir.path().join("ntx-builder.sqlite3"); + apply_migrations(&database_filepath)?; + + let output = Command::new("diesel") + .arg("print-schema") + .arg("--database-url") + .arg(&database_filepath) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .output() + .context( + "failed to run diesel CLI; install it with \ + `cargo install diesel_cli --no-default-features --features sqlite`", + )?; + + ensure!( + output.status.success(), + "diesel print-schema failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let generated = + String::from_utf8(output.stdout).context("diesel CLI output is not UTF-8")?; + assert_eq!(generated, include_str!("schema.rs")); + Ok(()) + } +} diff --git a/bin/ntx-builder/src/db/migrations/2026020900000_setup/up.sql b/bin/ntx-builder/src/db/migrations/001_initial.sql similarity index 95% rename from bin/ntx-builder/src/db/migrations/2026020900000_setup/up.sql rename to bin/ntx-builder/src/db/migrations/001_initial.sql index 46d71689c0..348fac8798 100644 --- a/bin/ntx-builder/src/db/migrations/2026020900000_setup/up.sql +++ b/bin/ntx-builder/src/db/migrations/001_initial.sql @@ -4,7 +4,7 @@ CREATE TABLE chain_state ( -- Singleton constraint: only one row allowed. id INTEGER NOT NULL PRIMARY KEY CHECK (id = 0), -- Block number of the chain tip. - block_num INTEGER NOT NULL, + block_num BIGINT NOT NULL, -- Serialized BlockHeader. block_header BLOB NOT NULL, @@ -33,7 +33,7 @@ CREATE UNIQUE INDEX idx_accounts_inflight ON accounts(account_id, transaction_id CREATE INDEX idx_accounts_account ON accounts(account_id); CREATE INDEX idx_accounts_tx ON accounts(transaction_id) WHERE transaction_id IS NOT NULL; --- Notes: committed, inflight, and nullified — all in one table. +-- Notes: committed, inflight, and nullified - all in one table. -- created_by = NULL means committed note; non-NULL means created by inflight tx. -- consumed_by = NULL means unconsumed; non-NULL means consumed by inflight tx. -- committed_at = block number when the consuming transaction was committed on-chain. @@ -49,7 +49,7 @@ CREATE TABLE notes ( -- Backoff tracking: number of failed execution attempts. attempt_count INTEGER NOT NULL DEFAULT 0, -- Backoff tracking: block number of the last failed attempt. NULL if never attempted. - last_attempt INTEGER, + last_attempt BIGINT, -- Latest execution error message. NULL if no error recorded. last_error TEXT, -- NULL if the note came from a committed block; transaction ID if created by inflight tx. @@ -58,7 +58,7 @@ CREATE TABLE notes ( consumed_by BLOB, -- Block number at which the note's consuming transaction was committed. -- NULL while the note is still pending or in-flight; set on block commit. - committed_at INTEGER, + committed_at BIGINT, CONSTRAINT notes_attempt_count_non_negative CHECK (attempt_count >= 0), CONSTRAINT notes_last_attempt_is_u32 CHECK (last_attempt BETWEEN 0 AND 0xFFFFFFFF), diff --git a/bin/ntx-builder/src/db/migrations/2026020900000_setup/down.sql b/bin/ntx-builder/src/db/migrations/2026020900000_setup/down.sql deleted file mode 100644 index 8b13789179..0000000000 --- a/bin/ntx-builder/src/db/migrations/2026020900000_setup/down.sql +++ /dev/null @@ -1 +0,0 @@ - diff --git a/bin/ntx-builder/src/db/mod.rs b/bin/ntx-builder/src/db/mod.rs index a7644f485f..ac0f657211 100644 --- a/bin/ntx-builder/src/db/mod.rs +++ b/bin/ntx-builder/src/db/mod.rs @@ -20,7 +20,6 @@ use crate::{COMPONENT, NoteError}; pub(crate) mod models; mod migrations; -mod schema_hash; /// [diesel](https://diesel.rs) generated schema. pub(crate) mod schema; @@ -58,6 +57,8 @@ impl Db { database_filepath: PathBuf, connection_pool_size: NonZeroUsize, ) -> anyhow::Result { + apply_migrations(&database_filepath).context("failed to apply migrations")?; + let inner = miden_node_db::Db::new_with_pool_size(&database_filepath, connection_pool_size) .context("failed to build connection pool")?; @@ -68,12 +69,7 @@ impl Db { "Connected to the database" ); - let me = Db { inner }; - me.inner - .query("migrations", apply_migrations) - .await - .context("failed to apply migrations on pool connection")?; - Ok(me) + Ok(Db { inner }) } // PUBLIC QUERY METHODS @@ -255,10 +251,10 @@ impl Db { let dir = tempfile::tempdir().expect("failed to create temp directory"); let db_path = dir.path().join("test.sqlite3"); + apply_migrations(&db_path).expect("migrations should apply on empty database"); let mut conn = SqliteConnection::establish(db_path.to_str().unwrap()) .expect("temp file sqlite should always work"); configure_connection_on_creation(&mut conn).expect("connection configuration should work"); - apply_migrations(&mut conn).expect("migrations should apply on empty database"); (conn, dir) } diff --git a/bin/ntx-builder/src/db/schema_hash.rs b/bin/ntx-builder/src/db/schema_hash.rs deleted file mode 100644 index 80d00b4c47..0000000000 --- a/bin/ntx-builder/src/db/schema_hash.rs +++ /dev/null @@ -1,191 +0,0 @@ -//! Schema verification to detect database schema changes. -//! -//! Detects: -//! -//! - Direct modifications to the database schema outside of migrations -//! - Running a node against a database created with different set of migrations -//! - Forgetting to reset the database after schema changes i.e. for a specific migration -//! -//! The verification works by creating an in-memory reference database, applying all -//! migrations to it, and comparing its schema against the actual database schema. - -use diesel::{Connection, RunQueryDsl, SqliteConnection}; -use diesel_migrations::MigrationHarness; -use miden_node_db::SchemaVerificationError; -use tracing::instrument; - -use crate::COMPONENT; -use crate::db::migrations::MIGRATIONS; - -/// Represents a schema object for comparison. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -struct SchemaObject { - object_type: String, - name: String, - sql: String, -} - -/// Represents a row from the `sqlite_schema` table. -#[derive(diesel::QueryableByName, Debug)] -struct SqliteSchemaRow { - #[diesel(sql_type = diesel::sql_types::Text)] - schema_type: String, - #[diesel(sql_type = diesel::sql_types::Text)] - name: String, - #[diesel(sql_type = diesel::sql_types::Nullable)] - sql: Option, -} - -/// Extracts all schema objects from a database connection. -fn extract_schema( - conn: &mut SqliteConnection, -) -> Result, SchemaVerificationError> { - let rows: Vec = diesel::sql_query( - "SELECT type as schema_type, name, sql FROM sqlite_schema \ - WHERE type IN ('table', 'index') \ - AND name NOT LIKE 'sqlite_%' \ - AND name NOT LIKE '__diesel_%' \ - ORDER BY type, name", - ) - .load(conn) - .map_err(SchemaVerificationError::SchemaExtraction)?; - - let mut objects: Vec = rows - .into_iter() - .filter_map(|row| { - row.sql.map(|sql| SchemaObject { - object_type: row.schema_type, - name: row.name, - sql, - }) - }) - .collect(); - - objects.sort(); - Ok(objects) -} - -/// Computes the expected schema by applying migrations to an in-memory database. -fn compute_expected_schema() -> Result, SchemaVerificationError> { - let mut conn = SqliteConnection::establish(":memory:") - .map_err(SchemaVerificationError::InMemoryDbCreation)?; - - conn.run_pending_migrations(MIGRATIONS) - .map_err(SchemaVerificationError::MigrationApplication)?; - - extract_schema(&mut conn) -} - -/// Verifies that the database schema matches the expected schema. -/// -/// Creates an in-memory database, applies all migrations, and compares schemas. -/// -/// # Errors -/// -/// Returns `SchemaVerificationError::Mismatch` if schemas differ. -#[instrument(level = "info", target = COMPONENT, skip_all, err)] -pub fn verify_schema(conn: &mut SqliteConnection) -> Result<(), SchemaVerificationError> { - let expected = compute_expected_schema()?; - let actual = extract_schema(conn)?; - - if actual != expected { - let expected_names: Vec<_> = expected.iter().map(|o| &o.name).collect(); - let actual_names: Vec<_> = actual.iter().map(|o| &o.name).collect(); - - // Find differences for better error messages. - let missing: Vec<_> = expected.iter().filter(|e| !actual.contains(e)).collect(); - let extra: Vec<_> = actual.iter().filter(|a| !expected.contains(a)).collect(); - - tracing::error!( - target: COMPONENT, - ?expected_names, - ?actual_names, - missing_count = missing.len(), - extra_count = extra.len(), - "Database schema mismatch detected" - ); - - // Log specific differences at debug level. - for obj in &missing { - tracing::debug!(target: COMPONENT, name = %obj.name, "Missing or modified: {}", obj.sql); - } - for obj in &extra { - tracing::debug!(target: COMPONENT, name = %obj.name, "Extra or modified: {}", obj.sql); - } - - return Err(SchemaVerificationError::Mismatch { - expected_count: expected.len(), - actual_count: actual.len(), - missing_count: missing.len(), - extra_count: extra.len(), - }); - } - - tracing::info!( - target: COMPONENT, - objects = expected.len(), - "Database schema verification passed" - ); - Ok(()) -} - -#[cfg(test)] -mod tests { - use miden_node_db::DatabaseError; - - use super::*; - use crate::db::migrations::apply_migrations; - - #[test] - fn verify_schema_passes_for_correct_schema() { - let mut conn = SqliteConnection::establish(":memory:").unwrap(); - conn.run_pending_migrations(MIGRATIONS).unwrap(); - verify_schema(&mut conn).expect("Should pass for correct schema"); - } - - #[test] - fn verify_schema_fails_for_added_object() { - let mut conn = SqliteConnection::establish(":memory:").unwrap(); - conn.run_pending_migrations(MIGRATIONS).unwrap(); - - diesel::sql_query("CREATE TABLE rogue_table (id INTEGER PRIMARY KEY)") - .execute(&mut conn) - .unwrap(); - - assert!(matches!( - verify_schema(&mut conn), - Err(SchemaVerificationError::Mismatch { .. }) - )); - } - - #[test] - fn verify_schema_fails_for_removed_object() { - let mut conn = SqliteConnection::establish(":memory:").unwrap(); - conn.run_pending_migrations(MIGRATIONS).unwrap(); - - diesel::sql_query("DROP TABLE notes").execute(&mut conn).unwrap(); - - assert!(matches!( - verify_schema(&mut conn), - Err(SchemaVerificationError::Mismatch { .. }) - )); - } - - #[test] - fn apply_migrations_succeeds_on_fresh_database() { - let mut conn = SqliteConnection::establish(":memory:").unwrap(); - apply_migrations(&mut conn).expect("Should succeed on fresh database"); - } - - #[test] - fn apply_migrations_fails_on_tampered_database() { - let mut conn = SqliteConnection::establish(":memory:").unwrap(); - conn.run_pending_migrations(MIGRATIONS).unwrap(); - - diesel::sql_query("CREATE TABLE tampered (id INTEGER)") - .execute(&mut conn) - .unwrap(); - - assert!(matches!(apply_migrations(&mut conn), Err(DatabaseError::SchemaVerification(_)))); - } -} diff --git a/bin/validator/Cargo.toml b/bin/validator/Cargo.toml index 62ffa2f855..b0b56fd4ae 100644 --- a/bin/validator/Cargo.toml +++ b/bin/validator/Cargo.toml @@ -23,7 +23,6 @@ aws-config = { version = "1.8.14" } aws-sdk-kms = { version = "1.100" } clap = { features = ["env", "string"], workspace = true } diesel = { workspace = true } -diesel_migrations = { workspace = true } fs-err = { workspace = true } hex = { workspace = true } miden-node-db = { workspace = true } @@ -42,7 +41,8 @@ tower-http = { features = ["util"], workspace = true } tracing = { workspace = true } [build-dependencies] -build-rs = { workspace = true } +build-rs = { workspace = true } +miden-node-db = { workspace = true } [dev-dependencies] miden-node-store = { workspace = true } diff --git a/bin/validator/build.rs b/bin/validator/build.rs index 59c416fafe..922ff977c8 100644 --- a/bin/validator/build.rs +++ b/bin/validator/build.rs @@ -1,10 +1,9 @@ -// This build.rs is required to trigger the `diesel_migrations::embed_migrations!` proc-macro in -// `validator/src/db/migrations.rs` to include the latest version of the migrations into the binary, -// see . -fn main() { - build_rs::output::rerun_if_changed("./src/db/migrations"); +fn main() -> Result<(), Box> { + miden_node_db::migration::Migrator::generate("src/db/migrations")?; + // If we do one re-write, the default rules are disabled, // hence we need to trigger explicitly on `Cargo.toml`. // build_rs::output::rerun_if_changed("Cargo.toml"); + Ok(()) } diff --git a/bin/validator/src/db/migrations.rs b/bin/validator/src/db/migrations.rs index 240c29033b..2e84d87343 100644 --- a/bin/validator/src/db/migrations.rs +++ b/bin/validator/src/db/migrations.rs @@ -1,25 +1,73 @@ -use diesel::SqliteConnection; -use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; +use std::path::Path; + use miden_node_db::DatabaseError; use tracing::instrument; use crate::COMPONENT; -// The rebuild is automatically triggered by `build.rs` as described in -// . -pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/db/migrations"); +include!(concat!(env!("OUT_DIR"), "/db_migrator.rs")); #[instrument(level = "debug", target = COMPONENT, skip_all, err)] -pub fn apply_migrations(conn: &mut SqliteConnection) -> std::result::Result<(), DatabaseError> { - let migrations = conn.pending_migrations(MIGRATIONS).expect("In memory migrations never fail"); - tracing::info!(target = COMPONENT, "Applying {} migration(s)", migrations.len()); - - let Err(e) = conn.run_pending_migrations(MIGRATIONS) else { - return Ok(()); - }; - tracing::warn!(target = COMPONENT, "Failed to apply migration: {e:?}"); - conn.revert_last_migration(MIGRATIONS) - .expect("Duality is maintained by the developer"); +pub fn apply_migrations(database_filepath: &Path) -> std::result::Result<(), DatabaseError> { + let migrator = migrator().map_err(DatabaseError::migration)?; + tracing::info!( + target: COMPONENT, + migration_count = migrator.schema_hashes().len(), + "Applying database migrations" + ); + migrator.migrate(database_filepath).map_err(DatabaseError::migration)?; Ok(()) } + +#[cfg(test)] +mod tests { + use std::process::Command; + + use anyhow::{Context, Result, ensure}; + use miden_node_db::migration::SchemaHash; + + use super::*; + + const EXPECTED_SCHEMA_HASHES: [SchemaHash; 1] = [SchemaHash::from_hex( + "f0631571c590d8b3d183b1fe2dca397e584337d935b7015c58c034a8289c5263", + )]; + + #[test] + fn migration_schema_hashes_are_stable() -> Result<()> { + let migrator = migrator()?; + + assert_eq!(migrator.schema_hashes(), &EXPECTED_SCHEMA_HASHES); + Ok(()) + } + + #[test] + #[ignore = "requires diesel CLI; CI runs this in the diesel-schema job"] + fn diesel_schema_is_in_sync_with_migrations() -> Result<()> { + let temp_dir = tempfile::tempdir()?; + let database_filepath = temp_dir.path().join("validator.sqlite3"); + apply_migrations(&database_filepath)?; + + let output = Command::new("diesel") + .arg("print-schema") + .arg("--database-url") + .arg(&database_filepath) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .output() + .context( + "failed to run diesel CLI; install it with \ + `cargo install diesel_cli --no-default-features --features sqlite`", + )?; + + ensure!( + output.status.success(), + "diesel print-schema failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let generated = + String::from_utf8(output.stdout).context("diesel CLI output is not UTF-8")?; + assert_eq!(generated, include_str!("schema.rs")); + Ok(()) + } +} diff --git a/bin/validator/src/db/migrations/2025062000000_setup/up.sql b/bin/validator/src/db/migrations/001_initial.sql similarity index 83% rename from bin/validator/src/db/migrations/2025062000000_setup/up.sql rename to bin/validator/src/db/migrations/001_initial.sql index b8883e2263..bacde42cb9 100644 --- a/bin/validator/src/db/migrations/2025062000000_setup/up.sql +++ b/bin/validator/src/db/migrations/001_initial.sql @@ -1,8 +1,8 @@ CREATE TABLE validated_transactions ( id BLOB NOT NULL, - block_num INTEGER NOT NULL, + block_num BIGINT NOT NULL, account_id BLOB NOT NULL, - account_delta BLOB, + account_delta BLOB NOT NULL, input_notes BLOB, output_notes BLOB, initial_account_hash BLOB NOT NULL, @@ -15,6 +15,6 @@ CREATE INDEX idx_validated_transactions_account_id ON validated_transactions(acc CREATE INDEX idx_validated_transactions_block_num ON validated_transactions(block_num); CREATE TABLE block_headers ( - block_num INTEGER PRIMARY KEY, + block_num BIGINT PRIMARY KEY, block_header BLOB NOT NULL ) WITHOUT ROWID; diff --git a/bin/validator/src/db/migrations/2025062000000_setup/down.sql b/bin/validator/src/db/migrations/2025062000000_setup/down.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/bin/validator/src/db/mod.rs b/bin/validator/src/db/mod.rs index 4fa5279d95..bc6b6a18af 100644 --- a/bin/validator/src/db/mod.rs +++ b/bin/validator/src/db/mod.rs @@ -31,6 +31,8 @@ pub async fn load_with_pool_size( database_filepath: PathBuf, connection_pool_size: NonZeroUsize, ) -> Result { + apply_migrations(&database_filepath)?; + let db = Db::new_with_pool_size(&database_filepath, connection_pool_size)?; tracing::info!( target: COMPONENT, @@ -38,8 +40,6 @@ pub async fn load_with_pool_size( connection_pool_size = %connection_pool_size, "Connected to the database" ); - - db.query("migrations", apply_migrations).await?; Ok(db) } diff --git a/bin/validator/src/db/schema.rs b/bin/validator/src/db/schema.rs index 72cd037c74..e78833e43b 100644 --- a/bin/validator/src/db/schema.rs +++ b/bin/validator/src/db/schema.rs @@ -1,20 +1,24 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + block_headers (block_num) { + block_num -> BigInt, + block_header -> Binary, + } +} + diesel::table! { validated_transactions (id) { id -> Binary, block_num -> BigInt, account_id -> Binary, account_delta -> Binary, - input_notes -> Binary, - output_notes -> Binary, + input_notes -> Nullable, + output_notes -> Nullable, initial_account_hash -> Binary, final_account_hash -> Binary, fee -> Binary, } } -diesel::table! { - block_headers (block_num) { - block_num -> BigInt, - block_header -> Binary, - } -} +diesel::allow_tables_to_appear_in_same_query!(block_headers, validated_transactions,); diff --git a/crates/db/src/errors.rs b/crates/db/src/errors.rs index 5e59ff4b9a..3737f50693 100644 --- a/crates/db/src/errors.rs +++ b/crates/db/src/errors.rs @@ -45,6 +45,8 @@ pub enum DatabaseError { }, #[error(transparent)] Diesel(#[from] diesel::result::Error), + #[error("failed to apply database migrations")] + Migration(#[source] Box), #[error("schema verification failed")] SchemaVerification(#[from] SchemaVerificationError), #[error("I/O error")] @@ -71,6 +73,13 @@ impl DatabaseError { Self::InteractError(format!("{msg} failed: {e:?}")) } + /// Creates a database migration error with the original source error. + pub fn migration( + source: impl Into>, + ) -> Self { + Self::Migration(source.into()) + } + /// Failed to convert an SQL entry to a rust representation pub fn conversiont_from_sql(err: MaybeE) -> DatabaseError where diff --git a/crates/db/src/migration/build_script.rs b/crates/db/src/migration/build_script.rs index 8ac01f5a39..0373146aa6 100644 --- a/crates/db/src/migration/build_script.rs +++ b/crates/db/src/migration/build_script.rs @@ -2,14 +2,13 @@ use std::collections::HashSet; use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use anyhow::{Context, Result, ensure}; +use anyhow::{Context, Result, bail, ensure}; use codegen::{Function, Scope}; use fs_err as fs; use super::Migrator; pub const GENERATED_MIGRATOR_FILE: &str = "db_migrator.rs"; -pub const CODE_MIGRATION_FILE: &str = "migration.rs"; impl Migrator { /// Generates Rust source for a migrator from a migration directory. @@ -57,21 +56,22 @@ impl Migrator { /// ```text /// migrations/ /// retired/ - /// 001_initial.sql - /// 002_indexes.sql - /// code/ - /// 003_backfill/ - /// migration.rs + /// 001_legacy.sql + /// 002_initial.sql + /// 003_backfill.rs + /// 003_backfill/ + /// fixture.bin /// ``` /// /// Retired migrations are loaded from lexicographically sorted `.sql` files in `retired`; - /// the migration name is the file stem. Code migrations are loaded from lexicographically - /// sorted folders in `code`; the migration name is the folder name. Each code folder must - /// contain `migration.rs` and that file must expose a `pub fn migrate(...)` matching - /// [`super::CodeMigrationFn`]. + /// the migration name is the file stem. Active migrations are loaded from lexicographically + /// sorted direct `.sql` and `.rs` files in the migration directory; the migration name is the + /// file stem. Rust migration files must expose a `pub fn migrate(...)` matching + /// [`super::CodeMigrationFn`]. Direct subdirectories other than `retired` are ignored by the + /// framework so callers can keep migration-specific support files next to a migration file. /// /// The `retired` directory contains SQL retained for fresh database initialization after the - /// corresponding code migrations no longer need to be supported. Relative migration paths are + /// corresponding active migrations no longer need to be supported. Relative migration paths are /// resolved from the package manifest directory, i.e. the crate root. pub fn generate(migration_dir: impl AsRef) -> Result { let migration_dir = migration_dir_path(migration_dir.as_ref()); @@ -81,7 +81,7 @@ impl Migrator { let migrations = discover_migrations(&migration_dir)?; fs::write( &out_path, - render_migrator(&migrations.retired_migrations, &migrations.code_migrations)?, + render_migrator(&migrations.retired_migrations, &migrations.active_migrations)?, ) .with_context(|| format!("failed to write generated migrator to {}", out_path.display()))?; Ok(out_path) @@ -99,7 +99,7 @@ fn migration_dir_path(migration_dir: &Path) -> PathBuf { #[derive(Debug)] struct DiscoveredMigrations { retired_migrations: Vec, - code_migrations: Vec, + active_migrations: Vec, } #[derive(Debug)] @@ -115,6 +115,12 @@ struct CodeMigration { path: PathBuf, } +#[derive(Debug)] +enum ActiveMigration { + Sql(SqlMigration), + Code(CodeMigration), +} + fn discover_migrations(migration_dir: &Path) -> Result { ensure!( migration_dir.is_dir(), @@ -123,14 +129,14 @@ fn discover_migrations(migration_dir: &Path) -> Result { ); let retired_migrations = discover_retired_migrations(migration_dir)?; - let code_migrations = discover_code_migrations(migration_dir)?; + let active_migrations = discover_active_migrations(migration_dir)?; ensure!( - !retired_migrations.is_empty() || !code_migrations.is_empty(), + !retired_migrations.is_empty() || !active_migrations.is_empty(), "migration directory contains no migrations: {}", migration_dir.display() ); - Ok(DiscoveredMigrations { retired_migrations, code_migrations }) + Ok(DiscoveredMigrations { retired_migrations, active_migrations }) } fn discover_retired_migrations(migration_dir: &Path) -> Result> { @@ -145,6 +151,7 @@ fn discover_retired_migrations(migration_dir: &Path) -> Result retired_dir.display() ); + let mut seen_prefixes = HashSet::new(); let mut migrations = Vec::new(); for entry in read_dir_sorted(&retired_dir)? { let path = entry.path(); @@ -155,56 +162,55 @@ fn discover_retired_migrations(migration_dir: &Path) -> Result path.display() ); - migrations.push(SqlMigration { - name: file_stem(&path)?, - path: absolute_path(&path)?, - }); + let name = file_stem(&path)?; + let prefix = migration_prefix(&name, &path)?; + ensure!( + seen_prefixes.insert(prefix.to_owned()), + "duplicate retired migration prefix {prefix:?}" + ); + + migrations.push(SqlMigration { name, path: absolute_path(&path)? }); } Ok(migrations) } -fn discover_code_migrations(migration_dir: &Path) -> Result> { - let code_dir = migration_dir.join("code"); - if !code_dir.exists() { - return Ok(Vec::new()); - } - - ensure!( - code_dir.is_dir(), - "code migration path is not a directory: {}", - code_dir.display() - ); - - // Folder names are converted into Rust module identifiers lossy, e.g. `001-backfill` and - // `001_backfill` both become `migration_001_backfill`. To prevent this, we track seen - // identifiers and reject any collisions. - let mut seen_idents = HashSet::new(); +fn discover_active_migrations(migration_dir: &Path) -> Result> { + let mut seen_prefixes = HashSet::new(); let mut migrations = Vec::new(); - for entry in read_dir_sorted(&code_dir)? { + for entry in read_dir_sorted(migration_dir)? { let path = entry.path(); - ensure!(path.is_dir(), "code migration entry is not a directory: {}", path.display()); + if path.is_dir() { + continue; + } - let name = file_name(&path)?; - let module_ident = module_ident(&name)?; - ensure!( - seen_idents.insert(module_ident.clone()), - "code migration module identifier collision for migration {name:?}" - ); + ensure!(path.is_file(), "active migration entry is not a file: {}", path.display()); - let migration_rs = path.join(CODE_MIGRATION_FILE); + let name = file_stem(&path)?; + let prefix = migration_prefix(&name, &path)?; ensure!( - migration_rs.is_file(), - "code migration {} is missing {}", - path.display(), - CODE_MIGRATION_FILE + seen_prefixes.insert(prefix.to_owned()), + "duplicate active migration prefix {prefix:?}" ); - migrations.push(CodeMigration { - name, - module_ident, - path: absolute_path(&migration_rs)?, - }); + match path.extension().and_then(OsStr::to_str) { + Some("sql") => { + migrations + .push(ActiveMigration::Sql(SqlMigration { name, path: absolute_path(&path)? })); + }, + Some("rs") => { + let module_ident = module_ident(&name)?; + + migrations.push(ActiveMigration::Code(CodeMigration { + name, + module_ident, + path: absolute_path(&path)?, + })); + }, + _ => { + bail!("active migration file must use .sql or .rs extension: {}", path.display()); + }, + } } Ok(migrations) @@ -212,27 +218,33 @@ fn discover_code_migrations(migration_dir: &Path) -> Result> /// Renders the Rust source written by [`Migrator::generate`]. /// -/// For one retired migration named `001_initial` and one code migration named `002_backfill`, +/// For one retired migration named `001_legacy`, one SQL migration named `002_initial`, and one +/// Rust migration named `003_backfill`, /// the generated file has this shape: /// /// ```ignore -/// #[path = "/path/to/migrations/code/002_backfill/migration.rs"] -/// mod migration_002_backfill; +/// #[path = "/path/to/migrations/003_backfill.rs"] +/// mod migration_003_backfill; /// /// pub fn migrator() -> ::anyhow::Result<::miden_node_db::migration::Migrator> { /// ::miden_node_db::migration::Migrator::builder()? -/// .push_retired("001_initial", include_str!("/path/to/migrations/retired/001_initial.sql"))? -/// .push_code("002_backfill", migration_002_backfill::migrate)? +/// .push_retired("001_legacy", include_str!("/path/to/migrations/retired/001_legacy.sql"))? +/// .push_sql("002_initial", include_str!("/path/to/migrations/002_initial.sql"))? +/// .push_code("003_backfill", migration_003_backfill::migrate)? /// .build() /// } /// ``` fn render_migrator( retired_migrations: &[SqlMigration], - code_migrations: &[CodeMigration], + active_migrations: &[ActiveMigration], ) -> Result { let mut scope = Scope::new(); - for migration in code_migrations { + for migration in active_migrations { + let ActiveMigration::Code(migration) = migration else { + continue; + }; + let path = format!("{:?}", rust_path(&migration.path)?); scope.raw(format!("#[path = {path}]\nmod {};", migration.module_ident)); } @@ -248,9 +260,19 @@ fn render_migrator( function.line(format!(" .push_retired({name}, include_str!({path}))?")); } - for migration in code_migrations { - let name = format!("{:?}", migration.name); - function.line(format!(" .push_code({name}, {}::migrate)?", migration.module_ident)); + for migration in active_migrations { + match migration { + ActiveMigration::Sql(migration) => { + let name = format!("{:?}", migration.name); + let path = format!("{:?}", rust_path(&migration.path)?); + function.line(format!(" .push_sql({name}, include_str!({path}))?")); + }, + ActiveMigration::Code(migration) => { + let name = format!("{:?}", migration.name); + function + .line(format!(" .push_code({name}, {}::migrate)?", migration.module_ident)); + }, + } } function.line(" .build()"); @@ -277,19 +299,35 @@ fn absolute_path(path: &Path) -> Result { .with_context(|| format!("failed to canonicalize migration path {}", path.display())) } -fn file_name(path: &Path) -> Result { - path.file_name() - .and_then(OsStr::to_str) - .map(str::to_owned) - .with_context(|| format!("migration path has invalid UTF-8 name: {}", path.display())) -} - fn file_stem(path: &Path) -> Result { path.file_stem().and_then(OsStr::to_str).map(str::to_owned).with_context(|| { format!("migration file has invalid UTF-8 stem or no stem: {}", path.display()) }) } +fn migration_prefix<'a>(name: &'a str, path: &Path) -> Result<&'a str> { + let bytes = name.as_bytes(); + ensure!( + bytes.len() > 4 + && bytes[0].is_ascii_digit() + && bytes[1].is_ascii_digit() + && bytes[2].is_ascii_digit() + && bytes[3] == b'_' + && name[4..].chars().any(|ch| ch.is_ascii_alphanumeric()), + "migration file name must start with a three-digit prefix followed by an underscore, e.g. \ + 001_initial: {}", + path.display() + ); + + ensure!( + &name[..3] != "000", + "migration file prefix must start at 001: {}", + path.display() + ); + + Ok(&name[..3]) +} + /// Converts a migration folder name into a Rust module identifier. /// /// The generated identifier is prefixed with `migration_`, ASCII alphanumeric characters are @@ -330,26 +368,29 @@ mod tests { fn renders_migrations_in_lexicographic_order() -> Result<()> { let root = unique_temp_dir("renders_migrations_in_lexicographic_order")?; fs::create_dir_all(root.join("retired"))?; - fs::create_dir_all(root.join("code").join("003_backfill"))?; - fs::write(root.join("retired").join("002_indexes.sql"), "CREATE INDEX idx ON t(id);")?; - fs::write(root.join("retired").join("001_init.sql"), "CREATE TABLE t (id INTEGER);")?; + fs::create_dir_all(root.join("003_backfill"))?; + fs::write(root.join("retired").join("001_legacy.sql"), "CREATE TABLE t (id INTEGER);")?; + fs::write(root.join("002_indexes.sql"), "CREATE INDEX idx ON t(id);")?; fs::write( - root.join("code").join("003_backfill").join(CODE_MIGRATION_FILE), + root.join("003_backfill.rs"), "pub fn migrate(_: &rusqlite::Transaction<'_>) -> anyhow::Result<()> { Ok(()) }", )?; + fs::write(root.join("003_backfill").join("fixture.bin"), "supporting data")?; let retired = discover_retired_migrations(&root)?; - let code = discover_code_migrations(&root)?; - let rendered = render_migrator(&retired, &code)?; + let active = discover_active_migrations(&root)?; + let rendered = render_migrator(&retired, &active)?; - let init = rendered.find("\"001_init\"").expect("init migration is rendered"); + let legacy = rendered.find("\"001_legacy\"").expect("legacy migration is rendered"); let indexes = rendered.find("\"002_indexes\"").expect("index migration is rendered"); let backfill = rendered.find("\"003_backfill\"").expect("code migration is rendered"); - assert!(init < indexes); + assert!(legacy < indexes); assert!(indexes < backfill); assert!(rendered.contains("include_str!(")); assert!(rendered.contains(".push_retired(")); + assert!(rendered.contains(".push_sql(")); + assert!(rendered.contains(".push_code(")); assert!(!rendered.contains(".push_base(")); assert!(rendered.contains("migration_003_backfill::migrate")); assert!(rendered.contains(".build()\n}\n")); @@ -385,34 +426,77 @@ mod tests { } #[test] - fn rejects_code_migration_missing_rust_file() -> Result<()> { - let root = unique_temp_dir("rejects_code_migration_missing_rust_file")?; - fs::create_dir_all(root.join("code").join("001_backfill"))?; + fn rejects_invalid_active_migration_file_extension() -> Result<()> { + let root = unique_temp_dir("rejects_invalid_active_migration_file_extension")?; + fs::write(root.join("001_init.txt"), "CREATE TABLE t (id INTEGER);")?; - let err = discover_code_migrations(&root).expect_err("missing migration.rs should fail"); + let err = discover_active_migrations(&root).expect_err("invalid entry should fail"); - assert!(err.to_string().contains("is missing migration.rs")); + assert!(err.to_string().contains("must use .sql or .rs extension")); fs::remove_dir_all(root)?; Ok(()) } #[test] - fn rejects_code_migration_module_identifier_collisions() -> Result<()> { - let root = unique_temp_dir("rejects_code_migration_module_identifier_collisions")?; - fs::create_dir_all(root.join("code").join("001-backfill"))?; - fs::create_dir_all(root.join("code").join("001_backfill"))?; - fs::write( - root.join("code").join("001-backfill").join(CODE_MIGRATION_FILE), - "pub fn migrate(_: &rusqlite::Transaction<'_>) -> anyhow::Result<()> { Ok(()) }", - )?; - fs::write( - root.join("code").join("001_backfill").join(CODE_MIGRATION_FILE), - "pub fn migrate(_: &rusqlite::Transaction<'_>) -> anyhow::Result<()> { Ok(()) }", - )?; + fn rejects_active_migrations_without_three_digit_prefix() -> Result<()> { + let root = unique_temp_dir("rejects_active_migrations_without_three_digit_prefix")?; + fs::write(root.join("1_init.sql"), "CREATE TABLE t (id INTEGER);")?; + + let err = discover_active_migrations(&root).expect_err("invalid prefix should fail"); + + assert!(err.to_string().contains("three-digit prefix")); + fs::remove_dir_all(root)?; + Ok(()) + } + + #[test] + fn rejects_retired_migrations_without_three_digit_prefix() -> Result<()> { + let root = unique_temp_dir("rejects_retired_migrations_without_three_digit_prefix")?; + fs::create_dir_all(root.join("retired"))?; + fs::write(root.join("retired").join("init.sql"), "CREATE TABLE t (id INTEGER);")?; + + let err = discover_retired_migrations(&root).expect_err("invalid prefix should fail"); + + assert!(err.to_string().contains("three-digit prefix")); + fs::remove_dir_all(root)?; + Ok(()) + } + + #[test] + fn rejects_duplicate_active_migration_prefixes() -> Result<()> { + let root = unique_temp_dir("rejects_duplicate_active_migration_prefixes")?; + fs::write(root.join("001_init.sql"), "CREATE TABLE t (id INTEGER);")?; + fs::write(root.join("001_indexes.sql"), "CREATE INDEX idx ON t(id);")?; + + let err = discover_active_migrations(&root).expect_err("duplicate prefix should fail"); + + assert!(err.to_string().contains("duplicate active migration prefix")); + fs::remove_dir_all(root)?; + Ok(()) + } + + #[test] + fn rejects_duplicate_retired_migration_prefixes() -> Result<()> { + let root = unique_temp_dir("rejects_duplicate_retired_migration_prefixes")?; + fs::create_dir_all(root.join("retired"))?; + fs::write(root.join("retired").join("001_init.sql"), "CREATE TABLE t (id INTEGER);")?; + fs::write(root.join("retired").join("001_indexes.sql"), "CREATE INDEX idx ON t(id);")?; + + let err = discover_retired_migrations(&root).expect_err("duplicate prefix should fail"); + + assert!(err.to_string().contains("duplicate retired migration prefix")); + fs::remove_dir_all(root)?; + Ok(()) + } + + #[test] + fn rejects_zero_migration_prefix() -> Result<()> { + let root = unique_temp_dir("rejects_zero_migration_prefix")?; + fs::write(root.join("000_init.sql"), "CREATE TABLE t (id INTEGER);")?; - let err = discover_code_migrations(&root).expect_err("module collision should fail"); + let err = discover_active_migrations(&root).expect_err("zero prefix should fail"); - assert!(err.to_string().contains("module identifier collision")); + assert!(err.to_string().contains("prefix must start at 001")); fs::remove_dir_all(root)?; Ok(()) } diff --git a/crates/db/src/migration/builder.rs b/crates/db/src/migration/builder.rs index d4afe66a81..ead4ddba16 100644 --- a/crates/db/src/migration/builder.rs +++ b/crates/db/src/migration/builder.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use rusqlite::Connection; -use super::entry::{CodeMigration, CodeMigrationFn, SqlMigration, apply_migration}; +use super::entry::{CodeMigrationFn, Migration, SqlMigration, apply_migration}; use super::{Migrator, SchemaHash}; /// Builds a [`Migrator`] while computing expected schema hashes on an in-memory database. @@ -21,17 +21,21 @@ use super::{Migrator, SchemaHash}; /// "001_create_items", /// "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);", /// )? -/// .push_code("002_add_item_height", add_item_height)? +/// .push_sql("002_index_items", "CREATE INDEX idx_items_value ON items(value);")? +/// .push_code("003_add_item_height", add_item_height)? /// .build() /// } /// -/// const EXPECTED_SCHEMA_HASHES: [SchemaHash; 2] = [ +/// const EXPECTED_SCHEMA_HASHES: [SchemaHash; 3] = [ /// SchemaHash::from_hex( /// "1111111111111111111111111111111111111111111111111111111111111111", /// ), /// SchemaHash::from_hex( /// "2222222222222222222222222222222222222222222222222222222222222222", /// ), +/// SchemaHash::from_hex( +/// "3333333333333333333333333333333333333333333333333333333333333333", +/// ), /// ]; /// /// #[test] @@ -59,8 +63,8 @@ impl MigratorBuilder { /// Adds a pure SQL retired migration. /// - /// Retired migrations initialize fresh databases from SQL that replaces old code migrations. - /// They must be pushed before any code migration. + /// Retired migrations initialize fresh databases from SQL that replaces old active migrations. They + /// must be pushed before any active migration. pub fn push_retired(mut self, name: &'static str, sql: &'static str) -> Result { let version = self.migrator.next_version(); let migration = SqlMigration::new(name, sql); @@ -71,14 +75,25 @@ impl MigratorBuilder { Ok(self) } + /// Adds a SQL migration that remains supported for existing databases. + pub fn push_sql(mut self, name: &'static str, sql: &'static str) -> Result { + let version = self.migrator.next_version(); + let migration = Migration::sql(name, sql); + let hash: SchemaHash = apply_migration(&mut self.reference, version, &migration) + .with_context(|| format!("failed to apply SQL migration {version} \"{name}\""))?; + + self.migrator.push_active_unchecked(migration, hash); + Ok(self) + } + /// Adds a Rust migration function. pub fn push_code(mut self, name: &'static str, apply: CodeMigrationFn) -> Result { let version = self.migrator.next_version(); - let migration = CodeMigration::new(name, apply); + let migration = Migration::code(name, apply); let hash: SchemaHash = apply_migration(&mut self.reference, version, &migration) .with_context(|| format!("failed to apply code migration {version} \"{name}\""))?; - self.migrator.push_code_unchecked(migration, hash); + self.migrator.push_active_unchecked(migration, hash); Ok(self) } @@ -101,11 +116,6 @@ mod tests { Ok(()) } - fn create_items_table(tx: &Transaction<'_>) -> Result<()> { - tx.execute_batch("CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);")?; - Ok(()) - } - #[test] fn empty_builder_returns_error() -> Result<()> { let err = Migrator::builder()?.build().expect_err("empty builder should fail"); @@ -114,12 +124,12 @@ mod tests { } #[test] - #[should_panic(expected = "cannot add retired migration after code migrations have started")] - fn panics_when_adding_retired_after_code() { + #[should_panic(expected = "cannot add retired migration after active migrations have started")] + fn panics_when_adding_retired_after_active_migration() { let _builder = Migrator::builder() .expect("builder should be created") - .push_code("create items", create_items_table) - .expect("code migration should be added") + .push_sql("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);") + .expect("SQL migration should be added") .push_retired("add notes", "CREATE TABLE notes (id INTEGER PRIMARY KEY);"); } @@ -128,6 +138,8 @@ mod tests { let reference = Connection::open_in_memory()?; reference.execute_batch("CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);")?; let retired_hash = SchemaHash::new(&reference)?; + reference.execute_batch("CREATE INDEX idx_items_value ON items(value);")?; + let sql_hash = SchemaHash::new(&reference)?; reference.execute_batch("ALTER TABLE items ADD COLUMN height INTEGER;")?; let final_hash = SchemaHash::new(&reference)?; @@ -136,10 +148,11 @@ mod tests { "create items", "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);", )? + .push_sql("index item values", "CREATE INDEX idx_items_value ON items(value);")? .push_code("add item height", add_item_height)? .build()?; - assert_eq!(migrator.schema_hashes(), &[retired_hash, final_hash]); + assert_eq!(migrator.schema_hashes(), &[retired_hash, sql_hash, final_hash]); Ok(()) } } diff --git a/crates/db/src/migration/entry.rs b/crates/db/src/migration/entry.rs index 7e0206c071..b3f0f9b952 100644 --- a/crates/db/src/migration/entry.rs +++ b/crates/db/src/migration/entry.rs @@ -72,6 +72,47 @@ impl fmt::Debug for CodeMigration { } } +/// An active migration that remains supported for existing databases. +pub(super) enum Migration { + Sql(SqlMigration), + Code(CodeMigration), +} + +impl Migration { + pub(super) fn sql(name: &'static str, sql: &'static str) -> Self { + Self::Sql(SqlMigration::new(name, sql)) + } + + pub(super) fn code(name: &'static str, apply: CodeMigrationFn) -> Self { + Self::Code(CodeMigration::new(name, apply)) + } +} + +impl MigrationEntry for Migration { + fn name(&self) -> &'static str { + match self { + Self::Sql(migration) => migration.name(), + Self::Code(migration) => migration.name(), + } + } + + fn execute_migration(&self, tx: &Transaction<'_>) -> Result<()> { + match self { + Self::Sql(migration) => migration.execute_migration(tx), + Self::Code(migration) => migration.execute_migration(tx), + } + } +} + +impl fmt::Debug for Migration { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Sql(migration) => fmt::Debug::fmt(migration, f), + Self::Code(migration) => fmt::Debug::fmt(migration, f), + } + } +} + /// Applies `migration`, sets `user_version`, commits, and returns the resulting schema hash. pub(super) fn apply_migration( conn: &mut Connection, diff --git a/crates/db/src/migration/migrator.rs b/crates/db/src/migration/migrator.rs index 13e691fe06..61cef874aa 100644 --- a/crates/db/src/migration/migrator.rs +++ b/crates/db/src/migration/migrator.rs @@ -1,39 +1,36 @@ +use std::path::Path; + use anyhow::{Context, Result, bail, ensure}; use rusqlite::Connection; -use super::entry::{ - CodeMigration, - MigrationEntry, - SqlMigration, - apply_migration_and_verify_schema, -}; +use super::entry::{Migration, MigrationEntry, SqlMigration, apply_migration_and_verify_schema}; use super::{MigratorBuilder, SchemaHash, schema}; /// Applies versioned database migrations. /// -/// A migrator is built from two ordered migration sets: retired SQL migrations followed by code -/// migrations. Retired migrations are pure SQL snapshots of older migrations whose schema we -/// retain, but whose Rust migration code we no longer want to support. Because that old migration -/// path is intentionally unsupported, retired migrations are only applied when creating a new -/// database whose `PRAGMA user_version` is zero. Existing databases are never allowed to run only -/// part of the retired SQL set; once a database has a non-zero version, it must already be at or -/// beyond the end of the retired migrations. +/// A migrator is built from two ordered migration sets: retired SQL migrations followed by active +/// migrations. Retired migrations are pure SQL snapshots of older active migrations whose schema we +/// retain, but whose upgrade path we no longer want to support. Because that old migration path is +/// intentionally unsupported, retired migrations are only applied when creating a new database whose +/// `PRAGMA user_version` is zero. Existing databases are never allowed to run only part of the +/// retired SQL set; once a database has a non-zero version, it must already be at or beyond the end +/// of the retired migrations. /// -/// Code migrations run after the retired SQL set. For existing databases, the migrator reads -/// `user_version`, verifies that the current schema hash matches the expected hash for that -/// version, and then applies only the missing code migrations. Each migration runs in its own -/// transaction and commits only after the resulting schema hash matches the hash computed by the -/// builder. +/// Active migrations run after the retired SQL set and can be pure SQL or Rust functions. For +/// existing databases, the migrator reads `user_version`, verifies that the current schema hash +/// matches the expected hash for that version, and then applies only the missing active migrations. +/// Each migration runs in its own transaction and commits only after the resulting schema hash +/// matches the hash computed by the builder. /// -/// Construct a migrator with [`Migrator::builder`] by pushing retired migrations first and code +/// Construct a migrator with [`Migrator::builder`] by pushing retired migrations first and active /// migrations second, or call [`Migrator::generate`] from a `build.rs` to generate that builder /// chain from a migration directory. Callers should snapshot [`Migrator::schema_hashes`] in tests -/// so accidental schema changes are caught, especially when replacing a code migration with +/// so accidental schema changes are caught, especially when replacing active migrations with /// equivalent retired SQL. #[derive(Debug)] pub struct Migrator { retired_migrations: Vec, - code_migrations: Vec, + active_migrations: Vec, expected_schema_hashes: Vec, } @@ -45,7 +42,7 @@ impl Migrator { pub(super) fn empty() -> Self { Self { retired_migrations: Vec::new(), - code_migrations: Vec::new(), + active_migrations: Vec::new(), expected_schema_hashes: Vec::new(), } } @@ -58,7 +55,7 @@ impl Migrator { /// Returns the version number that will be assigned to the next migration. /// /// Versions are one-based and follow insertion order across retired migrations first and then - /// code migrations. + /// active migrations. pub(super) fn next_version(&self) -> usize { self.expected_schema_hashes.len() + 1 } @@ -67,7 +64,7 @@ impl Migrator { /// /// This is used by [`MigratorBuilder`] after it has already applied the migration to its /// in-memory reference database. The caller must ensure `schema_hash` is the hash of that - /// reference database after this migration. Retired migrations must be added before any code + /// reference database after this migration. Retired migrations must be added before any active /// migration. pub(super) fn push_retired_unchecked( &mut self, @@ -75,24 +72,20 @@ impl Migrator { schema_hash: SchemaHash, ) { assert!( - self.code_migrations.is_empty(), - "cannot add retired migration after code migrations have started" + self.active_migrations.is_empty(), + "cannot add retired migration after active migrations have started" ); self.retired_migrations.push(migration); self.expected_schema_hashes.push(schema_hash); } - /// Adds a code migration and the schema hash expected after it runs. + /// Adds an active migration and the schema hash expected after it runs. /// /// This is used by [`MigratorBuilder`] after it has already applied the migration to its /// in-memory reference database. The caller must ensure `schema_hash` is the hash of that /// reference database after this migration. - pub(super) fn push_code_unchecked( - &mut self, - migration: CodeMigration, - schema_hash: SchemaHash, - ) { - self.code_migrations.push(migration); + pub(super) fn push_active_unchecked(&mut self, migration: Migration, schema_hash: SchemaHash) { + self.active_migrations.push(migration); self.expected_schema_hashes.push(schema_hash); } @@ -101,7 +94,7 @@ impl Migrator { /// A migrator must contain at least one migration and must have exactly one expected schema /// hash for each migration. pub(super) fn validate(&self) -> Result<()> { - let migration_count = self.retired_migrations.len() + self.code_migrations.len(); + let migration_count = self.retired_migrations.len() + self.active_migrations.len(); ensure!( !self.expected_schema_hashes.is_empty(), "cannot build migrator without migrations" @@ -116,20 +109,28 @@ impl Migrator { /// Returns the schema hashes expected after each migration. /// - /// Callers can use these hashes in tests when retiring a code migration into SQL: the + /// Callers can use these hashes in tests when retiring active migrations into SQL: the /// replacement SQL should produce the same hash at the same migration index. pub fn schema_hashes(&self) -> &[SchemaHash] { &self.expected_schema_hashes } - /// Applies missing migrations to `conn`. + /// Applies missing migrations to the database at `database_filepath`. /// /// New databases, where `PRAGMA user_version` is zero, receive all retired migrations followed - /// by all code migrations. Existing databases must already be past the retired migration range; - /// only missing code migrations are applied. Every migration runs in its own transaction, + /// by all active migrations. Existing databases must already be past the retired migration + /// range; only missing active migrations are applied. Every migration runs in its own transaction, /// updates `user_version`, and commits only after the resulting schema hash matches the /// expected hash. - pub fn migrate(&self, conn: &mut Connection) -> Result<()> { + pub fn migrate(&self, database_filepath: impl AsRef) -> Result<()> { + let database_filepath = database_filepath.as_ref(); + let mut conn = Connection::open(database_filepath) + .with_context(|| format!("failed to open database {}", database_filepath.display()))?; + + self.migrate_connection(&mut conn) + } + + fn migrate_connection(&self, conn: &mut Connection) -> Result<()> { let current_version = self.version_check(conn)?; let retired_versions = self.retired_migrations.len(); @@ -142,8 +143,8 @@ impl Migrator { } } - let code_start = applied_version.saturating_sub(retired_versions); - for (idx, migration) in self.code_migrations.iter().enumerate().skip(code_start) { + let active_start = applied_version.saturating_sub(retired_versions); + for (idx, migration) in self.active_migrations.iter().enumerate().skip(active_start) { let version = retired_versions + idx + 1; self.apply_migration(conn, version, migration)?; } @@ -221,7 +222,7 @@ impl Migrator { return Some(self.retired_migrations[version - 1].name()); } - self.code_migrations + self.active_migrations .get(version - self.retired_migrations.len() - 1) .map(MigrationEntry::name) } @@ -229,6 +230,8 @@ impl Migrator { #[cfg(test)] mod tests { + use std::path::{Path, PathBuf}; + use anyhow::Result; use rusqlite::{Connection, Transaction}; @@ -266,6 +269,40 @@ mod tests { Ok(exists) } + struct TestDatabase { + path: PathBuf, + } + + impl TestDatabase { + fn new(name: &str) -> Self { + let path = std::env::temp_dir() + .join(format!("miden-node-db-migrator-{name}-{}.sqlite3", std::process::id())); + let db = Self { path }; + db.remove_files(); + db + } + + fn path(&self) -> &Path { + &self.path + } + + fn open(&self) -> Result { + Connection::open(&self.path).map_err(Into::into) + } + + fn remove_files(&self) { + let _ = fs_err::remove_file(&self.path); + let _ = fs_err::remove_file(self.path.with_extension("sqlite3-wal")); + let _ = fs_err::remove_file(self.path.with_extension("sqlite3-shm")); + } + } + + impl Drop for TestDatabase { + fn drop(&mut self) { + self.remove_files(); + } + } + #[test] fn migrates_new_database_through_retired_and_code() -> Result<()> { let migrator = Migrator::builder()? @@ -276,9 +313,10 @@ mod tests { .push_code("add item height", add_item_height)? .build()?; - let mut conn = Connection::open_in_memory()?; - migrator.migrate(&mut conn)?; + let db = TestDatabase::new("migrates_new_database_through_retired_and_code"); + migrator.migrate(db.path())?; + let conn = db.open()?; assert_eq!(schema::get_version(&conn)?, 2); conn.execute("INSERT INTO items (id, value, height) VALUES (1, 'a', 10)", [])?; Ok(()) @@ -289,9 +327,25 @@ mod tests { let migrator = Migrator::builder()?.push_code("create items", create_items_table)?.build()?; - let mut conn = Connection::open_in_memory()?; - migrator.migrate(&mut conn)?; + let db = TestDatabase::new("migrates_new_database_with_code_only_migration"); + migrator.migrate(db.path())?; + + let conn = db.open()?; + assert_eq!(schema::get_version(&conn)?, 1); + conn.execute("INSERT INTO items (id, value) VALUES (1, 'a')", [])?; + Ok(()) + } + + #[test] + fn migrates_new_database_with_sql_only_migration() -> Result<()> { + let migrator = Migrator::builder()? + .push_sql("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT);")? + .build()?; + + let db = TestDatabase::new("migrates_new_database_with_sql_only_migration"); + migrator.migrate(db.path())?; + let conn = db.open()?; assert_eq!(schema::get_version(&conn)?, 1); conn.execute("INSERT INTO items (id, value) VALUES (1, 'a')", [])?; Ok(()) @@ -307,14 +361,18 @@ mod tests { .push_code("index item values", add_items_index)? .build()?; - let mut conn = Connection::open_in_memory()?; - conn.execute_batch( - "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT); - PRAGMA user_version = 1;", - )?; + let db = TestDatabase::new("applies_missing_code_migrations_to_existing_database"); + { + let conn = db.open()?; + conn.execute_batch( + "CREATE TABLE items (id INTEGER PRIMARY KEY, value TEXT); + PRAGMA user_version = 1;", + )?; + } - migrator.migrate(&mut conn)?; + migrator.migrate(db.path())?; + let conn = db.open()?; assert_eq!(schema::get_version(&conn)?, 2); assert!(object_exists(&conn, "idx_items_value")?); Ok(()) @@ -327,13 +385,16 @@ mod tests { .push_retired("create notes", "CREATE TABLE notes (id INTEGER PRIMARY KEY);")? .build()?; - let mut conn = Connection::open_in_memory()?; - conn.execute_batch( - "CREATE TABLE items (id INTEGER PRIMARY KEY); - PRAGMA user_version = 1;", - )?; + let db = TestDatabase::new("rejects_existing_database_inside_retired_migration_range"); + { + let conn = db.open()?; + conn.execute_batch( + "CREATE TABLE items (id INTEGER PRIMARY KEY); + PRAGMA user_version = 1;", + )?; + } - let err = migrator.migrate(&mut conn).expect_err("migration should fail"); + let err = migrator.migrate(db.path()).expect_err("migration should fail"); assert!(err.to_string().contains("inside the retired migration range")); Ok(()) } @@ -344,11 +405,14 @@ mod tests { .push_retired("create items", "CREATE TABLE items (id INTEGER PRIMARY KEY);")? .build()?; - let mut conn = Connection::open_in_memory()?; - migrator.migrate(&mut conn)?; - conn.execute_batch("CREATE TABLE tampered (id INTEGER PRIMARY KEY);")?; + let db = TestDatabase::new("verifies_current_schema_before_applying_missing_migrations"); + migrator.migrate(db.path())?; + { + let conn = db.open()?; + conn.execute_batch("CREATE TABLE tampered (id INTEGER PRIMARY KEY);")?; + } - let err = migrator.migrate(&mut conn).expect_err("migration should fail"); + let err = migrator.migrate(db.path()).expect_err("migration should fail"); assert!(err.to_string().contains("schema hash mismatch at database version 1")); Ok(()) } @@ -360,16 +424,21 @@ mod tests { .push_code("conditionally create extra", create_extra_table_when_items_exist)? .build()?; - let mut conn = Connection::open_in_memory()?; - conn.execute_batch( - "CREATE TABLE items (id INTEGER PRIMARY KEY); - INSERT INTO items (id) VALUES (1); - PRAGMA user_version = 1;", - )?; + let db = TestDatabase::new("rolls_back_code_migration_when_schema_hash_mismatches"); + { + let conn = db.open()?; + conn.execute_batch( + "CREATE TABLE items (id INTEGER PRIMARY KEY); + INSERT INTO items (id) VALUES (1); + PRAGMA user_version = 1;", + )?; + } - let err = migrator.migrate(&mut conn).expect_err("migration should fail"); + let err = migrator.migrate(db.path()).expect_err("migration should fail"); assert!(err.to_string().contains("failed to apply migration 2")); assert!(err.chain().any(|cause| cause.to_string().contains("schema hash mismatch"))); + + let conn = db.open()?; assert_eq!(schema::get_version(&conn)?, 1); assert!(!object_exists(&conn, "unexpected")?); Ok(()) diff --git a/crates/db/src/migration/mod.rs b/crates/db/src/migration/mod.rs index b41dc0d17c..86fbf0ff3d 100644 --- a/crates/db/src/migration/mod.rs +++ b/crates/db/src/migration/mod.rs @@ -1,10 +1,10 @@ //! Provides a framework for SQLite migrations. //! //! Migrations are built as an ordered [`Migrator`] with two phases. Retired migrations are retained -//! as pure SQL and are only used to initialize fresh databases. Code migrations are Rust functions -//! that run after the retired SQL set and remain supported for existing databases. This lets old -//! code migrations eventually be converted into retired SQL once their upgrade path no longer needs -//! to be supported. +//! as pure SQL and are only used to initialize fresh databases. Active migrations run after the +//! retired SQL set, remain supported for existing databases, and can be pure SQL or Rust functions. +//! This lets old active migrations eventually be converted into retired SQL once their upgrade path +//! no longer needs to be supported. //! //! The database version is stored in SQLite's `PRAGMA user_version`. Each migration also has an //! expected [`SchemaHash`] computed by applying migrations to an in-memory reference database @@ -14,7 +14,7 @@ //! Build migrators manually with [`Migrator::builder`], or generate one from a migration directory //! with [`Migrator::generate`] in a `build.rs`. Callers should snapshot [`Migrator::schema_hashes`] //! in tests to catch accidental schema drift and to prove that retired SQL still produces the same -//! schema as the code migration it replaced. +//! schema as the active migrations it replaced. mod build_script; mod builder; diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 2c72ffc7ff..44edd5357f 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -23,7 +23,6 @@ async-trait = { workspace = true } deadpool = { features = ["managed", "rt_tokio_1"], workspace = true } deadpool-diesel = { features = ["sqlite"], workspace = true } diesel = { features = ["numeric", "sqlite"], workspace = true } -diesel_migrations = { features = ["sqlite"], workspace = true } fs-err = { workspace = true } hex = { workspace = true } indexmap = { workspace = true } @@ -55,8 +54,9 @@ tracing = { workspace = true } url = { workspace = true } [build-dependencies] -build-rs = { workspace = true } -fs-err = { workspace = true } +build-rs = { workspace = true } +fs-err = { workspace = true } +miden-node-db = { workspace = true } # TODO: consider removing the `testing` from relevant functions in miden-agglayer miden-agglayer = { features = ["testing"], workspace = true } miden-protocol = { features = ["std"], workspace = true } diff --git a/crates/store/build.rs b/crates/store/build.rs index 09bf745d32..5df52daa5b 100644 --- a/crates/store/build.rs +++ b/crates/store/build.rs @@ -1,6 +1,3 @@ -// This build.rs is required to trigger the `diesel_migrations::embed_migrations!` proc-macro in -// `store/src/db/migrations.rs` to include the latest version of the migrations into the binary, see . - use std::path::PathBuf; use std::sync::Arc; @@ -18,8 +15,9 @@ use miden_protocol::{Felt, Word}; use miden_standards::AuthMethod; use miden_standards::account::wallets::create_basic_wallet; -fn main() { - build_rs::output::rerun_if_changed("src/db/migrations"); +fn main() -> Result<(), Box> { + miden_node_db::migration::Migrator::generate("src/db/migrations")?; + // If we do one re-write, the default rules are disabled, // hence we need to trigger explicitly on `Cargo.toml`. // @@ -27,6 +25,8 @@ fn main() { // Generate sample agglayer account files for genesis config samples. generate_agglayer_sample_accounts(); + + Ok(()) } /// Generates sample agglayer account files for the `02-with-account-files` genesis config sample. diff --git a/crates/store/src/db/migrations.rs b/crates/store/src/db/migrations.rs index 10ce01409e..c9ee04ebff 100644 --- a/crates/store/src/db/migrations.rs +++ b/crates/store/src/db/migrations.rs @@ -1,31 +1,93 @@ -use diesel::SqliteConnection; -use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; +use std::path::Path; + +use miden_node_db::DatabaseError; use tracing::instrument; use crate::COMPONENT; -use crate::db::schema_hash::verify_schema; -// The rebuild is automatically triggered by `build.rs` as described in -// . -pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("src/db/migrations"); +include!(concat!(env!("OUT_DIR"), "/db_migrator.rs")); -// TODO we have not tested this in practice! #[instrument(level = "debug", target = COMPONENT, skip_all, err)] -pub fn apply_migrations( - conn: &mut SqliteConnection, -) -> std::result::Result<(), miden_node_db::DatabaseError> { - let migrations = conn.pending_migrations(MIGRATIONS).expect("In memory migrations never fail"); - tracing::info!(target = COMPONENT, migrations = migrations.len(), "Applying migrations"); - - let Err(e) = conn.run_pending_migrations(MIGRATIONS) else { - // Migrations applied successfully, verify schema hash - verify_schema(conn)?; - return Ok(()); - }; - tracing::warn!(target = COMPONENT, error = ?e, "Failed to apply migration"); - // something went wrong, MIGRATIONS contains - conn.revert_last_migration(MIGRATIONS) - .expect("Duality is maintained by the developer"); +pub fn apply_migrations(database_filepath: &Path) -> std::result::Result<(), DatabaseError> { + let migrator = migrator().map_err(DatabaseError::migration)?; + tracing::info!( + target: COMPONENT, + migration_count = migrator.schema_hashes().len(), + "Applying database migrations" + ); + + migrator.migrate(database_filepath).map_err(DatabaseError::migration)?; Ok(()) } + +#[cfg(test)] +pub(crate) fn test_connection() -> diesel::SqliteConnection { + use diesel::{Connection, SqliteConnection}; + + let database_filepath = tempfile::NamedTempFile::new() + .expect("failed to create temp database file") + .into_temp_path(); + apply_migrations(&database_filepath).expect("migrations should apply on empty database"); + + let conn = SqliteConnection::establish( + database_filepath.to_str().expect("temp database path should be valid UTF-8"), + ) + .expect("temp file sqlite should always work"); + database_filepath + .keep() + .expect("failed to keep temp database file for test connection"); + conn +} + +#[cfg(test)] +mod tests { + use std::process::Command; + + use anyhow::{Context, Result, ensure}; + use miden_node_db::migration::SchemaHash; + + use super::*; + + const EXPECTED_SCHEMA_HASHES: [SchemaHash; 1] = [SchemaHash::from_hex( + "b3bdd2e530fbb66c9146cda9f3bf79df49c6a6bf99f7432aae0a8927a15406ac", + )]; + + #[test] + fn migration_schema_hashes_are_stable() -> Result<()> { + let migrator = migrator()?; + + assert_eq!(migrator.schema_hashes(), &EXPECTED_SCHEMA_HASHES); + Ok(()) + } + + #[test] + #[ignore = "requires diesel CLI; CI runs this in the diesel-schema job"] + fn diesel_schema_is_in_sync_with_migrations() -> Result<()> { + let temp_dir = tempfile::tempdir()?; + let database_filepath = temp_dir.path().join("store.sqlite3"); + apply_migrations(&database_filepath)?; + + let output = Command::new("diesel") + .arg("print-schema") + .arg("--database-url") + .arg(&database_filepath) + .current_dir(env!("CARGO_MANIFEST_DIR")) + .output() + .context( + "failed to run diesel CLI; install it with \ + `cargo install diesel_cli --no-default-features --features sqlite`", + )?; + + ensure!( + output.status.success(), + "diesel print-schema failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let generated = + String::from_utf8(output.stdout).context("diesel CLI output is not UTF-8")?; + assert_eq!(generated, include_str!("schema.rs")); + Ok(()) + } +} diff --git a/crates/store/src/db/migrations/2025062000000_setup/up.sql b/crates/store/src/db/migrations/001_initial.sql similarity index 76% rename from crates/store/src/db/migrations/2025062000000_setup/up.sql rename to crates/store/src/db/migrations/001_initial.sql index 2f8538d988..f0b4a8fb89 100644 --- a/crates/store/src/db/migrations/2025062000000_setup/up.sql +++ b/crates/store/src/db/migrations/001_initial.sql @@ -1,18 +1,13 @@ CREATE TABLE block_headers ( - block_num INTEGER NOT NULL, - block_header BLOB NOT NULL, - signature BLOB NOT NULL, - commitment BLOB NOT NULL, - proving_inputs BLOB, -- Serialized BlockProofRequest needed for deferred proving. NULL if it has been proven or never proven (genesis block). - proven_in_sequence BOOLEAN NOT NULL DEFAULT FALSE, -- TRUE when this block and all its ancestors have been proven. + block_num BIGINT NOT NULL, + block_header BLOB NOT NULL, + signature BLOB NOT NULL, + commitment BLOB NOT NULL, PRIMARY KEY (block_num), CONSTRAINT block_header_block_num_is_u32 CHECK (block_num BETWEEN 0 AND 0xFFFFFFFF) ); -CREATE INDEX block_headers_proven_desc ON block_headers(block_num DESC) WHERE proving_inputs IS NULL; -CREATE INDEX block_headers_proven_in_sequence ON block_headers(block_num DESC) WHERE proven_in_sequence = TRUE; - CREATE TABLE account_codes ( code_commitment BLOB NOT NULL, code BLOB NOT NULL, @@ -22,14 +17,14 @@ CREATE TABLE account_codes ( CREATE TABLE accounts ( account_id BLOB NOT NULL, network_account_type INTEGER NOT NULL, -- 0-not a network account, 1-network account - block_num INTEGER NOT NULL, + block_num BIGINT NOT NULL, account_commitment BLOB NOT NULL, code_commitment BLOB, - nonce INTEGER, + nonce BIGINT, storage_header BLOB, -- Serialized AccountStorageHeader from miden-objects vault_root BLOB, -- Vault root commitment is_latest BOOLEAN NOT NULL DEFAULT 0, -- Indicates if this is the latest state for this account_id - created_at_block INTEGER NOT NULL, + created_at_block BIGINT NOT NULL, PRIMARY KEY (account_id, block_num), CONSTRAINT all_null_or_none_null CHECK @@ -49,11 +44,15 @@ CREATE INDEX idx_accounts_created_at_block ON accounts(created_at_block); CREATE INDEX idx_accounts_block_num ON accounts(block_num); -- Index for joining with account_codes CREATE INDEX idx_accounts_code_commitment ON accounts(code_commitment) WHERE code_commitment IS NOT NULL; --- Covering index for the prune_account_codes subquery: filters rows by block_num/is_latest and projects code_commitment -CREATE INDEX idx_accounts_prune_code ON accounts(block_num, is_latest, code_commitment) WHERE code_commitment IS NOT NULL; +-- Covering index for the prune_account_codes recent-history branch. +CREATE INDEX idx_accounts_prune_code ON accounts(block_num, code_commitment) WHERE code_commitment IS NOT NULL; +-- Covering index for the prune_account_codes latest-state branch. +CREATE INDEX idx_accounts_latest_code_commitment + ON accounts(code_commitment) + WHERE is_latest = 1 AND code_commitment IS NOT NULL; CREATE TABLE notes ( - committed_at INTEGER NOT NULL, -- Block number when the note was committed + committed_at BIGINT NOT NULL, -- Block number when the note was committed batch_index INTEGER NOT NULL, -- Index of batch in block, starting from 0 note_index INTEGER NOT NULL, -- Index of note in batch, starting from 0 note_id BLOB NOT NULL, @@ -65,7 +64,7 @@ CREATE TABLE notes ( target_account_id BLOB, -- Full target account ID for single-target network notes attachment BLOB NOT NULL, -- Serialized note attachment data inclusion_path BLOB NOT NULL, -- Serialized sparse Merkle path of the note in the block's note tree - consumed_at INTEGER, -- Block number when the note was consumed + consumed_at BIGINT, -- Block number when the note was consumed nullifier BLOB, -- Only known for public notes, null for private notes assets BLOB, storage BLOB, @@ -102,7 +101,7 @@ CREATE TABLE note_scripts ( CREATE TABLE account_storage_map_values ( account_id BLOB NOT NULL, - block_num INTEGER NOT NULL, + block_num BIGINT NOT NULL, slot_name TEXT NOT NULL, key BLOB NOT NULL, value BLOB NOT NULL, @@ -119,7 +118,7 @@ CREATE INDEX idx_account_storage_latest ON account_storage_map_values(account_id CREATE TABLE account_vault_assets ( account_id BLOB NOT NULL, - block_num INTEGER NOT NULL, + block_num BIGINT NOT NULL, vault_key BLOB NOT NULL, asset BLOB, is_latest BOOLEAN NOT NULL, @@ -136,7 +135,7 @@ CREATE INDEX idx_vault_assets_latest ON account_vault_assets(account_id, is_late CREATE TABLE nullifiers ( nullifier BLOB NOT NULL, nullifier_prefix INTEGER NOT NULL, - block_num INTEGER NOT NULL, + block_num BIGINT NOT NULL, PRIMARY KEY (nullifier), CONSTRAINT nullifiers_nullifier_is_digest CHECK (length(nullifier) = 32), @@ -148,15 +147,15 @@ CREATE INDEX idx_nullifiers_prefix ON nullifiers(nullifier_prefix); CREATE INDEX idx_nullifiers_block_num ON nullifiers(block_num); CREATE TABLE transactions ( - transaction_id BLOB NOT NULL, - account_id BLOB NOT NULL, - block_num INTEGER NOT NULL, -- Block number in which the transaction was included. - initial_state_commitment BLOB NOT NULL, -- State of the account before applying the transaction. - final_state_commitment BLOB NOT NULL, -- State of the account after applying the transaction. - input_notes BLOB NOT NULL, -- Serialized Vec (nullifier + optional NoteHeader). - output_notes BLOB NOT NULL, -- Serialized Vec (NoteId + NoteMetadata). - size_in_bytes INTEGER NOT NULL, -- Estimated size of the row in bytes, considering the size of the input and output notes. - fee BLOB NOT NULL, -- Serialized FungibleAsset representing the fee paid by the transaction. + transaction_id BLOB NOT NULL, + account_id BLOB NOT NULL, + block_num BIGINT NOT NULL, -- Block number in which the transaction was included. + initial_state_commitment BLOB NOT NULL, -- State of the account before applying the transaction. + final_state_commitment BLOB NOT NULL, -- State of the account after applying the transaction. + input_notes BLOB NOT NULL, -- Serialized Vec (nullifier + optional NoteHeader). + output_notes BLOB NOT NULL, -- Serialized Vec (NoteId + NoteMetadata). + size_in_bytes BIGINT NOT NULL, -- Estimated size of the row in bytes, considering the size of the input and output notes. + fee BLOB NOT NULL, -- Serialized FungibleAsset representing the fee paid by the transaction. PRIMARY KEY (transaction_id) ) WITHOUT ROWID; @@ -165,6 +164,9 @@ CREATE TABLE transactions ( CREATE INDEX idx_transactions_account_id ON transactions(account_id); -- Index for joining with block_headers CREATE INDEX idx_transactions_block_num ON transactions(block_num); +-- Composite index to speed up select_transactions_records. +CREATE INDEX idx_transactions_account_block_txid + ON transactions(account_id, block_num, transaction_id); CREATE INDEX idx_vault_cleanup ON account_vault_assets(block_num) WHERE is_latest = 0; CREATE INDEX idx_storage_cleanup ON account_storage_map_values(block_num) WHERE is_latest = 0; diff --git a/crates/store/src/db/migrations/2025062000000_setup/down.sql b/crates/store/src/db/migrations/2025062000000_setup/down.sql deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/store/src/db/migrations/2026042800000_add_idx_transactions_sync/down.sql b/crates/store/src/db/migrations/2026042800000_add_idx_transactions_sync/down.sql deleted file mode 100644 index b9ea916663..0000000000 --- a/crates/store/src/db/migrations/2026042800000_add_idx_transactions_sync/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS idx_transactions_account_block_txid; diff --git a/crates/store/src/db/migrations/2026042800000_add_idx_transactions_sync/up.sql b/crates/store/src/db/migrations/2026042800000_add_idx_transactions_sync/up.sql deleted file mode 100644 index 4a84a1a830..0000000000 --- a/crates/store/src/db/migrations/2026042800000_add_idx_transactions_sync/up.sql +++ /dev/null @@ -1,13 +0,0 @@ --- Composite index to speed up select_transactions_records. --- --- The query filters by account_id (IN), block_num range, and paginates via a --- (block_num, transaction_id) cursor, then orders by (block_num, transaction_id). --- The existing idx_transactions_account_id index only covers account_id, forcing --- a full scan over matching rows to apply the block_num filter and sort. --- --- With (account_id, block_num, transaction_id) SQLite can: --- 1. Seek to each account_id bucket directly, --- 2. Range-scan block_num within that bucket, and --- 3. Use transaction_id for cursor comparison and ORDER BY — all index-only. -CREATE INDEX idx_transactions_account_block_txid - ON transactions(account_id, block_num, transaction_id); diff --git a/crates/store/src/db/migrations/2026051200000_remove_proving_inputs/down.sql b/crates/store/src/db/migrations/2026051200000_remove_proving_inputs/down.sql deleted file mode 100644 index 62614bdd90..0000000000 --- a/crates/store/src/db/migrations/2026051200000_remove_proving_inputs/down.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Restore proving_inputs and proven_in_sequence columns (data is not recovered). -ALTER TABLE block_headers ADD COLUMN proving_inputs BLOB; -ALTER TABLE block_headers ADD COLUMN proven_in_sequence BOOLEAN NOT NULL DEFAULT FALSE; -CREATE INDEX block_headers_proven_desc ON block_headers(block_num DESC) WHERE proving_inputs IS NULL; -CREATE INDEX block_headers_proven_in_sequence ON block_headers(block_num DESC) WHERE proven_in_sequence = TRUE; diff --git a/crates/store/src/db/migrations/2026051200000_remove_proving_inputs/up.sql b/crates/store/src/db/migrations/2026051200000_remove_proving_inputs/up.sql deleted file mode 100644 index 1591dd0f48..0000000000 --- a/crates/store/src/db/migrations/2026051200000_remove_proving_inputs/up.sql +++ /dev/null @@ -1,12 +0,0 @@ --- Move proving inputs out of the database. --- --- Proving inputs are large BLOBs that only serve the proof scheduler; they now live as --- `inputs_.dat` files in the block store alongside block data and proofs. --- The proven-in-sequence tip is tracked by a small `proven_tip` file in the data directory. --- --- Drop indexes that reference the columns being removed first (required by SQLite before DROP --- COLUMN can succeed for indexed columns). -DROP INDEX block_headers_proven_desc; -DROP INDEX block_headers_proven_in_sequence; -ALTER TABLE block_headers DROP COLUMN proving_inputs; -ALTER TABLE block_headers DROP COLUMN proven_in_sequence; diff --git a/crates/store/src/db/migrations/2026051800000_add_idx_accounts_latest_code_commitment/down.sql b/crates/store/src/db/migrations/2026051800000_add_idx_accounts_latest_code_commitment/down.sql deleted file mode 100644 index ce0a67d4ec..0000000000 --- a/crates/store/src/db/migrations/2026051800000_add_idx_accounts_latest_code_commitment/down.sql +++ /dev/null @@ -1,6 +0,0 @@ -DROP INDEX IF EXISTS idx_accounts_latest_code_commitment; -DROP INDEX IF EXISTS idx_accounts_prune_code; - -CREATE INDEX idx_accounts_prune_code - ON accounts(block_num, is_latest, code_commitment) - WHERE code_commitment IS NOT NULL; diff --git a/crates/store/src/db/migrations/2026051800000_add_idx_accounts_latest_code_commitment/up.sql b/crates/store/src/db/migrations/2026051800000_add_idx_accounts_latest_code_commitment/up.sql deleted file mode 100644 index d406a82b52..0000000000 --- a/crates/store/src/db/migrations/2026051800000_add_idx_accounts_latest_code_commitment/up.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Backfill/reshape the recent-history covering index for prune_account_codes. The old definition --- included is_latest, but the split recent-history branch only filters by block_num and projects --- code_commitment. -DROP INDEX IF EXISTS idx_accounts_prune_code; -CREATE INDEX idx_accounts_prune_code - ON accounts(block_num, code_commitment) - WHERE code_commitment IS NOT NULL; - --- Covering partial index for prune_account_codes. --- --- prune_account_codes keeps account code rows that are referenced either by recent account history --- or by the latest account state. The recent-history branch is covered by idx_accounts_prune_code. --- This index covers the latest-state branch without scanning historical public account rows. -CREATE INDEX idx_accounts_latest_code_commitment - ON accounts(code_commitment) - WHERE is_latest = 1 AND code_commitment IS NOT NULL; diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index 965ef439d9..306f4a0e16 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -52,7 +52,6 @@ fn default_storage_map_entries_limit() -> usize { } mod migrations; -mod schema_hash; #[cfg(test)] mod tests; @@ -61,23 +60,8 @@ pub(crate) mod models; /// [diesel](https://diesel.rs) generated schema /// -/// ```sh -/// cargo binstall diesel_cli -/// sqlite3 -init ./src/db/migrations/001-init.sql ephemeral_setup.db "" -/// diesel setup --database-url=./ephemeral_setup.db -/// diesel print-schema > src/db/schema.rs -/// ``` -/// -/// which assumes an _existing_ database. -/// -/// Unfortunately, there is no systematic way of modifying the schema other -/// than patching (in the diff sense) which is brittle at best. -/// So the above must be followed by a manual editing step, for now it's -/// limited to: -/// -/// * `i64`/`u64` being represented as `BigInt` -/// -/// The list might be extended. +/// The ignored `diesel_schema_is_in_sync_with_migrations` test verifies that this file matches the +/// schema produced by the current migrations. pub(crate) mod schema; pub type Result = std::result::Result; @@ -261,11 +245,11 @@ impl Db { err, )] pub fn bootstrap(database_filepath: PathBuf, genesis: GenesisBlock) -> anyhow::Result<()> { - // Create database. - // - // This will create the file if it does not exist, but will also happily open it if already - // exists. In the latter case we will error out when attempting to insert the genesis - // block so this isn't such a problem. + apply_migrations(&database_filepath).context("failed to apply database migrations")?; + + // Open the database. This will create the file if it does not exist, but will also happily + // open it if already exists. In the latter case we will error out when attempting to insert + // the genesis block so this isn't such a problem. let mut conn: SqliteConnection = diesel::sqlite::SqliteConnection::establish( database_filepath.to_str().context("database filepath is invalid")?, ) @@ -273,9 +257,6 @@ impl Db { miden_node_db::configure_connection_on_creation(&mut conn)?; - // Run migrations. - apply_migrations(&mut conn).context("failed to apply database migrations")?; - // Insert genesis block data. let genesis_block = genesis.into_inner(); conn.transaction(move |conn| models::queries::apply_block(conn, &genesis_block, &[])) @@ -296,6 +277,8 @@ impl Db { database_filepath: PathBuf, connection_pool_size: NonZeroUsize, ) -> Result { + apply_migrations(&database_filepath)?; + let db = miden_node_db::Db::new_with_pool_size(&database_filepath, connection_pool_size)?; info!( target: COMPONENT, @@ -304,7 +287,6 @@ impl Db { "Connected to the database" ); - db.query("migrations", apply_migrations).await?; Ok(Self { db }) } diff --git a/crates/store/src/db/models/queries/accounts.rs b/crates/store/src/db/models/queries/accounts.rs index 53dacbeab3..c7b7464a1e 100644 --- a/crates/store/src/db/models/queries/accounts.rs +++ b/crates/store/src/db/models/queries/accounts.rs @@ -10,6 +10,8 @@ use diesel::{ BoolExpressionMethods, ExpressionMethods, Insertable, + JoinOnDsl, + NullableExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, @@ -176,15 +178,17 @@ pub(crate) fn select_full_account( account_id: AccountId, ) -> Result { // Get account metadata (nonce, code_commitment) and code in a single join query - let (nonce, code_bytes): (Option, Vec) = SelectDsl::select( - schema::accounts::table.inner_join(schema::account_codes::table), - (schema::accounts::nonce, schema::account_codes::code), - ) - .filter(schema::accounts::account_id.eq(account_id.to_bytes())) - .filter(schema::accounts::is_latest.eq(true)) - .get_result(conn) - .optional()? - .ok_or(DatabaseError::AccountNotFoundInDb(account_id))?; + let joined = schema::accounts::table.inner_join(schema::account_codes::table.on( + schema::accounts::code_commitment.eq(schema::account_codes::code_commitment.nullable()), + )); + + let (nonce, code_bytes): (Option, Vec) = + SelectDsl::select(joined, (schema::accounts::nonce, schema::account_codes::code)) + .filter(schema::accounts::account_id.eq(account_id.to_bytes())) + .filter(schema::accounts::is_latest.eq(true)) + .get_result(conn) + .optional()? + .ok_or(DatabaseError::AccountNotFoundInDb(account_id))?; let nonce = raw_sql_to_nonce(nonce.ok_or_else(|| { DatabaseError::DataCorrupted(format!("No nonce found for account {account_id}")) diff --git a/crates/store/src/db/models/queries/accounts/delta/tests.rs b/crates/store/src/db/models/queries/accounts/delta/tests.rs index 3cacb46b0d..2f9b69ea94 100644 --- a/crates/store/src/db/models/queries/accounts/delta/tests.rs +++ b/crates/store/src/db/models/queries/accounts/delta/tests.rs @@ -4,8 +4,7 @@ use std::collections::BTreeMap; use assert_matches::assert_matches; -use diesel::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl, SqliteConnection}; -use diesel_migrations::MigrationHarness; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SqliteConnection}; use miden_node_utils::fee::test_fee_params; use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; use miden_protocol::account::component::AccountComponentMetadata; @@ -40,7 +39,6 @@ use miden_protocol::{EMPTY_WORD, Felt, Word}; use miden_standards::account::auth::AuthSingleSig; use miden_standards::code_builder::CodeBuilder; -use crate::db::migrations::MIGRATIONS; use crate::db::models::queries::accounts::tests::select_account_vault_at_block; use crate::db::models::queries::accounts::{ select_account_header_with_storage_header_at_block, @@ -50,12 +48,7 @@ use crate::db::models::queries::accounts::{ use crate::db::schema::accounts; fn setup_test_db() -> SqliteConnection { - let mut conn = - SqliteConnection::establish(":memory:").expect("Failed to create in-memory database"); - - conn.run_pending_migrations(MIGRATIONS).expect("Failed to run migrations"); - - conn + crate::db::migrations::test_connection() } fn insert_block_header(conn: &mut SqliteConnection, block_num: BlockNumber) { diff --git a/crates/store/src/db/models/queries/accounts/tests.rs b/crates/store/src/db/models/queries/accounts/tests.rs index eee9f69795..4b0e8f2da4 100644 --- a/crates/store/src/db/models/queries/accounts/tests.rs +++ b/crates/store/src/db/models/queries/accounts/tests.rs @@ -3,15 +3,7 @@ use std::collections::BTreeMap; use diesel::query_dsl::methods::SelectDsl; -use diesel::{ - BoolExpressionMethods, - Connection, - ExpressionMethods, - OptionalExtension, - QueryDsl, - RunQueryDsl, -}; -use diesel_migrations::MigrationHarness; +use diesel::{BoolExpressionMethods, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl}; use miden_node_utils::fee::test_fee_params; use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; use miden_protocol::account::component::AccountComponentMetadata; @@ -46,18 +38,12 @@ use miden_standards::account::auth::AuthSingleSig; use miden_standards::code_builder::CodeBuilder; use super::*; -use crate::db::migrations::MIGRATIONS; use crate::db::models::conv::SqlTypeConvert; use crate::db::schema; use crate::errors::DatabaseError; fn setup_test_db() -> SqliteConnection { - let mut conn = - SqliteConnection::establish(":memory:").expect("Failed to create in-memory database"); - - conn.run_pending_migrations(MIGRATIONS).expect("Failed to run migrations"); - - conn + crate::db::migrations::test_connection() } /// Test helper: reconstructs account storage at a given block from DB. diff --git a/crates/store/src/db/schema.rs b/crates/store/src/db/schema.rs index 09e3621383..82faec3924 100644 --- a/crates/store/src/db/schema.rs +++ b/crates/store/src/db/schema.rs @@ -1,5 +1,12 @@ // @generated automatically by Diesel CLI. +diesel::table! { + account_codes (code_commitment) { + code_commitment -> Binary, + code -> Binary, + } +} + diesel::table! { account_storage_map_values (account_id, block_num, slot_name, key) { account_id -> Binary, @@ -25,24 +32,17 @@ diesel::table! { accounts (account_id, block_num) { account_id -> Binary, network_account_type -> Integer, + block_num -> BigInt, account_commitment -> Binary, code_commitment -> Nullable, nonce -> Nullable, storage_header -> Nullable, vault_root -> Nullable, - block_num -> BigInt, is_latest -> Bool, created_at_block -> BigInt, } } -diesel::table! { - account_codes (code_commitment) { - code_commitment -> Binary, - code -> Binary, - } -} - diesel::table! { block_headers (block_num) { block_num -> BigInt, @@ -104,21 +104,11 @@ diesel::table! { } } -diesel::joinable!(accounts -> account_codes (code_commitment)); -diesel::joinable!(accounts -> block_headers (block_num)); -// Note: Cannot use diesel::joinable! with accounts table due to composite primary key -// diesel::joinable!(notes -> accounts (sender)); diesel::joinable!(transactions -> accounts -// (account_id)); -diesel::joinable!(notes -> block_headers (committed_at)); -diesel::joinable!(notes -> note_scripts (script_root)); -diesel::joinable!(nullifiers -> block_headers (block_num)); -diesel::joinable!(transactions -> block_headers (block_num)); - diesel::allow_tables_to_appear_in_same_query!( account_codes, account_storage_map_values, - accounts, account_vault_assets, + accounts, block_headers, note_scripts, notes, diff --git a/crates/store/src/db/schema_hash.rs b/crates/store/src/db/schema_hash.rs deleted file mode 100644 index 9a5ad1328a..0000000000 --- a/crates/store/src/db/schema_hash.rs +++ /dev/null @@ -1,198 +0,0 @@ -//! Schema verification to detect database schema changes. -//! -//! Detects: -//! -//! - Direct modifications to the database schema outside of migrations -//! - Running a node against a database created with different set of migrations -//! - Forgetting to reset the database after schema changes i.e. for a specific migration -//! -//! The verification works by creating an in-memory reference database, applying all -//! migrations to it, and comparing its schema against the actual database schema. - -use diesel::{Connection, RunQueryDsl, SqliteConnection}; -use diesel_migrations::MigrationHarness; -use miden_node_db::SchemaVerificationError; -use tracing::instrument; - -use crate::COMPONENT; -use crate::db::migrations::MIGRATIONS; - -/// Represents a schema object for comparison. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -struct SchemaObject { - object_type: String, - name: String, - sql: String, -} - -/// Represents a row from the `sqlite_schema` table. -#[derive(diesel::QueryableByName, Debug)] -struct SqliteSchemaRow { - #[diesel(sql_type = diesel::sql_types::Text)] - schema_type: String, - #[diesel(sql_type = diesel::sql_types::Text)] - name: String, - #[diesel(sql_type = diesel::sql_types::Nullable)] - sql: Option, -} - -/// Extracts all schema objects from a database connection. -fn extract_schema( - conn: &mut SqliteConnection, -) -> Result, SchemaVerificationError> { - let rows: Vec = diesel::sql_query( - "SELECT type as schema_type, name, sql FROM sqlite_schema \ - WHERE type IN ('table', 'index') \ - AND name NOT LIKE 'sqlite_%' \ - AND name NOT LIKE '__diesel_%' \ - ORDER BY type, name", - ) - .load(conn) - .map_err(SchemaVerificationError::SchemaExtraction)?; - - let mut objects: Vec = rows - .into_iter() - .filter_map(|row| { - row.sql.map(|sql| SchemaObject { - object_type: row.schema_type, - name: row.name, - sql, - }) - }) - .collect(); - - objects.sort(); - Ok(objects) -} - -/// Computes the expected schema by applying migrations to an in-memory database. -fn compute_expected_schema() -> Result, SchemaVerificationError> { - let mut conn = SqliteConnection::establish(":memory:") - .map_err(SchemaVerificationError::InMemoryDbCreation)?; - - conn.run_pending_migrations(MIGRATIONS) - .map_err(SchemaVerificationError::MigrationApplication)?; - - extract_schema(&mut conn) -} - -/// Verifies that the database schema matches the expected schema. -/// -/// Creates an in-memory database, applies all migrations, and compares schemas. -/// -/// # Errors -/// -/// Returns `SchemaVerificationError::Mismatch` if schemas differ. -#[instrument(level = "info", target = COMPONENT, skip_all, err)] -pub fn verify_schema(conn: &mut SqliteConnection) -> Result<(), SchemaVerificationError> { - let expected = compute_expected_schema()?; - let actual = extract_schema(conn)?; - - if actual != expected { - let expected_names: Vec<_> = expected.iter().map(|o| &o.name).collect(); - let actual_names: Vec<_> = actual.iter().map(|o| &o.name).collect(); - - // Find differences for better error messages - let missing: Vec<_> = expected.iter().filter(|e| !actual.contains(e)).collect(); - let extra: Vec<_> = actual.iter().filter(|a| !expected.contains(a)).collect(); - - tracing::error!( - target: COMPONENT, - ?expected_names, - ?actual_names, - missing_count = missing.len(), - extra_count = extra.len(), - "Database schema mismatch detected" - ); - - // Log specific differences at debug level - for obj in &missing { - tracing::debug!( - target: COMPONENT, - name = %obj.name, - sql = %obj.sql, - "Missing or modified" - ); - } - for obj in &extra { - tracing::debug!( - target: COMPONENT, - name = %obj.name, - sql = %obj.sql, - "Extra or modified" - ); - } - - return Err(SchemaVerificationError::Mismatch { - expected_count: expected.len(), - actual_count: actual.len(), - missing_count: missing.len(), - extra_count: extra.len(), - }); - } - - tracing::info!(target: COMPONENT, objects = expected.len(), "Database schema verification passed"); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::db::migrations::apply_migrations; - - #[test] - fn verify_schema_passes_for_correct_schema() { - let mut conn = SqliteConnection::establish(":memory:").unwrap(); - conn.run_pending_migrations(MIGRATIONS).unwrap(); - verify_schema(&mut conn).expect("Should pass for correct schema"); - } - - #[test] - fn verify_schema_fails_for_added_object() { - let mut conn = SqliteConnection::establish(":memory:").unwrap(); - conn.run_pending_migrations(MIGRATIONS).unwrap(); - - diesel::sql_query("CREATE TABLE rogue_table (id INTEGER PRIMARY KEY)") - .execute(&mut conn) - .unwrap(); - - assert!(matches!( - verify_schema(&mut conn), - Err(SchemaVerificationError::Mismatch { .. }) - )); - } - - #[test] - fn verify_schema_fails_for_removed_object() { - let mut conn = SqliteConnection::establish(":memory:").unwrap(); - conn.run_pending_migrations(MIGRATIONS).unwrap(); - - diesel::sql_query("DROP TABLE transactions").execute(&mut conn).unwrap(); - - assert!(matches!( - verify_schema(&mut conn), - Err(SchemaVerificationError::Mismatch { .. }) - )); - } - - #[test] - fn apply_migrations_succeeds_on_fresh_database() { - let mut conn = SqliteConnection::establish(":memory:").unwrap(); - apply_migrations(&mut conn).expect("Should succeed on fresh database"); - } - - #[test] - fn apply_migrations_fails_on_tampered_database() { - let mut conn = SqliteConnection::establish(":memory:").unwrap(); - conn.run_pending_migrations(MIGRATIONS).unwrap(); - - diesel::sql_query("CREATE TABLE tampered (id INTEGER)") - .execute(&mut conn) - .unwrap(); - - assert!(matches!( - apply_migrations(&mut conn), - Err(miden_node_db::DatabaseError::SchemaVerification(_)) - )); - } -} diff --git a/crates/store/src/db/tests.rs b/crates/store/src/db/tests.rs index 5a8b4df197..93153f0a39 100644 --- a/crates/store/src/db/tests.rs +++ b/crates/store/src/db/tests.rs @@ -80,15 +80,12 @@ use tempfile::tempdir; use super::{AccountInfo, NoteRecord, NoteSyncRecord, NullifierInfo, TransactionRecord}; use crate::account_state_forest::HISTORICAL_BLOCK_RETENTION; -use crate::db::migrations::apply_migrations; use crate::db::models::queries::{StorageMapValue, insert_account_storage_map_value}; use crate::db::models::{Page, queries, utils}; use crate::errors::DatabaseError; fn create_db() -> SqliteConnection { - let mut conn = SqliteConnection::establish(":memory:").expect("In memory sqlite always works"); - apply_migrations(&mut conn).expect("Migrations always work on an empty database"); - conn + crate::db::migrations::test_connection() } fn create_block(conn: &mut SqliteConnection, block_num: BlockNumber) { @@ -1584,7 +1581,6 @@ async fn reconstruct_storage_map_from_db_pages_until_latest() { let slot_name_for_db = slot_name.clone(); db.query("insert paged values", move |db_conn| { db_conn.transaction(|db_conn| { - apply_migrations(db_conn)?; create_block(db_conn, block1); create_block(db_conn, block2); create_block(db_conn, block3); @@ -1652,7 +1648,6 @@ async fn reconstruct_storage_map_from_db_returns_limit_exceeded_for_single_block let slot_name_for_db = slot_name.clone(); db.query("insert entries in single block", move |db_conn| { db_conn.transaction(|db_conn| { - apply_migrations(db_conn)?; create_block(db_conn, block5); queries::upsert_accounts(db_conn, &[mock_block_account_update(account_id, 0)], block5)?; @@ -2010,7 +2005,9 @@ async fn genesis_with_account_assets() { GenesisState::new(vec![account], test_fee_params(), 1, 0, signer.public_key()); let genesis_block = genesis_state.into_block(&signer).unwrap(); - crate::db::Db::bootstrap(":memory:".into(), genesis_block).unwrap(); + let temp_dir = tempdir().unwrap(); + let db_path = temp_dir.path().join("store.sqlite"); + crate::db::Db::bootstrap(db_path, genesis_block).unwrap(); } /// Verifies genesis block with account containing storage maps can be inserted. @@ -2066,7 +2063,9 @@ async fn genesis_with_account_storage_map() { GenesisState::new(vec![account], test_fee_params(), 1, 0, signer.public_key()); let genesis_block = genesis_state.into_block(&signer).unwrap(); - crate::db::Db::bootstrap(":memory:".into(), genesis_block).unwrap(); + let temp_dir = tempdir().unwrap(); + let db_path = temp_dir.path().join("store.sqlite"); + crate::db::Db::bootstrap(db_path, genesis_block).unwrap(); } /// Verifies genesis block with account containing both vault assets and storage maps. @@ -2120,7 +2119,9 @@ async fn genesis_with_account_assets_and_storage() { GenesisState::new(vec![account], test_fee_params(), 1, 0, signer.public_key()); let genesis_block = genesis_state.into_block(&signer).unwrap(); - crate::db::Db::bootstrap(":memory:".into(), genesis_block).unwrap(); + let temp_dir = tempdir().unwrap(); + let db_path = temp_dir.path().join("store.sqlite"); + crate::db::Db::bootstrap(db_path, genesis_block).unwrap(); } /// Verifies genesis block with multiple accounts of different types. Tests realistic genesis @@ -2217,7 +2218,9 @@ async fn genesis_with_multiple_accounts() { ); let genesis_block = genesis_state.into_block(&signer).unwrap(); - crate::db::Db::bootstrap(":memory:".into(), genesis_block).unwrap(); + let temp_dir = tempdir().unwrap(); + let db_path = temp_dir.path().join("store.sqlite"); + crate::db::Db::bootstrap(db_path, genesis_block).unwrap(); } #[test] From 0a0abaf30d6e9bb0f416cbb7cead67deec086d99 Mon Sep 17 00:00:00 2001 From: Santiago Pittella <87827390+SantiagoPittella@users.noreply.github.com> Date: Thu, 21 May 2026 04:54:08 -0300 Subject: [PATCH 07/10] chore: merge main to next (#2101) --- CHANGELOG.md | 6 + Cargo.lock | 30 ++- Cargo.toml | 1 + bin/ntx-builder/Cargo.toml | 1 + bin/ntx-builder/src/actor/execute.rs | 247 +++++++++++++++--- bin/ntx-builder/src/actor/mod.rs | 15 ++ bin/ntx-builder/src/clients/mod.rs | 2 +- bin/ntx-builder/src/lib.rs | 30 +++ .../block-producer/src/mempool/graph/batch.rs | 2 +- .../src/mempool/graph/transaction.rs | 148 +++++++---- crates/utils/src/tracing/grpc.rs | 31 +-- docs/external/src/img/node_architecture.svg | 4 - .../src/img/operator_architecture.svg | 5 +- docs/external/src/img/workspace_tree.svg | 4 - docs/external/src/operator/architecture.md | 36 ++- docs/external/src/operator/installation.md | 2 +- docs/external/src/operator/versioning.md | 1 - docs/internal/src/SUMMARY.md | 1 + .../internal/src/assets/node_architecture.svg | 4 +- .../src/assets/operator_architecture.svg | 4 - docs/internal/src/assets/workspace_tree.svg | 4 - docs/internal/src/block-producer.md | 24 +- docs/internal/src/codebase.md | 23 +- docs/internal/src/components.md | 2 +- docs/internal/src/ntx-builder.md | 11 +- docs/internal/src/oddities.md | 7 + docs/internal/src/rpc.md | 15 +- docs/internal/src/store.md | 5 - docs/internal/src/validator.md | 27 ++ proto/proto/types/transaction.proto | 29 +- 30 files changed, 545 insertions(+), 176 deletions(-) delete mode 100644 docs/external/src/img/node_architecture.svg delete mode 100644 docs/external/src/img/workspace_tree.svg delete mode 100644 docs/internal/src/assets/operator_architecture.svg delete mode 100644 docs/internal/src/assets/workspace_tree.svg create mode 100644 docs/internal/src/validator.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e2bc8f0b3d..67e18a81ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,11 @@ - [BREAKING] Changed `SyncChainMmr` endpoint: the upper end of the block range we're syncing is now the chain tip with the requested finality level. Validator signature is also returned ([#2075](https://github.com/0xMiden/node/pull/2075)). - [BREAKING] Renamed `SubmitProvenTransaction` RPC endpoint to `SubmitProvenTx` ([#2094](https://github.com/0xMiden/node/pull/2094)). - [BREAKING] Renamed `SubmitProvenBatch` RPC endpoint to `SubmitProvenTxBatch` ([#2094](https://github.com/0xMiden/node/pull/2094)). +## v0.14.11 (TBD) + +- Replaced blocking-in-async operations in the validator, remote prover, and ntx-builder with `spawn_blocking` to avoid starving the Tokio runtime ([#2041](https://github.com/0xMiden/node/pull/2041)). +- Implement persistent RocksDB backend for `AccountStateForest`, improving startup time ([#2020](https://github.com/0xMiden/node/pull/2020)). +- Fixed network transaction builder permanently dropping notes after transient infrastructure failures. These now retry with exponential backoff at the actor level instead of consuming per-note retry budget ([#2052](https://github.com/0xMiden/node/issues/2052)). ## v0.14.10 (2026-05-29) @@ -38,6 +43,7 @@ - Added `accept`, `origin`, `user-agent`, `forwarded`, `x-forwarded-for` and `x-real-ip` headers to telemetry for gRPC requests ([#1982](https://github.com/0xMiden/node/pull/1982)). - Trace additional RPC request properties e.g. `account.id` in `GetAccount` ([#1983](https://github.com/0xMiden/node/pull/1983)). - Fixed occasional mempool panic during transaction submission, causing the lock to be held for longer than expected ([#1984](https://github.com/0xMiden/node/pull/1984)). +- Optimize `GetAccount` implementation: `all_entries` requests now mostly use state from `AccountStateForest` ([#2012](https://github.com/0xMiden/node/pull/2012)). ## v0.14.9 (2026-04-21) diff --git a/Cargo.lock b/Cargo.lock index 34eab17397..e980114ea4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -688,6 +688,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -1963,6 +1974,18 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "governor" version = "0.10.4" @@ -2304,7 +2327,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "system-configuration", "tokio", "tower-service", @@ -3495,6 +3518,7 @@ name = "miden-ntx-builder" version = "0.15.0" dependencies = [ "anyhow", + "backon", "build-rs", "clap", "diesel", @@ -4973,7 +4997,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.37", - "socket2 0.5.10", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -5011,7 +5035,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] diff --git a/Cargo.toml b/Cargo.toml index 4e90f07a16..6ae414d7bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ miden-crypto = { version = "0.23" } anyhow = { version = "1.0" } assert_matches = { version = "1.5" } async-trait = { version = "0.1" } +backon = { version = "1.6" } build-rs = { version = "0.3" } clap = { features = ["derive"], version = "4.5" } codegen = { version = "0.3" } diff --git a/bin/ntx-builder/Cargo.toml b/bin/ntx-builder/Cargo.toml index 67106ffc19..dec37c2035 100644 --- a/bin/ntx-builder/Cargo.toml +++ b/bin/ntx-builder/Cargo.toml @@ -19,6 +19,7 @@ doctest = false [dependencies] anyhow = { workspace = true } +backon = { workspace = true } clap = { features = ["env", "string"], workspace = true } diesel = { features = ["numeric", "sqlite"], workspace = true } futures = { workspace = true } diff --git a/bin/ntx-builder/src/actor/execute.rs b/bin/ntx-builder/src/actor/execute.rs index c2152dab89..69bc220137 100644 --- a/bin/ntx-builder/src/actor/execute.rs +++ b/bin/ntx-builder/src/actor/execute.rs @@ -1,6 +1,8 @@ use std::collections::{BTreeSet, HashMap}; use std::sync::{Arc, Mutex}; +use std::time::Duration; +use backon::{ExponentialBuilder, Retryable}; use miden_node_utils::ErrorReport; use miden_node_utils::lru_cache::LruCache; use miden_node_utils::spawn::spawn_blocking_in_current_span; @@ -53,7 +55,7 @@ use tracing::{Instrument, instrument}; use crate::COMPONENT; use crate::actor::candidate::TransactionCandidate; -use crate::clients::{BlockProducerClient, StoreClient, ValidatorClient}; +use crate::clients::{BlockProducerClient, StoreClient, StoreError, ValidatorClient}; use crate::db::Db; #[derive(Debug, thiserror::Error)] @@ -74,6 +76,53 @@ pub enum NtxError { type NtxResult = Result; +/// Returns `true` for gRPC status codes that indicate a transient transport- or server-side problem +/// worth retrying. Content-rejection codes (`InvalidArgument`, `FailedPrecondition`, ...) reflect +/// the batch itself and are not retried. +fn is_transient_status(status: &tonic::Status) -> bool { + matches!( + status.code(), + tonic::Code::Unavailable + | tonic::Code::DeadlineExceeded + | tonic::Code::Cancelled + | tonic::Code::Aborted + | tonic::Code::Unknown + | tonic::Code::Internal + | tonic::Code::ResourceExhausted, + ) +} + +/// Returns `true` for `StoreError`s that originate from a transient gRPC condition. All other store +/// errors (deserialization, missing fields) are content errors and are not retried. +fn is_transient_store_error(err: &StoreError) -> bool { + matches!(err, StoreError::GrpcClientError(status) if is_transient_status(status)) +} + +/// Maximum number of retries applied to a single transient request before the error is propagated +/// to the actor-level retry. +const MAX_REQUEST_RETRIES: usize = 20; + +/// Builds the [`ExponentialBuilder`] used to back off retries on transient request failures. +fn request_backoff(initial: Duration, max: Duration) -> ExponentialBuilder { + ExponentialBuilder::default() + .with_min_delay(initial) + .with_max_delay(max) + .with_factor(2.0) + .with_max_times(MAX_REQUEST_RETRIES) + .with_jitter() +} + +/// Emits a structured warning for a transient NTX request failure that is about to be retried. +fn log_transient_retry(operation: &'static str, err: &E, sleep: Duration) { + tracing::warn!( + target: COMPONENT, + operation, + err = %err.as_report(), + sleep_ms = sleep.as_millis() as u64, + "ntx transient request failure; retrying after backoff", + ); +} + /// The result of a successful transaction execution. /// /// Contains the transaction ID, any notes that failed during filtering, and note scripts fetched @@ -109,10 +158,14 @@ pub struct NtxContext { /// Maximum number of VM execution cycles for network transactions. max_cycles: u32, + + /// [`ExponentialBuilder`] used to back off retries on transient request failures. + request_backoff: ExponentialBuilder, } impl NtxContext { /// Creates a new [`NtxContext`] instance. + #[expect(clippy::too_many_arguments)] pub fn new( block_producer: BlockProducerClient, validator: ValidatorClient, @@ -121,7 +174,10 @@ impl NtxContext { script_cache: LruCache, db: Db, max_cycles: u32, + request_backoff_initial: Duration, + request_backoff_max: Duration, ) -> Self { + let request_backoff = request_backoff(request_backoff_initial, request_backoff_max); Self { block_producer, validator, @@ -130,9 +186,15 @@ impl NtxContext { script_cache, db, max_cycles, + request_backoff, } } + /// Returns the [`ExponentialBuilder`] used for per-request retry backoff. + fn request_backoff(&self) -> ExponentialBuilder { + self.request_backoff + } + /// Creates a [`TransactionExecutor`] configured with the network transaction cycle limit. fn create_executor<'a, 'b>( &self, @@ -214,6 +276,7 @@ impl NtxContext { ctx.store.clone(), ctx.script_cache.clone(), ctx.db.clone(), + ctx.request_backoff, ); handle.block_on( async { @@ -332,10 +395,20 @@ impl NtxContext { /// Delegates the transaction proof to the remote prover if configured, otherwise performs the /// proof locally. + /// + /// Transient transport failures against the remote prover are retried in-place; intrinsic + /// proving errors (witness rejected, malformed inputs) escape on the first attempt. #[instrument(target = COMPONENT, name = "ntx.execute_transaction.prove", skip_all, err)] async fn prove(&self, tx_inputs: &TransactionInputs) -> NtxResult { if let Some(remote) = &self.prover { - remote.prove(tx_inputs).await + (|| async { remote.prove(tx_inputs).await }) + .retry(self.request_backoff()) + .when(|err| matches!(err, TransactionProverError::Other { .. })) + .notify(|err, dur| { + log_transient_retry("remote_prover.prove", err, dur); + }) + .await + .map_err(NtxError::Proving) } else { // Only perform tx inputs clone for local proving. let tx_inputs = tx_inputs.clone(); @@ -350,28 +423,41 @@ impl NtxContext { }) .await .unwrap_or_else(|e| std::panic::resume_unwind(e.into_panic())) + .map_err(NtxError::Proving) } - .map_err(NtxError::Proving) } /// Submits the transaction to the block producer. + /// + /// Transient gRPC failures (`Unavailable`, `DeadlineExceeded`, ...) are retried in-place; + /// content-rejection codes escape on the first attempt so the actor can mark the batch failed. #[instrument(target = COMPONENT, name = "ntx.execute_transaction.submit", skip_all, err)] async fn submit(&self, proven_tx: &ProvenTransaction) -> NtxResult<()> { - self.block_producer - .submit_proven_tx(proven_tx) + (|| async { self.block_producer.submit_proven_tx(proven_tx).await }) + .retry(self.request_backoff()) + .when(is_transient_status) + .notify(|status, dur| { + log_transient_retry("block_producer.submit_proven_tx", status, dur); + }) .await .map_err(NtxError::Submission) } /// Validates the transaction against the Validator. + /// + /// Transient gRPC failures are retried in-place; content-rejection codes escape immediately. #[instrument(target = COMPONENT, name = "ntx.execute_transaction.validate", skip_all, err)] async fn validate( &self, proven_tx: &ProvenTransaction, tx_inputs: &TransactionInputs, ) -> NtxResult<()> { - self.validator - .submit_proven_transaction(proven_tx, tx_inputs) + (|| async { self.validator.submit_proven_transaction(proven_tx, tx_inputs).await }) + .retry(self.request_backoff()) + .when(is_transient_status) + .notify(|status, dur| { + log_transient_retry("validator.submit_proven_transaction", status, dur); + }) .await .map_err(NtxError::Submission) } @@ -424,6 +510,8 @@ struct NtxDataStore { /// to the store could be "wrong", but given that two identical maps have identical witnesses /// this does not cause issues in practice. storage_slots: Arc>>, + /// Per-request retry backoff for transient store failures. + request_backoff: ExponentialBuilder, } impl NtxDataStore { @@ -435,6 +523,7 @@ impl NtxDataStore { store: StoreClient, script_cache: LruCache, db: Db, + request_backoff: ExponentialBuilder, ) -> Self { let mast_store = TransactionMastStore::new(); mast_store.load_account_code(account.code()); @@ -449,9 +538,15 @@ impl NtxDataStore { db, fetched_scripts: Arc::new(Mutex::new(Vec::new())), storage_slots: Arc::new(Mutex::new(HashMap::default())), + request_backoff, } } + /// Returns the [`ExponentialBuilder`] used for per-request retry backoff against the store. + fn store_backoff(&self) -> ExponentialBuilder { + self.request_backoff + } + /// Returns the list of note scripts fetched from the remote store during execution. fn take_fetched_scripts(&self) -> Vec<(Word, NoteScript)> { self.fetched_scripts @@ -513,11 +608,18 @@ impl DataStore for NtxDataStore { async move { debug_assert_eq!(ref_block, self.reference_block.block_num()); - // Get foreign account inputs from store. + // Get foreign account inputs from store, retrying on transient gRPC failures. let account_inputs = - self.store.get_account_inputs(foreign_account_id, ref_block).await.map_err( - |err| DataStoreError::other_with_source("failed to get account inputs", err), - )?; + (|| async { self.store.get_account_inputs(foreign_account_id, ref_block).await }) + .retry(self.store_backoff()) + .when(is_transient_store_error) + .notify(|err, dur| { + log_transient_retry("store.get_account_inputs", err, dur); + }) + .await + .map_err(|err| { + DataStoreError::other_with_source("failed to get account inputs", err) + })?; // Ensure foreign account procedures are available to the executor via the mast store. // This assumes the code was not loaded from before @@ -539,14 +641,24 @@ impl DataStore for NtxDataStore { async move { let ref_block = self.reference_block.block_num(); - // Get vault asset witnesses from the store. - let witnesses = self - .store - .get_vault_asset_witnesses(account_id, vault_keys, Some(ref_block)) - .await - .map_err(|err| { - DataStoreError::other_with_source("failed to get vault asset witnesses", err) - })?; + // Get vault asset witnesses from the store, retrying on transient gRPC failures. + let witnesses = (|| { + let vault_keys = vault_keys.clone(); + async move { + self.store + .get_vault_asset_witnesses(account_id, vault_keys, Some(ref_block)) + .await + } + }) + .retry(self.store_backoff()) + .when(is_transient_store_error) + .notify(|err, dur| { + log_transient_retry("store.get_vault_asset_witnesses", err, dur); + }) + .await + .map_err(|err| { + DataStoreError::other_with_source("failed to get vault asset witnesses", err) + })?; Ok(witnesses) } @@ -573,14 +685,24 @@ impl DataStore for NtxDataStore { let ref_block = self.reference_block.block_num(); - // Get storage map witness from the store. - let witness = self - .store - .get_storage_map_witness(account_id, slot_name, map_key, Some(ref_block)) - .await - .map_err(|err| { - DataStoreError::other_with_source("failed to get storage map witness", err) - })?; + // Get storage map witness from the store, retrying on transient gRPC failures. + let witness = (|| { + let slot_name = slot_name.clone(); + async move { + self.store + .get_storage_map_witness(account_id, slot_name, map_key, Some(ref_block)) + .await + } + }) + .retry(self.store_backoff()) + .when(is_transient_store_error) + .notify(|err, dur| { + log_transient_retry("store.get_storage_map_witness", err, dur); + }) + .await + .map_err(|err| { + DataStoreError::other_with_source("failed to get storage map witness", err) + })?; Ok(witness) } @@ -611,9 +733,15 @@ impl DataStore for NtxDataStore { return Ok(Some(script)); } - // 3. Remote store. - let maybe_script = - self.store.get_note_script_by_root(script_root).await.map_err(|err| { + // 3. Remote store, retrying on transient gRPC failures. + let maybe_script = (|| async { self.store.get_note_script_by_root(script_root).await }) + .retry(self.store_backoff()) + .when(is_transient_store_error) + .notify(|err, dur| { + log_transient_retry("store.get_note_script_by_root", err, dur); + }) + .await + .map_err(|err| { DataStoreError::other_with_source( "failed to retrieve note script from store", err, @@ -643,3 +771,62 @@ impl MastForestStore for NtxDataStore { self.mast_store.get(procedure_hash) } } + +#[cfg(test)] +mod tests { + use miden_tx::TransactionProverError; + + use super::{StoreError, is_transient_status, is_transient_store_error}; + + #[test] + fn transient_status_classifies_transport_codes() { + let transient = [ + tonic::Status::unavailable("u"), + tonic::Status::deadline_exceeded("d"), + tonic::Status::cancelled("c"), + tonic::Status::aborted("a"), + tonic::Status::unknown("u"), + tonic::Status::internal("i"), + tonic::Status::resource_exhausted("r"), + ]; + for s in &transient { + assert!(is_transient_status(s), "{:?} should be transient", s.code()); + } + + let terminal = [ + tonic::Status::invalid_argument("ia"), + tonic::Status::failed_precondition("fp"), + tonic::Status::out_of_range("oor"), + tonic::Status::not_found("nf"), + tonic::Status::already_exists("ae"), + tonic::Status::unauthenticated("ua"), + tonic::Status::permission_denied("pd"), + tonic::Status::unimplemented("ui"), + tonic::Status::data_loss("dl"), + ]; + for s in &terminal { + assert!(!is_transient_status(s), "{:?} should be terminal", s.code()); + } + } + + #[test] + fn transient_store_error_only_for_transient_grpc() { + let transient = StoreError::GrpcClientError(tonic::Status::unavailable("down")); + assert!(is_transient_store_error(&transient)); + + let terminal_grpc = + StoreError::GrpcClientError(tonic::Status::invalid_argument("bad input")); + assert!(!is_transient_store_error(&terminal_grpc)); + + let malformed = StoreError::MalformedResponse("bad".into()); + assert!(!is_transient_store_error(&malformed)); + } + + /// Smoke-test that the predicates used by the request-level retry wrappers compile and select + /// the expected variants. Prover transport failures live behind `Other` only. + #[test] + fn prover_other_is_the_retried_variant() { + let err = TransactionProverError::other("remote prover unreachable"); + assert!(matches!(err, TransactionProverError::Other { .. })); + } +} diff --git a/bin/ntx-builder/src/actor/mod.rs b/bin/ntx-builder/src/actor/mod.rs index f53815f484..cefb9bdb65 100644 --- a/bin/ntx-builder/src/actor/mod.rs +++ b/bin/ntx-builder/src/actor/mod.rs @@ -81,6 +81,12 @@ pub struct ActorConfig { pub idle_timeout: Duration, /// Maximum number of VM execution cycles for network transactions. pub max_cycles: u32, + /// Initial sleep applied between per-request retries on transient infrastructure failures + /// (prover unreachable, validator/block-producer transport error, store gRPC hiccup). Doubles + /// each retry up to [`Self::request_backoff_max`]. + pub request_backoff_initial: Duration, + /// Upper bound on the per-request retry backoff sleep. + pub request_backoff_max: Duration, } // ACCOUNT ACTOR CONTEXT @@ -133,6 +139,8 @@ impl AccountActorContext { max_note_attempts: 1, idle_timeout: Duration::from_secs(60), max_cycles: 1 << 18, + request_backoff_initial: Duration::from_millis(1), + request_backoff_max: Duration::from_millis(10), }, request_tx, } @@ -401,6 +409,11 @@ impl AccountActor { /// Execute a transaction candidate and mark notes as failed as required. /// /// Returns the new actor mode based on the execution result. + /// + /// Transient infrastructure failures (prover unreachable, validator/block-producer transport + /// hiccup, store gRPC error) are retried inside [`execute::NtxContext::execute_transaction`]. + /// Any error reaching this method is therefore terminal for the candidate: the batch's notes + /// are marked failed and the actor moves on. #[tracing::instrument(name = "ntx.actor.execute_transactions", skip(self, tx_candidate))] async fn execute_transactions( &self, @@ -418,6 +431,8 @@ impl AccountActor { self.state.script_cache.clone(), self.state.db.clone(), self.config.max_cycles, + self.config.request_backoff_initial, + self.config.request_backoff_max, ); let notes = tx_candidate.notes.clone(); diff --git a/bin/ntx-builder/src/clients/mod.rs b/bin/ntx-builder/src/clients/mod.rs index 19814602bb..6c7a616c92 100644 --- a/bin/ntx-builder/src/clients/mod.rs +++ b/bin/ntx-builder/src/clients/mod.rs @@ -3,5 +3,5 @@ mod store; mod validator; pub use block_producer::BlockProducerClient; -pub use store::StoreClient; +pub use store::{StoreClient, StoreError}; pub use validator::ValidatorClient; diff --git a/bin/ntx-builder/src/lib.rs b/bin/ntx-builder/src/lib.rs index ec6cb147c9..c280a5e4a1 100644 --- a/bin/ntx-builder/src/lib.rs +++ b/bin/ntx-builder/src/lib.rs @@ -66,6 +66,14 @@ const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60); /// Default maximum number of crashes an account actor is allowed before being deactivated. const DEFAULT_MAX_ACCOUNT_CRASHES: usize = 10; +/// Default initial sleep applied between per-request retries on transient infrastructure failures +/// (downed prover, transport error, validator/block-producer crash, store gRPC hiccup). Doubles on +/// each retry up to [`DEFAULT_REQUEST_BACKOFF_MAX`]. +const DEFAULT_REQUEST_BACKOFF_INITIAL: Duration = Duration::from_millis(100); + +/// Default upper bound on the per-request retry backoff sleep. +const DEFAULT_REQUEST_BACKOFF_MAX: Duration = Duration::from_secs(30); + /// Default maximum number of VM execution cycles allowed for a network transaction. /// /// This limits the computational cost of network transactions. The protocol maximum is @@ -130,6 +138,15 @@ pub struct NtxBuilderConfig { /// Defaults to 2^18 cycles. pub max_cycles: u32, + /// Initial sleep applied between per-request retries on transient infrastructure failures (e.g. + /// prover unreachable, validator/block-producer crash, transport error, store gRPC hiccup). + /// Doubles on each retry up to [`Self::request_backoff_max`]. Per-note `attempt_count` is *not* + /// advanced while retries are in progress. + pub request_backoff_initial: Duration, + + /// Upper bound on the per-request retry backoff sleep. + pub request_backoff_max: Duration, + /// Path to the SQLite database file used for persistent state. pub database_filepath: PathBuf, @@ -158,6 +175,8 @@ impl NtxBuilderConfig { idle_timeout: DEFAULT_IDLE_TIMEOUT, max_account_crashes: DEFAULT_MAX_ACCOUNT_CRASHES, max_cycles: DEFAULT_MAX_TX_CYCLES, + request_backoff_initial: DEFAULT_REQUEST_BACKOFF_INITIAL, + request_backoff_max: DEFAULT_REQUEST_BACKOFF_MAX, database_filepath, sqlite_connection_pool_size: miden_node_db::default_connection_pool_size(), } @@ -247,6 +266,15 @@ impl NtxBuilderConfig { self } + /// Sets the per-request retry backoff bounds (initial sleep and cap) used when retrying + /// transient infrastructure failures inside a single transaction attempt. + #[must_use] + pub fn with_request_backoff(mut self, initial: Duration, max: Duration) -> Self { + self.request_backoff_initial = initial; + self.request_backoff_max = max; + self + } + /// Sets the SQLite connection pool size. #[must_use] pub fn with_sqlite_connection_pool_size(mut self, size: NonZeroUsize) -> Self { @@ -325,6 +353,8 @@ impl NtxBuilderConfig { max_note_attempts: self.max_note_attempts, idle_timeout: self.idle_timeout, max_cycles: self.max_cycles, + request_backoff_initial: self.request_backoff_initial, + request_backoff_max: self.request_backoff_max, }, request_tx, }; diff --git a/crates/block-producer/src/mempool/graph/batch.rs b/crates/block-producer/src/mempool/graph/batch.rs index 03a3c276f3..c42257c8ca 100644 --- a/crates/block-producer/src/mempool/graph/batch.rs +++ b/crates/block-producer/src/mempool/graph/batch.rs @@ -98,7 +98,7 @@ impl BatchGraph { /// /// Batches are returned in reverse-chronological order. pub fn revert_expired(&mut self, chain_tip: BlockNumber) -> Vec { - // We only revert transactions which are _not_ included in batches. + // We only revert batches which are _not_ included in blocks. let mut to_revert = self.inner.expired(chain_tip); to_revert.retain(|batch| !self.inner.is_selected(batch)); diff --git a/crates/block-producer/src/mempool/graph/transaction.rs b/crates/block-producer/src/mempool/graph/transaction.rs index fe2a5570d3..d9583b03c5 100644 --- a/crates/block-producer/src/mempool/graph/transaction.rs +++ b/crates/block-producer/src/mempool/graph/transaction.rs @@ -77,12 +77,8 @@ pub struct TransactionGraph { /// used to identify potentially buggy transactions that should be evicted. failures: HashMap, - /// Defines the transactions that belong to a user batch. - user_batch_txs: HashMap>, - /// A mapping of transactions to their user batch (if any). - /// - /// Inverse map of `user_batch_txs`. - txs_user_batch: HashMap, + /// Bijective mapping of user batches and their transactions. + user_batches: BatchTxMap, } impl TransactionGraph { @@ -93,6 +89,9 @@ impl TransactionGraph { self.inner.append(tx) } + /// Appends the transactions into the graph as an atomic unit. + /// + /// These transactions can only be selected as a batch, and are reverted and pruned together. pub fn append_user_batch( &mut self, batch: &[Arc], @@ -115,67 +114,66 @@ impl TransactionGraph { } let txs = batch.iter().map(GraphNode::id).collect::>(); - for tx in &txs { - self.txs_user_batch.insert(*tx, batch_id); - } - self.user_batch_txs.insert(batch_id, txs); + self.user_batches.insert(batch_id, txs); Ok(()) } pub fn select_batch(&mut self, budget: BatchBudget) -> Option { - self.select_user_batch().or_else(|| self.select_conventional_batch(budget)) + self.select_user_batch().or_else(|| self.select_internal_batch(budget)) } fn select_user_batch(&mut self) -> Option { - // Comb through all user batch candidates. - let candidate_batches = self - .inner - .selection_candidates() - .values() - .filter_map(|tx| self.txs_user_batch.get(&tx.id())) - .copied() - .collect::>(); - - 'outer: for candidate in candidate_batches { - let mut selected = SelectedBatch::builder(); - - let txs = self - .user_batch_txs - .get(&candidate) - .cloned() - .expect("bi-directional mapping should be coherent"); - - for tx in txs { - let Some(tx) = self.inner.selection_candidates().get(&tx).copied() else { - // Rollback this batch selection since it cannot complete. - for tx in selected.txs.into_iter().rev() { - self.inner.deselect(tx.id()); - } - - continue 'outer; - }; - let tx = Arc::clone(tx); - - self.inner.select_candidate(tx.id()); - selected.push(tx); + let candidate_batches = self.user_batches.batches().copied().collect::>(); + for candidate in candidate_batches { + if let Some(batch) = self.try_select_user_batch_candidate(candidate) { + return Some(batch); } - - assert!(!selected.is_empty(), "User batch should not be empty"); - return Some(selected.build()); } None } - fn select_conventional_batch(&mut self, mut budget: BatchBudget) -> Option { + /// Attempts to select all transactions from the candidate user batch. + /// + /// This action succeeds if all transactions in the batch can be sequentially selected. + /// If any transaction cannot be selected, then all previous selections are rolled back, + /// and `None` is returned. This makes this function atomic. + /// + /// Transactions can fail selection if they depend on any external transactions that have + /// not yet been selected. + fn try_select_user_batch_candidate(&mut self, candidate: BatchId) -> Option { + let mut selected = SelectedBatch::builder(); + + let txs = self.user_batches.get_txs_contained_in_batch(&candidate)?; + + for tx in txs { + let Some(tx) = self.inner.selection_candidates().get(&tx).copied() else { + // Rollback this batch selection since it cannot complete. + for tx in selected.txs.into_iter().rev() { + self.inner.deselect(tx.id()); + } + + return None; + }; + let tx = Arc::clone(tx); + + self.inner.select_candidate(tx.id()); + selected.push(tx); + } + + assert!(!selected.is_empty(), "User batch should not be empty"); + Some(selected.build()) + } + + fn select_internal_batch(&mut self, mut budget: BatchBudget) -> Option { let mut selected = SelectedBatch::builder(); loop { // Select arbitrary candidate which is _not_ part of a user batch. let candidates = self.inner.selection_candidates(); let Some(candidate) = - candidates.values().find(|tx| !self.txs_user_batch.contains_key(&tx.id())) + candidates.values().find(|tx| !self.user_batches.contains_tx(&tx.id())) else { break; }; @@ -213,7 +211,6 @@ impl TransactionGraph { /// _before_ calling this function. i.e. first revert expired batches and deselect their /// transactions, then call this. pub fn revert_expired(&mut self, chain_tip: BlockNumber) -> HashSet { - // We only revert transactions which are _not_ included in batches. let mut to_revert = self.inner.expired(chain_tip); to_revert.retain(|tx| !self.inner.is_selected(tx)); @@ -256,10 +253,10 @@ impl TransactionGraph { // transactions in, which will result in at least the current transaction being // duplicated in `to_revert`. This isn't a concern though since we skip already // processed transactions at the top of the loop. - if let Some(batch) = self.txs_user_batch.remove(&tx.id()) { - if let Some(batch) = self.user_batch_txs.remove(&batch) { - to_revert.extend(batch); - } + if let Some(batch_id) = self.user_batches.get_batch_containing_tx(&tx.id()).copied() + { + let batch_txs = self.user_batches.remove(&batch_id); + to_revert.extend_from_slice(&batch_txs); } } @@ -322,9 +319,8 @@ impl TransactionGraph { for tx in batch.transactions() { self.inner.prune(tx.id()); self.failures.remove(&tx.id()); - self.txs_user_batch.remove(&tx.id()); } - self.user_batch_txs.remove(&batch.id()); + self.user_batches.remove(&batch.id()); } /// Number of transactions which have not been selected for inclusion in a batch. @@ -349,3 +345,47 @@ impl TransactionGraph { self.inner.output_note_count() } } + +// BIJECTIVE MAP +// ================================================================================================ + +/// A bijective mapping of batches and their transactions. +#[derive(Clone, Debug, PartialEq, Default)] +struct BatchTxMap { + by_batch: HashMap>, + by_tx: HashMap, +} + +impl BatchTxMap { + fn insert(&mut self, batch: BatchId, txs: Vec) { + for tx in &txs { + assert!(self.by_tx.insert(*tx, batch).is_none()); + } + assert!(self.by_batch.insert(batch, txs).is_none()); + } + + fn remove(&mut self, batch: &BatchId) -> Vec { + let txs = self.by_batch.remove(batch).unwrap_or_default(); + for tx in &txs { + self.by_tx.remove(tx); + } + txs + } + + fn batches(&self) -> impl Iterator { + self.by_batch.keys() + } + + /// Returns the [`BatchId`] mapped to this transaction, if any. + fn get_batch_containing_tx(&self, tx: &TransactionId) -> Option<&BatchId> { + self.by_tx.get(tx) + } + + fn get_txs_contained_in_batch(&self, batch: &BatchId) -> Option<&[TransactionId]> { + self.by_batch.get(batch).map(Vec::as_slice) + } + + fn contains_tx(&self, tx: &TransactionId) -> bool { + self.by_tx.contains_key(tx) + } +} diff --git a/crates/utils/src/tracing/grpc.rs b/crates/utils/src/tracing/grpc.rs index 1a7b0ff1fc..30d0f9501e 100644 --- a/crates/utils/src/tracing/grpc.rs +++ b/crates/utils/src/tracing/grpc.rs @@ -1,4 +1,5 @@ use http::header::HeaderName; +use tower_governor::key_extractor::{KeyExtractor, SmartIpKeyExtractor}; use tracing::field; use crate::tracing::OpenTelemetrySpanExt; @@ -20,20 +21,12 @@ pub fn grpc_trace_fn(request: &http::Request) -> tracing::Span { // Create a span with a generic, static name. Fields to be recorded after needs to be // initialized as empty since otherwise the assignment will have no effect. - let span = match method { - "SyncState" | "SyncNullifiers" => tracing::debug_span!( - "rpc", - otel.name = field::Empty, - rpc.service = service, - rpc.method = method - ), - _ => tracing::info_span!( - "rpc", - otel.name = field::Empty, - rpc.service = service, - rpc.method = method - ), - }; + let span = tracing::info_span!( + "rpc", + otel.name = field::Empty, + rpc.service = service, + rpc.method = method + ); // Set the span name via otel.name let otel_name = format!("{service}/{method}"); @@ -63,9 +56,17 @@ pub fn grpc_trace_fn(request: &http::Request) -> tracing::Span { .extensions() .get::() .and_then(tonic::transport::server::TcpConnectInfo::remote_addr); - if let Some(addr) = remote_addr { + + // client.address should be the resolved IP address of the client, if available. In the case of + // a reverse proxy, this may not be the same as the remote address. + if let Ok(ip) = SmartIpKeyExtractor.extract(request) { + span.set_attribute("client.address", ip); + } else if let Some(addr) = remote_addr { span.set_attribute("client.address", addr.ip()); span.set_attribute("client.port", addr.port()); + } + + if let Some(addr) = remote_addr { span.set_attribute("network.peer.address", addr.ip()); span.set_attribute("network.peer.port", addr.port()); span.set_attribute("network.transport", "tcp"); diff --git a/docs/external/src/img/node_architecture.svg b/docs/external/src/img/node_architecture.svg deleted file mode 100644 index ba697edf6b..0000000000 --- a/docs/external/src/img/node_architecture.svg +++ /dev/null @@ -1,4 +0,0 @@ - - -eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO19WXfiyJb1+/1cdTAwMTVe1Y9foY556Dc8z/P8VS8vwFx1MDAxOGMzmcHG7nX/e58jXHUwMDBmXGIpJCRQkmQvXFy36mZcdTAwMWFcdTAwMTBcdTAwMTFS7HP2mf/nXysrf/XfO9W//mvlr+qwUmrU77ult7/+xt+/Vru9ertcdTAwMDUvMf/vvfagW/Hf+djvd3r/9Z//Wep0vNGnvEq7+fnJaqParLb6PXjv/4e/r6z8j/9feKV+j5/fIOv7tWHlcv9071rrouaN0+O6/1H/Td9cdTAwMGLqViv9UqvWqI5eXHUwMDFhwu8ptcojllx1MDAxM6aY1YLzn1ff4VUpqPC0ZZpcdTAwMTmrLGH259W3+n3/XHUwMDExP6+ZJy3X8ueVx2q99tiHl6z1tIBcdTAwMGZxXHUwMDAxL1x1MDAxM0JGl/5cXMd/rZCf3/T63fZzda3daHdxsf9Bq/jPaKnlUuW51m1cdTAwMGZa9z/v6XdLrV6n1IV7M3rfQ73ROOu/+1eHe1xm9/Kv0HdcXH0vPPT7uE/Bl9ZcdTAwMWVb1V5v7DPtTqlS7+MtomS0XHUwMDBiXFxhZ+fef1T/PVpTt9Ss7uCzalxyXHUwMDFhjZ9f11v3VXxcdTAwMDJ/lcXB2Ne17r++buztvWpcdTAwMTUvQVxyXHUwMDEzVnK4rz+vjI5cdTAwMTY8TFx1MDAxM/71YbvlnzNcdTAwMDNPiWrJ2GhcdTAwMDG9dThbff+yXHUwMDBmpUavOrrbuIiNwLlcdTAwMWJtZtC5L31+XHUwMDA0L8ZcdTAwMTWnTFx1MDAxODl6+o166zm89Ea78jz6XHUwMDE2/7f//tt1lE376GC3d7Y7eLo1xb3zi/c9ubqX/ihr7jFtpbBcdTAwMWHuXHUwMDBmh+M8fpa1klx1MDAxZTH4XHUwMDAyp8QwXHUwMDE1OctGWVx1MDAwZs669lx1MDAwZmz0PHNDPFx1MDAxYvjR8z7PlW671ys8lvqVxz/gVJtt96n+fuajXHUwMDA3yr9+8+/IYVdGwYGnMnKq4UVBZNxZZ9pwIanI+ahzq6Tg+Vx1MDAxY/X1gjxcdTAwMTPi7uqj3W1cdTAwMWPQ2+5up2BthqNOPUlcdTAwMDSVllx1MDAxMus46iCuPa6kskpYw11H3dDoXHUwMDAxXHUwMDE3jHtcdTAwMWGkvVVcdTAwMDZcdTAwMDQ3/IjlXHRPPOG7s59wRqRcdTAwMTJcdTAwMDLOueOEc6ZjTzilijEltcn5jEuhVFx1MDAwMDgznfGBum2R182zrdftTqV0/PJsb2t3qc94XHUwMDAxZLlcdTAwMDesQUlcdTAwMDBcdTAwMWUhSpNcdTAwMTA3oUJ4Qlx1MDAxM2qMMtIlz5nRcIHRj+PIXHUwMDAzRDyqlYZcdTAwMWJJmVxccpT4s34481mHO8xcZlx1MDAwN+7iOOpSxlxuc9DVVILO1lMxl5/ljVx1MDAxNlx1MDAxYTh6/epw9GBcdTAwMDInl9dcdTAwMGXLjV6ldNlsXHUwMDE3Lq/ahf7HqX7+6+d9//7bfdnPXHUwMDBmV9hdbevoqdFcdTAwMTfnW6u73cfuXHUwMDAzK6yOf8v395e63fZb4Lpff4rFp1x1MDAwMUmvXGbNSVx1MDAwN7l3XHUwMDE5wefYTfqEJlXC41x1MDAwNiRcdTAwMTe1VFxuYFZhZNpkZErtacWt1kRcdTAwMTJcdTAwMWZ5XHUwMDBltiWXUIyB4lFcdTAwMTZzwVrJZJBcdTAwMDJcdTAwMDRAR2ks6JRCaahcdTAwMDJPNlx1MDAwZv0yxflcdTAwMWRcdTAwMWRHPIaw/9PjtcBcdTAwMDNst/pn9Vx1MDAwZl8lmrHfbpaa9YZ/XHUwMDFhx65QbNRrLVx1MDAxZqWw2Gr3r+BccujXwfr+eUO/3Vx1MDAxOb1agSuW6q1qdyeNZmt367V6q9Q4dy25NOi3T6u9z0X3u4Nq8GZUt7/PP/WYTMBuspBcdFx0mHHwXG6tXHUwMDEy9aqiXGbVplCMMu5cIo9wnjyGMlnB85Q6wM5/0Es8XHUwMDAzp4sqMLU4MfDoRydtieZxNFx1MDAxZqdVrCyWRFx1MDAwMmlcdTAwMTNoiDtBLmJBLlxymMOKk1xc7SSpLaWaaFx1MDAxYTzWaTHeadfDenv0p5XRkfH/8vPn//7b+e7YY4o/hehcdFx1MDAxZF0vooZcdTAwMWKlXn+t3WzW+7DRY1xcZETe9kvd/io81nqrXHUwMDE2fq3aulx1MDAxZr1cdTAwMTJ4rl+evU+RcvXQ2nqrV2/uqjeMdl/3zyv7RydcdTAwMDHWXHUwMDAwQq0ywFtcdTAwMDMrJ1x1MDAwMF240WBcdTAwMDRcdTAwMTB47pRpXHUwMDExeF+t1IF3Wdg6XHUwMDExRlxiRlx1MDAwNFx1MDAwMzUtoodcdTAwMDdXXFxE+fBYLUVcdTAwMGUorDn4XHUwMDFhXHUwMDAwrlx1MDAxZeLo1Ua5/ZaKZ9TPzl5qW1fdXHUwMDE3XbiSz2+Vl6tXvZ2KZ1xiIcHOjVx1MDAxN1XSXHUwMDE4eD1BVFHuIP1sXHUwMDA02qUwXHUwMDFhXHUwMDE3RidcdTAwMTmoXHUwMDA1UVZKbqMuR19FRFj+t9RcdTAwMTEodpRcIvkyXHUwMDBisIZcdTAwMDHNbCZm0Vx1MDAxYpRcdTAwMDHhK51u+7XaWulcdTAwMGadNIOqsd/+SpoxtvYwp4hfbD5cdTAwMDSjt/5eulx1MDAxMN33gXku1fZ443BrvZrFeGfUU9pcblx1MDAwZarJhVxcZpinwHCINVx1MDAxMahcdTAwMWE5P36QS+koXHUwMDAysYTuOHRP00PXXHUwMDAw+lxic2BcdTAwMTTtLsvCv/1xOoHeIUDg2VROp1x1MDAxZlxylc1cdTAwMTLnj6u2VnzrvOyWrpvDQ3N4/vKQ1lx1MDAxMj842NrfPTvrbeuLer32PJD86kDkZIlzn1fk5Clz7zKNhlx1MDAwNFx1MDAwM8+zQnNt4MGAJpQhnHHGPJqIM8I9yUGRXHUwMDFhpalxxT3EUl3GYe4sPeZcdTAwMThRXHUwMDAyfbZO0Fx0XHUwMDEyb4ljuEpcdTAwMWJcdTAwMWF4XG65WOLZz29EX5ZLvXplpVt9XHUwMDE5VHv9f1qw8PpcdTAwMDPotj7uaVx1MDAxZaqzWb+/XHUwMDBmKp6QkT5Bg4VcdTAwMTXq2G5W3JvJR7UmU/3JqpVcdTAwMDJitTbA/52qVVxu41FhjVZ0qVpzgfl5XHUwMDE2h1x1MDAxYkVggUXi0q0qPmRJMVx1MDAwM4OxqWzx6VTrdUOdic29w4+W7OqD5tXjaaN8O1x1MDAwNyd34nVf+Eapd25qW++6M3xb2zh7fumc5KKyv1x1MDAxY1x1MDAxM8pcdTAwMDYxO73Kdt+9dCrbYJqBgbVQXHUwMDAxf6EshF+rPZWIX1xub8D4rLJcdTAwMDJEOFx1MDAwYiB3aeNORvNFXHUwMDA2NHNhXGI8XHUwMDA36sq24SY22Vx1MDAwNlx1MDAxZVx1MDAxYyOcSp6rkfvtWiNcdTAwMTlOcERp+4rtXHUwMDFkXGZGNFx1MDAxZNtcdTAwMGaLoKgn6MOwoo7dQT7auf5Rurvr7WxVTlx1MDAwN7W702L/YLXddXBxt2fdeFx1MDAxY4lcdTAwMWXjnCpcdTAwMWHIn/jk4pp4VjOGOXHMXHUwMDEwM7qdP8i2wjOGK1xuXHUwMDA0XHLlgFx1MDAxOF1hpKg9XHUwMDA1XFzfgCFmkLBrtlxmlMUh/XJ21zrwdcpcdTAwMDHOzqA1jzWVlVx1MDAwNKhcbv0rnFxcnLDguf4trvX4c4o/hehcdTAwMTFcdTAwMWRdMKKlM/vW4zzoonV43uze7N/r3tCuX7/0d9bNR4xcdTAwMDddoXyWVEhcclx1MDAxYqCCXHUwMDA13vXpP8ctcFx1MDAwMoJfXHUwMDFhIVBcdTAwMTObyFx1MDAxMVx1MDAxOXPrR1x1MDAxNzxcdTAwMTfXunq+XFynR7esUlx1MDAxYlxcXHUwMDEy9fq0unN9V0znWvdcdTAwMWRcdTAwMDNcdTAwMWNzfi3nJpRAxoX2RKKosnCH8EfAXHUwMDE5kHArRyd9yUEmS6ar9Fx1MDAxY8RcdTAwMWFcdTAwMGJcdTAwMDREOyP4YMbHUlx1MDAxMK2U5EBgck74zcHPXHUwMDBlXHUwMDE2dvd9XHUwMDA1QNKvzoV+TOtid64zXHUwMDFmktF6a2xcdTAwMWW/1596u6/2tLt2fX/aOjqM4lx1MDAxNi9cdTAwMTnJijOJ0XskXHUwMDE5idF7qTwmXHUwMDE0cefsj1x1MDAwZctCI/a+jcpivpC9nplMSCWkYNqZXHUwMDAwx3S8212j61ZO51x1MDAxYUhwXHUwMDAwckKkncqWyJVLOM4j/rDR52anXGbx4fjp9fbYa512473mP8hJXHUwMDFhe1dcdTAwMWTcq4ve5VGRyf3nl4Pi4ep5K4r8uMRvZTysb2BcdTAwMWM0gogmflNhQPCAaKAmpsbBSE9IRaT4SvKOyoBAQHehhcBvUNs3MyfBgvkuiKFOc0LFxsxcdTAwMTlcdTAwMTVKwdOmOebAzlx1MDAxY1wi+zvpuvL65fytf77fOdxcdTAwMWFcdTAwMGVcbsMtfVYukVx1MDAxY67Lnnduj99cdTAwMWUvmlx1MDAxZNXZ5Y3VrUvNXHUwMDFm0l03XCJGot5cdTAwMTVGXHLLYl0l4Hxt7eisNdx+6eyc6sfO5eZd4eRcIn38nDGEKVx1MDAwMJlY46pl4tp6XHUwMDE2Y35aXHUwMDBiZ4FcdTAwMDfGXHUwMDA1YzJr0X1cdTAwMThMi59/0vtcdTAwMWZW5lHK4DCkzEpBpXG5/42JredQhFqpxTzd/82WXHUwMDFlKFNsXZ2b7YP76+vtfbW+k92dbvKBi3s1XHUwMDExuERcclmmMJecKcYtKEUlQz43oelcdTAwMDSgXHUwMDEw6dGlITstNspZsIHpQNKdMObwsP2kqTLGjJKL50qvt1x1MDAxZVx1MDAxYXhC5mjKlsHw+SpcZvffXHUwMDEw8qRPUDphXHUwMDAzN25cdTAwMDP52LjJ1Fwi2ZFux5PHQvUlXFxcdTAwMTJPcUPQxKVERJ1TXHUwMDE4XCJcdTAwMGaWdVx1MDAwNVTcSFxySu5cdFxm1FrB4Ep/XG7r/Vx1MDAxZKZvJVx1MDAwM8ilJlxmZKlwKkBcdTAwMTVxYv0wXFyrXHUwMDE0k4xMleUynVx1MDAwMmRbr6dk/fH1eGv3vXk5rNrti+bg1/LFgL2YpzeegVxu5GD0mXCnhVx1MDAxYz5eiOBk0vXiwTfpepE7nltsIDlcdTAwMTdoZVx1MDAxNFx1MDAxYiCeYCBUODVEYXxgVFwitfJcdTAwMWRcdTAwMWHgwY/Vh6OFjd1UjzBrXHUwMDA1XHUwMDAxpadcdTAwMTVcdTAwMTHajj1cdTAwMTJPasrQoDNcbsu3mY3cg5gwQ9z+kr1cbmP7I4RcdTAwMTE0QeHrleSEXHUwMDA3XHUwMDFmz9dcdTAwMGVlmlx1MDAxZGJcdTAwMWRcdTAwMDJcdTAwMTYvK4yQ+50ghFx1MDAxY9+ksMC7rLTUcKXBklFcdLvML2Qypq/81Z9Va5Ns4S9Zx8bEhL+m0K4nwSWItzGEfO5cIvPVXHUwMDEy4DK6jS5h5S9+p3fWqVbqpUZEXHUwMDAwwVpcIi8m6HK3rExBz1x1MDAwYkLBaZdcdTAwMTTsWFx0XHUwMDFh28ApXHUwMDE41+WUKo9cdTAwMWFBgZ+rMZY3lqBq4Fx1MDAxNkmj4bTBvVhcdTAwMDSC/oeZsPfpNbjCRjFRRY2Cz4Z/+Vx1MDAxM2zCXHUwMDEwqcBypJxJOrGa6ZmCTZ1ue1iv3q/4wZxFyHaZQIvDXHUwMDFjPWb9+VD0pjk+vtg5Llx1MDAxZD1V7jf1y0dnIMub6Z1UIJo0XHUwMDA1ocQ1hpEjTipBmKcl6Fx1MDAxOVx1MDAxYdOFhHLlMVRcdTAwMWTM7amSy1x1MDAwNiTJsK66YZ2lXHUwMDAxXHRBmiac+S1KxkeXjYGHLuV0gJ+Or7+V1MFT326/7D/3P7b3RXmjrlx1MDAwYnPIK0287l3fljW/+nj62Hx87N2+2KLV5Vx1MDAxY677x/i53U8lXHUwMDA1M2BaeNxqgsRcdTAwMTBsP1x1MDAxNnbcUZssPDBcdTAwMDVcdFx1MDAwM1hGaFx1MDAwMVx1MDAxYlrWeWZcdTAwMTFcdTAwMWNcdTAwMGbp+Vx1MDAwMHZcdTAwMWFcdTAwMTLAv9xcdTAwMTJcItZrRy1cdTAwMDPLhoJ1kDMlyOx1jlCCr1x1MDAwNNL5ee0mMIJcdFo4Jv/1l/jsksVkss+OjlelhHg+QNlDOCumseekI6OMWeIxg8lm9tNcdTAwMTRyeOLh2p6likgmli0lk1x1MDAwMF7LXHUwMDAwcLib8HhcdTAwMTRzueyA18dcIlx1MDAxYzszXHUwMDExrqcrXHUwMDA3naSeplwi/fm61YT0qFx1MDAxMET4XHLxZGa3WuLHXHUwMDBi4ZM86XLx6JhwuVxiXHUwMDFkyM2plqllhYDTXCI1oVx1MDAxYZtvSlx1MDAxNmjG8Z1xXHUwMDFi/OCUbjWMvjLBpMKYXHUwMDExMTO71ZIl80rQraZcdTAwMDFcZtYwoTnaM46OXHUwMDFjwlx1MDAwM3PHKKGsIdhMUqTZL945XHUwMDBl2+WYsSNcdTAwMTjVmLUzvme4KFx1MDAxN1x1MDAxNlBcdTAwMDP/4M5cdTAwMTP2PCcnW/QsXHUwMDA1XU3h71xmv5agnbrPrfvas3yUR1x1MDAxN53D62OlOut7XHUwMDBlL1RsToVAlyScXHUwMDFmd39YuIlcdTAwMWXTmGlcdTAwMTdcdTAwMTcqXHUwMDA2XGLC4Vx1MDAxZPnqXHUwMDFjXG7KXHUwMDFhz1x1MDAxMKVcdTAwMDRRvut7mVsxQU89ZdBT2mpquCQuPUVJNKdq5J2iWCvH+Fx1MDAxYztcYlx1MDAwZfY/9tlcdTAwMDHdqDZ69avNYnW/vX5K0lx1MDAxYX9qfWetsS8363q7evSmjlYrhYPi+LcsRlx1MDAxMaR7l2mMP1BXoFx1MDAxY6lSlFx1MDAxOatlXHUwMDE4i5IrjFx1MDAwMIO2IMqZx0glXHUwMDAwXHLzNqRcdTAwMTJWgPZbpm1kgd1zXHUwMDE22GklLXNXNCN3jIVcdTAwMWRBfYRcXP9cdTAwMTdYgDllbviPtFTBKv/eXCKYglx1MDAxMzRcXGxcdTAwMDKHe1x1MDAxZvnYhLXbx5vtrep+dWPI1i7oS/O2XFxbz6R1XHUwMDE1XGJtaoQzk1FaQLo0XHUwMDAwZFx1MDAxNq91gS9yLKhlYqzv+lx1MDAwZtKN8L5f/fxZat1E+DczwF+BeCaGOJtcdTAwMGJSXHUwMDFhkLGRXHUwMDE2osKgbJ5nTqOs3fItWl1cdTAwMTdXu4+XXHUwMDFiV5dXldfzrbRa97DxsVs/erzevGj3d043mD5ZPXtKp3WTtXnR2lVxev642322XHUwMDA3O6JHO7t5pEJfPVx1MDAxZu2X1m9cdTAwMWaGXHUwMDFkUahcdTAwMTT35MExpyldz/NlXHTup5KGJYCS15xJXG7iXHUwMDFlLDtcdTAwMTJoXHUwMDEy/dmplFx1MDAxOWBcdFx1MDAwMqtcdTAwMTRjZFx1MDAwN2NcdTAwMWXni1x1MDAxNjv+Y1hCK4OYkFozIOjOUVxylIpYco5WK1CM6SpcdTAwMWLmQ1x1MDAxMsoo1qtcdTAwMGLBXHUwMDBmJujiWH5cdTAwMTDZQj7UYOPu7GHjo655QVx1MDAxZL5LVryi+qCcmlx1MDAxYWDzfY1TheBcYrDo6CHBPWxRxaWMMVx1MDAwMZajh2aHeDs9xFx1MDAxOVx1MDAwMzHrMLP9lcR3XHUwMDE5plx1MDAwNFx1MDAwYshcdTAwMTmnc+xcdTAwMWFYXHUwMDE2J6+NPblz0bw11ePBUat5uMbmYH3/XHUwMDE2frG5vbX3tLM/OD9fXHUwMDFkXlx1MDAxY91cdTAwMWM/XHUwMDE1XG67ezlcXPfooPZ8ctjudDbX7rffTyq11+t+PVx1MDAxN37BXHUwMDE1s1xcWZZcdTAwMTO/cD/tXHUwMDE0/EJcdTAwMTHpXHUwMDE5XHUwMDBl+lx1MDAwN+SPRVM1JIFcZveMYFqgZlx1MDAwYlx1MDAwZTlcdTAwMTilpmGSozRWcoPVO8tGTJlkTydcdTAwMDO9MCDMQZDEOCFsbKKKP9Mu6G3PqVx1MDAwYoKkUsyWmOar5JXyoN64XHUwMDBmMoLfxy0mKPNov0Tn+vMhXHUwMDE2yTI4KVx1MDAwZS2o9Fx1MDAxNFHKXHUwMDFhd4W05Fg7XCK4wVxu+GBcdTAwMTbMyLPIsPdLXGaz4Mqzflo7U1SZQMrz0tPgxPhL2lxmtfhcdTAwMTZMVGmmlHampJr4IXCKYT3flN7H6WhH7UVV2PXg/aW/9rRZWj9cdTAwMWVcdTAwMGXfVzNWlCzG6Fx1MDAwNFx1MDAxN1x1MDAwNvCnXHUwMDEwOf6jK0U2l1ucOdkpujJW3GAt2JqSXHUwMDEyqlx1MDAxNbGUXHUwMDA1guTfYebIOUtcdTAwMTVcdE5cdTAwMTaOY2vgXHUwMDA0TVx1MDAxNlxuVJlgYMJEQ8HUk5FVzK9nlPuUpqBLQlx1MDAxOTCrXGb69IxcdTAwMTCWh7wxwjKct6GJUERcdTAwMDBcdTAwMWaKSFahPOHPYYOjbrSxrpjN/JtN/2FcdTAwMDK1m540aVx1MDAwMmY1iEBX80pcdTAwMTYrN6mxyp9cdTAwMWTyK1x1MDAxMvdm88j0YFx1MDAxNZX+P63y+MP6fZRpXHUwMDAyTYnMbHCvP1x1MDAxZsqUbF4mUSaplMdGflx1MDAxOFx1MDAxNjKDpMBcYlxyt/BcbsGkzihjgjfo7zR+d09cdTAwMTlFPWpcdNFcdTAwMWH+XHUwMDAzxEzQZWZ/Msx7s/MmkMVcbrOtXFy0ScdcdTAwMTbiXHUwMDAyWFxy09O2rp2ON1x1MDAxZDSKx7Xb3fPC7eb1+8nTQVx1MDAwMbRt/0/kTYVYKOBPXHUwMDE0XHUwMDA082BPqZlLgXhUXHUwMDFiI5VcdTAwMDFcdTAwMWJcdOs6XHUwMDAzLcd+qFx1MDAwYndQl1RcdTAwMDQq2XM9RqAox+JYXHUwMDA1VrvEXHUwMDAxV9yVSydcYv+MXGL/TiblPrepmJT2XGYjmJjE4G6rXHUwMDEwk5JSg6xXaKZcbqNcdTAwMWNMSlx1MDAxMmBaXG6MXCI8/pa5QuJLXCI1QcL201x1MDAxMykwg5RkTt9TPI+SnCGMXHUwMDE2kEZ9zoxaIFx1MDAxYTWBujhKXCJd68+HRq1cdTAwMWW9r1Xl6fXZ+lx1MDAxZaB0+L5uXHUwMDBmSjx9SEsrXHUwMDBme1x1MDAwZTFtpLXhkJbUXHUwMDFhzGpcdTAwMDXS38ZcdTAwMTREYpFcdTAwMDSjwk8wjWJcdTAwMWH9zVxm/c2RYvGlb3llXHUwMDFj3YP06MboM87Xsc7IXHUwMDE2tVx0VEn7M1x1MDAxONhcdTAwMWOp0snFdvFjtzhcdTAwMTSvT1x1MDAxZi12frv9RIZHvztcdTAwMTPlVzX72+ypXHUwMDEz8np8IXeKO7un1cstcqZzmqStOPIwldMkbfdTSUFcdTAwMDRcdTAwMTQl/iRsqimRkptQXHUwMDExpKLAXHUwMDEzOFx1MDAwN8gzlPqOXCJI6Vx1MDAxMVx1MDAwM3xSYj2u26OyXGZAxVxuiddcZsFvwlxmXHUwMDA2v51FUuhcdTAwMTCNXHUwMDEzXHUwMDEy2LLDWJ17XHUwMDFibsXggc9cdTAwMTaAwtdcdTAwMTcpXHUwMDAwNUH1Rlx1MDAwMlDu9edcdTAwMTSASlx1MDAxNJXJXHUwMDAxKONcdIaDlf2uXGLh4dpcdTAwMWNcdTAwMTBNXHUwMDAw78AptbSuxHbuXHUwMDAxcfiu8lxutt5cdTAwMWZxe89QTiynmFx1MDAxOcODV1lSfVx1MDAxN87f3DjPNGJbg6gmVLuzX0j8JCBswm9cdTAwMTmdI0Xo3Z+0brc3NrXobl9stY9fXHUwMDFl3+GWpVaNi+NNiYdcdTAwMDL+XHUwMDE0XCIoXHUwMDE4XS+yxdy8KVx1MDAxOdxcdTAwMThEWCZcdTAwMThY8JJwZlx1MDAwNdGBd307U5iSX4Vi8CN15Pilcq0ky82VlfE6TGUlcFZcdTAwMWPaYpjmNFx1MDAxYSFcdTAwMTPYj0nqr4R6XHUwMDAy5Fwiioq5uVjchzlcdTAwMDWzXHUwMDEy2ENSXHUwMDAyq5doisnArK6v1GHpXHTCXHUwMDE0XHUwMDBlWmMqmFlcdTAwMWNcdTAwMThwgm1cdTAwMGaY1Vx1MDAxYZ6eXHIo/mXbqa+VTVx1MDAxNLzDTFx1MDAwNFx1MDAwYngwc5ZcdTAwMTmw+OxhwpDFL+Cctc9Yz4pPUlx1MDAxNoFcXE0gNO5QVXj5+XCrZHMxiVtpXHUwMDAx9o4yijKGJbxhbqW0xzF2KaWWNJCROlx1MDAxYZxoPb+lXHUwMDE4yDauXHUwMDE5NY5IlcB+dGBTwVx1MDAxYpTgwcT1JcRdXHUwMDEwf09cdTAwMGZxUNF4Z6O2XHUwMDEyLkUkRKR9N4s0cywk0ofXj297pXelzsRdYa9/yZh4zN5jaVx1MDAwMZpYxFx1MDAxZflcdTAwMWM+XoiAZVx1MDAxZVx1MDAxNCw13SGeNvBcZog0XHUwMDEyo5rGkVxylKrTKfVcYlxcRGvCkFx1MDAxOFx1MDAxOWHM2D30jFx1MDAxNZRiXHUwMDFjRlx1MDAxYTDrVLSh7a9q5lrARrJcdTAwMWNLb4BcdTAwMDZcdTAwMWJgmERGdzg261x1MDAwMZ5lqi3jrVNcdTAwMTKjgcBcdTAwMTdxVN5Y01x0uCGSjtrIarFcdTAwMDCdXX9Z01x0tyhIwUCNwlxcMIpcdTAwMDRcdTAwMDXUXHUwMDExIaFEVEWFXHUwMDA3v5aCXHUwMDEzt7bCwtiFm/L7h2mnj/TaiVxuPMmYM+VioLHlLZoojvXxOVx1MDAxM9DZXHUwMDFin1Z8Wbs4XHUwMDA0dFx1MDAwMutcdTAwMGJcdTAwMTNQ9/LzIaDJlbNJXHUwMDA0lDNcdTAwMTC5ilx1MDAxOa2pXHUwMDA1uUjDOZDKSJx9amlcXDW7JFx1MDAxZahcdTAwMTDKXHUwMDE5ldjMh4/M0lx1MDAxMahcdTAwMDV+XHUwMDA39f1cdTAwMWGMXHUwMDFiZuY+lO/PXHUwMDAyuV3LkFx1MDAxMqko5Sqml4WJn6bJXHUwMDE4zoqdazL5Pjk9lefv21x1MDAxZqWnzcOL5tvwovdQyO7G+/3jXHTG3i2Fp7myRP+EsSd9vFx1MDAxMFx1MDAwYplfcb0o+CZej1x1MDAxOc9cdTAwMWEmceKmYHg9lvaCv44jp3ZT+l38hZLUUKqwY4NcZpjVKyOXoMLeZnC7hCVUqGD3tlx1MDAwNFx1MDAwZSlcdFx1MDAwNymIQ1xcgMPwMe8tUkgrXHRVXHUwMDFh3lwipNKRu5KRNL9sssPK7i19eSrbfdtrKn17vurcMShcdTAwMDeGjYUs/Fx1MDAwZj2A2jH9Oc3+/N5swO+IMNQy0FxiXHUwMDA0zt+4ZYCjpTG9XHUwMDBiwKixU9Hvp8k5XGZAXHUwMDE4w3NcIlx1MDAwMtNcZkBIhuOky4/E4d/JXHUwMDFi4yk2NttSZt5pgqSY56hcdTAwMDe33klh7zCKjXpwXHUwMDE4/Fd5yEj1+NxcYiDnoYBkQKLAao1QI/Q1UFx1MDAxOahcdTAwMGZZ2juZqdB6eirEuDWaWWd+OI81d3BkPDZcdTAwMDPKe9DD7Fx1MDAwZfdmtdlpt1x1MDAxYivVV//rXHUwMDE2wOKZYGaELZ64XHLkY/M8KLZXb2zfXHUwMDE0xH67VHrvPlx1MDAxNldbNpXNU1x1MDAxMNpi4jpgWlxuiikwoSwlo5RcdTAwMDfmKsNcXG2weUa4/enszC2wXGYrqGLaXHUwMDFheNYuq8dcdTAwMDOSYKxcdTAwMTDcwonQNJD1vIS6XHUwMDBi6lx1MDAxYrMnNVhO0aPpLFx1MDAxMFx1MDAwM1JcdTAwMTMrXHUwMDAzXGJRVFk1XeNnt56eZFxyPTytkYuPzunu02nr7eVW3eyy4nGGfD9cdTAwMDNbXHUwMDE1PJBcdTAwMWb3u6yheCz4r0ZQXHUwMDEwoYy/wFpcdTAwMTCtw/Nm92b/XveGdv36pb+zXHUwMDFl8JytXHUwMDA0PeqYq0gwvEY4pjZETVx1MDAwNe5cdTAwMDHV/8zZSCby009enymhwH2QUtCbXHUwMDAyXHUwMDE4XHUwMDBiXHUwMDFlZ9pcYssk+lxuwrmaXG7oIahHXHUwMDAy3IW7p91cYlx1MDAwZjuRWfpVIbTkN5mF3mZ6fmONXHUwMDAxmFx07WpIXHUwMDE2n1BArSVA0YPjQPPJ12RYsDRVTtU3wVx1MDAxOfSq3ZXqsFpcdTAwMTnAhVf6w0WgOFx1MDAxM1hFmOLEbyFcdTAwMWaSY9vPp2+n7HA4uDvaur3cOKm/bFbTkVx1MDAxY1BcdTAwMTHYZlx1MDAxNHVcdTAwMWLhQHNCRbCWKo9p4idcdTAwMDRcdTAwMDFfXHUwMDFknZ8gx7HCWm2MXHUwMDAxI8ZKh/lcdTAwMDLiXHUwMDEzpCeYR9QwkO6SL/uUJsN9a2aOg0FKJaXgLjHAZXxtXHUwMDA3qDfLcVbm/DjOXHUwMDBiOd5cdTAwMTNFQldPau3u7qGp1Ml750/kOIV4MPifj8AgT5JcdTAwMTNmXHUwMDBludJcdTAwMWatmcRG8oJcbi1cdTAwMDNJ/SuB1lx1MDAxZUxKzS2hSuC5U9GTOTeu4z5QabiOsdL7KWIjXFyGsic1IVx1MDAxM7hcdTAwMGXF1F2QmvxrYMmS62RcdTAwMTZ+21x1MDAxOWLXUihOjXW3+ohv0Yx9alxmyTV5Mj+q81nmuSBEZ1x1MDAwMrNwXHUwMDEyXHUwMDFkx1x1MDAwNvKhOclcdTAwMDJrUo1qQXFMXG7iSnJcdTAwMTBTRIXyUj5cclSC4SzmxrZ0oFmrZSe0ZDjvpOUysbM6XHUwMDE5kZgq5M6f1LFzXHUwMDE4ONUgr9VcdTAwMWOHn5jahpLF8vr1zvmA2+7Nx/BtmHpSZ/2jdHfX29mqnFx1MDAwZWp3p8X+wWq7m3LyZeJ1k02V6a+bLFx1MDAxOVx1MDAxMq87idCBKGWcZspcIk2QXHUwMDFh7qeShlxySGM8RanCyUdcdTAwMDQrXHUwMDBmxyWGXCJcIlliKL8xmNVcZsRccohcdTAwMWPlmvXLl/IjWX7spqdcdTAwMDNcXIBcdTAwMGXGyLpLUND4VDacXHUwMDAxyPIuVGXwjdMlQn+zgYteTH1qwIU2XHUwMDBmXHUwMDBlMEHthjnA+LJzmsWyWlx1MDAxNne3zZ1er0yvLt4uaFE/rKbW/IZgw3XK1Ocsllx1MDAxMIyNVVx1MDAxZWb70ljFr1x1MDAxY+Xlcknjk3G7l1x1MDAxZbdovoM0dfdY5/GTljSO8J7nKO6z5/JcdTAwMDPhO4X+xmrj/WntomaGXHUwMDFivbRcbrPcXHUwMDFm3Dduerec765cdTAwMWWsXXZON+/aV/koTMO0XHUwMDE1wZD2TFxu073LXHUwMDE0XG7TMMBcdTAwMTlFy9nCmqxcdTAwMGVZz5bQXHRAY8qDo2CA7IEgx0rMKO6W/Z0m4G4/g77Ep6CCXHUwMDEzXG6CsIuNXHUwMDE1KIFFJXK6sYJcdGdYXHUwMDFhxVx1MDAwM32Np9CY8NxA0620XHUwMDA3/ZV667VcdTAwMDT3/p9Wq91fkFx1MDAxOSbJOiysRqN7WVx0bSVcdTAwMWbVWjpcdTAwMWbcmqP7J73KhX69Kcjn26u79KqVXHUwMDEyzy+RssIxy1x1MDAwNISSX1BjMS3KOctkqVqngPhBlupcdTAwMGUlUFx1MDAxY7vKi4WMzftcdTAwMTZYMGW4nqPxvHU8OCtcbvWx+sL22D15XudXO/VMOjCz/EhAhXs1aXQgx9HEoPtgOYLIQH3TJ1wiMJkyXHUwMDExXHUwMDExTHpcdTAwMDazXHUwMDAwudJKS22WOjA7QFx1MDAwZdNcdTAwMDNEUCz0I85weWBcXHd0XGK8YYIwm2+bw1xclOBXnHmlP/ynXHUwMDA1Kqg3aNZbtbDu+H1qcIK+XHSrwdFuVmI3k49cInzcXb99eC2dlfUr2yfnsvl42krvXTaU4aBzq6lcIpFcdTAwMGWIYNtcdTAwMTCgvpRbq+JsTIGDgeXPNE9Ha9OlXpxcdTAwMDD7o/SwZ1JcdTAwMTIzltBcdTAwMTCAvbbh346qXHUwMDFlmWLETpdcdTAwMDY8nV48aF1ebFfPXHUwMDA2V1x1MDAwN+U1tcPq5nG3maGtXHUwMDExiFx1MDAxNCVh2SaIien1ons1afSi5lx1MDAxZVx1MDAwN8NPgrCVQlx1MDAxMFx1MDAxM1x1MDAwMlxilclcdTAwMDBcdTAwMTHUw8ZhmojvXCKEKECWodVcdFx1MDAwMDnOQFx1MDAxYzFJXHUwMDFlQOIsXHUwMDBiVibeJ0ONXHUwMDExU1x1MDAwMiTPM1x1MDAxY1GLflRyXHUwMDExXHUwMDE04Fx1MDAwND3j7PqbeyS1s/PRIZfH/cvd7YvHzqmtdlonmYw+zGQh8Fx1MDAxZoeuk8RTiihcbvbD0p+aXHUwMDFidk8yYFcyIZVcZiZzXHUwMDA2rL6ozvtx7FhJuFx1MDAwMVx1MDAwMjM/7dbcYaXLgVwi3UtSPy2Wr4q28faS1qN617dlza8+nj42XHUwMDFmXHUwMDFme7cvtmj1Qk6Cdu8yjdZUXHUwMDE2XHUwMDFiP2lBLVx1MDAwZlx1MDAxNlx1MDAxZn1cdTAwMDJNyWSgYSs4wzlcdTAwMTdcXGIzXHUwMDE4u1x1MDAxMFOg/zDYnWaAnWWUoP5zOVuCbZzCRfZEalxuZy33Zlx1MDAxYTl0c1x1MDAxYpSb9f5cImjNXHRcdTAwMWEr0scttPB81GZysfUktcms8qQhXHUwMDE4XHUwMDFlwTBkZESjXHUwMDA1I1x1MDAxMSeqXG5DYiY/L1x1MDAxNedcdTAwMTRcYr7I4i1lQnClnFxittFcdTAwMWWNPzMvsLDX8HkmTW/SfpuUdu8qb+/tq4PiycFDU+7MIVx1MDAxNJl43eTKz8Trzlchu+9eXG6FXGbY9dBcbiWUUeBXQTP0q15CTsAwpogrpdBcdTAwMTPsW7JLpZxcdTAwMWTSl+khja1+XHUwMDE15c5cdTAwMDCIVbG9XHUwMDE3rSVKUEZyde/mopJLlVxufF9/ha78v8Vx6k5QjGHtnLCHfFx1MDAxNPXZ9muhODh4eVZnXHUwMDFiXHUwMDA3Q1HunFx1MDAxNJo0k6KGf6mk2NbBoajBwoV/lYRcdTAwMTeXYc3cUH2VXHUwMDFl1UxJXHUwMDEwnjGotrHOKVx1MDAxMNmg3eU83bdb189cdTAwMDel9WGp/L6nL2yz09zv8bNFSFx1MDAxOeIgjqTNK1xc6txlKn2qMWzCiFGaXGJtTVifgo2bXGY1ITwsWzUgXuNybJf6dFx1MDAwMvKus/iWJOGWulx1MDAwYlx1MDAwYikxsVx1MDAxMVOhtVx1MDAwNtKUt2c4+zGO1ahskTTqXHUwMDA0XHJcdTAwMTanUVx1MDAxZHvIR6Ou9Xb6rXq7rkivsflinkmr1nUgPFajXCLMOeCYY2ldWKMqQpjfQ0nGT4hbatTsuL5Jj2tlLHyjcFx1MDAwZoMjNDZcIkqJVJzMNVNo93qobzd7pz1e/7hUw429yvYh/22qz72aNKpPc09KTVxm9jjWNqi5vkKielx1MDAwMia4XHUwMDA3XHUwMDE0XHUwMDE1PshcZmGaXHUwMDBiZ1PVJUSSIXKbwZTUfmayc1RcdTAwMDelTMdBXHUwMDA0aFx0k2pcdTAwMTE1n+d5i6DrJuiWsK5cdTAwMWJbdT7arVFcdTAwMWXI1uC4Vlh7oCcv18dmq3Kosmk3opVcdTAwMTaaO1x1MDAxY7uKSO7B81eaxzt2PY5eXG5DXfx1qecmgLiUXHUwMDAxxIQzXHUwMDEyk++H0ex4+ipwXmpgXHUwMDE25S/Xc1x1MDAxZmc3/dezvVe7dta7uTgpq+7N5f1v03Pu1aQy8YynNONWXHUwMDE4g5HMQPORrzCmmYBcdTAwMGXBPcYpZsVSjVxyQpdqLjNCyulcdTAwMTFCgdJcdTAwMTmKSstcdTAwMDVcdTAwMTFcdTAwMTbfI5NcdTAwMGIpKDDIfFtI5WnhXHUwMDFkLpKFN0HnxFl4jj3ko1x1MDAwMzfs0cv63klzs7Tf6Xb37vpcdTAwMDfre6lcdTAwMDZ+SEk8XCJcdTAwMTkgXFxKbHRcdTAwMWJqIcVcdTAwMTn1/J5CXHUwMDE2XHUwMDEwXHUwMDBlUjQ6clx1MDAwZVx1MDAwNLJcdTAwMDdcdTAwMTT3p0FcdTAwMWN1zJybf5104jhfXHUwMDE2+v3vxHYlXHUwMDBitlx1MDAwNSgxRrizYUpS+1slpZ+7mSe2/XiIXHUwMDEygVx1MDAxMNhcdTAwMTTYXvXH4Xa67ftBZU710v12J1x1MDAwZdVjK1x1MDAwZkM4bqn54Ld3X9l4e1x1MDAxZr5+sIvtI37XfHt/rT+nwS/D+XFcdTAwMDJcdTAwMDde+qI2XFzExbnyXGbjWklcdTAwMTDwNpD5+aOfifaAOVkhQZVjXHUwMDBiTIeCXsI3XHUwMDE2vvdcdTAwMTngK1x1MDAxOZb5ups7XHUwMDA2Z/ZFOlx1MDAxY/iet+nyXHUwMDEz8lxmyUfQ+9X8eaFhXHUwMDFiWWNO9Sbn99X+2c3Ny8bOVks/vfWbj1x1MDAwNzpccl6FtZ7ERFx1MDAxNUNcdTAwMDTBZIRcdTAwMTBgteFcdTAwMWVcdTAwMTFcdTAwMTI9ekC3XHUwMDAz87d+9K1iaK9cdTAwMWFcdTAwMWMhapR2Tdha4jVcdTAwMTav1VxmeMXOXCKU8Gi6PO7JxqpbpUBRg0DOu7hMKNDgU03G+cbrYbX/1u4+r5xfRyfQLyB0k5abXHUwMDBmioetnfvto423XHUwMDEzXnm7erXbd+9n8jy154hSQz2plWCfrqNcdTAwMTCSXHUwMDA1155cdTAwMDWyZkA146TFqOrVzIvpM1xiQkJcdTAwMGIw4rj4XHUwMDFjIz66dC6A/o9cdTAwMDf/Z1x1MDAxYTAvkl38mFx1MDAxZcxcdTAwMDBkwi3c7Jj2Qlx0WVx1MDAwN0IpXHUwMDFjZThccpq/XsjqOtI35WG1UD06kf3revG19rCvu2mzXHUwMDBlNre39p529lx1MDAwN+fnq8OLo5vjp0Jhd2/8W6bKXHUwMDBlPDqoPZ9cdTAwMWO2O53Ntfvt95NK7fW6X0933a8/xbdbwrpJI/NydTnvXlxuzUxcdTAwMTlcdTAwMTVcdTAwMWUx1irs9MqYXG7BWWtPY1x1MDAxNVx1MDAxYmNcdTAwMTJcdTAwMDd4O5m0VMpPUbFcdTAwMDJUgCPoOX9X11x1MDAxZlx1MDAwM+Z6XHUwMDA2zYzRTtDAMb3CopUzP2BcdTAwMTbWXHUwMDAwlrXMOVnfwOlVs1x1MDAxNX6X0Vx1MDAxOfnZfbO7XHUwMDEwXHUwMDFlrlx0ujGsrGPWn4+eTlx1MDAxNmrJw9e1h2PXQZVSy4hcZoNaXHUwMDE59H5RTZGPO3S0YMZcdTAwMDOR4MNcdTAwMTmLu12ZXGbYXHUwMDFhmvtcdTAwMWTKMMmJXHUwMDA0e14t/dkuqD+5oZ6lQzrcb2Rezlx1MDAxNEJcdTAwMWXbXHUwMDE5xVx1MDAwMlx1MDAxYuNApOdcdTAwMThcdTAwMDfaPWxcdTAwMGXPqttXRTE8Lt9sV47qp7dP2ZQj/D+Zyk7PtT96PFx1MDAxMvyPRzAwul5ki7nNgNm4O3vY+KhrXlCH75JcdTAwMTWvqD5cYpYsjFxyXHUwMDFkp0ZRgLi1XHUwMDEyXGZnXHUwMDFkmFxy/jMv0khiYO1WYcd0XHUwMDFlPXypXHUwMDFhsyfLzcCaiMeBTkhQSZpcIjtVVEXWJD0hgLpcdTAwMWFcdTAwMGXLwZHlJrKm+TVmd5/kXHUwMDE0xMpcdTAwMTLh4aBOhekuWpHQiD0hjSfAXHUwMDE2XHUwMDEyWCupXcSKSo/iXGZcdTAwMWGgYJ9pootArP4wmfucnl5JrjRYrM5cdTAwMDLkeNlcblx1MDAxZlCCS0Hz9XsoTjC8PFx1MDAxM7fqwSoqOLylPP60flx1MDAxZrmaQGhcIlx1MDAwNZExXHUwMDFiyIddJZt2SexcblST8pjRjFq/bsqGxkuB3vJAgXGOXHUwMDAzarSJOjNBXHUwMDFl/4ze8lvnRJFNPSGlhmvj/CpBXHUwMDAyKn9cdHRcdTAwMTfQXHUwMDFiM5Mrylxmtlx1MDAwMHU0kfSXXHUwMDE431uSXHUwMDFhbiiYx3SOJVx1MDAxYXf7XHUwMDFksdqR7faT2a7cX5dcdTAwMGaOb2+LmflVpu6rv4ZfXHUwMDE1YrGAP1x1MDAxMVx1MDAxNMyDXqWmMlx1MDAwNVx1MDAxY5JrXHUwMDA19lx1MDAxNFx1MDAxMUJYJKxRfiU9PFdcdTAwMDSTXuFw8Z/B0lx1MDAxOflVas5cdTAwMDf8SmDuraCYNMapiNIr7llQc9hcco5cIkGBw/BcdTAwMWL5lfskp+FX2NFFW6mtUMyokIkrXHTcXGKwl7Sxylx1MDAwNtvwjPxWQN41Wsdawy1wRpSW9GqC1G1mcEVjoo00zlx1MDAxOcbxUWAlXGbT0tKcR/xxwpRgM7Grr4kxXHUwMDBiw60m0Fx1MDAxOWeLpsjy82FWu7RQPL9vbFx1MDAxZeyU7sXJeW/4SEw7S3yJYO4kI58zV0PxJVBcdTAwMDcodS3HXHUwMDA2hU6H9DK+9PnbaUHdTlx1MDAwZmpq0XlcdTAwMDBmrDO5g0XB/o1rIzEzh003unM6ynR02/l42i3IQuNwWGW95+bH3odKXHUwMDFiXHUwMDA3MqdcdTAwMTXePn4rs93+XHUwMDAzU7W3u4vt/Z3xb5kqvrTZUyfk9fhC7lx1MDAxNHd2T6uXW+RMr6a7blxuipdjfMl991KoaThcdTAwMDV+Q22BKexcdTAwMDJLh8bxrKj0OFx1MDAwMf6kQVx1MDAwNVx1MDAxM4cv2nJP+LM8NXbstmwh3CB/XGaYO+nBbLRcdTAwMDJcdTAwMDOIOFx1MDAwN9NRZmM7yVhBgNra6UaMJ0aXXHUwMDA0XHUwMDFjjll0dPk7efE1Judjzjp6gmKMXHUwMDA0l5zLz0dHJ1x1MDAwYrTk2Fx1MDAxMpg9nGmmOVx1MDAxOJDChom3VVx1MDAxZXZvN5ZcdTAwMGLCRbQ0QnD53TbYoaKJR6wwXHUwMDA056pcdTAwMWKFXHItlsPqJiD8xY3wLJ5cdTAwMGaD9ivRzr6p8IhjnZ9cdTAwMTJccidGpmPn02nxRvtU3DQvjs+23ktv6mnz8XTQXHUwMDFi/omOXHUwMDBmXHUwMDE3XGb8XHUwMDBmRlx1MDAwMTC6VGR3c1x1MDAxOLmbLLRWgp5cdTAwMDdcdTAwMTTaQuK0XYrFozzwpu9gk5LcYo9cdTAwMWFCudNcdTAwMTkyP8eD+ySlYDSWcHhy2EuWc39wUEhcdTAwMDBcdTAwMWFcdTAwMTCAUimLXkGtXZ5cdTAwMDcgRGC5XHUwMDEw6VdAL7tCTyH0ulx1MDAxOWxcdTAwMTRcdTAwMDI2n5FUOz1cdTAwMGbxgVx1MDAxZMaYwqFQuVx1MDAwN3bUtPInXHUwMDFh2ME3Llx1MDAwMrGZwCbiXHUwMDAzO+NcdTAwMWLIKW0m0aZKXHUwMDBl7CjrXHRCtFx1MDAwNuZieDhkqzj3cNRcdTAwMDX6XHUwMDFktFx1MDAxMa64jvFwnrI0XHUwMDE0xJuydHS6XHUwMDAyXHUwMDA0R2BcdTAwMDFcdTAwMTEzXHUwMDAwfVx1MDAwNWaRXU7TnID13uxcdTAwMDSHgCFcZprJ3Skk2OUgLFx1MDAwNLCXmiHBNpq/PnXm9npDrO6VnteKa5tcdTAwMDdd+UhcdTAwMWVPXHUwMDBm/kSGU4iHg/+yXHUwMDAzXHR5Mp2Z+VxmLJBij1TJXHSIXHUwMDAyyShcdMSmvlxiXHIlXHUwMDFlyFx0XHUwMDAxnFx1MDAwNvZcdTAwMDGszZGqkopkrVx1MDAxZb2vVeXp9dn6nlB6+L5uXHUwMDBmStxNsiiFxehPiiVcdTAwMDOFiKM1/UZS5T68qUiV8KglOGdcbvQuXHL3VFOMe36GsZExXt9lOGd2SdvPwKo48HxcdTAwMTZjM1x1MDAwNpyOkVi5JiBcdTAwMDfIdIVcdTAwMDVJrFxuLixmYlWLXHUwMDE20JlAY+ZcdTAwMTnQab9cdTAwMWWUr3dVcevuuFk7a1fW75ut3Siq41x1MDAwMjqMKE/h2Fs4MCDpw1x1MDAwM1x1MDAxNznFXHUwMDEwOdiLXHUwMDE0az6jyP5t8Zz/Q27gQVx1MDAwNmRjS8vxjLZcdTAwMDBZUjK+4Fx1MDAxZdVcdTAwMWZcdTAwMDd1mHMzXHKmXHUwMDE5KLssJYBcdEe5efI8eN46v14/ON3ePn6929pcdTAwMWM8pu9cdEhBwXjAWVxmnGNcdTAwMTGNTeKkRG4tZtJyI1x1MDAxN+ko/1x1MDAxZlx0Tb5mMftcdTAwMTWRUlpnX3zgTrGtzyizklx1MDAxOPjgXHUwMDFjvZpX6mP1st6o1Nmbbm731OF6/z71kG72UWhU1NUjf9XsoXp3SU+u1m/Hv2Wq2OR24/TkpHN8crz6cjpcdTAwMTT9ZrlweHuf7rpcdTAwMTH2/ksx7b57KUgnaCbpSVxycDOCYsJUyOC3VnhMXHUwMDBiXHUwMDEwiELqQKeRUZcn6WnDuSDYRohcdTAwMDTbRC1jk5PR/JZcdTAwMWXNflx1MDAxN0N4XHUwMDEyzlxihZKxpW8gqjHxjuTax9B3c2NcdTAwMWLLWehmXHUwMDFk2SHwN5xcdTAwMTK6QPVvXHUwMDEz9GOYdSbuXCJcdTAwMWbymdxQPMmdJ/RnXHUwMDA1hmCWcNDW4/NcdTAwMWKlZdTTliuwp0HzXHUwMDA2Tc5vfHOgpqCNucbotjHUXHUwMDAxb4Nmqd9kk1x1MDAxMMqXWdrJiH+f3ZWnsFx1MDAxYr5cci4m6MrT8f58eEhcdTAwMDbs1TnOu3nev3lcdTAwMWWer69f1zpPdVBLW1x1MDAxN7dbtczzY/RUzS9y9eTF4lx1MDAwMH9cbiFcYowuXHUwMDE22V9uPrzk2Vx1MDAxZCvjPjzfMVx1MDAwN1Ymx1xuuEAvkZVcdTAwMWaHXHUwMDE59aTvNrXUgFx1MDAxOFx1MDAwMFx1MDAxNlx1MDAxZTl9qZx4tdWyuLtt7vR6ZXp18XZBi/rBuSriYatCarTvu6CBovuVQI42XHUwMDEzWLMpXHUwMDE5zoyQXHUwMDBlQMzNqec+xyn4lVA4zVx1MDAwZr3v1lx1MDAwMLtcbjSPQYFcIqRUXHUwMDFlXHUwMDE38Fwi+vS4y6nHjCd48jTA5WD5XHQy9yM9y6JcdTAwMDa4rsGqJYdwVbF9Zlxyt0Jxa3ImWTNIv/FI6UqlXHUwMDA00MWL/9P6akm5XGJUa1x1MDAwMrFxXHUwMDA3TVx1MDAxM/eSXHUwMDBm4Uq2J5NcYpfVylx1MDAwM7BcdTAwMGKce2yjdXFcdTAwMTbEXHUwMDAxXHUwMDE2tFJiXHUwMDE54N1BuNBTmJBcdTAwMTZBPcNB18A/wlx1MDAwMumXbPRcdTAwMDVL7DuwXyQz8y1cdTAwMGVcdTAwMTjEXHUwMDA28i62peNnkVGjQIyAVTxHJ8rBzdt1o7axcXve2ehdrN/sbdTeWDanhNbB+Wq/i25RMEwsgEBcdTAwMTlJwNTkcoxvXHUwMDAxeim8aoVmglNMXHUwMDE1m3Q9J6o+L1x1MDAxNcZTnuQte6pZsvE5RqCUIFx1MDAxNmd4oNPFKlx1MDAxYU01o8QjiktrQCgpzqXUv5FBuY9mXG5cdTAwMDZFXHRnSL0plVx1MDAxODqjJFwiUuGocOzDXHUwMDA2P0ZFXHUwMDFinVrpMSsxXHUwMDFkV+JhcrUpXoZFJ0hRmp5BaVwipFHKTaDiXHUwMDFiNMFcdTAwMDNcdTAwMDKSr/KdpoxcdTAwMDJNjLWLmoJBVYfVylx1MDAwMFO1+sNF4ExcdTAwMTO4SZgzOVefXHUwMDBmS0r2jidnmVx1MDAxOeExTKBX1Fx1MDAxMlx1MDAxM8Tk53BcdTAwMDGm4KtcdTAwMTXwaVx0XHUwMDE2XCKlLppkPMqxr1xikHVcdTAwMTOXZ2YtxbRcdTAwMTRmQY8oXHUwMDEzaHG+RLlcdTAwMGLlbGauRDFrXHUwMDEw77TbS01ifVNY58jh9TlmmdHd58KLeC5cdTAwMTbKhWr7qNKs18v1LFlmIFtAzFx1MDAwNYo/fluWmbZcdTAwMDAmXHUwMDAy2pFbdCYwOvZxXHUwMDBm+Fx1MDAxMzBYQzSxQHBcdTAwMTibeL14cPkvXHUwMDEzj1x1MDAxMsPAyGFcZuCJTVro6JKzk6aZqVx1MDAxMabVXHRcdTAwMTD6XHUwMDE05FxuXHUwMDBlKNDEXHUwMDA0ON9cdTAwMTc50khcdTAwMWGAXHUwMDExXG5JOKHafDvtYlx1MDAxY17RXHUwMDA1z4U1uc9oKtYkgDVRjk9cdTAwMTFcZlx1MDAxOCnDXHUwMDEyloKEXHUwMDA1XHUwMDFiXHUwMDE1IKeVoNFEXi08IExcdTAwMTbuzuf0lpHMWbKmr5VNlKc8PWui2MJcdTAwMDH9XHUwMDA2rli9im9Si9mZIFdzXHUwMDFlUsa0XHUwMDE0era2ll/JWIvBmSYwlZhEsvxcdTAwMTnTXd+WNb/6ePrYfHzs3b7YotXlVIzJWO6hXHUwMDFimaDrKNxxWlFcIj1g3IqCpOeuoYPaMLB/SdyoXW1cYvr/rdYoXHUwMDEwzDKIN1x1MDAwMdhcIktcdTAwMTJcdTAwMGVcYlgrtHD2XHUwMDA3MPE1xThVx1xiMV03+elcdTAwMTjR2/mFvji1+9fH18PT5vVqsUjPK9midYxcdTAwMWHGg9j4LYxo7N3YRVBbJVx1MDAwMFx1MDAwM5qCiOVcdTAwMTO9RVx1MDAwNSdcXPK+VkEp4WlBhLXSXHUwMDAwZbaTiZlcdTAwMTLSw1x1MDAxMKON1EwmXezXUTLR2fnokMvj/uXu9sVj59RWO61cdTAwMTO3t1xuVs1cYqaiMDDosL5z5Fx1MDAwYvjhZEGW9lBcdTAwMWaO1jb2bD3YI5A6abFzplx1MDAxMFKFXHUwMDFjelEymjGI2TTHx1x1MDAxNzvHpaOnyv2mfvnoXGZkeTOOaFx1MDAwMs9cdTAwMDTarbmUsDNqXHUwMDFknTzTbFxurlx1MDAwNJZcdTAwMTPGjonExp9ajTUqxV1rojSIaKPQQ1x1MDAwYiw/YZf58dMxTeev/qxamyBlvuUkXHUwMDFikzD+mkK7XHUwMDBlXHUwMDAxNVx1MDAxMV5joPjcReRq8VibdOmRjPs7eVM8xaamX8ZMO0xcdTAwMTBcdTAwMDFjO4yIXHUwMDAzf1x1MDAxNzu9s061Ui81XCKSXHUwMDE51lx1MDAxMXkxgeO4lUhcbpNcdTAwMDUsXHUwMDBliZOoXHUwMDE4/CtBM46Hylx1MDAxNaPYXG5cdTAwMGZsWIWnP5qJXGJcdTAwMWbH7nJcdTAwMWNtPuOeRrc0WCbwXHUwMDFhmcHNq+FcdTAwMDBcblx1MDAxZS1cdTAwMWPE+1x1MDAxY2+vWIYjXHUwMDEyzHSDxydcdTAwMDXKZyspXHUwMDFllEEhLojBMsFQiFx1MDAwNMaja8/HYPk4q2y3X3dZca9zLtbt25VYf0g1U0NcdTAwMTHrgUaUfuU52KehxEOqNYDZXHUwMDEwzVx1MDAxOXGlXHUwMDE1a5CSlFrsf66VdNosy1lXsTBW6WFcZs9GS2x77oAxj5+rXGZoXHUwMDAz5mNFvrPpsHJcdTAwMDauamdcbtec9dvdqlx1MDAxM8OLMt4qtMKc5sCuXW/fko1cdTAwMTfKhsPbu/tr0tpsfKRBKtiYnlx1MDAwNIBir1x1MDAwZVx1MDAwMKpcblx1MDAxNfRcdTAwMThhPU5cco66XHUwMDAy8lx1MDAxOUWq0Z5k2nJcdTAwMDNv0ZpcdTAwMGJHftpcdTAwMTKpsUjV6ZFKKTZbXHUwMDAx3udyJIBWjYMqqFuGg2BzXHUwMDBlrH6O/abBs5q5N9lcdTAwMWYwiu5cdTAwMTfNn7t837s7rnWvblx1MDAwN1x1MDAxZGWqQ3vFyEk/XHJauaBY5O3HyoxcdTAwMTI81HnHSO2BXGLVXFwzJ1pB7WKfaWVcYjxFdy7EXHUwMDEyrbFoNenRKsDIo4Q53fk6mks20qtcdTAwMWEnXFyR3PXq7NPYwUrsL7ZeXHKtMFx1MDAxZqTuPJVKZ63y+/rNxs7rzunRll5vpi/8LjCmPSzq9qOkmElcdTAwMThWr/C60cZq6rZql6XfOeDWZtKyTCrscORcdTAwMDAuXHL21FxiIVx1MDAxNz6F2YjTlcwmXHUwMDAwl1I4XCJZXGbbhLPcfNpYt3eX4vistHdkuH69Jzp95XeBoe6x2JqEcWCL0bMsiWeN5SzGqFvWfs94jovpz7G06Ei27qxlxWOzljU8PMmlmGPUqa4/upXV6nWxe18u1T+OzjdvOvUseTi5YsS9mlx1MDAxNMxcZkS99ODYK/RqOOFhXHT2TjNcdTAwMWNcdTAwMWWLs8uH8Pv3mMXohvjHgGI1g3C3QIsxiOLMTtMsvnJSgIqmiuSdZ4HOas5mclt2q812v7poJdTJiiZM3Fx1MDAxMvaQUz3P887t8dvjRbOjOru8sbp1qflDXHUwMDE01K5cdTAwMDJqwzxuKZXuMaJM4jeDXU5ccubERUFt0M+pOcHAXHUwMDA1g9PnKunBYVx1MDAwZlxue8ZqgdMtzTL7XCJcdTAwMTnxa1x1MDAxOaJcdTAwMTQ4XHUwMDFjRzHpXHUwMDA0vDGxw1x1MDAxOSg8bspMcDz4r9eDb5dPr1ulV1O/I8WSoFvPN0+N7NlcdTAwMTe/v3hnPPtcInK6J328XHUwMDEwi5lJ14vcofw6JqqDe3XRuzwqMrn//HJQPFxcPW+5XHUwMDEzXHUwMDFkXHUwMDE4VuZcdTAwMTOt/YaJctRcInUlY5ZcdTAwMDNH/oZ1PcKgO0eP3Vx1MDAwNI9IMFSIIFx1MDAwNshcdTAwMWPcXHUwMDBlXHUwMDFluVx0vyrnXHUwMDAxOIwm1HBOXHUwMDA1lVpoSyNcdTAwMWJknvCxg0Fuf3Zpmlxy01BcdTAwMDLE2H4xO42BgWzVZ2vapFx1MDAxNI85JT9ET1EwSFx1MDAxZf7O8GtJXHUwMDA01ClcdTAwMDZSXHUwMDEwUCG4R6iKU1VcdTAwMDJr/Kmlwko4lcJRU6GN5+Key/LyXHT6aD1cdTAwMGJcdTAwMDM1XHUwMDAwmpj6iID7J+LDXHUwMDA3i1tJlatX8FthzFReXvmWrv+04lx1MDAxZITzrpBKZnth9lx1MDAxOb+FzOTzX1/K569Sp3PmX+tb4v71Wq++rcZ7Jv71JVx1MDAwZVx1MDAxMFxiVb/o5t//+vf/XHUwMDAyk+RsRyJ9RPCsubmit proven txbasic requestverificationverify tx proofquery stateinflight stateproxied queryverify stateinflight transactionsinflight batchesbatch builderselectbatchprovenbatchblock builderselect blockcommit blockmempool eventsuser executed txuser proven txUserfilter out invalidnotesexecute txconsuming notesprovesubmitaccount 1 + notesaccount 2 + notes...account N + notesBlock producermempoolNetwork TX builderbatch proversselected batchproven batchblock proverselected blockproven batchinternal tx proversselect candidateaccountexecuted txproven txsubmit txStorebuilderstateremote tx proverscommittedstate \ No newline at end of file diff --git a/docs/external/src/img/operator_architecture.svg b/docs/external/src/img/operator_architecture.svg index 961c58faa3..3450bc7674 100644 --- a/docs/external/src/img/operator_architecture.svg +++ b/docs/external/src/img/operator_architecture.svg @@ -1,4 +1,5 @@ -eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWXPiSNZ971/hqHlcdTAwMWRycl/mzftWhfFu19dcdTAwMTNcdTAwMGVcZlx1MDAxOFPGgFmMYaL/+3cv2CBQSlxiUNluXHUwMDBmVHRHXHUwMDE1XHUwMDAykZLynHtu5l3++8fa2rd2r1H69u+1b6WXQr5aKTbz3W//xPefS81WpV6DQ3zw71a90yxcZj553243Wv/+17/yjVx1MDAwNlx1MDAxOX+LXHUwMDE06o/Db5aqpcdSrd2Cz/5cdTAwMWb8e23tv4P/w5FKXHUwMDExv9882eLHpYuGeTgo9be3M/eb2YfTwVdcdTAwMDdcdTAwMWZ6XHUwMDFiULNUaOdr5WppfOhcdTAwMDXezzDtJIH/qFx1MDAxNdRcdTAwMTiljVx1MDAxYVx1MDAxZO/Bcam0IEI4J5zWXFxYOzrarVx1MDAxNNv3eD1MXHUwMDEzpfn4a/elSvm+XHJHmLGEwlx1MDAxMfr2cqPPXGZH8u81Onqn1W7WXHUwMDFmSpv1ar2Jw/1cdTAwMDcr4Z/xYG/zhYdys96pXHUwMDE1R59pN/O1ViPfhLsz/txdpVo9bfdcdTAwMDZnh7tcZnfz29RvXFy+XHUwMDBlnE29XHUwMDFm9S340fJ9rdRqTXyn3shcdTAwMTcqbbxFjI6vXHUwMDAyR9jYL1x1MDAwZVx1MDAxZdZ/xmNq5lx1MDAxZkv7+LRqnWp19HalVizhM/h2q+tcdTAwMTM/Vyu+/tzbo1x1MDAxZT9H8frOX+PBl0p4ZiaUMfCA5Pgmj+ecXHUwMDEwYvrdbL02mH+cXHUwMDFhTqk0xozH1dqCSddcdTAwMWWc9i5fbZXGXHUwMDBmXHUwMDAxx7Y9PSGDk3JizrVLL+MnXHUwMDEzmLInt1vC3j5eZJ9cdTAwMGV2X07V9/Wd68fut9Hn/nr92/hcdTAwMDZ2XHUwMDFhxfxwPMxI5oQ0XHUwMDFh5ipcdTAwMWZcdTAwMWSvVmpcdTAwMGbTd7daLzyML+GPwE2bXHUwMDAykH80IVx1MDAwME1cXMxcdTAwMWJ2XHUwMDE0gMNauHlcbuDB6Fx1MDAxNHZcZkBDUi4jscOEIE4xa1x1MDAxZFx1MDAxNZZa5WRcdTAwMThFnK5QXHUwMDEzgZqGXHUwMDFmNVx1MDAxM1x1MDAxZn+FXHUwMDA3Z4YzYcM4wKFYXHUwMDE2hVx1MDAwZabhXHUwMDE5Sk2tW1x1MDAwNFx1MDAxZGlO4PF8xHlcYpe//dIuNWv56lqr1HyuXHUwMDE0Sq3A06zX2qeV/vBcdTAwMDIm3t3JP1aqePvVxPnWq5Uy3olvXHUwMDA1XHUwMDE4eqn5LXg72lx1MDAxNbBJo1x1MDAwZtzW2+1XyzT4QFx1MDAwMU6ar9RKzf0kxqjerJQrMOaz2deQ77TrJ6XW8CrazU4peK9KeyNcdTAwMWJDuIrB9vHlRrnzcNzd46Xb2l3m9rZePXxcbmNcdTAwMWJPOY1tITWxVnEntaHM6fHMXHUwMDE5YJsqTpy2kjLO4WHaMXLfwG1cdDdCXG7jsYzaOVwiXHUwMDFjXHJazVSw/Y+7wesjce2Wh/VTclhcdTAwMGJONXNKy+CMfYO1iTR6TDFBXHUwMDA1oDB9WFx1MDAwM4/L4FRNXG7rRr0ybVPHf1tcdTAwMWJPi8E/Rn//zz+9n86E5lx1MDAxZb7Gs278/ZCpreZb7c3642OlXHJcdTAwMTeWw0GFbn0732xvwPOq1MrTx0q1YsSRwbfWm816976UXHUwMDBmPX34XuSxRr3aK1x1MDAwZp7oLEtOj1x1MDAwZa73W81f27ubL3m6TW/PnvaP5pDCXHUwMDAwZsJB6irDfJBcdTAwMDdcdTAwMWJCmFx1MDAwNDttzVx1MDAwMPIhxDtBrFDOXHUwMDBieW5cdTAwMTlx0nFmjXT4XHUwMDFhz5KvXHUwMDAy/TRMejM59lx1MDAwMcKaWSetXHUwMDBm/IJF23SUXHUwMDAxzoo0XHUwMDE173D+5XPPlVx1MDAxYvuw3ji5/9H5dXNeOm5vNCdcdTAwMDXw21x1MDAxY8zjbFx1MDAwZkjef/rPO0tJ7/063toq0/Of33uV5+7Zy0OeuufZp1x1MDAxZH75PPOYr5+fZLv9851dtpHJ5V86dqnhXHUwMDBlz3uXzb6U+4W9evbC6MPczcND76iXwnl/Xl6f7Gycnfeu1Pn9WVlU2J14SuG8p9ubXHUwMDE3/Kad/fm0337eqj0/PuV6xVx1MDAxNM57eLBrMzcv5e7R5Vx1MDAwZlHKnJ/x+4ZK4byslz/IXHUwMDE1lap2vre2u7aYK7bLuymct7qXPT2612ePnZPDrWJnc6/Lm1x070PIjExZRnCBJNdiXHUwMDFly1x1MDAxOMPz/lmfyGNcdTAwMTPA0dKBs1xmLFx1MDAwZTyvpime83iKN2BZmXNcdTAwMWOYXFxcdTAwMWJlXHUwMDAynsOI6VXKXHUwMDBl21x1MDAxN2H2VnJml4pR7uCPh9i5iiZ2qlx1MDAxNXPglC9E7DGqXHUwMDBlXFxAI9VcdTAwMWNzN+Ssfa/ni3/WbvPVPFxmNWDjXHUwMDAynlrAz0/FU2vXXHUwMDAz7vGUmzZDKE27aTj6tfDg03HR4o3mXHUwMDE040yh2UhBlGRK+n00blxyUdRILVwi0CxcdTAwMDR8XHUwMDAw/Fx1MDAwN8GGq5TjXHQ3QrMgwlxiXHUwMDAzQlx1MDAwM1jB4Fx1MDAwZo1/46ugO1x1MDAwNZet7Vx1MDAwN3d4/ZK/vlx1MDAxM1q/5Fx1MDAxY1SZVkpcdTAwMDdn9CvmlYvEvIOnXHUwMDBm1oWNn0pcdTAwMWGQfzVXXHUwMDBiQT5VRy5yguIrXHUwMDEznpvj84UscoqOXeCxvu5OJKGVXHUwMDAxsVx1MDAxNTp4ayhRYD6lXHUwMDE11mmp4Ops4EPlfFx1MDAwM8FLXGbnWo2d1b8mx5nUlfzWblamnL1S9bbeTSQ0XFy++Ou+dcrV5pbKdFx1MDAxZjNCybN6mJtcIlx1MDAxZEqmXHUwMDE4kdRcdTAwMWHBvVx1MDAwZSVz4FByq5SRxu9QSkKn1olGxGRcdTAwMWPhUtNcdG9zxUgjRurM4UhS6lx1MDAxY9xjOf7VgCPp1PS7I+7RWlvu2EKLSH4hPsvfu/l+eLXJvqv129rDXcm52nnN8Ll0uJYgcsfzcClcdTAwMWTuXHUwMDFmTVwiXHUwMDFkXHUwMDBlfEZcdTAwMDBcdTAwMTFcZu+ftDyswyWoXHUwMDA2pqlcdTAwMTTIXHUwMDE0YWRIVFx1MDAxNfDUwJF33K/DV4urPlxcPM+BXHUwMDBiMC5gdaz0LbDA3Y/U4WCTwbuyMl1cdTAwMWS+wNxccunwk9zmu4jvx0qxXHUwMDE4NFx1MDAwN1P6e4ZdmdbfXHUwMDEzo05HdW9lq4V18ySK6q6S3c11nsBmy+SWXHI8aLA+1IBLzz2GTShHmJDgOFk/fFx1MDAxOYdcdTAwMGaoQGzA+Fx1MDAwNCP82lx1MDAxNX49+O0mx69yIFx1MDAxZoFCffCVzky/O3ajXHUwMDE1XHUwMDEzXHUwMDAymHd8XHUwMDA1v92u5Z5y1bx8PKg/XHUwMDFk3+V0+fqht3+ReFx1MDAxZDP7Yvt78uKqdl1cdTAwMTPlp++n99+Pa9lcdTAwMTTWw84vj8p5/tg+uDrf29+93C00Ly76KZw3W9y9VqJ7dFTey3V6h3Inw053UjhvvZwtXHUwMDE2XHUwMDFm9jKX94rnmmXZ67DL41x1MDAxNM7b7WbM9Xd6dbi/n1x1MDAxNUeZy7ueeDpIYV1QcaZcdTAwMTmGR8iUXCI5/LMoiVx1MDAxZVx1MDAwMVx1MDAxMVx1MDAwMb5cdTAwMTRcdTAwMDc5zVx1MDAxNafgmkzxmdFEx/GZNVx1MDAwNCxcdTAwMTQ4o1pcdTAwMGJcdTAwMDOUOFx1MDAwNtdXXlx1MDAxNkyBzl7mXHThYFJYwY1viUDwyM1eeGaOmlRDOHDmgluH3usyamRcdTAwMDOP/1nLNevFznstXHUwMDBizlAmM3TBtDJcdTAwMTlcXMFa+Fx1MDAwMtJcdTAwMTEp/f3ds/Vcct5061l1cFY6XHUwMDEzj7XaQXKRMnC/mUL3wvlUXG5u98JRaVxchJOxcr9cdTAwMTfFddBccpzpZnDUKHCTfcv9gkW634xcdTAwMTnqrLaLXHUwMDAxezGdsrVO2/3dbuGJ9ot7N+vHt7nb0+pcXP63dFpcdTAwMDVcdTAwMThsKXvnXHUwMDFmTVx1MDAxMntcdTAwMDf+tySOKW7B01x1MDAxNpZPXHUwMDAxg1uiXHJcdTAwMWPV3L9uXHUwMDBl3rdcdTAwMDLv2ygmXHKXRjBf3OJcblx1MDAxN1x1MDAxZVxciDlim1x1MDAxY9hcdTAwMTgupPXAgoWN4Fx1MDAxYiwsRlx1MDAxNmpcdTAwMTAj6Zm7YWiTcmq5iMVP4nzPsCrv4Hzv5GVh29W2Xurm8Ojmtnd/dVm6SVx1MDAxOHHsXGbhjGpHgTGDS4/DZTMtXHSLXHUwMDAzLnOUSOGsklx1MDAxNP6TaoXb4ZBm4lbNYc+M4NRK5l1O5jJk5d6AqyjnXFwzm/ZWloWpwE1wos5cdTAwMGLcZqNA2qVWu1ZqXHUwMDEzXHUwMDAwV6lGKvV3QXLMXHUwMDFl9sT4pzFcdTAwMWI/4HRA3MrXr/ZcdTAwMWF721x1MDAwZv1fxf7d1U++U6mo5OLUMfA4XHUwMDAxpVx1MDAxMeHFmFVcdTAwMDPqkkYjebWCtiCU9Vx1MDAxY1BcdTAwMDYpJJAvfVBWMibEkEsnXHJdLFx1MDAxMmUxadr4sStcdTAwMWXqz7/uMs3W6Wlm09Q2srmPXkJTL9tcdTAwMDUmLlxmP7t92t/pt8/7XHUwMDE3ZzzZeWOl9GDpiHFl05LS/ruXxCRbI4mxUisluFx1MDAwNp2kp4AsZ1x1MDAwMFmBk2lASzPBMH48XHUwMDEwXHUwMDAwsTLJsTg2yXFcZlrHgHHl3lxyXlx1MDAxM71cdTAwMTJcdTAwMGXe/SD2XCJdLVxyJtnCqZdcbig7bdebpXexwTPU9FxmMzhtmafGnY4pvixdXlxu2fm1l+uXbp9OSoe10+plMj1cco+DXGJcZsCQ4OBINVx1MDAwNV5M8oEnZsVrOGg4yUdpXCJcckZcdTAwMGYw3DZxcrVcdTAwMGY9XHUwMDFj0kz02uToVVZcbuGMNzSMRW9DS0ZccrOKpq2njaQs8JxcdTAwMTdcdTAwMDBvrnNbrVx1MDAxND61gp5cdTAwMWViSlx1MDAwYrrXXHUwMDFifVa6zD5v25dcdTAwMWWzN1uFi41MMqBcbsFcYnPGaInuLZteskKkgmhcdTAwMWV5vmGkXHUwMDFhQzDtZlx1MDAxMHJm/Wl5K6T6kOqSI1x1MDAxNWhQaHg4viUrYULW91xyqcZyaUE8pVx1MDAxZC9ihJWByPVcdTAwMDWQul9cdTAwMWImqH5qrIZcdTAwMDeZXHUwMDBlWnefLCvou9t24+nutP24ccp4dX2e7Vx1MDAxN62JdVx1MDAwMmnY5+IqxVxieFaMOeePflxcubiLQnY9OWRcdTAwMTmlnFHwX3za2PHI1SqBUfdUL1x1MDAxNuP1emBOXHUwMDBm90dJXbv6xf2jfX7sn1x1MDAxZVxc986vjlx1MDAxM3u4SySlvf4tTs/jPmBKnqj/Klx1MDAxM9lIJlx1MDAxNPia0imutGUhNavsXGbAXHUwMDAxXlx1MDAxNYhZXHT+qLIuXHUwMDE497cykbF425hjV0eAelHWu9lpdMyKXHUwMDEyzDP4o1x1MDAxNlxuykpz6oZs5E6+Uyi138VCzvBFZ1x1MDAxOKxpuzk98HSsZnyW6qx8JoX1ZDh3mnIqxFx1MDAxOKLDfVmMulx1MDAxNJZppVx1MDAxNVx1MDAxN2GJK6Qg1GLuolx1MDAxNGxcIsF5jF9iuYBcdTAwMTNcdTAwMDPEuaOW6lx1MDAxNZ49eN7043mOdCajpFx1MDAxMcoxb6ySilTCXHUwMDFjyFx1MDAxOWwqtWnDfJDPtFx1MDAxMMzTzWeKmqH4yoQn5/h8ISP8mfKZMpRcYm2wpojkXHUwMDE0Y1xigVNcdTAwMDNcdTAwMWZcdTAwMWJmNLHwXFx5t0Sm7P7L0XFr52kj89J6eX6q9i6LP34kXHUwMDEzXHUwMDE1hjFcdTAwMTRcdTAwMTVWWVxy16emIyM5JVx1MDAxMmYst8xLSZwr0PDOcqqU1iBcdTAwMTg9KZYrTeHjoK05XCIjXHLHwFRvXHUwMDFkXGZcdTAwMWHyxcfxU+CcKblgXHUwMDFkjNhccmelzFJcdTAwMWLOd1x1MDAwM8v899pznjnmdORFfLGKWfKCXHUwMDExyozBfFmAspmOh9ScUKqdZkxyzsZIXHUwMDFkp0uDQ1x1MDAxZlHfRlx1MDAxMMVcdTAwMTmmoVqnwNJcdTAwMDSXXVegXHUwMDFlgXp7eWFhtbMuWJAhkNTBXCKFXHUwMDA1Y1x1MDAwNlxmrmGBMlx1MDAwNl9LWISmJr4y4Vn591FcdTAwMTRcdTAwMTg+XHUwMDAwXHUwMDBmjCthuFx1MDAxYS9oj/RcdTAwMDTHXHUwMDBipoyCr8A0XHUwMDA1j91TXHUwMDFi9P1cdTAwMDTGdnvjeqPPb3LPNb6VLz02mvvbXHRcdTAwMDVcdTAwMDbWXFzR2lx1MDAxOO1A/LrAitKQlYRcdTAwMDFZaOBwxFx1MDAwNjqHR1x1MDAwZlLbwlRcdTAwMDRkOG08y4QrieFjo53kXHUwMDEyQ4IyZ456d9C5idRcdTAwMTjWYnRTMGkzJYlcdTAwMDFkxpbagyu9NOBZlpp/L5GRYNTpyFxm2egpd9b63jtSe8d71eNm/1b1w3iOriotwNHTXHUwMDAwSH/lXHUwMDAz8IyJ4ehORKVexJTSg+8qsGdgzlao9qB6f46ddUOlXHUwMDEz/lxuejq6Kq7EMtPGLrZfNzJjc639l+3zuaCbzcLPzMGR2OKKO73/XHUwMDBlhdhCXCLht5ai9l9lXCIrqrkkUoE2tNRRXHUwMDFkXFzXXHUwMDFiXHUwMDAwXHUwMDBlXHUwMDBmO4P7s1GFzTTBpFx1MDAwMZxcdTAwMGVcdTAwMTS8dbFcbmRcdTAwMTlcdTAwMGVpJtxcdTAwMGWSw41ZyzU8JG9cdTAwMDajltHlalFka1xyTzdVM5pGXHUwMDE16jeD9Fx1MDAxOZb/Z9isKFuatuksV/mP9cNSp3O1e7N72MlcdTAwMTaz3aeLebbNrSVCOoXBS9IwN1x1MDAxNenCOCOOSVxmKGU0qKxG2+bGXHUwMDEwv4vuNFx1MDAwMb6nuHWL32RcdTAwMWU8XHUwMDEzdChcZmapScbgt1x1MDAwM2Vq/6drzZsjP8zn6NAgKFx1MDAwNpJcdTAwMDdWhic897hcdTAwMTL0lCpOaerlan9X/df061x1MDAxYsxSXHUwMDA3zVx1MDAxZvZmm9Wvv7+Yo6fde7F3Vrc3nyMyQINcdTAwMTROSVx1MDAxZPivMpE6XHUwMDAwJ5tQQWFcdTAwMTKB5p5og/DKKXpcdTAwMDanSEmMUFx1MDAxNr6umeCWeZbxXHUwMDAz+U5/Mzop1nHF55355FwiuW5cdTAwMTDcXHUwMDE5ZbVcdTAwMGWlQk8qmGnfm1x1MDAxYYtcdTAwMDVRU49/pZJcdTAwMDW6zCwgXHUwMDFh9sBcdTAwMTT30XZXq721XHUwMDE2WPhS8c/aSW5zrdGsP4ND23yfXlx1MDAxNnFcdTAwMTVSZ1x1MDAxOPFpLeG5orWIXHUwMDBiSkdk6PXWy0a/dlcsZ1x1MDAwZmljU9ZcdTAwMWIv9/N0fWKKY1xyRiypwT1dn1x1MDAwNOWgQZh13FgjXHUwMDAyXHUwMDE59qvSXGJLI/80OfC1VYIqqn1da0R0JilcdTAwMDc3Qzn+ju75nr10ZbvRO3xcdTAwMTAnldr3w/7Oz9zVfG70vOnlMeDwjyaZoWSCXHUwMDE4jiVcdTAwMWG0tsHsstfdboe73W/N0MKwwIZPwjlcdTAwMGLaXHUwMDFkwGWCkdArNzpcdTAwMTZcdTAwMTVnc7jRXHUwMDFja/NqXHUwMDEzdpi/XHJcImhcImGBXHUwMDE1Q2C2pVx1MDAxY0OXRmlcdTAwMDRCyGfwoGdYlWmrNzHqdOxavE9cdTAwMTK/vY31fISw8EiGKdXjx/xaXHJcXKNcdTAwMDbmRjBcdTAwMGXzIFxmXs44MfBdXHUwMDA3/IlBsNIjcuFcdTAwMTeMXHUwMDAzw2hcdTAwMDVcdTAwMDVcdTAwMTHkWCCe5VN3ZyvmW/fzSFxcvTyir5IjmlOnMWg5nFGNclwipuK/tlx1MDAwMlx1MDAwM+1SLnZcdTAwMDJcdTAwMTRusL/DXCKITnVXO3pG4is8XHUwMDE3x+dcdTAwMGJcdTAwMTncuTe3ly/yXHUwMDFkeC8qXHUwMDAyLtGmebxcdTAwMTZcdTAwMGb+XCLBgoKCYVx1MDAxMy5upWJWJ1x1MDAxOMP7bZLHrzTMXCI3Q4DWXHUwMDAwJOCkY77UlDKRjGhF2aAwXGajY8SMyE1cblx1MDAwMlLfXG4nXHUwMDA05zRYRW+sTDglXG6DTy1cdTAwMTVcXJpgKZJcdTAwMTW5TZLbz6VcdTAwMDN5UIywiWYlgaxWXHUwMDFkk1x0YFx1MDAwNGhTo1PfU3dcdTAwMDYwM55WXHUwMDFmxXmg7ohQ3FxmdLR2kk9wnuRcdTAwMDSLLFx1MDAxOGNcdTAwMWR10s0k0MhZPzhcdTAwMWGa75+cQDUwPrVcdTAwMGUuXHUwMDFkSUBSvVx1MDAxOKHG50msTUQhgWyyIJkwhEo5oWk4rplcdTAwMTOlsCBcdTAwMTVcdTAwMTdOwVNTXHUwMDFmybDxO72z5SNcdTAwMTbhsNZb01pqmCxcdTAwMWFcdTAwMGVYRv01rY0gblAxffjinjgkpjThXHUwMDEyq+Y5LrkwX5RgzfJcdTAwMDSbX5pgse4rcGW4XHUwMDBi6MBcdTAwMDJEV69XXHUwMDE458SZSp1flWNzdUD7PfwqsOeItFRcdIkkYidcdTAwMTnRYMFU9JHh3oEon0mwXHUwMDE5hoGXjFx1MDAwM1GDKVx1MDAxM4aJiZSO0Hz/5Fx1MDAwNLugQo3feZ5UqLiealxyc8xcdTAwMTmmg+Gqb0MgwOzgvKpcdTAwMDHHayE+MqxzvVI9zOzwl+1GvXCzeXj+424z61x0XHUwMDAz81x1MDAxNmzA5ltOSVx1MDAwNWxcdPZhaoVcdTAwMTmMu1x1MDAwNsFcdTAwMWHTXHUwMDFiXHUwMDFkd6uNUlHx5jLlVbTPQp1p7C1cdTAwMTWTe96MO9RHOtxcdTAwMGVgQIXRvW+ExoJccilHpGBxXHUwMDE1R5cr2bD9kn9sVEtrlVx1MDAxYdwoeFx1MDAxOJ1Cu1x1MDAxM1EqSbzfhlJsVOesIafUaG89c0jN7f7l5nrzR7t+eSy08mSORG9cdTAwMTlcdTAwMTlJNFx1MDAwNo9p69syMiifxoD2rI2vyjksiOfSXHUwMDFjeFx1MDAwNvVcbrdfeveKrYhJXHUwMDA2c9ZcdTAwMTlcdE9vXHUwMDExQI9s2Fxcm0ZX51fdnev8Ra6g9lvH992d5+puYb7oXG6FXHUwMDAxXHUwMDE2c7BFXGY8/KNJZOqYttjMSoJNkzC9p4rygl9cdTAwMTm0dFx1MDAxZWAwXHUwMDA1joPDXHUwMDE0XHUwMDE17sDb+lx1MDAxZmkqm1x1MDAwMjDuklx1MDAwM4NcdTAwMGbLzEtcdTAwMWJcdTAwMTL+6Fx1MDAwZURnZFs4JrVx6e5cdTAwMTktMHVDhu5cZlx1MDAxNUu+0IbB/lnDQIPPXHUwMDExhTnDzEzbvtirSKnq2IbLle9cdTAwMWKFg1x1MDAwMq90tuV67eCEeXKTXCJcclx1MDAxZqVcdTAwMTZ+gEdcdTAwMTk+7Vx1MDAxYzGYVDnE91hIrVxm37L4Ls9h+FxmV0xST9ZcdTAwMDJcdTAwMGXFRMZKMINcdTAwMWRcZrBcdTAwMGLp+1x1MDAxOb7+82G2eXL6/JC9KZxtPVx1MDAxNzi/3/75YYbPP5okhs/hilx1MDAxN5VKYMFcdTAwMTLBQoqQXHUwMDE5wuOAoSmR2MSR+l28lc3zYeI+OSaMXHUwMDEyXHUwMDEyXHUwMDFkcJ9cdTAwMTY04e7qozhcdKAqxcUnNHlcdTAwMWL5duH+M1x1MDAxObtcdTAwMTmmJdQuXHTHv/Z7rNyx/dXrdfP32Xz1VjaOrnqVXHUwMDA3ypJbOSspwaWaSO9OXHUwMDEwzuJcIp9WRm5BQFfmMHJMYq1Mp31cIpZRXHUwMDE27d4pimuOPM0kgllWrlTJP12c1C7sfpHy7nWt8Vwic/bDrJx/NEmsnKVcdTAwMWFcdTAwMTfFI61cdTAwMWOnxMQhY2XlXHUwMDE2XHUwMDAwxa85QKFgciuqqU/52eglTEa5hS+yxeJkf6+dXHUwMDFiNlx1MDAwNvw8dm6GcfG3XHUwMDA1/D12bqvb3Wjv72RPzMX1IbN0/9fm2VlyO4dVj1xiY5wq5vfmjFx1MDAwNE1cdTAwMWKzL6FwndP6kMzk32Q791x1MDAwM9LnXHUwMDFlkm7nRqbPMVxmf1x1MDAwM/PndfBUeJc3sJ9rwINcdTAwMTc0RVx1MDAwN2/pxLF/xp33d/VcdTAwMWSu7mVPj+712WPn5HCr2Nnc6/JmMYXztphq7Vx1MDAxNHdfNjZcdTAwMWVcdTAwMWaL21x1MDAxN1V6/7hXTXbeWFxyMGzyYmVgj3o5XHLAevmDXFxRqWrne2u7a4u5Yru8mzA6xFxiSpxcdTAwMTC4PcWpZtNl8DAuSTDJmFx1MDAwMFx1MDAwMlx09IBcdTAwMTntZnJJhOZmkNArYDZ6ausyrGCPXUmVlcroQFDrSlx1MDAxOIyIpJqUSKJcdTAwMDPvpGNcbuid+9xixaNcdTAwMDNDwFx0cpTxxZo6/VxySmhFTlF8ZUKzc3y+XHUwMDEwij9TJS1KXHUwMDE4jFx1MDAxYjtDOFx1MDAwMcN3elx1MDAxY9W69lx1MDAxNnOhXGI3XGZ0m1aWXG6pw2F171x1MDAxN3Gxa+0+z262XHUwMDBlOrdN11CFcr32azORn8JcZmdEUCxcIqxcdTAwMDU8pOlcdTAwMTZcdTAwMTngm1x1MDAxMCqBpFx1MDAxY8VCpYFcdTAwMDCKUYywXHUwMDAy7YVcdTAwMDFullxyqnmKVfbScEgzWelxXHUwMDFlXHUwMDFm3qH65N7AX1x1MDAxMd63XHUwMDFkXHUwMDE13KcgfThLvZ1cclx1MDAwMJ0u5620XzJDqf/3qqWVZNjpuCzxwmtWVConhjvskO1LapLUYlx1MDAxZmdQuFx1MDAxYYuuepKapMbNZW2A4Fx1MDAxY1x1MDAxM8x6XHUwMDFjXHUwMDE37jThWLrYUudcdTAwMDR87ovGVqVcdTAwMTD3X1tefmCkgKLOpz5cdTAwMDDdoVJcXONSekpwoI3UXHUwMDBieFx1MDAxYaXkx6tcdTAwMGaGXHJPjZOOWzlcZnxcZn5dXHUwMDBiXHUwMDAyw+VcdTAwMDLUt9bUcTnrdNHTfnA0NOHTXHUwMDE0M78jLlx1MDAxNcRcdTAwMTcoXHUwMDE3I7GFlTOGL1x1MDAxOPlcdTAwMWa/eFx1MDAxMlx1MDAxOEKGXHUwMDEyreFJgCDkXGZ72/FwXHUwMDAxUvCIwCGXXHUwMDFh01x1MDAxMVxmxXanXHUwMDFmqJviO5jGcqxVnMBcdTAwMTTAXHUwMDFjOqXgLk/XQlAqPlKVXHUwMDEx9FXBOktuqaaBXHUwMDEwqnG0qiNcdTAwMGWblmnMaGEuIKw+NcHOKaRSXGL7by7Nr5piVTPqXVxy5uGe3G/0qo1cdTAwMDXN7NKsj4zrXHUwMDE31Fx1MDAxYWw49/H0XHUwMDFhNUfxlVx0Tc/3oMOtbLWwbp5EUd1Vsru5zlx1MDAxM/CijOBcImqEsIZyXHUwMDFj5SCQK0xGRFrsXHUwMDBmiMlcdTAwMWLo4NgwXHUwMDE5JWLI+H6mXHUwMDEzfiVcdTAwMTVcdTAwMWHzKFx1MDAxODdgSpzR4dQoSahcdTAwMDV/edCvJWJQ78eQ8cuKM1x1MDAxOFJcdTAwMTGjcL1cdTAwMWPbQEg2VVx1MDAxNlx1MDAwM3xJwsCjZNiFwJdcdTAwMWFFiXRot1x1MDAxZNpcbkYtXHUwMDE3Plx1MDAxNcotUSBGXHUwMDE0uulKikD7qVx1MDAxNUlOkGR7eVx1MDAxMao0aFBhmbdcdTAwMTidil5Mp0I5zlx1MDAxNUvRXHUwMDBiXHUwMDFk0+RCXHUwMDFkkVOlyZiJiq/wXHUwMDE0/VRESVx0SDUgSCGAlFx1MDAxNFx1MDAwNVx1MDAxMZ0k/z1d4UhcdHY401i2XGJ8XHUwMDE1LYVHOMrBXHUwMDA22keKxdhcboXxXHUwMDBlOWUge7EnODhPRunpQrsgXHUwMDEwXHSIXHUwMDBm50BcdTAwMTebYDGaUeUsRsDQ4nqjRi3PPGtsmIfqrEGZL1x1MDAxNKeB3JxcdTAwMTVcdTAwMTFOXHUwMDEw4fPyRFxik1VgN2CvXFyMXHRcdTAwMWK11kkrxFclwshZiq9MaIK+XHUwMDA3XHLOUVx1MDAwNlx1MDAwNJ4o+K1aweBpMFx1MDAxNiOgXHUwMDE2lVx1MDAwNlx1MDAxZIxcdTAwMTeHYLXh+ZGMXHUwMDE1kzPzoG4m1Vj8mdLAdFtcdTAwMWLvQTDMXHUwMDFlMWyQRlx1MDAwYvf3XHUwMDAz+VGVy13a0Y1cIn88t9dcdTAwMTeX1/Sg6MmFieRHKkDrSoF7ZW46jZ5zglkujltkT0+pUSpcYvOVXHUwMDE1pESnXvH/61Bhb2kqdEZyQ1x1MDAxZPcxYTByLVxcj1x1MDAwNFtKXHUwMDA1XHUwMDFmZUpcdTAwMWJcdTAwMTPgz4mPZ8Kp+YivzHAqpkl60Vx1MDAxYqDhb71cdTAwMGJcdTAwMDXk9N3uyU3ntnR1UN7e7fZE5XDnLjFcdTAwMDUwXCKpjaRcdTAwMDAqicRQyS9NXHUwMDAx81x1MDAxNlx1MDAxNU6BXHUwMDAz+inIIWe0kqBtfSRcdTAwMTCdJceU1Fx1MDAxOE3H096dWJHAR5LASbVcXOQ/mtnur9rhXHUwMDEx7Z10Ls7yx0lJgDqCScWUamq4tVNZsVxcgrfqNFx1MDAxN05orJ/5NUng3XWApcvrXHUwMDAwXHUwMDA2ytlcbn911fCy+tgjwrZcdTAwMTJ2sVx1MDAwNPJcdTAwMTVcdTAwMDN8Ulx1MDAwNmjcXFzWj93V7e3J0e5Gj/aeLzZ+eKKtI2WAZlxcYOt6KUMrJVxcaeKUdlxcaPB8lWdf7SswwPvLXHUwMDAwy5aXXHUwMDAxUlHJqPZcdTAwMDYpXGJcdTAwMWItXHUwMDAzgNZcdTAwMTW8+IpcdTAwMDS+XHUwMDEyXHRk3fFJQ1x1MDAxNZ9+tjbyRXq5aU4v+zyxXGbA2lx1MDAxOFx1MDAwMH+MkcZQsylcdTAwMTIwnDClmOPKYora1ySB95dcdTAwMDF8eVx1MDAwZXCOKuy56nNcdTAwMDVkTL6F1FRcdTAwMWGTcpH1XHUwMDE1XHUwMDA1fCxcdTAwMDX0e+r5olf9SVx1MDAxZlx1MDAxZlx1MDAwZU8vzEm2ITY9YclRWVdcdTAwMWF7/CqsZlx1MDAwN/9pRqd6/FxuLYKVXHUwMDAyVtnFqUUmWzFHZLJcdTAwMDEthkWJvVx1MDAxOVbR9Vx1MDAwMpiQWihcdTAwMTmstPnbk4srd3u75afT40r+7vy03OxsXHUwMDE1XHUwMDBl69dJXHUwMDEzlurlbLH4sJe5vFc81yzLXoddXHUwMDFlT/7KQolQ3W7GXFx/p1eH+/tZcZS5vOuJp4NcdTAwMTTOq162XHUwMDBiTFxcXHUwMDE4fnb7tL/Tb5/3L854XG7n/f2JW/PwdVxm+/ifdoh9PElcdTAwMTGaKYKKXHUwMDE0XHUwMDAziIxcdTAwMTFsumy6pcHaPVx1MDAxMbW5MOpSO2m55MxcdTAwMTPct8rg9lx1MDAxMY9MTjxW4611Xt6RMlx1MDAxNKIy4lx1MDAxZMyGsFLJlPdgwfHBUKdFpMZbSkS21O7Wm1x1MDAwZmtnL3/WNjqVavFzpHHPsOLTXHUwMDE5XHUwMDEysVeRTmpEPFx1MDAxNce6XHUwMDE2xlx1MDAwMrQtVdRcdTAwMGU1xXSvJsuJXHUwMDEy2ljMyFxmXHUwMDA2nI2grVx1MDAxY7GDiqOvL5+PwSUmVjhcdTAwMTCfXG7DaeTXXGbcTSHD26o5hFx1MDAwNipccmqd8tbii6nMbSXlTqVZmXuId2OCjXw+zLWImpCDb4en4vh8IWv8IUG6WGlKU8rBUaNWXHUwMDE5T+xcdTAwMTmR8MydktiuRHIpXHUwMDE3zGKIp7G1YNiFkjDZNOYmWLAxKjwmQZSxjH9kNNpBu3K5v311scvbx/r7zuWPVqvrSV3wqFx1MDAxYmP0XHUwMDA0XHUwMDA1htwqXHUwMDFlVDeeXHUwMDE1VjHG/ThcdTAwMGU3ZTnzhTjOzMFxyiqmWHBJK5hlXHUwMDFlxXFCUFxmXHUwMDAyc4vso05cZiOsxynMh4Xau7yJmsfSY6Ner65cck+dSo/WaumuXHUwMDFkI2dcdTAwMTZN7YxcdTAwMWNpOqIl3s+LXHUwMDE1LUBDXHUwMDAxxNLpXHUwMDFjbak40do4inwlXHUwMDAzNmnco1x1MDAwZXNcbjRcdTAwMWNcdTAwMDSW1TbQMG2ca2SIwO8jrU2Emq3gvDZcdGfrh/NcdTAwMWOLpMCuXHUwMDAwqkBXnGAyp4hcXCNVXHUwMDFhbDk3iyyRxoN84Ll8fDJnhnFDsHUt11x1MDAxYYthOT6hZXDrnzolXHUwMDFjeOWOazXzdJGzfnDUXHUwMDEw5oDdsLFcdTAwMDa1uOHwXHUwMDFlwiixXHUwMDA2XHUwMDAxYYSNX1x1MDAxOHiqg/VcdTAwMDPjfHlCXHUwMDAy+7ZhWydquHK/P1x1MDAxY1x1MDAxNcSkhLtcdEPCsu9GhoP0XHUwMDE5XHUwMDExcFRITDelXHUwMDFjV3M+UFwircvj5lnt0pby9rmw3n583tXZm2RcdTAwMTJJxUokqVm8RHKKUFxyL4Csk1hgeCWY5mBYl1xcMGmhnPR3a1x1MDAxMtFBJ1RTJfhiva5nUCny11KLQLXX5ZP2y2fWSt5RpqOT4tetZyzuyDjYcjXLs3G+5r30a2bSpIHUjaW1kMb0T8a9zeqjM1xuXHUwMDFkXHUwMDE1WGRgob2jXHUwMDE5XHUwMDBlj1x1MDAwMDXw8VqI0YncmVHlhugvuMkuv/RdsmtcdTAwMTLnPWdcdTAwMDZ1ooXBmrdMO4zf9GhcdTAwMDfJXHUwMDAwmoPm6ExcbmXDVVx1MDAxZNNe6GEwXHUwMDE2jtnWmPzorPNliI92j+HlqPvINZ+NRv/5+qbSyOmfrX7/6fKgdiN+JFx1MDAxMzTaxjKjXHUwMDE28cxoLPH2U1uJmChq3EwuYlxm+iCce1x1MDAxN31YuFx1MDAwZf+4+5K2VDK1UFx1MDAxZvNcdTAwMTksqLilS8mYwlx1MDAxYr38WVx1MDAwM3y0/a3UPoeWiVx1MDAxZWo6giZ+wzx+4Vx1MDAwN1x1MDAxY404P0TReNhK51uqVauKw1Go3Vo+XHUwMDAyjlPKXHUwMDA0N85fbT+62D6XXHUwMDA2i1x1MDAxNS9UKHRcdTAwMDaa562G+3s0TSYwXHUwMDFi8Vx1MDAwNfPwPTTKXFxrLljGQWtcdHpgkKdcdTAwMWSSXHUwMDAznIz22Vx1MDAxY3bU9lSvSr2kXHUwMDE2XGInM+g3hKGVNqybuCXvVlx1MDAxOOGP10f0Ld9onFx1MDAwZcjy7Vx1MDAxYb89V0rdjei4lD9ex4bEUFx1MDAxYWDqrz/++n9cdTAwMWFBYUUifQ==External servicesLoadbalancerRPCBlockProducerRPCrpc.testnet.miden.ioStorePublicInternalFaucetfaucet.testnet.miden.ioexplorer.testnet.miden.ioexplorerHorizontally scaledRPC providers...Example infrastructureTransactionproverBatchproverBlockprovertx-prover.testnet.miden.ioNetwork TxBuildermempool updatesnetwork txscommittedstate \ No newline at end of file +eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1daVfiWrP+fn6Fq+/Xl7x7XHUwMDFl3m/OYyPO2vfc5UJAQJlkXHUwMDEw8K7z329cdTAwMTUqhGQnXHUwMDA0iLbthT7H1U0g7iRVTz1Vu4b//Wtt7Ud32Cr9+M/aj9KgkK9Vi+18/8e/8P3nUrtTbTbgXHUwMDEwXHUwMDFi/bvT7LVcdTAwMGKjT1a63VbnP//+d77V8ibf8lxuzfrrN0u1Ur3U6Hbgs/9ccv9eW/vf0U84Ui3i99unW+ykdNnSj1x1MDAwN6WX7e1MZTP7eDb66uhD71x1MDAwYmqXXG7dfKNcXCtNXHUwMDBlXHLg/VxmVZZ6imqqXGK8lFx1MDAxMGJ8fFxix4XkxtNMSCmJ0ITw8dF+tdit4PVQ7lx0I6VWlnGi4UTjj1RK1XKlXHUwMDBin+FMe8xy9vpcdTAwMTFlJ7/jdUn/WSPjdzrddvOxtNmsNdu47v+iJfwzWfVdvvBYbjd7jeL4M912vtFp5dtwmyafu6/Wamfd4ejscLvhtv5cYvyOq7croIH3o75cdTAwMDW/tFxcaZQ6nanvNFv5QrWL94qSyVXgXG5b+8XRU/ufyZra+XppXHUwMDFmXHUwMDFmW6NXq43frjaKJXxcdTAwMTg/7lRz6tc1im+/7v2ZT1x1MDAxZSh/e+efyeJLJTwz5VJrxo2w4yNcdTAwMTPhXHUwMDEzllx1MDAwNt/NNlx1MDAxYiNBVJZIeDJm8oSrnS1cdTAwMTC+7uis9/lapzR5XHUwMDA2uLTtoGD6hXNK9rqlweTB+ET39G6Lm7v6ZfbpYHdwJo/Wd27q/Vx1MDAxZuPP/fP2t8n967WK+df1UK2VNlJxzoxcdTAwMWNcdTAwMWavVVx1MDAxYo/Bm1trXHUwMDE2XHUwMDFlJ5fwl++eXHUwMDA1XHUwMDE0yb2akFwiTV3Mu1x1MDAwZUnqXHUwMDE5pYggXGb+01pP65BcdTAwMTHUI1RcdTAwMWIm8JhcdTAwMGXrXHUwMDEw5dyzhnOu4etcXHAtwzrEyEppXCKUpuVWmqmPv2lcdTAwMDejXHUwMDFhXHUwMDAwXHUwMDBibrVDObiIVFx1MDAwZUaYVFJcbr2IcqQpv1x1MDAxM3FEMYSr31x1MDAxZXRL7Ua+ttYptZ+rhVLH9zCbje5Z9Vx1MDAwNdfvXHUwMDAzZXx3J1+v1vDuy6nzrdeqZbxcdTAwMTE/XG6w9FL7h/9udKtgmsZcdTAwMWa4a3a7b1x1MDAwNmr0gVx1MDAwMpw0X22U2vtJbFKzXS1XYc3ns68h3+s2T0ud16votnsl/70q7b0rXHUwMDA39ZiMUe2Tq41y7/Gkv8dKd437zN1ds3b4XHUwMDE0Vm08ZVC1uVCeMZJZoTShVk1cdTAwMDRnpNpEMs8qI1xiZVxmn+jEsr3rtvGYjlBpZa3HLVFscixcdTAwMTXV/q/70et3qrVdXqufkms1Z0RRK5XwS+ybVlNcdTAwMWTS9XetppJywrnQXHUwMDEzU7m0Wlx1MDAwYmrhhIpcdTAwMWJcIvyimlStW81q0KRO/rY2XHUwMDExi9E/xn//n385P51cdMlcdTAwMWW+JlI3+X7I0tbyne5ms16vduHCcrio0K3v5tvdXHJ4XtVGOXis1ChGXHUwMDFjXHUwMDE5fWu93W72K6V86OnD91wij7WatWF59ERnXHUwMDE5cnJ8cLPfaT9s725cdTAwMGXyZJvcnT/tXHUwMDFmz8GIQZmBrVrgUdSl8mBCPCpcZjFGj1Q+pPGWe4ZL67bihnpWWEaNXHUwMDE2XHUwMDE2XylT4S+g+mlY9HZy3Vx1MDAwN1x1MDAxNVbUXHUwMDAwcXUpP+cm+O5Y+Y3W1mguJ6v9cMK793CytVUmXHUwMDE3v46G1ef++eAxT+yzj/D+y33a1y/nc8/VW/O43jqt/Ow93F6UTrpcdTAwMWLt6d/y/vvzqERJz/vr6uZ0Z+P8YngtLyrnZV6l9/wphfPeZ7OD8kthr5m91Oowd/v4ODxcdTAwMWWmcN7Dg12TuVx1MDAxZJT7x1c/eSlzcc4qLZnCeekwf5ArSlnrXHUwMDFkdbb7ppgrdsu7KZz3XCJTzzcvTrP9l4udXbqRyeVcdTAwMDc9k8J5a1x1MDAxN6e/dlx1MDAwNc3q4sWxXi9cdTAwMDBcdTAwMDfRXHUwMDFij2mcdy97dlxcUef13unhVrG3uddn7WKy84bMyFx1MDAxNOE1VHHKXHUwMDAxTydcdTAwMWG3lMPm1qZEXHUwMDBlXHUwMDFiXHUwMDA3jFx1MDAxNtZcdTAwMTDJXHUwMDAxPZSewNgrwjNcdTAwMTaP8Fx1MDAxYVxmK3hrzIKnjWjOwkAvU3bXvlx0sHeSXHUwMDAzu5CUMFx1MDAwYn9cdTAwMWO4zmJIXHUwMDFkl+DIXHQqJ/ZgeV9tLLp8XHUwMDBl0Vxy+WpHzXzx78ZdvpaHpfpMnM9R83n5qThq3abPOVx1MDAwZXhpM3hS0EvD1a+FXHUwMDE3n46HXHUwMDE2b9xcdTAwMDKAXHUwMDEzUGYtuCfxgbtdNGa0J4lcdTAwMTaKR2gz58rTYkLJJp+YXHUwMDA0MD2upLVUU2qtllZ8P9qWgsfWdSt3OHjJ3t5cdFx1MDAwNS9cdTAwMTkz8DSkVH6JftN5aUNcZm+s89RSJlxmJanHZ5TRvsX8LkcuUkLxlVx0XHUwMDBi5+R8IYucomPne65vm1x1MDAxNElwXHUwMDA1Pl1vXHUwMDE2R1x1MDAxMtps31X9PFx1MDAxOaR+MFnU1O1cdTAwMDJ7q8BjXHUwMDA1i00xui2nXdrgYeHwbf+ZvsCkPuiPbrtcdTAwMWHwXHUwMDEyS7W7Zt/xyOvN59LP6uv6O1fVbuVNyFx1MDAxMlBcdTAwMTmbLz5UOmdMbm7JTL+e4VKcN8PwXHUwMDE36bJSST1BwJthTpeVWnBZXHUwMDE5btJcYu12WYVHXHUwMDAykagx9mnrMaHIlD+7XHUwMDAyvTHo9eZwVVx0sVx1MDAxNu6xmPxWn6tqZfDdd3izXG5wiFm6UJjKTfVneaq3R4fXm/RIrt81XHUwMDFl70vWNi5cdTAwMWGaJWX6IG+CK1x1MDAwMTx6XHUwMDFlulx1MDAxNKNcdTAwMWXu1SRi+pRJXHUwMDBmNILi/Vx1MDAxM4bJIDlgXHUwMDAyiFx0VURwyVxcmiGQuMBT49ZYJoFcYoZcdTAwMTVkXHUwMDE1vnXpxfNcdTAwMWN6oVx1MDAwMVtcYjHCXHUwMDE1woG7XHUwMDFmafaB9Fx1MDAxOcNNmtsyi8luiOqf5jY/hd/Xq8Wi31x1MDAxY1x1MDAwNCj+XGa7XHUwMDEypPhTq06H2G9la4V1/cSL8r6a3c31noBcdTAwMTWI5JbNUlx1MDAwZdaHaFx1MDAwNTbdYdiA3HuUXHUwMDBiQZVxqy9l1qNcdTAwMTIwl7y9JidcdTAwMTjrr1npr0N/+8n1V1rONUCoS32lXHUwMDBlXHUwMDE5u4mnXHUwMDBlqGtcYlnMU1/MruWecrW8qFx1MDAxZjSfTu5zqnzzONy/TFx1MDAxY4HNXHUwMDE2d28k71x1MDAxZlx1MDAxZpf3cr3hodjJ0LOdXHUwMDE0XCJuzXK2WHzcy1xcVSTLtcti2KNXJymct9/P6Jsjcn24v5/lx5mr+yF/Okgjonl1XFzOs3r34Ppib3/3arfQvrx8+YpcdTAwMTFC99NOwlx1MDAxYoylnqFcZsy+XHUwMDA17quDtEFcYumpONwxyuMjuEGPxE1cdTAwMWK+YYQwXHUwMDA12Fx1MDAxOcyTy0FcdTAwMDE/ONOuaFx1MDAwMY+LXHUwMDE2IOEzhC608/OhXHUwMDExwlxyPP53I9duXHUwMDE2e59cdTAwMTVcIpxBIWZcdTAwMTjwIIVcdTAwMThdwVr4XHUwMDAy0mFcdTAwMTMv+7vn61x1MDAxYqxt17Py4Lx0zuuNxkFyNjHyk6lEP8A66Vx1MDAwNKFcdTAwMWVcdTAwMWNcdTAwMTXaRnhcdTAwMDMrP3lRxfb7azP9XHUwMDAxhmRcdTAwMDJusiv0z2mkn0ypJtYoYz7RUd5aJ92X3X7hibxcdTAwMTT3btdP7nJ3Z7W5XHUwMDFjZWGV1PPEXHUwMDE449i2czVJXGZcdTAwMWU4ysKzVDJcdTAwMDMuMTcsoFx1MDAxOMCzlYajirlj6OAmS4nRRyo0XHUwMDEzmlNcdTAwMTFWkJWf7NJcdTAwMGI+R5qTXHUwMDA1s8W4XGLlNIyEKWRcdTAwMDXf1cIoraWiNvUsJ2mlT0X/XFwveYZV+Vx1MDAwNC95Jy9cbtu2sTVo6sPj27th5fqqdJsw99hqj1GiLFx1MDAwMcT0x1xiX+NbSng0TnGpJZ7g1khB4H8hV3r7uqSZeivnsGeaM2JcdTAwMDR1xn2ZXGJZuXfFlYQxpuhiKfkxXHUwMDE2x4AoMO1cdTAwMTfUeVx1MDAxNbfdKnjdUqfbKHU9UK5Sw6s2P0WTY/azp9ZcdTAwMWbU2fhcdTAwMDWno8SdfPN6r7W3/fjyUHy5v/7FdqpVmZycWso8XHUwMDBlWlx1MDAxYZFpXGY+hlx1MDAwN+ySRGvyKtS1oCqrOVRcdTAwMTmoXHUwMDEwR7x0qbJmkaFq1H9FpKaT5/bh1LT1c5c/Np9cdTAwMWbuM+3O2VlmUzc2srnEsa6BedlcdTAwMTOX142bXHUwMDA2Lz9cdTAwMWSdVY5OXHUwMDFh2Vx1MDAxNGJHcrBdoPxSs/O7p/2dl+7Fy+U5S+G85UKn1zNNW7i73lx1MDAxZjC7UyNH91x0z1x1MDAxYkvRR469XHUwMDEwYFx1MDAxYqlcdTAwMWZcdTAwMTRcdTAwMTan6O6nksTUXHUwMDFiLTxccmIkJVbScTEhfK+mnsxcdTAwMDBcYlx0zqtcdTAwMDaOTjnFXHUwMDE0deko1FuZelx1MDAxNz7o5PhcdTAwMDByosFoM1x1MDAxNzxIXHUwMDE5XHUwMDE5kpKYjMitTtHSj/JXXHUwMDAwc4hYKlwiddZttkufYtpnkPRcdTAwMTnWNWjwXHUwMDAz607Hwl+Vrq646D3s5V5Kd0+npcPGWe0qXHUwMDE5TVx1MDAwN+LlcUzAXHUwMDEw4DdcdFx1MDAxOdBdLCNcdTAwMDJuZvhbxmm4jEgqT2jMXHUwMDFloJRIIHGrfejXJc1UXpNcXHmlXHUwMDExXHUwMDFjlNCZfUajt6FcdTAwMDUlYNclSZuma0HoXFxWJ6S8ud5drVr40sQ8uMSU4sQ3XHUwMDFiL7R0lX3eNoMhNbdbhcuNTDJF5Zx61Fx1MDAwMnxcbvSaaTBcdTAwMTKGmirYxKFcdTAwMGVrKvB0zNHjxiphXCJqeVea6tBUm1xcU1x1MDAwMVx1MDAwNrmCh+OKhPHoXHUwMDFkZyzBNsCd0s5cdTAwMTfR3FxiMs+eZUhT91x1MDAxYq8lsF9aV8OLTEdbd59cZi2o+7tu6+n+rFvfOKOstj7Pro5SnrFcdTAwMWNh2OU5S0k9oFej7NiV55yqyq4nV1lKXGKjIy/YobOWRdZzWCu5tuYz+1L8LMlcdTAwMWLbvKzUzXP95ezgZnhxfZLYcV6i7O3tb7Py0edcdLnHaJ37Klx1MDAxM9lIyiW4mkpJgk+U+WD4VeHMXGaFXHUwMDAzfZXY7kJcdTAwMWEws8pfurkykbH6tjHHZlx1MDAxMVx1MDAwN/ZcIo1zXHUwMDBmVavImDMwXHUwMDFiXG5cdTAwMWNY+WhLiqVcdTAwMTRLtbrYyfdcbqXup1jIXHUwMDE5vuhcZoNcdTAwMTW0m8GFp2M14+t2Z5VMSY9cdTAwMWLDmFWEXHUwMDExf5X063YvZl1yQ5VUkvEwxYUvgFx1MDAwMmN5pODgtGrhUmBPXHUwMDFixYnSXHUwMDE2LKv2o9ZKn8f6vOnW5zkqprRcdTAwMTSaS0udOVAyukqSUI0k2nzbiqkoXHTFVyYknJPzhYzwn1oxxalcdTAwMTRMSTPKUrSMTt1cdTAwMDHiKSOk5uDkgiWmxFEz9qVcdTAwMGKmsvuD45POztNGZtBcdTAwMTk8P9WGV8WfP5ORXHUwMDE3TSmQXHUwMDE3YSRKXHUwMDAwl4F+d5xcdTAwMTFcdTAwMGYoiWaGOqGPMVx0voI1jEiQdUGUo/Z7xV1cXFi3NUdmp2aYWOvs6EGiO3pQcFx1MDAwMiV442m794ZJqZfaL79cdTAwMWYxgD9ry3zmmtOhMWfbm5fstpv99bTffd5qPNefcsNiYlx1MDAxYTNqrKdcdTAwMDHjUZV1MJ1cdTAwMTNzW1x1MDAwMOcssFx1MDAxY39cdTAwMWPOX/htXCJcdTAwMWH1cFx1MDAwZoiwIVpcdTAwMWEwXHUwMDExXFz5o7srnVx1MDAxZev09vL8xShrrL+1hK9bJdfBd8dcdTAwMWJmzKB7k2bnrjf6gpT4XHUwMDBi0JegZOIrXHUwMDEzXHUwMDE2ys/hLYu38JrNXHUwMDA24oz8dnfjZuOF3eaeXHUwMDFibCtfqrfa+9tcdI08UYBcZsxiu1mgQL7o3SsyKO5RsFx1MDAxNWBlgCT7+r+MzTw8XHUwMDAwoNWGW1x1MDAwMeJplXaEXHUwMDA0V2beXHUwMDA1XHQ7yc28MIxTS5yb5ZxGhyisYEJZf+PMtJRf2OVcdTAwMTJaS4NcdTAwMTY8zFL7z7L0XHRWnY6tXHUwMDE3raG0552j4bHcO9mrnbRf7uRLWKGje1Vz7lx1MDAxMXhIxt3mXHUwMDAw3GBPM+T0UeVcdTAwMWIxnfngu5KCc2BW5Vx1MDAxYi613p9jXHUwMDFiXVx1MDAxM2G5uyGfMjGRR2Ep0IGF2PvYXHUwMDBlzlx1MDAxNegvm+dcdTAwMGJONtuFX5mDY77FJFx1MDAxOP/9T+hDXHUwMDE3stVcdTAwMWZcdTAwMWHod19lXCIzqpjwwP9RXHUwMDA2jKUj0I+H7SiOXHUwMDE01ShtXHUwMDE16F9M3Vx1MDAwZZKrXHUwMDFiNUCFhVTOMkhcdTAwMTUuyvZ1T2BcdTAwMTL5UdpJZ8tH+t9cctJXiPXPsFlRtjRt01musZ/rh6Ve73r3dvewly1m+0+X8+yRXHUwMDFi43GgN5ipJDS1gbRcdTAwMTbKqGepwORRSvy9L8Z75Fp7bkfZXHUwMDAy01x1MDAwNk9cdTAwMWLDjfhN6tBnoOLEaqx0XHUwMDEzlMLv9tXm/r/uXFyvj91qPse4XHUwMDA3TjBcdTAwMTndhj3l0VONLpU0QHYx/j9cdTAwMDGNpa3tRzVJmGXF2z/N7TZt3lx1MDAxY1xy9PHTboXvnTfN7e/uUvtRXV9zh+RkuJvrUpFn5lHuNC51PeHtjWdcdTAwMWSY586Vmi/QXHUwMDE5XHUwMDAzWO6nkoh1XHUwMDAwuVx1MDAwN+9cdTAwMWP4XHUwMDA2VkwgWFx1MDAwNbFKeZZcdCG4JlNcdHpjrFx1MDAxMsJzQZWv/upcdTAwMGaDpmJcdTAwMTODOJ+MTZfJOVxitsKVXHUwMDAwKSG8XHUwMDE5OVaRIMSV5ZSKNCtTU2Ige2DXX5BcYtRqw7VcdTAwMGXQhVLx78ZpbnOt1W4+g3fc/pw5XHUwMDFice1bZzCCIDFxXFzRWsRcdTAwMDWlw1jUemew8dK4L5azh6S1KZqtQWWewVRUMlx1MDAwZjNwqeBMS39CyutGXHUwMDFkYUBoqLFMXHUwMDFizX0l/6teXHJLq/5Zcs3HXGIqkUSFMlxyXHUwMDEwXHUwMDE1oktbMXwvLftEX3/PXFzZstlcdTAwMThcdTAwMWU+8tNq4+jwZedX7nqORlxyXHUwMDBi1LvHKId7NcmsI+WeZtgzXHUwMDAyYM7oQCVcdCAx7l9boOWKudSCKqz3tlx1MDAwNlx1MDAxY1x1MDAwMYZz34yj3fHKJ3dpxflcdTAwMWM+OWOKXHUwMDAzZlx0l1owXHUwMDFhPWdcbrlcdTAwMGZIm6SLKMaH9mrwPO8ruOMzrErQ6k2tOlx1MDAxZLtcdTAwMTbvMcRvWGODISzeMsZcdTAwMTJiVbBcZoxz63FcIoRcdTAwMTVguaRccisvXHUwMDFi7Vx1MDAwYlx1MDAwMsuhVlx1MDAxMy64cm1LMVx1MDAwNq48Qlx1MDAwMFx1MDAwMf3XcM50lfnDOG6+U5mH46rlNfrX0tvXqK+UXHUwMDAyXHUwMDE4O/RcdTAwMWPANXL/mlx1MDAxMWNcdTAwMTj2L019XHUwMDBmS0vNXHUwMDE2Klx1MDAxYUt1XHUwMDAzXHUwMDFi2JtcdTAwMDe2ScF/XFxcdTAwMTk+wTN8XHSJzFxme1xyWS0kkXLW2aLFfnQ0JPCT84Ws+ty74kvn7FVcdTAwMWJcdTAwMWTg11x0k/ZcYkhcdTAwMTMjmH5NqDRcdTAwMDLLzKbuO9huzjDWi2Wg2M07nFx1MDAwMTCR0US5h/G5zFx1MDAwYl9cdTAwMDc8KVx1MDAwMexcdTAwMDQ8aklcZu5cYk1fXHUwMDA2XFykMERcdTAwMWKLw1ZcZlxcTcxlfMH0w/i9n3hcdTAwMWKAXHUwMDAxU7hkLFx1MDAxMpfg2JCAXHJcdTAwMTBcdTAwMDb9XHUwMDE2zENknFx1MDAxOGvDaUtUXHUwMDFij2LBXHUwMDE5tk6lUyRjbFx1MDAwNHCiKI6mMXiL4U77hlp8KyOglzdcdTAwMDL5pY1cdTAwMDD4QFx1MDAwMDsqPJNcdTAwMDZcdTAwMTeoYoaKakHAdeWp21x1MDAwMGtcYlmI7KVqXHUwMDAzXHUwMDA0wZr2UXqeXHUwMDAym6SnQFx1MDAxYuxcdTAwMDNcdTAwMTZ1wWGwXHUwMDA0IJ+azTpcdTAwMWRojvawXHUwMDEzXHUwMDFk9kTSQkxs7uh8IYH/UkZgnsRtXCLBcVx1MDAxNkKN6jOwX8c0dFpcdTAwMDF0Q0q8dVxuXHUwMDBlxyFnXCJcdTAwMDNcdTAwMTC/wbXgNUjLlVx1MDAxNVx1MDAxY7xcdTAwMGVcdTAwMGLozlx1MDAwM+g/dVx1MDAxNKzAn1x1MDAwNf7r1dphZodccrZbzcLt5uHFz/vNrCOLxVlcXI6ziKxcdTAwMDTSXHUwMDAzXHUwMDBlPFx0xrTALVeekky8Oe+OUUXwXHUwMDAxePZRSatcImW//augfFx1MDAxYeHsolx1MDAxYubd3rvlXGZcdTAwMTNTnaxeRm6qgVRjcblKt9FcIjaCXHUwMDAwb3Gp8vLtQb7eqpXWqlxyuFHwMHqFbi+irVx1MDAwYv+8XHUwMDEwdmxS2qwlpzR3bD1zSPTd/tXmevtnt3l1wpV0ZJ9HXHUwMDA3qVx1MDAwNfVcdTAwMTTmvijjXG5Sa/T3J1xu7YjGrUrPXHUwMDE31OfSXHUwMDFj+myMgNsvnNtTRkdcdTAwMTI0YHREa7tYn/CxTZsrSn19cd3fuclf5lxucr9zUunvPNd2XHUwMDBi8/XPx0F3Kc3dca8mkaXDXHUwMDEycXBcdTAwMDKx9kKAdFx1MDAwN7ZwNTV+Q+fQXHUwMDBi5Ic4c1x1MDAwN3RcdTAwMDN4XCJbNdB/W9JMvbhPrlx1MDAxN+y10bYwodZGI6cmsnjUjGI6ZLFehmmKbsjOnSNhyVx1MDAxN7qw2L9cdTAwMWK4s/k1cshmWJmg6Yu9ipRcdTAwMWEkbdhcXLnSKlx1MDAxY1x1MDAxNFi1ty3WXHUwMDFiXHUwMDA3p9RRWlx1MDAxMdmrVEs4P4syezhiXqPf96reXHUwMDEzXHUwMDFhtTJ7y6p3eVx1MDAwZbOnmaSCKFx1MDAxMzJwr5Q6OjlMXHUwMDBiZlx0Z585mOfl+TDbPj17fszeXHUwMDE2zreeXHUwMDBijFW2f/02w+deTVx1MDAxMsNnXHUwMDA1eOhESI69XHUwMDE1OFxyXHUwMDExQqo9XHUwMDE2p1x1MDAxOYp4XHUwMDAy581cdTAwMTG3h7eyeS6lqCRXXG4tuUD/20lcdTAwMDVZXFy+JJfa6sXyJT/U5m3ku4XKV7J2M2xLaGJcZq5/7WPM3Il5XHUwMDE4XHUwMDBl+/lKNl+7XHUwMDEzrePrYfWR0ORmTlx042HHTi6JsrjvMq3MXHUwMDE0+1x1MDAxYzHLmOS41Vx1MDAxNW5cdTAwMTawMnOLanR1XHUwMDBlM0dcdTAwMDX29bPKxWIp981cdTAwMTJcdTAwMGLG38HGXHUwMDAxTItcdTAwMTRToGdZuVI1/3R52rg0+0XC+jeN1kDkTNLUX2POn+47zevsVa04OGXZUqt8fJdW6q+w8MP6lWxx6+m+yiTWU1HhgWmEJyM51jlcdTAwMDdcdTAwMTVcdTAwMGV4pYhTuJX1XFxA11x1MDAxZebQNYm7uURcdTAwMTEnpaThzrmT3lx1MDAxY1xm6Cg2m0nZflxuXHI6nMbMta9jP2dcdTAwMTgt98S1j7GfW/3+Rnd/J3uqL29cdTAwMGWpIftcdTAwMGab5+fJ7SdKhIdcdFxykrr9RC2ALMftd8D3LaHaSE1cdTAwMDUz0pGrSEXKnTm+UXnRY9Kt7cjyXCJKwFx1MDAxN1x1MDAwNPvq9CF1dC9cdTAwMWVLNcd2PCl6kFx1MDAxZluvU9vLnlx1MDAxZFfUeb13erhV7G3u9Vm7mMJ5l5iVXHUwMDFhe95cdTAwMGWVnZ3i7mBjo14vbl/WSKW+V0uLXGak6UrTYf4gV5Sy1jvqbPdNMVfslndcdTAwMTOmymhcdTAwMGXONOe4/cWIosFWXYJ5uFZKOVx1MDAwMImPxo3Rg1x0nFx1MDAwZU05IFx1MDAwN1x1MDAxMdTFXGIo8Fx0nELMwT5ZQZXvLCuGMMaRWlJcdTAwMWOJzpNcdTAwMDR+KZHZufxuySPrXHUwMDA04FtUXHUwMDE49jFcdTAwMTVCXHUwMDBitfRKt89PlITiK1x1MDAxM1x1MDAxNs7J+UJK/Ke2KVx1MDAwNONcdTAwMGaMnitcdTAwMTBcdTAwMDJthZy6XHUwMDAzwaMmLtvxXHUwMDBiZorsXHUwMDFhs8+ym52D3l3btmSh3Gw8bIaxz9nAiFGPXHUwMDEzbNSqsPVCcFxmXHUwMDAxN9ZcdTAwMDPRoFh4TbhcdTAwMTDhLEEmgdtRa6iho06GfFXn8bqkmWhXnyf2YJHdMmf+N1x1MDAwZu83v8NcdTAwMWEnwKhcdTAwMThNfWSI4YSohbK/372h7iDz6kr8WS2Mkiw7XHUwMDFklyieKM5K/dVcdTAwMWVcdTAwMTeGcEmkXHUwMDAwvVxmpv5S6lElcXSIUYyGN1x1MDAwN3Cap1VSgFx0XHUwMDEzRFx1MDAxYTL5xESlLfewbVx1MDAxOeYhXHUwMDE5sHHfNO83heKPxvKkXHUwMDA2XHUwMDEzXHUwMDFjJLEuTkOZjSY1XFxhWrwvRfd37yWkXFz7XHUwMDAxKmTBZuE0SybEVP6qXCJYu09cdTAwMDCmNFZcdTAwMWZcdTAwMTI762yRMj86XHUwMDE4lPY0XHTSJ1d+XGJcdTAwMWOPYbiinOAl2al77sHlwVx1MDAwMVx1MDAxY9tcdTAwMDc3TzNcdTAwMTm60DmzfuMjO1x1MDAwYl+EUsJooYjRryVcZtNcdTAwMTfBkIpcdTAwMTCtmDGKYILzn0Xn9qq/6N1cdTAwMTUpb1x1MDAwMTE77WRcdTAwMWKmclpMVrRrifGshMs3OIuZyFx1MDAxMJvjnqHSXHUwMDEyIFx1MDAxM8RcYke+XHUwMDA0lVx1MDAxZcFdJE4keFx02pVcdTAwMGXFvl9wO41cdTAwMThYZ1x1MDAwZTZnQLtcZpfcXHUwMDE53Y6c/0YxXHUwMDE1nzKZei1cdTAwMWZ4f9THZ1x1MDAxN2BzwIlazU6puHaHXHUwMDFmdDK4tFtWzFxibsfPlVxy8bqoXHUwMDBiSIfLxVx1MDAwN+diuVx1MDAxY/jrnrBWMMVcclx1MDAwM/VcdTAwMGVQOYlcdI581HpcdTAwMWbTpFx1MDAxY82n4ftcdTAwMWHwXFxKXHUwMDAybMD4UvAmsSn8iDKEMayPoSTl0NRcdTAwMTeJc6dQwdVdnslJXHUwMDA1RI6bcMEucs3obS3GNVU4dPcjmJzwy/RvYXKZKCHFV0g8P4N7bWVrhXX9xIvyvprdzfWegISJZUNQYFinOOrSJVWJydU8q1QgXHUwMDEyzFx1MDAwMoNQXHUwMDE0u9mbqSfpca6YJoZiNjX8LTzQ5Eszq/imcfFONaHcXHUwMDAzoiDgf5BCf/XNq1PNsfEhUURZaalVYSSmRGCvbyVwoJkxRDj2XHRA1ClFJcBOplx1MDAxMu51yuOMvlx1MDAwZlx1MDAxOD8vXHUwMDBmxlx1MDAxY4wqXHUwMDBlTnXRMFx1MDAxMfa2JzzMYltcdTAwMDGRZlRtxMOYXHUwMDE1XHUwMDBiXHUwMDBl4k3Xr46U09HXQ1x1MDAxMvpcdTAwMTl4XHUwMDFj34RsUTdcdTAwMTJnPlJcZpFZSZWvz/vbdWI4YFx1MDAxNFx1MDAxOVeCWKaXRuukNmU+V1x1MDAxODtgU1x1MDAxY9WtNdeKXHUwMDA3XFxhynA28CiwwS2Y0D9sZ0OWy33SU61cIqtfmJvLq1x1MDAxYnJQdFRcdTAwMDZFXHUwMDAwNt5cdTAwMWRcdTAwMGWOLlx1MDAwN69J2+DUXHUwMDE2rC1AssFwkphytKIlXHUwMDAw+K6ublx1MDAxOCVJu6Hb91x1MDAwMebh0sBstVx1MDAwMI5hmVx1MDAxM5dZdC5cYlaBMbZYY/c4WDbW+Gz174PlKXHEV+ZVXHUwMDEy01x1MDAwNOAvN5wlp+53T297d6Xrg/L2bn/Iq4c794lcdTAwMTGAeoKYSFx1MDAwNJDaXHUwMDEzSGS/NVx1MDAwMszb0zVcdTAwMDVcYnhJgZtZXGZB23A0XGZcdTAwMWZcdTAwMWOJLFx1MDAxOdTWWlx1MDAwZVZuhVx1MDAwMd9cdFx1MDAwM05r5Vwi+9nO9lx1MDAxZlx1MDAxYYfHZHjauzzPnyTFXHUwMDAwYj1qsVx1MDAwZp5cIpr5XHUwMDAzYG+1XHUwMDE1XHUwMDEyXHUwMDBlK8YtV9i88HtiwKezXHUwMDAwQ5ZnXHUwMDAxXHUwMDE0p4pyZ2tLXHUwMDFlPVhcdTAwMWH0XHUwMDE0vqZSz+RcdTAwMTJcbnz/L7Dp+f9cdTAwMTNcdTAwMDFat1fNXHUwMDEze313d3q8uzEkw+fLjZ+OXHUwMDA08UhcdTAwMTagKONcdTAwMWN8JCH8zcNHXGLAXGL1rFSj7C9GZTg5/FsgwOezXHUwMDAwQ5dnXHUwMDAxXHUwMDAys1x1MDAxNTGi5sBcdTAwMDApQv7BpOOZXHUwMDE41WqlvlG2woDfOabRnpy2ZPHpV2cjXyRXm/rs6oUlZlx1MDAwMcYjXHUwMDE0tFx1MDAxZrO5MXktgFx1MDAwMVR7VErsXCJcdN6CY07jt8CAz2dcdTAwMDFseVxisJZIxZh29lx1MDAwZeGRU6ew/JpcdTAwMWGa+tz5XHUwMDE1XHUwMDA0/E5cYnhcdTAwMTnK58th7Vx1MDAxN6k/XHUwMDFlnl3q02yLbzpcdTAwMTKdI+usufak4ExYJrC9YyA9Rlxi6zFCJFx1MDAwNVx1MDAwMlx0XHUwMDBmOlxc6LGqs15cdTAwMTRcYvhcdTAwMWPZMVx1MDAxYahcdTAwMTh2u3aWgkVnO1JMklx1MDAwNFwiRz6xzrp6v7dbfjo7qebvL87K7d5W4bB5k7S0qlnOXHUwMDE2i497mauKZLl2WVxme/TqZPq3LFSy1e9n9M1cdTAwMTG5Ptzfz/LjzNX9kD9cdTAwMWSkcF452C5QfqnZ+d3T/s5L9+Ll8pylcN6PLTHjVvF5cpti0Mf9tEPo46o3x1x1MDAwMjNhhNRcdTAwMDZnVPrmkbyNiVaejlx1MDAwNVx1MDAxZWxTpuClqZbMMuvIyF5cdTAwMTWdu4BHJFx1MDAwN1x1MDAxZaPAZWDWiTtKxLTYV1j3YljqLVtQdJcqOc+Wuv1m+3HtfPB3Y6NXrVx1MDAxNb9G4flcZitcdTAwMWXMzYu9inRcdTAwMTL04qE41rVcdTAwMDBO6IHTgDvliitcdTAwMWFqsy6pJzkwR6xcdTAwMWRl3Fx1MDAxMV9cdTAwMTRcZj6gmbSKSlx1MDAwZVx1MDAxMOHqsq48y+Gg1tiGXHUwMDFku1x1MDAxYX9LhyOFNFxcI+cgXHUwMDFhyDSIsdLlWjBcdTAwMWSp8FxcKnzgaWd/XGJBifn93dSj5XF0NCSJk/OFjPHnZ+PNl/1hjKBwXHUwMDFkUlnOXHJcdFx1MDAxNEIwLTnDnVx1MDAwMSCUUpLwrI85sz/iQW9tkVxcPfD4XGLIKYGnpIlcdTAwMDH3hTGjXHUwMDAzV4E5RVSD+VwiwDuoXG4/ri+d/nHQrV7tb19f7rLuiTraufrZ6fSziVx1MDAxOJdcdTAwMWWNXHUwMDA0n8BykHFx6u9cdTAwMWPpXGL6+kjjh5U+fCPc1XPgLpBcXCqpoyHyyMWOXHUwMDFl42klJ1x1MDAwNqfzpIy8i2/uvlx1MDAxM616qd5qNmtrr6dOZWZnrXTfjaFYi1x1MDAxNrBGrjRcdTAwMWRcIlx1MDAxNe97xlx1MDAxMilFrMc5VVxmXHUwMDFkJb9cdTAwMTi8XHUwMDE2OnA7w0VcdTAwMDJcdTAwMWWmseUlfIpLqXx0fDK3gHtcdTAwMTRcdTAwMDd6futChzS02bi1eY64LYArVzh0w6HjPHrnhjJcdTAwMWPwzv3F5d+KXWVA6sDwWExcdTAwMTTnXHUwMDFjmeRURVx1MDAwMGNcdTAwMWVmljM++lx0gGdmnzBS7keHXHUwMDE5xWJJYq3GUVx1MDAxM0aJcFx1MDAwMuhcdTAwMDdcdTAwMTC2XHUwMDBmIDvAZaS1hsBcdTAwMTUorE/0ZdjjXHUwMDBiLlx1MDAxM7GBXHRDsSCYfF627jyXYKRcdTAwMDJcdMdUXHUwMDEzWKSwOnBcdTAwMDVcdTAwMTRz1LhkllxuwCnxZ3G1dXHSPm9cXJlS3jxcdTAwMTfWu/XnXZVNNohdy1iuXHUwMDA2cFx1MDAxMM/VrERcdTAwMTlXQORcdTAwMDVm0Fx1MDAxM1x1MDAwN/KvmFsk1tvkzE1xaYV7/ph/JcE2/pRLXG5cdTAwMGVW6uVcdTAwMTJL07bGW2SpO/jKlM25ynToWnxIPz7uhdNcdTAwMDFcdTAwMDGIXHUwMDA10nlDXHUwMDAzdFxyuylgd3ehXHUwMDAxrI2vimkyXHUwMDFl2nhcdTAwMWHdbsA7YZTx1UJPwl5cdTAwMWGr2Vx1MDAwMVx1MDAxZPBBK1x1MDAwMU7DSokjlHhjacKmsISYMueAdUEjXHUwMDEzbqmwlnLtn1x1MDAxZvnhu27iQfZPd3vl+vH6Tan5eH3RXHUwMDFiZquJd4Ved/GBXHUwMDBl/n42SFx09SS1wFc0jukk01x1MDAxYvqGY3GPsNiSUILpm3m2SJVcdTAwMWFcdTAwMWRccinTZ1x1MDAxMMFOvnm919rbfnx5KL7cX/9iO9WqXFyWRYGdVzijl+F4WkGnblx1MDAxYbZyw6FcYoZo5FxuQH2XZoJcdTAwMWZDZpkllFxi+KPA3fWxXHUwMDFhfGFgb1x1MDAxMrbDRDZcdTAwMWJcdTAwMTd//IJk0K2iicigQJslJbY8k+BCXHUwMDA2sjU51zNcdTAwMDJ3zHpcZtkguFx1MDAwYlpJ5UraWrUwcVx1MDAxYpHL5FRcdTAwMTAwhlvrXHUwMDFm+u7jgpElWiDS1FCmXHUwMDE364U/Y690sbSsdy5YeIe7NVCnrntm4Sf3MJnBzoI8MfJcbtLhivFpXHUwMDFhsVxcXHUwMDExMMxTODKdMqyNZ4E6rJmxPaGsXHUwMDA3KCngXHUwMDAz3ChLJ5bbt0UqpkMnq+BelJpvLZ+UXHRcXIVip1RndVZ0dE9cdTAwMWLNjZTpa/9cdTAwMTdpY1x1MDAxMimn+FxuS+hnXHUwMDEwsFx1MDAwZiEvv6+RyVxcXHUwMDFivGBnsGm3NFZcIs/iU5FROEzo+8NcdTAwMTj9/MNK45+a20P1PMibrDnpVGq0d3tSyYfxOHK0XHUwMDFlI57FTlx1MDAwZXhcdTAwMTewcjrAs6jxXGZ4JVx1MDAxNKdigtlcdTAwMGXzLOx3YvT4XHUwMDFlunJhsXG6/1x1MDAxNqfczORcdTAwMGLQrlx1MDAxNDLUtpOTLiGEXHUwMDE22NDQRbqUXGJVyYyT4bFP4tSW2If76M/73f31+0bRnNRcdTAwMWVcdTAwMGXM3ZA8VdRm0ozQ+OZtU79/rkzTcqHT65mmLdxd71x1MDAwZpjdqZGj+zQyWHOH5GS4m+tSkWfmUe40LnV9J9l5Q8hcdTAwMWYybuBcdTAwMDP5J6culcE6oytebCM6UGaqXHUwMDAxXHRcYtdcdTAwMDa4XFzAL1x1MDAwM8fMXHUwMDEzhIPHqlx1MDAwMDNIeNKu8rBcdTAwMTUxUECiXHQzUlx1MDAxMYdfpjH1iEpmKNeaaTIx6d+Jw6VQV7NcdTAwMWThqc1cdTAwMTPvXHUwMDAzPNaaRExYXG43qFx1MDAxYrtwklx1MDAwYsqYYVx1MDAwYlXWLFx1MDAwNibuhqhJlVxiXHUwMDAzfpJcdTAwMDNcdTAwMDWYR4k+iCEyXHUwMDBmR8FpMKpcdTAwMDDIXHUwMDE0fk5tXHUwMDAxZlx1MDAxOFx1MDAxOF2K2TFcblx1MDAwZXLqj9FFnDBarUaHQ1x1MDAxYZUm55x/skM8Z1mU50mGzVx1MDAxMVx1MDAwMZOMoXj3pmmeNVx1MDAwMFhUcEZcdTAwMDGXpKNd1ZdmeW47XHUwMDFhQmxHIM3gtqhgcHO4ICD9XHUwMDEz0vBK8Jj2NFximeaaXHUwMDE5zsKAbZSnsNBcdKRHXHUwMDFh7WpcdTAwMWO6XHUwMDFh6+DC5p3kfG6kujTU9Vx1MDAxM9cmwrus71xirFx1MDAxOFx1MDAwN9LtzyVPZz91cYx8j6Fd5uFm57vNL1FlMMM/XG5Gz1x1MDAxY2tPJ25cdTAwMTZPOmc0/6VcdTAwMWWgN/iuSlx1MDAxM85FIDGCU+7xiY9cdTAwMTZu5809zJnUOLPDcGKFazKVkFx1MDAxZVMgUFxcSSGskSlzri+g02nEzH7Ok95KON7ucFx1MDAwMcE091x1MDAwYvIqUGtcIrBcdTAwMDL983jVwUn5qrZzzsu9593n4kY+V3oonM/HqzSz4vdH3qJcdTAwMDVcdTAwMWRfmZCMx7CghHG1xExmmbhcdTAwMWFcdK1zzrjax2y/zlx1MDAxMf37vNJwtywnI0rK44KBb2vVSFx1MDAwM6dRllx1MDAxOeJpgGBQTyksc6SfcW+1yfi+pplImk2OpNj1XHUwMDFlJ6I791x1MDAxOCM7P1COU02IXiwjJVx1MDAxNuuoL4a8XGI/6lx1MDAwMHP5SkNcdTAwMTJmUJMgRXIvP1x1MDAxZJZUusn2L1x1MDAxZtePt8nGc5d1dzvd+s+NsPpGTlx1MDAwMFZcdTAwMTiespRxi+k92lx1MDAwNsowNZWexjJdi11BpKNcZpMpXHUwMDBmvm7AgVx1MDAxZUWruWOPXHUwMDExPFwiXG4wXHUwMDAxTFxm93eEXs3wdOh3blx1MDAwZaaksFx1MDAwZVxinocr50yL6EEoXG7z1LiVn1x1MDAxOII6r1x1MDAxY+4/llx1MDAxYedD8at1srd+9HKU6V8kXHUwMDFl+ntx+mtX0KwuXlx1MDAxY+v1glx1MDAxMURvPE7/lsXiw1xiS5rh7ZhcdTAwMDOWYrTQfZVJjCjQV9Qw8FulJZZrXHUwMDFhiFx1MDAwZms+Y/SQXHUwMDE12KxcdTAwMTn0zjChiXZtJ606XHUwMDFjuFx1MDAxNO4kucJcdTAwMTljXHLVXCK8UYT6XHUwMDE23VhZXHUwMDAz9aE4/yBlg6qwXHUwMDE2aymD2mh2S383XuP6zXbXaVM/Oewww5CFkrvhXG7WXHUwMDFjXHUwMDE3kNLgoUw937w4zfZfLnZ26UYml1x1MDAxZvRMWJ+dLdNcdTAwMDQhnmBgMFx1MDAwMZ+pXHUwMDBlXHUwMDA1XHUwMDFmmJFcdTAwMWVOT6A4MM9frjdcdTAwMWVcIkmwp5rAUVeYj+fP+plk7Vx1MDAxOFx1MDAwZnTdUFwiXHUwMDE1zqv8U5J2Pn+KpDlNruVcZmujXHUwMDE4aJdza4dE90xjhFx1MDAwM/aCRU6bOStLyVxcW5jjt1ONXHUwMDEyREskvjIhYZycL2SFv/qQRzDB1HBDXHUwMDE55lpKo6eulHhMUKNcdTAwMTmAr+KMSVx1MDAxYbcnkijOkHg8xzxxXHUwMDA2XHUwMDBlvlx1MDAxZk6kXHUwMDE2llx1MDAxM6FooLPB9FHtSGf/0vs6WXXfLO9X7uXzcat+U8q3ysruJkRmTJFcdTAwMDY5tuDPcFx1MDAxMFVcdTAwMTJqay9xoDuTmFbu74A4XHUwMDBlXHUwMDBic+VcdTAwMTlcdCTNXHUwMDExteC4m2aIxqQp8Hal7+QrzjVG47Olt9+xXHUwMDFjmWp/SZOvs210cENYjd6PTbs8WuOUhN/f3z4smfjKhIUyTWj+cl0uXHUwMDFmTobXXHUwMDBmz6p6dbW/k23c3VxmTn82npN5YVx1MDAxOOdAaiZcdTAwMTWxRtvAni9cdTAwMWPxXGJm9WEvbP9Y7zFnk8KzNLbN3GrT11x1MDAwNVxi58npXHUwMDE5oLZhYLucfe3Dsc5cdDmjuNXHWOqqP6q0WcZcdTAwMGJcdTAwMWK7L1631Ok2Sl1cdTAwMGZ8pFLDqzY/xSFbtK42ybLTccPi4z/xxp5bjcXx2FxuUIBcdTAwMWZcdTAwMTZsWYvDMTmmvmDokznimlZ5RHCg4PCcJXdccnRcdTAwMDbnXHUwMDFlls+ENMCJiV/pZys4rCkvyKcr+LxcdTAwMGVYXHUwMDFhXHUwMDFiXHUwMDE3XHUwMDE3S9t8quA5XHUwMDAyI5eunWFmXCK3hqVcdTAwMTZ29HRS1Pxxg8mF4i+pXHUwMDFhfVx1MDAxY0ouXpvNqSlcdTAwMTHFl9SBXHUwMDE08llny1CKO1x1MDAwMVx1MDAxYevSR2H6KdcnJOsxROL3enBcdTAwMWNbz1x1MDAwMYWnwM3ASVx1MDAwYjhwhoHvIzGBUzNcdTAwMTOb05bIf4tcdTAwMGZaLeS/XHUwMDA1svBw0zhmlV/QQ4tP6p41bkB6uFtoXHQ4p9Jv619cdTAwMTN3XHUwMDA084TFKZqSYJfBXHUwMDEwalx1MDAxYuVhpSTBpn0g+dZVW4FdfzhcdTAwMTA5bH2CI87SpWWzQ2eVfKHSa5fSJGeTXHUwMDE1p1x1MDAwYt1X81x1MDAwNM+4XHUwMDA0XHUwMDA2zFxcSXkmOilac6FcdTAwMDR+b1x1MDAwMYieWkY4/Vx1MDAxZi3GXHUwMDE3XHUwMDAw6SiJxFcmJIzLw+qHhJRcdTAwMDAxLaPacsXhiYkgRk1cdTAwMWRVS6Pqx2Q7XHUwMDEzKijHqbTSXHUwMDAwvlx1MDAxOMJcdTAwMDKBMfDywEGG64CfRPFPjIvFgKkx50/3neZ19qpWXHUwMDFjnLJsqVU+vktcdTAwMDamYFx1MDAwND0mhMCAn+VMXHUwMDA2R7dcdTAwMTDuWbDl0jJjuXF0mmHWXHUwMDAzmsNx1u8osz6MpcRcdTAwMDP2RYlcdTAwMTBAXHQ03r/vV6iWXHUwMDA2jF7PsbVvOSHYiSREatHhXGJcdTAwMTNg31x1MDAxMFx1MDAwN2k5XHUwMDBlfVmE645Ffa6tfX2q2j/bZ6J8cDHcXHUwMDFi3Na3XHUwMDBi5dzFXFwlWsLAsn8/RmdcIkV9dDQs5cuj9Il5XHUwMDE4XHUwMDBl+/lKNl+7XHUwMDEzrePrYfWR0GVRminCkLtcdTAwMTPsss3Ev2JcdTAwMGXy2I7GXyGY55auXHUwMDEw8kW0xbNKw9NkXG6el1x0XHUwMDAwXHUwMDFmXHUwMDE4Ks9gilx1MDAxY2NEMelcYvNcdTAwMWJPjWZGICzCfXRkNIlPb4v3XHUwMDA3scabOeBcdTAwMGWeXHUwMDEyXHUwMDExmlpcdTAwMTfchbnkXHUwMDA07VxmJ5i5v1xi2sXTRvBwrW9cdTAwMDN4gajeKM3v70ar3Wz6LdfsZEU1darUXHUwMDEyK2ZQiGB0z738ueN5f72B4o98q3U2aqvyXHUwMDBlhj+eq6X+RrSx/+tcclx1MDAxMVD+S6NI0T9//fN/uS9cYigifQ==External servicesLoadbalancerRPCBlockProducerRPCrpc.testnet.miden.ioStorePublicInternalFaucetfaucet.testnet.miden.ioexplorer.testnet.miden.ioexplorerHorizontally scaledRPC providers...Example infrastructureTransactionproverBatchproverBlockprovertx-prover.testnet.miden.ioNetwork TxBuildermempool updatesnetwork txscommitted stateValidatorproposed blocksigned blocknotetransporttransport.testnet.miden.ioblockproof \ No newline at end of file diff --git a/docs/external/src/img/workspace_tree.svg b/docs/external/src/img/workspace_tree.svg deleted file mode 100644 index 016e69a633..0000000000 --- a/docs/external/src/img/workspace_tree.svg +++ /dev/null @@ -1,4 +0,0 @@ - - -eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXFlT20hcdTAwMTd9z6+g+F5cdTAwMDdN78u8sS9J2MP2zVRKWMJcdTAwMTbYkpFlXGaZyn+f2zJYsjYvXGJCpaypmlwiUku+3brnnHu7b+vfT0tLy9FT113+a2nZfWzYbc9cdO3B8lx1MDAxZub8g1x1MDAxYva8wIdLJP53L+iHjbhlK4q6vb/+/NPudq3kLqtcdTAwMTF0hne6bbfj+lFcdTAwMGba/lx1MDAxZv69tPRv/H+44jnmfrt72nWf9qLPXHUwMDA3XHUwMDE33e1cdTAwMWLXsa/Do5X41rjRi0Gh24hsv9l2k0uPcH5FIGJJglx1MDAxOVx1MDAxMYpILiRcdTAwMWZdfoLLnFwibWlEXHUwMDE41UJcdTAwMTCq9OjqwHOiXHUwMDE2tMBEW5hcdTAwMGLC0fNBR01artdsRdBGJVx1MDAwZlx1MDAxZJrw11x1MDAxMlx1MDAxYZ3pRWFw565cdTAwMDftIDR2/lx1MDAwZrvmv8TKa7tx11xmg77vJG1u4iNpc+O12yfRU/xkXHUwMDE4Wlx1MDAxOMLlzPPPX8zNnC+7XHUwMDBifrDZ8t1eb+yeoGs3vMhcZoxOOmCM6+468Vx1MDAwYvonMSm0O+6ueUN+v91cdTAwMWWd9nzHNeO+fC22x37Nd55/bax5z3WdeJBcdTAwMTlnhFx1MDAxMiZGV1x1MDAxMoeCkc+e3Vx1MDAwZvzYubBcdTAwMTJaXHUwMDEzTVx1MDAwNUlcZuhtgEdF8WNv7HbPTVx1MDAwNtpcdTAwMTixmfW2tMeNOVTkPkajXHUwMDFlpPyxtXaI2ofR3e6mdKR/7j8+9Fx1MDAwNuvLo3Y//yh+7PDm3ire3rn8et/yzvd31lvisDPY/D7+Ky+/b4dhMEg99/mv5Fxy9LuOPewnllxmK6koZUgkY9X2/LvseLeDxl0yNJ9SXHUwMDA2Z1BX3MtcdTAwMWPqxlx1MDAwNmlcYjguscUoJZJcdTAwMTDCOVx1MDAxNjJcdTAwMDM4Olx1MDAwMXBCWFxcas2Q1FxcacVYXHUwMDFlb2SBt1x1MDAwMrztTI83RrhcdTAwMTZIcFZcdTAwMDA3cKIyuIGHUc2FXHUwMDEw86CtwnFcdKaCq1x1MDAxOVx1MDAxYzfxQ+N/0Pstu99wU554XHUwMDEz+NGJ98NcdTAwMThN0NjZLbvjtWNPXHUwMDFje8hq22ua3i83wF43XFxOXHUwMDBmQeSBao1cdTAwMWF0PMdJq0xcdTAwMDNcdTAwMWVqe75cdTAwMWLuTiNXQeg1Pd9un5ZcdTAwMThu96Pg2O1cck2Pwr6bXHUwMDFlXHUwMDE1d+dcdTAwMDVcdTAwMDHYXCK8XHUwMDAyvWxD76w621x1MDAxYmtXXHUwMDE3q963jZuHXtd1ptdMxaTFZIVmUlx1MDAwYlx1MDAxMflcdTAwMDLhXHUwMDA04Fx1MDAwYs18LYZ3Z9FMzSCwUWlPfYYwkzJ7dqSYWCosQG5cdTAwMTNb31xcMTtP97x/1du9WD9k+P76fHBPXHUwMDBlrqdVzKfdjfD7gX7avXSO+fnKw8n64PPOdIpZ+dzPbO389sfBXHUwMDBmPOh1dptHzYPv3/drU2JcIjRLXHTXq5S4ePSmUWKFhCVAiTVBXHUwMDAya0ayMKZcdTAwMTNgXHUwMDFjs4DWXG4xoVJcdTAwMWOwkOFKXGLvzVx1MDAwMGGqmYZgSOdcdTAwMTTX6DAhZSAmXGJhjbhIgb8mIUacMjyD3+aEeD9w3I8gw1x1MDAxM1x1MDAxNDArw+Nm1yPCZM+/v7tEO0H40FljX44j3e99nl6EIfq1XHUwMDE0XHUwMDEzZVwizDTktVIuXHUwMDEy1+H7rFx1MDAxM8Ffp0ewwlxmXHUwMDEzqlVR3kpUTptHKiwpRlx1MDAwMt7wXFyR9HwqfNE/vrv88rjd31xiwvVz/8z+7GytzaZqSEHsUI+qXHUwMDE1WzONqmlcbrJEQGIlZURcIkbGccExskhcdTAwMTUulLAo1lx1MDAxYV5cdTAwMDBcdTAwMDX1o0X5ZSo4WsBiXHUwMDA0i/3pYWFSXHUwMDA0znCRrOFU1JBFXHUwMDA1eJlmb1wiazM6bk7W1sz1v/3DMHAgX1x1MDAwYj+Cwk2Ql6zCxT1YynegXHUwMDFlrfNuKHnyVVdcdTAwMDTbh0fbrTZpfuHR9FqnkKzWOmaxxSTty5utXHUwMDEz1Fx1MDAwN9ODXHUwMDFhXHUwMDEyXHUwMDAxLDWnhVx1MDAxOSfT2bPJXHUwMDFjLcNSIPWeXHUwMDE5py02Ns5WNzna37389rDPXHUwMDFmoofByrRax4dcdTAwMTOXmqFZXCLhXG5wXHUwMDE0WzON1kmJLIpcdTAwMTRRkilGUWqObKh1ZFx1MDAwMi44s1x1MDAxMGRwXHUwMDEyU0yZXHUwMDE0XFzkYbFI4opgcTjDXFyq5EhcdCVcbkPAfGaXwEJTJECXalx1MDAxNLv5PDcndidRXHUwMDEwfogkboKqZCUuY3c9ynbUvjl19OGpaovrtvTPTu4vXHUwMDFjb4blR4EnZXGYk8VU6vCF1lx04ZNcdTAwMTnmYVx1MDAxMCZMYo5yMy5cdTAwMDbfvDyN44aVJUPvmMa198VX3DknXHUwMDBmq9tX67p73fLo9sWvXn5cdTAwMWNcdTAwMTJcdTAwMGZcdTAwMTFcdTAwMTjPsopTgbriXk4jmVx1MDAwMkMoqUEyqVwiXHUwMDA0a8HGXHUwMDAxXHUwMDA3llZcdTAwMDOOXHUwMDE5PtBaQFKvXHSXXG7n8baQzFwivJ1OjzeCzZw0YYXL/USX4o1IZlx1MDAxMEfZPHCr03Nzknl8uP5cdTAwMTFcdTAwMDRzglhlXHUwMDA1c8zqeuTye6SO7txcdTAwMGIsv+701vvnZ61cdTAwMWS3WVA3UL7yWDnpKbFKT+4s5LI++H6bQS6lVlx1MDAwMutCtaSkNFx1MDAxMdRcdTAwMDRIXHUwMDE1XHT8jnmg39JcdTAwMWR8dSld4ZydXHUwMDFkba1+OZBcdTAwMDK/w1xuYeVz9fWj5+3rrb67s9J4jC5a+LB/UoNcYtdeXHUwMDAzVDx604iwmc8h2Igwx1x1MDAxMpFUfDSEMZtcdTAwMDBjxkHDjVxiU6nAZchi8XFo0kRcdTAwMTSfzSDCiFxiKSFcdTAwMTIqnKVcdTAwMTWlXCKsiFx1MDAxNlxcgGTWJ8LDSVqIylJ8P4dcYn+LvHbvI8jwXHUwMDA0XHUwMDExzMpwxu56hPjpXCJyLlr8cnswOOvpq69cdTAwMDdcdTAwMWTpXHUwMDBlZshbKa8qm5VcdTAwMTBGL2Zk31x1MDAwNMLnM1xisSBEXGKBRKFcdTAwMTKr8rknjjRWXHUwMDE0v+fyo33q2WdcdTAwMGZcdTAwMTf3W61cdTAwMWbN1sr+/q1e5edTS+be3sn2/tFaXHUwMDBmXHUwMDFkiOPOce/cd1x1MDAwNqguyWSIXHRRU95a3MtpJJNrZWFcdTAwMDHhXHUwMDEzUvGSciZvhURl8lQv1VqqeFGTLup1nk2aiLeLXHUwMDE58EYoMaVQhZEvqVjYRFjDe8F6rtD3TTWzXHUwMDFiXHUwMDA2UfBcdTAwMTE0c4JeZTUzY3c9mon2NndcdTAwMGb3XHUwMDAy7Fx1MDAxZWxdXHUwMDEye3s7XFxVx1x1MDAwNUVcdGWaiVx1MDAxMYS9glGKcPFkL2BUJHNPXHTljErfTcmPZHp05DGMOejySFTHdLVcdTAwMTZMR6Ht97p2XGJv81fiXHUwMDFho9dcdTAwMDP7slx1MDAxONiJvLy8UPp85mdi/HNcdTAwMTmDkIAwwlxu4Y4q4E45RUrMXHUwMDA39ypcdTAwMWR8q2LYV6Skb6XbJc+dXHUwMDE0Zkgq2bq/vv/5dmflylx1MDAxYlxc0yC825xtXHUwMDFhXHUwMDFi1JPWXHUwMDE0XHUwMDBlXHUwMDE0WzNNOCBcdLU0uFx1MDAxZThcdTAwMTFcIkqgXGaVcMXT4UCeSnS8XHSHQl84gsa6aFx1MDAxN03NVU6/XHUwMDExdVxcTVx1MDAxZlx1MDAxM0gkgO5xcVx1MDAxNk1zXHUwMDFibJJcdTAwMTBcdTAwMWNkgs5cdTAwMTWAj1nxevfNhVx1MDAwNOtBp1x1MDAxYvjxj1x1MDAxNcVcdTAwMDWpQqpa4oLrIIqe94LGXHIyccFcdTAwMDRNzsZcdTAwMDVFxteUUFdyb4bHMmCWwlx1MDAxMoBjJmhcXIufaPawXHUwMDEwX2mLJ4X4eTBcdTAwMTNcdTAwMGJpXHLhI1x1MDAwNJ5cdTAwMTJcdTAwMWNcdTAwMDel4s9cdTAwMTGaXHUwMDA1s5hcdTAwMTSQ9LF4Y4iuOdb/jcBtT1x1MDAxYlx1MDAxN5CyuFx1MDAwMCOuQeJcdTAwMGJWqmJaLVx1MDAwNT3QNqTrhNde8jE35ruBl1xyO5K/llx1MDAxMpeJ/zH6+58/XG5br5T7qTlyXHUwMDFlmjwvp8ptu1x1MDAxN1x1MDAwMZQ7Xlx1MDAwNFx1MDAxZD00RuboN7LDaFxyXqvnN8ff3vOG8mn2XHUwMDAxxFx1MDAxY9bom1x1MDAwMVhBYLykRCqwXHUwMDBlIGqWKlLNmnY3poh06K3RqFx1MDAwZomLuL4z2apqUlx1MDAxYreKYlx1MDAxOFRcdTAwMGVcdTAwMTZx+G3Jkz2bI6tcYmRcdTAwMDR5VzXjs2rIqOXaOTiAlelrWdZy29fBYKpcYqd6kaSSXHUwMDE1zS5DjlVZtkQlT29wyLNcIjXbm4qWXHUwMDA2qFwiVt1zXHUwMDFkv1x1MDAxMf9dv5r/XHUwMDA0IIVQgou2XHUwMDBmY5lb1Fx1MDAxZsU8lEA8rJSaa7d+1TSIQlxcJKL6y/gv45HmwIK8hFx1MDAxNVV3XG4rtcPOXHUwMDFjQ1x1MDAxZv5QXGZcdFx1MDAwNMkxXHUwMDEzSnHGXHUwMDE1pLgqqTJMXGJyPj6sXjBcdTAwMTnnQ86wpphcbiW1RCxZPFx1MDAxY9nAoYmZpftVlFhdZFVJiZxcdTAwMTHge1x1MDAwMXQoITmQODNcdMzAv1BloKgspokgjGmMXHUwMDExXHUwMDA0m1x1MDAwNeyokJWaYkrXny6IcpwoXHUwMDFirydKriDiXHUwMDExxfPF5XFcItBcdTAwMWFEXHUwMDFmtE6efKlzYnSu6eKa48RSNzVHzkHfg1x1MDAwNas/2zDOQFgxIc1SjYnMmCR5XG7C2MIk6Vx1MDAwM9Lz0WJ1OddcdTAwMTg1Y4K4woxQKlx1MDAwNUNcYlx1MDAxN9FcIk1NXHUwMDFjXHUwMDE3xq7vRJDVs43VmbSCoFx1MDAxMFJcYlx1MDAwNNFcdTAwMDdcdTAwMDGmTKBcdTAwMWZcdTAwMTOkwMRikF4oXCJMNW+Cn2RZ2sLArVx1MDAxOJtpMaHSXHUwMDEwXHUwMDFiXHUwMDExpKZcdTAwMTalwjRjklx1MDAwMtEmv7EgyHGCdF6fSWNiUkVV/CWaiukz8F/JIdasnVwiP0YqXe6n5sh56Hsw5NQ5K6SskOczgrSKXHUwMDBifbnOk5FKgtu3XG5cdTAwMTWRZUrLTJlcdTAwMDPDWFxiSFhUzlxmQ1x1MDAxN8lqW1x1MDAxMU+/XHUwMDEzJZ45p97Z5lO4s3J8uXrFelxy70fE8pRo5iuzhTqIWbh8zZEyXj23yGnBKiNcdTAwMTHaWiTQZbTnvZr2pNlKqXjhXHUwMDA26XTpRob1XHUwMDA03CRY3dkzXHUwMDE1TPBcdTAwMGZcdTAwMTBcdTAwMTVigbJZ8FxuRtxKUrnye1NebFx1MDAwZSyoScTrpMUs7kuuzMRcdTAwMTdj17pB+6lcdTAwMTl7wMT4qXJVtTrBVNhcdTAwMTKQXFwwU4cgXHUwMDE5z3ycT0DgakhTUqmVLijMXHUwMDA13kZcZljGRO1cdTAwMWH4JuGTpEJcdTAwMDFeXHUwMDE51YhA7iMoQ5gv4qcyXCK5rWElwrA64YU7abAq/1xmmCn+o0ih3zR+Wil1VHPkXfRDXHUwMDA1UGbSXHUwMDFmca5cdTAwMThcdTAwMDJao9zsYkux4nPoXHUwMDAyqj9vXGJVXWqVMYRcdTAwMTPFhVx1MDAwNlx1MDAxZlx1MDAwM+lcdTAwMDHVyq8+cCtV5ftL88q9x6fDe9c5woNvT7eis7Fy2mdcdTAwMDV5ZdF+XHUwMDA1rKqWXCKw1tWFW+BJXHUwMDE2vCZcdFKlzTb799iw8Fx1MDAxYvFge5ZdXHUwMDBiXG6iXHUwMDE2iFiKXGKv/INLVCGlmZA17lx1MDAxY1x1MDAxY9ZcdTAwMGVTrlJlenNUW5xcdTAwMDfhXHUwMDFkvMaGu+S4XYCC6zeelqLQrWVcdTAwMDN+271JOUeu9FwiXG66ZXVcdTAwMTdjXchcdTAwMTZZTGFzPTVcdTAwMTfrnq+um3fdxplq3eCVPtm4dGb4rFxmx9LSXHUwMDE4fFx1MDAwNXFmcqTc5nuUzo1cdTAwMTa7XHTrK6pcdTAwMGVmQLSE6IVcdTAwMTZ+QS39XHUwMDFki1xmoM1GYKzQu374+1x1MDAxYu3cSXHTdE/Xd24vb69cdTAwMGY8XHUwMDFl/bJcdTAwMWTyxdZMI3bcXHUwMDA0+YhcdTAwMTJTpCEkXHUwMDExLFx1MDAwN1x1MDAwYj5cdTAwMDFcdTAwMTaYW3pCcaFcXMCiXHUwMDAwXHUwMDE23elhgcH5hSZUXHUwMDE2zVx1MDAxMVx1MDAxMFG6y5YgJDhGsv5N8kCE8lWFhftuNFx1MDAwMOX42z81oYvdiMDsv/21vtd2PsYn1SbITe6jocP+LKW6s5TrzcxC+OmZQpbtbvckgncwiuSXXHUwMDFmPHewVo6NT89cXGGw4caJ5M9PP/9cdTAwMDPDXHUwMDE1Zk4ifQ==FaucetNodeBlockProducerStoreRPCUtilsprotoComponentsWorkspace dependency treeNetworkTransactionBuilder \ No newline at end of file diff --git a/docs/external/src/operator/architecture.md b/docs/external/src/operator/architecture.md index aeb371ceab..de7b3690a3 100644 --- a/docs/external/src/operator/architecture.md +++ b/docs/external/src/operator/architecture.md @@ -3,18 +3,20 @@ title: "Architecture" sidebar_position: 2 --- -# Node architecture +# Network architecture -The node itself consists of four distributed components: store, block-producer, network transaction builder, and RPC. +The network itself consists of five distributed components: store, block-producer, network transaction builder, validator, and RPC. The components can be run on separate instances when optimised for performance, but can also be run as a single process -for convenience. The exception to this is the network transaction builder which can currently only be run as part of -the single process. At the moment both of Miden's public networks (testnet and devnet) are operating in single process +for convenience. At the moment both of Miden's public networks (testnet and devnet) are operating in single process mode. -The inter-component communication is done using a gRPC API which is assumed trusted. In other words this _must not_ be +Inter-component communication is done using a gRPC API which is assumed trusted. In other words this _must not_ be public. External communication is handled by the RPC component with a separate external-only gRPC API. +The image below shows a rough example of what a network architecture may look like. Only the more important data +flows are pictured to improve clarity. + [![node architecture](../img/operator_architecture.svg)](../img/operator_architecture.svg) ## RPC @@ -33,6 +35,10 @@ It can be trivially scaled horizontally e.g. with a load-balancer in front as sh The store is responsible for persisting the chain state. It is effectively a database which holds the current state of the chain, wrapped in a gRPC interface which allows querying this state and submitting new blocks. +It receives new blocks from the block-producer, which it then submits to the validator for signing before it is committed +on chain. It then submits the block to the prover whereafter the block is marked as proven. Blocks therefore undergo +two levels of finalization, `committed` and then `proven`. + It expects that this gRPC interface is _only_ accessible internally i.e. there is an implicit assumption of trust. ## Block-producer @@ -40,7 +46,9 @@ It expects that this gRPC interface is _only_ accessible internally i.e. there i The block-producer is responsible for aggregating received transactions into blocks and submitting them to the store. Transactions are placed in a mempool and are periodically sampled to form batches of transactions. These batches are -proved, and then periodically aggregated into a block. This block is then proved and committed to the store. +proved, and then periodically aggregated into a block. This constructed block is sent to the validator, which verifies the +contents of the block before signing the block's commitment and returning the signature to the block-producer. This signed +block is then submitted to the store where it is proven and committed. Proof generation in production is typically outsourced to a remote machine with appropriate resources. For convenience, it is also possible to perform proving in-process. This is useful when running a local node for test purposes. @@ -66,3 +74,19 @@ number of failures, preventing resource exhaustion. The threshold can be set wit The builder also exposes an internal gRPC server that the RPC component uses to proxy debugging endpoints such as `GetNetworkNoteStatus`. In bundled mode this is wired automatically; in distributed mode operators must set `--ntx-builder.url` (or `MIDEN_NODE_NTX_BUILDER_URL`) on the RPC component. + +## Validator + +The validator is responsible for verifying the integrity of the blockchain by signing new blocks before they can be committed. + +At the moment this is implemented by having all transactions sent here to be re-executed to double-check their integrity. This +also guards against bugs in the proving or execution systems, by backing up the transactions and their private inputs. This +forms part of our training wheels while Miden is maturing. + +The validator signs a new block if: + +- all transactions were previously verified +- block proof is valid +- block delta matches the aggregated transaction deltas +- block header is valid and matches the data +- block builds on the current chain tip diff --git a/docs/external/src/operator/installation.md b/docs/external/src/operator/installation.md index b8288edeb4..d7f87fb609 100644 --- a/docs/external/src/operator/installation.md +++ b/docs/external/src/operator/installation.md @@ -29,7 +29,7 @@ can be used so long as the checksum file and the package file are in the same fo ## Install using `cargo` -Install Rust version **1.89** or greater using the official Rust installation +Install Rust using the official Rust installation [instructions](https://www.rust-lang.org/tools/install). Depending on the platform, you may need to install additional libraries. For example, on Ubuntu 22.04 the following diff --git a/docs/external/src/operator/versioning.md b/docs/external/src/operator/versioning.md index d678d69b64..d3d6bad943 100644 --- a/docs/external/src/operator/versioning.md +++ b/docs/external/src/operator/versioning.md @@ -10,7 +10,6 @@ The following is considered the node's public API, and will therefore be conside - RPC gRPC specification (note that this _excludes_ internal inter-component gRPC schemas). - Node configuration options. -- Database schema changes which cannot be reverted. - Large protocol and behavioral changes. We intend to include our OpenTelemetry trace specification in this once it stabilizes. diff --git a/docs/internal/src/SUMMARY.md b/docs/internal/src/SUMMARY.md index a8bf4eb82e..6f78b0e127 100644 --- a/docs/internal/src/SUMMARY.md +++ b/docs/internal/src/SUMMARY.md @@ -11,4 +11,5 @@ - [Store](./store.md) - [Block producer](./block-producer.md) - [Network transaction builder](./ntx-builder.md) + - [Validator](./validator.md) - [Common issues other oddities](./oddities.md) diff --git a/docs/internal/src/assets/node_architecture.svg b/docs/internal/src/assets/node_architecture.svg index ba697edf6b..19f9de1310 100644 --- a/docs/internal/src/assets/node_architecture.svg +++ b/docs/internal/src/assets/node_architecture.svg @@ -1,4 +1,4 @@ -eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO19WXfiyJb1+/1cdTAwMTVe1Y9foY556Dc8z/P8VS8vwFx1MDAxOGMzmcHG7nX/e58jXHUwMDBmXGIpJCRQkmQvXFy36mZcdTAwMWFcdTAwMTBcdTAwMTFS7HP2mf/nXysrf/XfO9W//mvlr+qwUmrU77ult7/+xt+/Vru9ertcdTAwMDUvMf/vvfagW/Hf+djvd3r/9Z//Wep0vNGnvEq7+fnJaqParLb6PXjv/4e/r6z8j/9feKV+j5/fIOv7tWHlcv9071rrouaN0+O6/1H/Td9cdTAwMGLqViv9UqvWqI5eXHUwMDFhwu8ptcojllx1MDAxM6aY1YLzn1ff4VUpqPC0ZZpcdTAwMTmrLGH259W3+n3/XHUwMDExP6+ZJy3X8ueVx2q99tiHl6z1tIBcdTAwMGZxXHUwMDAxL1x1MDAxM0JGl/5cXMd/rZCf3/T63fZzda3daHdxsf9Bq/jPaKnlUuW51m1cdTAwMGZa9z/v6XdLrV6n1IV7M3rfQ73ROOu/+1eHe1xm9/Kv0HdcXH0vPPT7uE/Bl9ZcdTAwMWVb1V5v7DPtTqlS7+MtomS0XHUwMDBiXFxhZ+fef1T/PVpTt9Ss7uCzalxyXHUwMDFhjZ9f11v3VXxcdTAwMDJ/lcXB2Ne17r++buztvWpcdTAwMTUvQVxyXHUwMDEzVnK4rz+vjI5cdTAwMTY8TFx1MDAxM/71YbvlnzNcdTAwMDNPiWrJ2GhcdTAwMDG9dThbff+yXHUwMDBmpUavOrrbuIiNwLlcdTAwMWJtZtC5L31+XHUwMDA0L8ZcdTAwMTWnTFx1MDAxODl6+o166zm89Ea78jz6XHUwMDE2/7f//tt1lE376GC3d7Y7eLo1xb3zi/c9ubqX/ihr7jFtpbBcdTAwMWHuXHUwMDBmh+M8fpa1klx1MDAxZTH4XHUwMDAyp8QwXHUwMDE1OctGWVx1MDAwZs669lx1MDAwZmz0PHNDPFx1MDAxYvjR8z7PlW671ys8lvqVxz/gVJtt96n+fuajXHUwMDA3yr9+8+/IYVdGwYGnMnKq4UVBZNxZZ9pwIanI+ahzq6Tg+Vx1MDAxY/X1gjxcdTAwMTPi7uqj3W1cdTAwMWPQ2+5up2BthqNOPUlcdTAwMDSVllx1MDAxMus46iCuPa6kskpYw11H3dDoXHUwMDAxXHUwMDE3jHtcdTAwMWGkvVVcdTAwMDZcdTAwMDQ3/IjlXHRPPOG7s59wRqRcdTAwMTJcdTAwMDLOueOEc6ZjTzilijEltcn5jEuhVFx1MDAwMDgznfGBum2R182zrdftTqV0/PJsb2t3qc94XHUwMDAxZLlcdTAwMDesQUlcdTAwMDBcdTAwMWUhSpNcdTAwMTA3oUJ4Qlx1MDAxM2qMMtIlz5nRcIHRj+PIXHUwMDAzRDyqlYZcdTAwMWJJmVxccpT4s34481mHO8xcZlx1MDAwN+7iOOpSxlxuc9DVVILO1lMxl5/ljVx1MDAxNlx1MDAxYTh6/epw9GBcdTAwMDInl9dcdTAwMGXLjV6ldNlsXHUwMDE3Lq/ahf7HqX7+6+d9//7bfdnPXHUwMDBmV9hdbevoqdFcdTAwMTfnW6u73cfuXHUwMDAzK6yOf8v395e63fZb4Lpff4rFp1x1MDAwMUmvXGbNSVx1MDAwN7l3XHUwMDE5wefYTfqEJlXC41x1MDAwNiRcdTAwMTe1VFxuYFZhZNpkZErtacWt1kRcdTAwMTJcdTAwMWZ5XHUwMDBltiWXUIyB4lFcdTAwMTZzwVrJZJBcdTAwMDJcdTAwMDRAR2ks6JRCaahcdTAwMDJPNlx1MDAwZv0yxflcdTAwMWRcdTAwMWRHPIaw/9PjtcBcdTAwMDNst/pn9Vx1MDAwZl8lmrHfbpaa9YZ/XHUwMDFhx65QbNRrLVx1MDAxZqWw2Gr3r+BccujXwfr+eUO/3Vx1MDAxOb1agSuW6q1qdyeNZmt367V6q9Q4dy25NOi3T6u9z0X3u4Nq8GZUt7/PP/WYTMBuspBcdFx0mHHwXG6tXHUwMDEy9aqiXGbVplCMMu5cIo9wnjyGMlnB85Q6wM5/0Es8XHUwMDAzp4sqMLU4MfDoRydtieZxNFx1MDAxZqdVrCyWRFx1MDAwMmlcdTAwMTNoiDtBLmJBLlxymMOKk1xc7SSpLaWaaFx1MDAxYTzWaTHeadfDenv0p5XRkfH/8vPn//7b+e7YY4o/hehcdFx1MDAxZF0vooZcdTAwMWKlXn+t3WzW+7DRY1xcZETe9kvd/io81nqrXHUwMDE2fq3aulx1MDAxZr1cdTAwMTJ4rl+evU+RcvXQ2nqrV2/uqjeMdl/3zyv7RydcdTAwMDHWXHUwMDAwQq0ywFtcdTAwMDMrJ1x1MDAwMF240WBcdTAwMDRcdTAwMTB47pRpXHUwMDExeF+t1IF3Wdg6XHUwMDExRlxiRlx1MDAwNFx1MDAwMzUtoodcdTAwMDdXXFxE+fBYLUVcdTAwMGUorDn4XHUwMDFhXHUwMDAwrlx1MDAxZeLo1Ua5/ZaKZ9TPzl5qW1fdXHUwMDE3XbiSz2+Vl6tXvZ2KZ1xiIcHOjVx1MDAxN1XSXHUwMDE4eD1BVFHuIP1sXHUwMDA02qUwXHUwMDFhXHUwMDE3RidcdTAwMTmoXHUwMDA1UVZKbqMuR19FRFj+t9RcdTAwMTEodpRcIvkyXHUwMDBisIZcdTAwMDHNbCZm0Vx1MDAxYpRcdTAwMDHhK51u+7XaWulcdTAwMGadNIOqsd/+SpoxtvYwp4hfbD5cdTAwMDSjt/5eulx1MDAxMN33gXku1fZ443BrvZrFeGfUU9pcblx1MDAwZarJhVxcZpinwHCINVx1MDAxMahcdTAwMWE5P36QS+koXHUwMDAysYTuOHRP00PXXHUwMDAw+lxic2BcdTAwMTTtLsvCv/1xOoHeIUDg2VROp1x1MDAxZlxylc1cdTAwMTLnj6u2VnzrvOyWrpvDQ3N4/vKQ1lx1MDAxMj842NrfPTvrbeuLer32PJD86kDkZIlzn1fk5Clz7zKNhlx1MDAwNFx1MDAwM8+zQnNt4MGAJpQhnHHGPJqIM8I9yUGRXHUwMDFhpalxxT3EUl3GYe4sPeZcdTAwMThRXHUwMDAyfbZO0Fx0XHUwMDEyb4ljuEpcdTAwMWJcdTAwMWF4XG65WOLZz29EX5ZLvXplpVt9XHUwMDE5VHv9f1qw8PpcdTAwMDPotj7uaVx1MDAxZaqzWb+/XHUwMDBmKp6QkT5Bg4VcdTAwMTXq2G5W3JvJR7UmU/3JqpVcdTAwMDJitTbA/52qVVxu41FhjVZ0qVpzgfl5XHUwMDE2h1x1MDAxYkVggUXi0q0qPmRJMVx1MDAwM4OxqWzx6VTrdUOdic29w4+W7OqD5tXjaaN8O1x1MDAwNyd34nVf+Eapd25qW++6M3xb2zh7fumc5KKyv1x1MDAxY1x1MDAxM8pcdTAwMDYxO73Kdt+9dCrbYJqBgbVQXHUwMDAxf6EshF+rPZWIX1xub8D4rLJcdTAwMDJEOFx1MDAwYiB3aeNORvNFXHUwMDA2NHNhXGI8XHUwMDA36sq24SY22Vx1MDAwNlx1MDAxZVx1MDAxYyOcSp6rkfvtWiNcdTAwMTlOcERp+4rtXHUwMDFkXGZGNFx1MDAxZNtcdTAwMGaLoKgn6MOwoo7dQT7auf5Rurvr7WxVTlx1MDAwN7W702L/YLXddXBxt2fdeFx1MDAxY4lcdTAwMWXjnCpcdTAwMWHIn/jk4pp4VjOGOXHMXHUwMDEwM7qdP8i2wjOGK1xuXHUwMDA0XHLlgFx1MDAxOF1hpKg9XHUwMDA1XFzfgCFmkLBrtlxmlMUh/XJ21zrwdcpcdTAwMDHOzqA1jzWVlVx1MDAwNKhcbv0rnFxcnLDguf4trvX4c4o/hehcdTAwMTFcdTAwMWRdMKKlM/vW4zzoonV43uze7N/r3tCuX7/0d9bNR4xcdTAwMDddoXyWVEhcclx1MDAxYqCCXHUwMDA13vXpP8ctcFx1MDAwMoJfXHUwMDFhIVBcdTAwMTObyFx1MDAxMVx1MDAxOXPrR1x1MDAxNzxcdTAwMTfXunq+XFynR7esUlx1MDAxYlxcXHUwMDEy9fq0unN9V0znWvdcdTAwMWRcdTAwMDNcdTAwMWNzfi3nJpRAxoX2RKKosnCH8EfAXHUwMDE5kHArRyd9yUEmS6ar9Fx1MDAxY8RcdTAwMWFcdTAwMGJcdTAwMDREOyP4YMbHUlx1MDAxMK2U5EBgck74zcHPXHUwMDBlXHUwMDE2dvd9XHUwMDA1QNKvzoV+TOtid64zXHUwMDFmktF6a2xcdTAwMWW/1596u6/2tLt2fX/aOjqM4lx1MDAxNi9cdTAwMTnJijOJ0XskXHUwMDE5idF7qTwmXHUwMDE0cefsj1x1MDAwZctCI/a+jcpivpC9nplMSCWkYNqZXHUwMDAwx3S8212j61ZO51x1MDAxYUhwXHUwMDAwckKkncqWyJVLOM4j/rDR52anXGbx4fjp9fbYa512473mP8hJXHUwMDFhe1dcdTAwMWTcq4ve5VGRyf3nl4Pi4ep5K4r8uMRvZTysb2BcdTAwMWM0gogmflNhQPCAaKAmpsbBSE9IRaT4SvKOyoBAQHehhcBvUNs3MyfBgvkuiKFOc0LFxsxcdTAwMTlcdTAwMTVKwdOmOebAzlx1MDAxY1wi+zvpuvL65fytf77fOdxcdTAwMWFcdTAwMGVcbsMtfVYukVx1MDAxY67Lnnduj99cdTAwMWUvmlx1MDAxZNXZ5Y3VrUvNXHUwMDFm0l03XCJGot5cdTAwMTVGXHLLYl0l4Hxt7eisNdx+6eyc6sfO5eZd4eRcIn38nDGEKVx1MDAwMJlY46pl4tp6XHUwMDE2Y35aXHUwMDBiZ4FcdTAwMDfGXHUwMDA1YzJr0X1cdTAwMThMi59/0vtcdTAwMWZW5lHK4DCkzEpBpXG5/42JredQhFqpxTzd/82WXHUwMDFlKFNsXZ2b7YP76+vtfbW+k92dbvKBi3s1XHUwMDExuERcclmmMJecKcYtKEUlQz43oelcdTAwMDSgXHUwMDEw6dGlITstNspZsIHpQNKdMObwsP2kqTLGjJKL50qvt1x1MDAxZVx1MDAxYXhC5mjKlsHw+SpcZvffXHUwMDEw8qRPUDphXHUwMDAzN25cdTAwMDP52LjJ1Fwi2ZFux5PHQvUlXFxcdTAwMTJPcUPQxKVERJ1TXHUwMDE4XCJcdTAwMGaWdVx1MDAwNVTcSFxySu5cdFxm1FrB4Ep/XG7r/Vx1MDAxZKZvJVx1MDAwM8ilJlxmZKlwKkBcdTAwMTVxYv0wXFyrXHUwMDE0k4xMleUynVx1MDAwMmRbr6dk/fH1eGv3vXk5rNrti+bg1/LFgL2YpzeegVxu5GD0mXCnhVx1MDAxYz5eiOBk0vXiwTfpepE7nltsIDlcdTAwMTdoZVx1MDAxNFx1MDAxYiCeYCBUODVEYXxgVFwitfJcdTAwMWRcdTAwMWHgwY/Vh6OFjd1UjzBrXHUwMDA1XHUwMDAxpadcdTAwMTVcdTAwMTHajj1cdTAwMTJPasrQoDNcbsu3mY3cg5gwQ9z+kr1cbmP7I4RcdTAwMTE0QeHrleSEXHUwMDA3XHUwMDFmz9dcdTAwMGVlmlx1MDAxZGJcdTAwMWRcdTAwMDJcdTAwMTYvK4yQ+50ghFx1MDAxY9+ksMC7rLTUcKXBklFcdLvML2Qypq/81Z9Va5Ns4S9Zx8bEhL+m0K4nwSWItzGEfO5cIvPVXHUwMDEy4DK6jS5h5S9+p3fWqVbqpUZEXHUwMDAwwVpcIi8m6HK3rExBz1x1MDAwYkLBaZdcdTAwMTTsWFx0XHUwMDFh28ApXHUwMDE41+WUKo9cdTAwMWFBgZ+rMZY3lqBq4Fx1MDAxNkmj4bTBvVhcdTAwMDSC/oeZsPfpNbjCRjFRRY2Cz4Z/+Vx1MDAxM2zCXHUwMDEwqcBypJxJOrGa6ZmCTZ1ue1iv3q/4wZxFyHaZQIvDXHUwMDFjPWb9+VD0pjk+vtg5Llx1MDAxZD1V7jf1y0dnIMub6Z1UIJo0XHUwMDA1ocQ1hpEjTipBmKcl6Fx1MDAxOVx1MDAxYdOFhHLlMVRcdTAwMWTM7amSy1x1MDAwNiTJsK66YZ2lXHUwMDAxXHRBmiac+S1KxkeXjYGHLuV0gJ+Or7+V1MFT326/7D/3P7b3RXmjrlx1MDAwYnPIK0287l3fljW/+nj62Hx87N2+2KLV5Vx1MDAxY677x/i53U8lXHUwMDA1M2BaeNxqgsRcdTAwMTBsP1x1MDAxNnbcUZssPDBcdTAwMDVcdFx1MDAwM1hGaFx1MDAwMVx1MDAxYlrWeWZcdTAwMTFcdTAwMWNcdTAwMGbp+Vx1MDAwMHZcdTAwMWFcdTAwMTLAv9xcdTAwMTJcItZrRy1cdTAwMDPLhoJ1kDMlyOx1jlCCr1x1MDAwNNL5ee0mMIJcdFo4Jv/1l/jsksVkss+OjlelhHg+QNlDOCumseekI6OMWeIxg8lm9tNcdTAwMTRyeOLh2p6likgmli0lk1x1MDAwMF7LXHUwMDAwcLib8HhcdTAwMTRzueyA18dcIlx1MDAxYzszXHUwMDExrqcrXHUwMDA3naSeplwi/fm61YT0qFx1MDAxMET4XHLxZGa3WuLHXHUwMDBi4ZM86XLx6JhwuVxiXHUwMDFkyM2plqllhYDTXCI1oVx1MDAxYZtvSlx1MDAxNmjG8Z1xXHUwMDFi/OCUbjWMvjLBpMKYXHUwMDExMTO71ZIl80rQraZcdTAwMDFcZtYwoTnaM46OXHUwMDFjwlx1MDAwM3PHKKGsIdhMUqTZL945XHUwMDBl2+WYsSNcdTAwMTjVmLUzvme4KFx1MDAxN1x1MDAxNlBcdTAwMDP/4M5cdTAwMTP2PCcnW/QsXHUwMDA1XU3h71xmv5agnbrPrfvas3yUR1x1MDAxN53D62OlOut7XHUwMDBlL1RsToVAlyScXHUwMDFmd39YuIlcdTAwMWXTmGlcdTAwMTdcdTAwMTcqXHUwMDA2XGLC4Vx1MDAxZPnqXHUwMDFjXG7KXHUwMDFhz1x1MDAxMKVcdTAwMDRRvut7mVsxQU89ZdBT2mpquCQuPUVJNKdq5J2iWCvH+Fx1MDAxYztcYlx1MDAwZfY/9tlcdTAwMDHdqDZ69avNYnW/vX5K0lx1MDAxYX9qfWetsS8363q7evSmjlYrhYPi+LcsRlx1MDAxMaR7l2mMP1BXoFx1MDAxY6lSlFx1MDAxOatlXHUwMDE4i5IrjFx1MDAwMIO2IMqZx0glXHUwMDAwXHLzNqRcdTAwMTJWgPZbpm1kgd1zXHUwMDE22GklLXNXNCN3jIVcdTAwMWRBfYRcXP9cdTAwMTdYgDllbviPtFTBKv/eXCKYglx1MDAxMzRcXGxcdTAwMDKHe1x1MDAxZvnYhLXbx5vtrep+dWPI1i7oS/O2XFxbz6R1XHUwMDE1XGJtaoQzk1FaQLo0XHUwMDAwZFx1MDAxNq91gS9yLKhlYqzv+lx1MDAwZtKN8L5f/fxZat1E+DczwF+BeCaGOJtcdTAwMGJSXHUwMDFhkLGRXHUwMDE2osKgbJ5nTqOs3fItWl1cdTAwMTdXu4+XXHUwMDFiV5dXldfzrbRa97DxsVs/erzevGj3d043mD5ZPXtKp3WTtXnR2lVxev642322XHUwMDA3O6JHO7t5pEJfPVx1MDAxZu2X1m9cdTAwMWaGXHUwMDFkUahcdTAwMTT35MExpyldz/NlXHTup5KGJYCS15xJXG7iXHUwMDFlLDtcdTAwMTJoXHUwMDEy/dmplFx1MDAxOWBcdFx1MDAwMqtcdTAwMTRjZFx1MDAwN2NcdTAwMWXni1x1MDAxNjv+Y1hCK4OYkFozIOjOUVxylIpYco5WK1CM6SpcdTAwMWLmQ1x1MDAxMsoo1qtcdTAwMGLBXHUwMDBmJujiWH5cdTAwMTDZQj7UYOPu7GHjo655QVx1MDAxZL5LVryi+qCcmlx1MDAxYWDzfY1TheBcYrDo6CHBPWxRxaWMMVx1MDAwMZajh2aHeDs9xFx1MDAxOVx1MDAwMzHrMLP9lcR3XHUwMDE5plx1MDAwNFx1MDAwYshcdTAwMTmnc+xcdTAwMWFYXHUwMDE2J6+NPblz0bw11ePBUat5uMbmYH3/XHUwMDE2frG5vbX3tLM/OD9fXHUwMDFkXlx1MDAxY91cdTAwMWM/XHUwMDE1XG67ezlcXPfooPZ8ctjudDbX7rffTyq11+t+PVx1MDAxN37BXHUwMDE1s1xcWZZcdTAwMTO/cD/tXHUwMDE0/EJcdTAwMTHpXHUwMDE5XHUwMDBl+lx1MDAwN+SPRVM1JIFcZveMYFqgZlx1MDAwYlx1MDAwZTlcdTAwMTilpmGSozRWcoPVO8tGTJlkTydcdTAwMDO9MCDMQZDEOCFsbKKKP9Mu6G3PqVx1MDAwYoKkUsyWmOar5JXyoN64XHUwMDBmMoLfxy0mKPNov0Tn+vMhXHUwMDE2yTI4KVx1MDAwZS2o9Fx1MDAxNFHKXHUwMDFhd4W05Fg7XCK4wVxu+GBcdTAwMTbMyLPIsPdLXGaz4Mqzflo7U1SZQMrz0tPgxPhL2lxmtfhcdTAwMTZMVGmmlHampJr4IXCKYT3flN7H6WhH7UVV2PXg/aW/9rRZWj9cdTAwMWVcdTAwMGXfVzNWlCzG6Fx1MDAwNFx1MDAxN1x1MDAwNvCnXHUwMDEwOf6jK0U2l1ucOdkpujJW3GAt2JqSXHUwMDEyqlx1MDAxNbGUXHUwMDA1guTfYebIOUtcdTAwMTVcdE5cdTAwMTaOY2vgXHUwMDA0TVx1MDAxNlxuVJlgYMJEQ8HUk5FVzK9nlPuUpqBLQlx1MDAxOTCrXGb69IxcdTAwMTCWh7wxwjKct6GJUERcdTAwMDBcdTAwMWaKSFahPOHPYYOjbrSxrpjN/JtN/2FcdTAwMDK1m540aVx1MDAwMmY1iEBX80pcdTAwMTYrN6mxyp9cdTAwMWTyK1x1MDAxMvdm88j0YFx1MDAxNZX+P63y+MP6fZRpXHUwMDAyTYnMbHCvP1x1MDAxZsqUbF4mUSaplMdGflx1MDAxOFx1MDAxNjKDpMBcYlxyt/BcbsGkzihjgjfo7zR+d09cdTAwMTlFPWpcdNFcdTAwMWH+XHUwMDAzxEzQZWZ/Msx7s/MmkMVcbrOtXFy0ScdcdTAwMTbiXHUwMDAyWFxy09O2rp2ON1x1MDAxZDSKx7Xb3fPC7eb1+8nTQVx1MDAwMbRt/0/kTYVYKOBPXHUwMDE0XHUwMDA082BPqZlLgXhUXHUwMDFiI5VcdTAwMDFcdTAwMWJcdOs6XHUwMDAzLcd+qFx1MDAwYndQl1RcdTAwMDQq2XM9RqAox+JYXHUwMDA1VrvEXHUwMDAxV9yVSydcYv+MXGL/TiblPrepmJT2XGYjmJjE4G6rXHUwMDEwk5JSg6xXaKZcbqNcdTAwMWNMSlx1MDAxMmBaXG6MXCI8/pa5QuJLXCI1QcL201x1MDAxMykwg5RkTt9TPI+SnCGMXHUwMDE2kEZ9zoxaIFx1MDAxYTWBujhKXCJd68+HRq1cdTAwMWW9r1Xl6fXZ+lx1MDAxZaB0+L5uXHUwMDBmSjx9SEsrXHUwMDBme1x1MDAwZTFtpLXhkJbUXHUwMDFhzGpcdTAwMDXS38ZcdTAwMTREYpFcdTAwMDSjwk8wjWJcdTAwMWH9zVxm/c2RYvGlb3llXHUwMDFj3YP06MboM87Xsc7IXHUwMDE2tVx0VEn7M1x1MDAxONhcdTAwMWOp0snFdvFjtzhcdTAwMTSvT1x1MDAxZi12frv9RIZHvztcdTAwMTPlVzX72+ypXHUwMDEz8np8IXeKO7un1cstcqZzmqStOPIwldMkbfdTSUFcdTAwMDRcdTAwMTQl/iRsqimRkptQXHUwMDExpKLAXHUwMDEzOFx1MDAwN8gzlPqOXCJI6Vx1MDAxMVx1MDAwM3xSYj2u26OyXGZAxVxuiddcZsFvwlxmXHUwMDA2v51FUuhcdTAwMTCNXHUwMDEzXHUwMDEy2LLDWJ17XHUwMDFibsXggc9cdTAwMTaAwtdcdTAwMTcpXHUwMDAwNUH1Rlx1MDAwMlDu9edcdTAwMTSASlx1MDAxNJXJXHUwMDAxKONcdIaDlf2uXGLh4dpcdTAwMWNcdTAwMTBNXHUwMDAw78AptbSuxHbuXHUwMDAxcfiu8lxutt5cdTAwMWZxe89QTiynmFx1MDAxOcODV1lSfVx1MDAxN87f3DjPNGJbg6gmVLuzX0j8JCBswm9cdTAwMTmdI0Xo3Z+0brc3NrXobl9stY9fXHUwMDFl3+GWpVaNi+NNiYdcdTAwMDL+XHUwMDE0XCIoXHUwMDE4XS+yxdy8KVx1MDAxOdxcdTAwMThEWCZcdTAwMThY8JJwZlx1MDAwNdGBd307U5iSX4Vi8CN15Pilcq0ky82VlfE6TGUlcFZcdTAwMWPaYpjmNFx1MDAxYSFcdTAwMTPYj0nqr4R6XHUwMDAy5Fwiioq5uVjchzlcdTAwMDWzXHUwMDEy2ENSXHUwMDAyq5doisnArK6v1GHpXHTCXHUwMDE0XHUwMDBlWmMqmFlcdTAwMWNcdTAwMThwgm1cdTAwMGaY1Vx1MDAxYZ6eXHIo/mXbqa+VTVx1MDAxNLzDTFx1MDAwNFx1MDAwYngwc5ZcdTAwMTmw+OxhwpDFL+Cctc9Yz4pPUlx1MDAxNoFcXE0gNO5QVXj5+XCrZHMxiVtpXHUwMDAx9o4yijKGJbxhbqW0xzF2KaWWNJCROlx1MDAxYZxoPb+lXHUwMDE4yDauXHUwMDE5NY5IlcB+dGBTwVx1MDAxYpTgwcT1JcRdXHUwMDEwf09cdTAwMGZxUNF4Z6O2XHUwMDEyLkUkRKR9N4s0cywk0ofXj297pXelzsRdYa9/yZh4zN5jaVx1MDAwMZpYxFx1MDAxZflcdTAwMWM+XoiAZVx1MDAxZVx1MDAxNCw13SGeNvBcZog0XHUwMDEyo5rGkVxylKrTKfVcYlxcRGvCkFx1MDAxOFx1MDAxOWHM2D30jFx1MDAxNZRiXHUwMDFjRlx1MDAxYTDrVLSh7a9q5lrARrJcdTAwMWNLb4BcdTAwMDZcdTAwMWJgmERGdzg261x1MDAwMZ5lqi3jrVNcdTAwMTKjgcBcdTAwMTdxVN5Y01x0uCGSjtrIarFcdTAwMDCdXX9Z01x0tyhIwUCNwlxcMIpcdTAwMDRcdTAwMDXUXHUwMDExIaFEVEWFXHUwMDA3v5aCXHUwMDEzt7bCwtiFm/L7h2mnj/TaiVxuPMmYM+VioLHlLZoojvXxOVx1MDAxM9DZXHUwMDFin1Z8Wbs4XHUwMDA0dFx1MDAwMutcdTAwMGJcdTAwMTNQ9/LzIaDJlbNJXHUwMDA0lDNcdTAwMTC5ilx1MDAxOa2pXHUwMDA1uUjDOZDKSJx9amlcXDW7JFx1MDAxZahcdTAwMTDKXHUwMDE5ldjMh4/M0lx1MDAxMahcdTAwMDV+XHUwMDA39f1cdTAwMWGMXHUwMDFiZuY+lO/PXHUwMDAyuV3LkFx1MDAxMqko5Sqml4WJn6bJXHUwMDE4zoqdazL5Pjk9lefv21x1MDAxZqWnzcOL5tvwovdQyO7G+/3jXHTG3i2Fp7myRP+EsSd9vFx1MDAxMFx1MDAwYplfcb0o+CZej1x1MDAxOc9cdTAwMWEmceKmYHg9lvaCv44jp3ZT+l38hZLUUKqwY4NcZpjVKyOXoMLeZnC7hCVUqGD3tlx1MDAwNFx1MDAwZSlcdFx1MDAwNymIQ1xcgMPwMe8tUkgrXHRVXHUwMDFh3lwipNKRu5KRNL9sssPK7i19eSrbfdtrKn17vurcMShcdTAwMDeGjYUs/Fx1MDAwZj2A2jH9Oc3+/N5swO+IMNQy0FxiXHUwMDA0zt+4ZYCjpTG9XHUwMDBiwKixU9Hvp8k5XGZAXHUwMDE4w3NcIlx1MDAwMtNcZkBIhuOky4/E4d/JXHUwMDFi4yk2NttSZt5pgqSY56hcdTAwMDe33klh7zCKjXpwXHUwMDE4/Fd5yEj1+NxcYiDnoYBkQKLAao1QI/Q1UFx1MDAxOahcdTAwMGZZ2juZqdB6eirEuDWaWWd+OI81d3BkPDZcdTAwMDPKe9DD7Fx1MDAwZfdmtdlpt1x1MDAxYivVV//rXHUwMDE2wOKZYGaELZ64XHLkY/M8KLZXb2zfXHUwMDE0xH67VHrvPlx1MDAxNldbNpXNU1x1MDAxMNpi4jpgWlxuiikwoSwlo5RcdTAwMDfmKsNcXG2weUa4/enszC2wXGYrqGLaXHUwMDFheNYuq8dcdTAwMDOSYKxcdTAwMTDcwonQNJD1vIS6XHUwMDBi6lx1MDAxYrMnNVhO0aPpLFx1MDAxMFx1MDAwM1JcdTAwMTMrXHUwMDAzXGJRVFk1XeNnt56eZFxyPTytkYuPzunu02nr7eVW3eyy4nGGfD9cdTAwMDNbXHUwMDE1PJBcdTAwMWb3u6yheCz4r0ZQXHUwMDEwoYy/wFpcdTAwMTCtw/Nm92b/XveGdv36pb+zXHUwMDFl8JytXHUwMDA0PeqYq0gwvEY4pjZETVx1MDAwNe5cdTAwMDHV/8zZSCby009enymhwH2QUtCbXHUwMDAyXHUwMDE4XHUwMDBiXHUwMDFlZ9pcYssk+lxuwrmaXG7oIahHXHUwMDAy3IW7p91cYlx1MDAwZjuRWfpVIbTkN5mF3mZ6fmONXHUwMDAxmFx07WpIXHUwMDE2n1BArSVA0YPjQPPJ12RYsDRVTtU3wVx1MDAxOfSq3ZXqsFpcdTAwMTnAhVf6w0WgOFx1MDAxM1hFmOLEbyFcdTAwMWaSY9vPp2+n7HA4uDvaur3cOKm/bFbTkVx1MDAxY1BcdTAwMTHYZlx1MDAxNHVcdTAwMWLhQHNCRbCWKo9p4idcdTAwMDRcdTAwMDFfXHUwMDFknZ8gx7HCWm2MXHUwMDAxI8ZKh/lcdTAwMDLiXHUwMDEzpCeYR9QwkO6SL/uUJsN9a2aOg0FKJaXgLjHAZXxtXHUwMDA3qDfLcVbm/DjOXHUwMDBiOd5cdTAwMTNFQldPau3u7qGp1Ml750/kOIV4MPifj8AgT5JcdTAwMTNmXHUwMDBludJcdTAwMWatmcRG8oJcbi1cdTAwMDNJ/SuB1lx1MDAxZUxKzS2hSuC5U9GTOTeu4z5QabiOsdL7KWIjXFyGsic1IVx1MDAxM7hcdTAwMGXF1F2QmvxrYMmS62RcdTAwMTZ+21x1MDAxOWLXUihOjXW3+ohv0Yx9alxmyTV5Mj+q81nmuSBEZ1x1MDAwMrNwXHUwMDEyXHUwMDFkx1x1MDAwNvKhOclcdTAwMDJrUo1qQXFMXG7iSnJcdTAwMTBTRIXyUj5cclSC4SzmxrZ0oFmrZSe0ZDjvpOUysbM6XHUwMDE5kZgq5M6f1LFzXHUwMDE4ONUgr9VcdTAwMWOHn5jahpLF8vr1zvmA2+7Nx/BtmHpSZ/2jdHfX29mqnFx1MDAwZWp3p8X+wWq7m3LyZeJ1k02V6a+bLFx1MDAxOVx1MDAxMq87idCBKGWcZspcIk2QXHUwMDFh7qeShlxySGM8RanCyUdcdTAwMDQrXHUwMDBmxyWGXCJcIlliKL8xmNVcZsRccohcdTAwMWPlmvXLl/IjWX7spqdcdTAwMDNcXIBcdTAwMGXGyLpLUND4VDacXHUwMDAxyPIuVGXwjdMlQn+zgYteTH1qwIU2XHUwMDBmXHUwMDBlMEHthjnA+LJzmsWyWlx1MDAxNne3zZ1er0yvLt4uaFE/rKbW/IZgw3XK1Ocsllx1MDAxMIyNVVx1MDAxZWb70ljFr1x1MDAxY+Xlcknjk3G7l1x1MDAxZbdovoM0dfdY5/GTljSO8J7nKO6z5/JcdTAwMDPhO4X+xmrj/WntomaGXHUwMDFivbRcbrPcXHUwMDFm3Dduerec765cdTAwMWWsXXZON+/aV/koTMO0XHUwMDE1wZD2TFxu073LXHUwMDE0XG7TMMBcdTAwMTlFy9nCmqxcdTAwMGVZz5bQXHRAY8qDo2CA7IEgx0rMKO6W/Z0m4G4/g77Ep6CCXHUwMDEzXG6CsIuNXHUwMDE1KIFFJXK6sYJcdGdYXHUwMDFhxVx1MDAwM32Np9CY8NxA0620XHUwMDA3/ZV667VcdTAwMDT3/p9Wq91fkFx1MDAxOSbJOiysRqN7WVx0bSVcdTAwMWbVWjpcdTAwMWbcmqP7J73KhX69Kcjn26u79KqVXHUwMDEyzy+RssIxy1x1MDAwNISSX1BjMS3KOctkqVqngPhBlupcdTAwMGUlUFx1MDAxY7vKi4WMzftcdTAwMTZYMGW4nqPxvHU8OCtcbvWx+sL22D15XudXO/VMOjCz/EhAhXs1aXQgx9HEoPtgOYLIQH3TJ1wiMJkyXHUwMDExXHUwMDExTHpcdTAwMDazXHUwMDAwudJKS22WOjA7QFx1MDAwZdNcdTAwMDNEUCz0I85weWBcXHd0XGK8YYIwm2+bw1xclOBXnHmlP/ynXHUwMDA1Kqg3aNZbtbDu+H1qcIK+XHSrwdFuVmI3k49cInzcXb99eC2dlfUr2yfnsvl42krvXTaU4aBzq6lcIpFcdTAwMGWIYNtcdTAwMTCgvpRbq+JsTIGDgeXPNE9Ha9OlXpxcdTAwMDD7o/SwZ1JcdTAwMTIzltBcdTAwMTCAvbbh346qXHUwMDFlmWLETpdcdTAwMDY8nV48aF1ebFfPXHUwMDA2V1x1MDAwN+U1tcPq5nG3maGtXHUwMDExiFx1MDAxNCVh2SaIien1ons1afSi5lx1MDAxZVx1MDAwN8NPgrCVQlx1MDAxMFx1MDAxM1x1MDAwMlxilclcdTAwMDBcdTAwMTHUw8ZhmojvXCKEKECWodVcdFx1MDAwMDnOQFx1MDAxYzFJXHUwMDFlQOIsXHUwMDBiVibeJ0ONXHUwMDExU1x1MDAwMiTPM1x1MDAxY1GLflRyXHUwMDExXHUwMDE04Fx1MDAwND3j7PqbeyS1s/PRIZfH/cvd7YvHzqmtdlonmYw+zGQh8Fx1MDAxZoeuk8RTiihcbvbD0p+aXHUwMDFidk8yYFcyIZVcZiZzXHUwMDA2rL6ozvtx7FhJuFx1MDAwMVx1MDAwMjM/7dbcYaXLgVwi3UtSPy2Wr4q28faS1qN617dlza8+nj42XHUwMDFmXHUwMDFme7cvtmj1Qk6Cdu8yjdZUXHUwMDE2XHUwMDFiP2lBLVx1MDAwZlx1MDAxNlx1MDAxZn1cdTAwMDJNyWSgYSs4wzlcdTAwMTdcXGIzXHUwMDE4u1x1MDAxMFOg/zDYnWaAnWWUoP5zOVuCbZzCRfZEalxuZy33Zlx1MDAxYTl0c1x1MDAxYpSb9f5cImjNXHRcdTAwMWEr0scttPB81GZysfUktcms8qQhXHUwMDE4XHUwMDFlwTBkZESjXHUwMDA1I1x1MDAxMSeqXG5DYiY/L1x1MDAxNedcdTAwMTRcYr7I4i1lQnClnFxittFcdTAwMWWNPzMvsLDX8HkmTW/SfpuUdu8qb+/tq4PiycFDU+7MIVx1MDAxNJl43eTKz8Trzlchu+9eXG6FXGbY9dBcbiWUUeBXQTP0q15CTsAwpogrpdBcdTAwMTPsW7JLpZxcdTAwMWTSl+khja1+XHUwMDE15c5cdTAwMDCIVbG9XHUwMDE3rSVKUEZyde/mopJLlVxufF9/ha78v8Vx6k5QjGHtnLCHfFx1MDAxNPXZ9muhODh4eVZnXHUwMDFiXHUwMDA3Q1HunFx1MDAxNJo0k6KGf6mk2NbBoajBwoV/lYRcdTAwMTeXYc3cUH2VXHUwMDFl1UxJXHUwMDEwnjGotrHOKVx1MDAxMNmg3eU83bdb189cdTAwMDel9WGp/L6nL2yz09zv8bNFSFx1MDAxOeIgjqTNK1xc6txlKn2qMWzCiFGaXGJtTVifgo2bXGY1ITwsWzUgXuNybJf6dFx1MDAwMvKus/iWJOGWulx1MDAwYlx1MDAwYikxsVx1MDAxMVOhtVx1MDAwNtKUt2c4+zGO1ahskTTqXHUwMDA0XHJcdTAwMTanUVx1MDAxZHvIR6Ou9Xb6rXq7rkivsflinkmr1nUgPFajXCLMOeCYY2ldWKMqQpjfQ0nGT4hbatTsuL5Jj2tlLHyjcFx1MDAwZoMjNDZcIkqJVJzMNVNo93qobzd7pz1e/7hUw429yvYh/22qz72aNKpPc09KTVxm9jjWNqi5vkKielx1MDAwMia4XHUwMDA3XHUwMDE0XHUwMDE1PshcZmGaXHUwMDBiZ1PVJUSSIXKbwZTUfmayc1RcdTAwMDelTMdBXHUwMDA0aFx0k2pcdTAwMTE1n+d5i6DrJuiWsK5cdTAwMWJbdT7arVFcdTAwMWXI1uC4Vlh7oCcv18dmq3Kosmk3opVcdTAwMTaaO1x1MDAxY7uKSO7B81eaxzt2PY5eXG5DXfx1qecmgLiUXHUwMDAxxIQzXHUwMDEyk++H0ex4+ipwXmpgXHUwMDE25S/Xc1x1MDAxZmc3/dezvVe7dta7uTgpq+7N5f1v03Pu1aQy8YynNONWXHUwMDE4g5HMQPORrzCmmYBcdTAwMGXBPcYpZsVSjVxyQpdqLjNCyulcdTAwMTFCgdJcdTAwMTmKSstcdTAwMDVcdTAwMTFcdTAwMTbfI5NcdTAwMGIpKDDIfFtI5WnhXHUwMDFkLpKFN0HnxFl4jj3ko1x1MDAwMzfs0cv63klzs7Tf6Xb37vpcdTAwMDfre6lcdTAwMDZ+SEk8XCJcdTAwMTkgXFxKbHRcdTAwMWJqIcVcdTAwMTn1/J5CXHUwMDE2XHUwMDEwXHUwMDBlUjQ6clx1MDAwZVx1MDAwNLJcdTAwMDdcdTAwMTT3p0FcdTAwMWN1zJybf5104jhfXHUwMDE2+v3vxHYlXHUwMDBitlx1MDAwNSgxRrizYUpS+1slpZ+7mSe2/XiIXHUwMDEygVx1MDAxMNhcdTAwMTTYXvXH4Xa67ftBZU710v12J1x1MDAwZdVjK1x1MDAwZkM4bqn54Ld3X9l4e1x1MDAxZr5+sIvtI37XfHt/rT+nwS/D+XFcdTAwMDJcdTAwMDde+qI2XFzExbnyXGbjWklcdTAwMTDwNpD5+aOfifaAOVkhQZVjXHUwMDBiTIeCXsI3XHUwMDE2vvdcdTAwMTngK1x1MDAxOZb5ups7XHUwMDA2Z/ZFOlx1MDAxY/iet+nyXHUwMDEz8lxmyUfQ+9X8eaFhXHUwMDFiWWNO9Sbn99X+2c3Ny8bOVks/vfWbj1x1MDAwNzpccl6FtZ7ERFx1MDAxNUNcdTAwMDTBZIRcdTAwMTBgteFcdTAwMWVcdTAwMTFcdTAwMTI9ekC3XHUwMDAz87d+9K1iaK9cdTAwMWFcdTAwMWMhapR2Tdha4jVcdTAwMTav1VxmeMXOXCKU8Gi6PO7JxqpbpUBRg0DOu7hMKNDgU03G+cbrYbX/1u4+r5xfRyfQLyB0k5abXHUwMDBmioetnfvto423XHUwMDEzXnm7erXbd+9n8jy154hSQz2plWCfrqNcdTAwMTCSXHUwMDA1155cdTAwMDWyZkA146TFqOrVzIvpM1xiQkJcdTAwMGIw4rj4XHUwMDFjIz66dC6A/o9cdTAwMDf/Z1x1MDAxYTAvkl38mFx1MDAxZcxcdTAwMDBkwi3c7Jj2Qlx0WVx1MDAwN0IpXHUwMDFjZThccpq/XsjqOtI35WG1UD06kf3revG19rCvu2mzXHUwMDBlNre39p529lx1MDAwN+fnq8OLo5vjp0Jhd2/8W6bKXHUwMDBlPDqoPZ9cdTAwMWO2O53Ntfvt95NK7fW6X0933a8/xbdbwrpJI/NydTnvXlxuzUxcdTAwMTlcdTAwMTVcdTAwMWUx1irs9MqYXG7BWWtPY1x1MDAxNVx1MDAxYmNcdTAwMTJcdTAwMDd4O5m0VMpPUbFcdTAwMDJUgCPoOX9X11x1MDAxZlx1MDAwM+Z6XHUwMDA2zYzRTtDAMb3CopUzP2BcdTAwMTbWXHUwMDAwlrXMOVnfwOlVs1x1MDAxNX6X0Vx1MDAxOfnZfbO7XHUwMDEwXHUwMDFlrlx0ujGsrGPWn4+eTlx1MDAxNmrJw9e1h2PXQZVSy4hcZoNaXHUwMDE59H5RTZGPO3S0YMZcdTAwMDOR4MNcdTAwMTmLu12ZXGbYXHUwMDFhmvtcdTAwMWTKMMmJXHUwMDA0e14t/dkuqD+5oZ6lQzrcb2Rezlx1MDAxNEJcdTAwMWXbXHUwMDE5xVx1MDAwMlx1MDAxYuNApOdcdTAwMThcdTAwMDfaPWxcdTAwMGXPqttXRTE8Lt9sV47qp7dP2ZQj/D+Zyk7PtT96PFx1MDAxMvyPRzAwul5ki7nNgNm4O3vY+KhrXlCH75JcdTAwMTWvqD5cYpYsjFxyXHUwMDFkp0ZRgLi1XHUwMDEyXGZnXHUwMDFkmFxy/jMv0khiYO1WYcd0XHUwMDFlPXypXHUwMDFhsyfLzcCaiMeBTkhQSZpcIjtVVEXWJD0hgLpcdTAwMWFcdTAwMGXLwZHlJrKm+TVmd5/kXHUwMDE0xMpcdTAwMTLh4aBOhekuWpHQiD0hjSfAXHUwMDE2XHUwMDEyWCupXcSKSo/iXGZcdTAwMWGgYJ9pootArP4wmfucnl5JrjRYrM5cdTAwMDLkeNlcblx1MDAxZlCCS0Hz9XsoTjC8PFx1MDAxM7fqwSoqOLylPP60flx1MDAxZrmaQGhcIlx1MDAwNZExXHUwMDFiyIddJZt2SexcblST8pjRjFq/bsqGxkuB3vJAgXGOXHUwMDAzarSJOjNBXHUwMDFl/4ze8lvnRJFNPSGlhmvj/CpBXHUwMDAyKn9cdHRcdTAwMTfQXHUwMDFiM5Mrylxmtlx1MDAwMHU0kfSXXHUwMDE431uSXHUwMDFhbiiYx3SOJVx1MDAxYXf7XHUwMDFksdqR7faT2a7cX5dcdTAwMGaOb2+LmflVpu6rv4ZfXHUwMDE1YrGAP1x1MDAxMVx1MDAxNMyDXqWmMlx1MDAwNVx1MDAxY5JrXHUwMDA19lx1MDAxNFx1MDAxMUJYJKxRfiU9PFdcdTAwMDSTXuFw8Z/B0lx1MDAxOflVas5cdTAwMDf8SmDuraCYNMapiNIr7llQc9hcco5cIkGBw/BcdTAwMWL5lfskp+FX2NFFW6mtUMyokIkrXHTcXGKwl7Sxylx1MDAwNtvwjPxWQN41Wsdawy1wRpSW9GqC1G1mcEVjoo00zlx1MDAxOcbxUWAlXGbT0tKcR/xxwpRgM7Grr4kxXHUwMDBiw60m0Fx1MDAxOWeLpsjy82FWu7RQPL9vbFx1MDAxZeyU7sXJeW/4SEw7S3yJYO4kI58zV0PxJVBcdTAwMDcodS3HXHUwMDA2hU6H9DK+9PnbaUHdTlx1MDAwZmpq0XlcdTAwMDBmrDO5g0XB/o1rIzEzh003unM6ynR02/l42i3IQuNwWGW95+bH3odKXHUwMDFiXHUwMDA3MqdcdTAwMTXePn4rs93+XHUwMDAzU7W3u4vt/Z3xb5kqvrTZUyfk9fhC7lx1MDAxNHd2T6uXW+RMr6a7blxuipdjfMl991KoaThcdTAwMDV+Q22BKexcdTAwMDJLh8bxrKj0OFx1MDAwMf6kQVx1MDAwNVx1MDAxM4cv2nJP+LM8NXbstmwh3CB/XGaYO+nBbLRcdTAwMDJcdTAwMDOIOFx1MDAwN9NRZmM7yVhBgNra6UaMJ0aXXHUwMDA0XHUwMDFjjll0dPk7efE1Judjzjp6gmKMXHUwMDA0l5zLz0dHJ1x1MDAwYrTk2Fx1MDAxMpg9nGmmOVx1MDAxOJDChom3VVx1MDAxZXZvN5ZcdTAwMGLCRbQ0QnD53TbYoaKJR6wwXHUwMDA056pcdTAwMWKFXHItlsPqJiD8xY3wLJ5cdTAwMGaD9ivRzr6p8IhjnZ9cdTAwMTJccidGpmPn02nxRvtU3DQvjs+23ktv6mnz8XTQXHUwMDFi/omOXHUwMDBmXHUwMDE3XGb8XHUwMDBmRlx1MDAwMTC6VGR3c1x1MDAxOLmbLLRWgp5cdTAwMDdcdTAwMTTaQuK0XYrFozzwpu9gk5LcYo9cdTAwMWFCudNcdTAwMTkyP8eD+ySlYDSWcHhy2EuWc39wUEhcdTAwMDBcdTAwMWFcdTAwMTCAUimLXkGtXZ5cdTAwMDcgRGC5XHUwMDEw6VdAL7tCTyH0ulx1MDAxOWxcdTAwMTRcdTAwMDI2n5FUOz1cdTAwMGbxgVx1MDAxZMaYwqFQuVx1MDAwN3bUtPInXHUwMDFh2ME3Llx1MDAwMrGZwCbiXHUwMDAzO+NcdTAwMWLIKW0m0aZKXHUwMDBl7CjrXHRCtFx1MDAwNuZieDhkqzj3cNRcdTAwMDX6XHUwMDFktFx1MDAxMa64jvFwnrI0XHUwMDE0xJuydHS6XHUwMDAyXHUwMDA0R2BcdTAwMDFcdTAwMTEzXHUwMDAwfVx1MDAwNWaRXU7TnID13uxcdTAwMDSHgCFcZprJ3Skk2OUgLFx1MDAwNLCXmiHBNpq/PnXm9npDrO6VnteKa5tcdTAwMDdd+UhcdTAwMWVPXHUwMDBm/kSGU4iHg/+yXHUwMDAzXHR5Mp2Z+VxmLJBij1TJXHSIXHUwMDAyyShcdMSmvlxiXHIlXHUwMDFlyFx0XHUwMDAxnFx1MDAwNvZcdTAwMDGszZGqkopkrVx1MDAxZb2vVeXp9dn6nlB6+L5uXHUwMDBmStxNsiiFxehPiiVcdTAwMDOFiKM1/UZS5T68qUiV8KglOGdcbvQuXHL3VFOMe36GsZExXt9lOGd2SdvPwKo48HxcdTAwMTZjM1x1MDAwNpyOkVi5JiBcdTAwMDfIdIVcdTAwMDVJrFxuLixmYlWLXHUwMDE20JlAY+ZcdTAwMTnQab9cdTAwMWWUr3dVcevuuFk7a1fW75ut3Siq41x1MDAwMjqMKE/h2Fs4MCDpw1x1MDAwM1x1MDAxNznFXHUwMDEwOdiLXHUwMDE0az6jyP5t8Zz/Q27gQVx1MDAwNmRjS8vxjLZcdTAwMDBZUjK+4Fx1MDAxZdVcdTAwMWZcdTAwMDd1mHMzXHKmXHUwMDE5KLssJYBcdEe5efI8eN46v14/ON3ePn6929pcdTAwMWM8pu9cdEhBwXjAWVxmnGNcdTAwMTGNTeKkRG4tZtJyI1x1MDAxN+ko/1x1MDAxZlx0Tb5mMftcdTAwMTWRUlpnX3zgTrGtzyizklx1MDAxOPjgXHUwMDFjvZpX6mP1st6o1Nmbbm731OF6/z71kG72UWhU1NUjf9XsoXp3SU+u1m/Hv2Wq2OR24/TkpHN8crz6cjpcdTAwMTT9ZrlweHuf7rpcdTAwMTH2/ksx7b57KUgnaCbpSVxycDOCYsJUyOC3VnhMXHUwMDBiXHUwMDEwiELqQKeRUZcn6WnDuSDYRohcdTAwMDTbRC1jk5PR/JZcdTAwMWXNflx1MDAxN0N4XHUwMDEyzlxihZKxpW8gqjHxjuTax9B3c2NcdTAwMWLLWehmXHUwMDFk2SHwN5xcdTAwMTK6QPVvXHUwMDEz9GOYdSbuXCJcdTAwMWbymdxQPMmdJ/RnXHUwMDA1hmCWcNDW4/NcdTAwMWKlZdTTliuwp0HzXHUwMDA2Tc5vfHOgpqCNucbotjHUXHUwMDAxb4Nmqd9kk1x1MDAxMMqXWdrJiH+f3ZWnsFx1MDAxYr5cci4m6MrT8f58eEhcdTAwMDbs1TnOu3nev3lcdTAwMWWer69f1zpPdVBLW1x1MDAxN7dbtczzY/RUzS9y9eTF4lx1MDAwMH9cbiFcYowuXHUwMDE22V9uPrzk2Vx1MDAxZCvjPjzfMVx1MDAwN1Ymx1xuuEAvkZVcdTAwMWaHXHUwMDE59aTvNrXUgFx1MDAxOFx1MDAwMFx1MDAxNlx1MDAxZTl9qZx4tdWyuLtt7vR6ZXp18XZBi/rBuSriYatCarTvu6CBovuVQI42XHUwMDEzWLMpXHUwMDE5zoyQXHUwMDBlQMzNqec+xyn4lVA4zVx1MDAwZr3v1lx1MDAwMLtcbjSPQYFcIqRUXHUwMDFlXHUwMDE38Fwi+vS4y6nHjCd48jTA5WD5XHQy9yM9y6JcdTAwMDa4rsGqJYdwVbF9Zlxyt0Jxa3ImWTNIv/FI6UqlXHUwMDA00MWL/9P6akm5XGJUa1x1MDAwMrFxXHUwMDA3TVx1MDAxM/eSXHUwMDBm4Uq2J5NcYpfVylx1MDAwM7BcdTAwMGKce2yjdXFcdTAwMTbEXHUwMDAxXHUwMDE2tFJiXHUwMDE54N1BuNBTmJBcdTAwMTZBPcNB18A/wlx1MDAwMumXbPRcdTAwMDVL7DuwXyQz8y1cdTAwMGVcdTAwMTjEXHUwMDA28i62peNnkVGjQIyAVTxHJ8rBzdt1o7axcXve2ehdrN/sbdTeWDanhNbB+Wq/i25RMEwsgEBcdTAwMTlJwNTkcoxvXHUwMDAxeim8aoVmglNMXHUwMDE1m3Q9J6o+L1x1MDAxNcZTnuQte6pZsvE5RqCUIFx1MDAxNmd4oNPFKlx1MDAxYU01o8QjiktrQCgpzqXUv5FBuY9mXG5cdTAwMDZFXHRnSL0plVx1MDAxODqjJFwiUuGocOzDXHUwMDA2P0ZFXHUwMDFinVrpMSsxXHUwMDFkV+JhcrUpXoZFJ0hRmp5BaVwipFHKTaDiXHUwMDFiNMFcdTAwMDNcdTAwMDKSr/KdpoxcdTAwMDJNjLWLmoJBVYfVylx1MDAwMFO1+sNF4ExcdTAwMTO4SZgzOVefXHUwMDBmS0r2jidnmVx1MDAxOeExTKBX1Fx1MDAxMlx1MDAxM8Tk53BcdTAwMDGm4KtcdTAwMTXwaVx0XHUwMDE2XCKlLppkPMqxr1xikHVcdTAwMTOXZ2YtxbRcdTAwMTRmQY8oXHUwMDEzaHG+RLlcdTAwMGLlbGauRDFrXHUwMDEw77TbS01ifVNY58jh9TlmmdHd58KLeC5cdTAwMTbKhWr7qNKs18v1LFlmIFtAzFx1MDAwNYo/fluWmbZcdTAwMDAmXHUwMDAy2pFbdCYwOvZxXHUwMDBm+Fx1MDAxMzBYQzSxQHBcdTAwMTibeL14cPkvXHUwMDEzj1x1MDAxMsPAyGFcZuCJTVro6JKzk6aZqVx1MDAxMabVXHRcdTAwMTD6XHUwMDE05FxuXHUwMDBlKNDEXHUwMDA0ON9cdTAwMTc50khcdTAwMWGAXHUwMDExXG5JOKHafDvtYlx1MDAxY17RXHUwMDA1z4U1uc9oKtYkgDVRjk9cdTAwMTFcZlx1MDAxOCnDXHUwMDEyloKEXHUwMDA1XHUwMDFiXHUwMDE1IKeVoNFEXi08IExcdTAwMTbuzuf0lpHMWbKmr5VNlKc8PWui2MJcdTAwMDH9XHUwMDA2rli9im9Si9mZIFdzXHUwMDFlUsa0XHUwMDE0era2ll/JWIvBmSYwlZhEsvxcdTAwMTnTXd+WNb/6ePrYfHzs3b7YotXlVIzJWO6hXHUwMDFimaDrKNxxWlFcIj1g3IqCpOeuoYPaMLB/SdyoXW1cYvr/rdYoXHUwMDEwzDKIN1x1MDAwMdhcIktcdTAwMTJcdTAwMGVcYlgrtHD2XHUwMDA3MPE1xThVx1xiMV03+elcdTAwMTjR2/mFvji1+9fH18PT5vVqsUjPK9midYxcdTAwMWHGg9j4LYxo7N3YRVBbJVx1MDAwMFx1MDAwM5qCiOVcdTAwMTO9RVx1MDAwNSdcXPK+VkEp4WlBhLXSXHUwMDAwZbaTiZlcdTAwMTLSw1x1MDAxMKON1EwmXezXUTLR2fnokMvj/uXu9sVj59RWO61cdTAwMTO3t1xuVs1cYqaiMDDosL5z5Fx1MDAwYvjhZEGW9lBcdTAwMWaO1jb2bD3YI5A6abFzplx1MDAxMFKFXHUwMDFjelEymjGI2TTHx1x1MDAxNzvHpaOnyv2mfvnoXGZkeTOOaFx1MDAwMs9cdTAwMDTarbmUsDNqXHUwMDFknTzTbFxurlx1MDAwNJZcdTAwMTPGjonExp9ajTUqxV1rojSIaKPQQ1x1MDAwYiw/YZf58dMxTeev/qxamyBlvuUkXHUwMDFikzD+mkK7XHUwMDBlXHUwMDAxNVx1MDAxMV5joPjcReRq8VibdOmRjPs7eVM8xaamX8ZMO0xcdTAwMTBcdTAwMDFjO4yIXHUwMDAzf1x1MDAxNzu9s061Ui81XCKSXHUwMDE51lx1MDAxMXkxgeO4lUhcbpNcdTAwMDUsXHUwMDBliZOoXHUwMDE4/CtBM46Hylx1MDAxNaPYXG5cdTAwMGZsWIWnP5qJXGJcdTAwMWbH7nJcdTAwMWNtPuOeRrc0WCbwXHUwMDFhmcHNq+FcdTAwMDBcblx1MDAxZS1cdTAwMWPE+1x1MDAxY2+vWIYjXHUwMDEyzHSDxydcdTAwMDXKZyspXHUwMDFllEEhLojBMsFQiFx1MDAwNMaja8/HYPk4q2y3X3dZca9zLtbt25VYf0g1U0NcdTAwMTHrgUaUfuU52KehxEOqNYDZXHUwMDEwzVx1MDAxOXGlXHUwMDE1a5CSlFrsf66VdNosy1lXsTBW6WFcZs9GS2x77oAxj5+rXGZoXHUwMDAz5mNFvrPpsHJcdTAwMDauamdcbtec9dvdqlx1MDAxM8OLMt4qtMKc5sCuXW/fko1cdTAwMTfKhsPbu/tr0tpsfKRBKtiYnlx1MDAwNIBir1x1MDAwZVx1MDAwMKpcblx1MDAxNfRcdTAwMThhPU5cco66XHUwMDAy8lx1MDAxOUWq0Z5k2nJcdTAwMDNv0ZpcdTAwMGJHftpcdTAwMTKpsUjV6ZFKKTZbXHUwMDAx3udyJIBWjYMqqFuGg2BzXHUwMDBlrH6O/abBs5q5N9lcdTAwMWYwiu5cdTAwMTfNn7t837s7rnWvblx1MDAwN1x1MDAxZGWqQ3vFyEk/XHJauaBY5O3HyoxcdTAwMTI81HnHSO2BXGLVXFwzJ1pB7WKfaWVcYjxFdy7EXHUwMDEyrbFoNenRKsDIo4Q53fk6mks20qtcdTAwMWEnXFyR3PXq7NPYwUrsL7ZeXHKtMFx1MDAxZqTuPJVKZ63y+/rNxs7rzunRll5vpi/8LjCmPSzq9qOkmElcdTAwMThWr/C60cZq6rZql6XfOeDWZtKyTCrscORcdTAwMDAuXHL21FxiIVx1MDAxNz6F2YjTlcwmXHUwMDAwl1I4XCJZXGbbhLPcfNpYt3eX4vistHdkuH69Jzp95XeBoe6x2JqEcWCL0bMsiWeN5SzGqFvWfs94jovpz7G06Ei27qxlxWOzljU8PMmlmGPUqa4/upXV6nWxe18u1T+OzjdvOvUseTi5YsS9mlx1MDAxNMxcZkS99ODYK/RqOOFhXHT2TjNcdTAwMWNcdTAwMWWLs8uH8Pv3mMXohvjHgGI1g3C3QIsxiOLMTtMsvnJSgIqmiuSdZ4HOas5mclt2q812v7poJdTJiiZM3Fx1MDAxMvaQUz3P887t8dvjRbOjOru8sbp1qflDXHUwMDE01K5cdTAwMDJqwzxuKZXuMaJM4jeDXU5ccubERUFt0M+pOcHAXHUwMDA1g9PnKunBYVx1MDAwZlxue8ZqgdMtzTL7XCJcdTAwMTnxa1x1MDAxOaJcdTAwMTQ4XHUwMDFjRzHpXHUwMDA0vDGxw1x1MDAxOSg8bspMcDz4r9eDb5dPr1ulV1O/I8WSoFvPN0+N7NlcdTAwMTe/v3hnPPtcInK6J328XHUwMDEwi5lJ14vcofw6JqqDe3XRuzwqMrn//HJQPFxcPW+5XHUwMDEzXHUwMDFkXHUwMDE4VuZcdTAwMTOt/YaJctRcInUlY5ZcdTAwMDNH/oZ1PcKgO0eP3Vx1MDAwNI9IMFSIIFx1MDAwNshcdTAwMWPcXHUwMDBlXHUwMDFluVx0vyrnXHUwMDAxOIwm1HBOXHUwMDA1lVpoSyNcdTAwMWJknvCxg0Fuf3Zpmlxy01BcdTAwMDLE2H4xO42BgWzVZ2vapFx1MDAxNI85JT9ET1EwSFx1MDAxZf7O8GtJXHUwMDA01ClcdTAwMDZSXHUwMDEwUCG4R6iKU1VcdTAwMDJr/Kmlwko4lcJRU6GN5+Key/LyXHT6aD1cdTAwMGJcdTAwMDM1XHUwMDAwmpj6iID7J+LDXHUwMDA3i1tJlatX8FthzFReXvmWrv+04lx1MDAxZITzrpBKZnth9lx1MDAxOb+FzOTzX1/K569Sp3PmX+tb4v71Wq++rcZ7Jv71JVx1MDAwZVx1MDAxMFxiVb/o5t//+vf/XHUwMDAyk+RsRyJ9RPCsubmit proven txbasic requestverificationverify tx proofquery stateinflight stateproxied queryverify stateinflight transactionsinflight batchesbatch builderselectbatchprovenbatchblock builderselect blockcommit blockmempool eventsuser executed txuser proven txUserfilter out invalidnotesexecute txconsuming notesprovesubmitaccount 1 + notesaccount 2 + notes...account N + notesBlock producermempoolNetwork TX builderbatch proversselected batchproven batchblock proverselected blockproven batchinternal tx proversselect candidateaccountexecuted txproven txsubmit txStorebuilderstateremote tx proverscommittedstate \ No newline at end of file +eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO19WVNcItu27vv5XHUwMDE1xjpcdTAwMTH3aZFn9s15uWHfYoNtefdcdFx1MDAwM1x1MDAwMVx1MDAwNaVcdTAwMTNQ0Vx1MDAxM/u/3zHQkiRzZlx1MDAwNymFK8hcdTAwMWRVu5ZcdGnOzPmN8Y3+f/9jZeWvwVu39td/r/xVXHUwMDFiVsrNRrVXfv3rb/z5S63Xb3TacIqN/rvfee5VRp+sXHUwMDBmXHUwMDA23f5//9d/lbtdb/wtr9JpfXyz1qy1au1BXHUwMDFmPvv/4L9XVv539DecaVRH3z9orl5WLso7W1x1MDAxYrv15/Zaae/6dW301dGHft9Qr1ZcdTAwMTmU2/fN2vjUXHUwMDEwfq6Z9CwjmjNChLHafp19g7PSUOVcdTAwMTnJJKdSXG5GxNfZ10Z1UMfVXGLtad9h+NdH6rXGfX2An5GTn1x1MDAxOV/l44b+e4V8/aQ/6HVcdTAwMWVr651mp4d3/Z+0hv9cdTAwMWLf82258njf6zy3q1+fXHUwMDE59Mrtfrfcg4c0/txdo9k8XHUwMDFkvI2uXHUwMDBlXHUwMDBmXHUwMDFiXHUwMDFl6l+B33H5uVx1MDAwMlx1MDAxYfh51Lfgl97X27V+f+I7nW650lx1MDAxOOCzomS8XG68w+5udfTO/md8T71yq7aLL6393Gx+/bjRrtbwVfx1K/Ynfl27+vnrJj7er9Wqo1+niZVK6/G9jPdcdTAwMTiVPPjTw057tN+s0vCqqVx1MDAxZX+g0d+APTZcdTAwMThd9a7c7NfGXHUwMDBmXHUwMDFi72EzuP/8e9C3XHUwMDBmj0937qrX+1x1MDAwZo8vtVx1MDAxZHrafr1+r7Hxi5vYi+Ver/P619eZf3/+a/yknrvV8sdcclGtlTaEw6HV1/lmo/1cdTAwMTh8Ls1O5XG8htFP//23XHUwMDBiMJtk4+B+WLk4KO1fab2qebN03Ehcclx1MDAxOEq59IjlhClmteB8XHUwMDEyMIJcbk9bppmxylx1MDAxMmZDgKGaedJyLcM4sdbTXHUwMDAyvsRcdTAwMDWcJoTwJU5cInBSzIBcdTAwMTPDhJVcdTAwMWOeq1x1MDAwYidM2iigUGZcZrVcXFk7XHJSXCL3sraSSqt4PnvZdI6Ke/3TveeHa7O6f3b+ti/X9tPvZc09XHUwMDA2NySshlx1MDAwN8RhP09uZq1gq1x1MDAxYjzBKTFMhTazUVx1MDAxNrSH1qNcdTAwMWRcdTAwMWLe0NxcdTAwMTDP+lx1MDAwZT3vXHJd6XX6/UK9PKjUf8C2NjvubT1cdTAwMTa2v18o//zJv0O7XVx1MDAxOVx1MDAwNTueSuPY7YLIqM3OQHFcdTAwMGJJRX5bXTKuOLdKipy2+kZBnlxucXP53uk1i/S6t9ctWJthq1NPXHUwMDEyXHUwMDAx0KPEOrY6yGuPK6msXHUwMDEy1nDXVjc0vMFcdTAwMDXjwGwss8qA5IZj7tTmh+3wvdl3OCNSXHRcdTAwMDH73LHDOdORO5xSxZiS2uS8x6VQylx1MDAwN5yZ9vizum6Tl63T7ZedbqV8/PRor+9vUu/xXHUwMDAyyHJcdTAwMGZog5JcdTAwMDA8QpQmXHUwMDAxckKF8IQm1Fx1MDAxOGWkS54zo+FcdTAwMDLjw7HlXHUwMDAxXCJcdTAwMWVcdTAwMDU9XHUwMDA2XHUwMDBmXHUwMDEytOeSpETv9cOZ9zo8YWY4kFx1MDAxN8dWlzJcdTAwMDSA31vdKGYoZdNcdTAwMTFcdTAwMTc3xf+981x1MDAwNrXhYJLTf2xcXH5/eNvsV8pcdTAwMTetTuHislNcdTAwMTi8l/Sjj+H/7b7sx5cr7OZ+++ihOVx1MDAxMGfba3u9eu+OXHUwMDE11tJZXHUwMDBlsdc96lx1MDAxNVx1MDAwZSu3w9Xiw4BXXHUwMDBl68+ls9L1ej5cdTAwMTaJtpZIy3KCvfvphWA/8fA/XHUwMDEwT5XwuFx1MDAwMYFILZVC+kykT8DbeMCjZa641ZpIMlx1MDAwMrSDxMklwiNcdTAwMTB+lMVcZrFcdTAwMTb9KMyltiRcclx1MDAxOfFfVohSKGSV783mobZcZlx1MDAxMCBlaFx1MDAxNmo23o64XHJh/aXjdd9cdTAwMGLstFx1MDAwN6eN95GmNVx1MDAxMz/dKrdcdTAwMWHN0W6cuMJqs3HfXHUwMDFloVx1MDAxZm621vvL/1x1MDAwMFx1MDAwNo1Kufn1gUGnOz5bgSuWXHUwMDFi7VpvN43C7PRcdTAwMWH3jXa5eea65fLzoFOq9T9uetB7rvlcdTAwMWZGbef3/qdcdTAwMWWTMdhtnJ4+3W9f9p504VI+vlaeLl/0TirsXG7JgZJGa2uhNZwnQjHKuIuSUu7Qz2y8rZdwnYTrSVx1MDAxNu+aslJya112XHUwMDE015HeNaRYxFxuM97r+fhcZozgQln/XHUwMDA2zYrW/vNtqzFY6fY6L7X2ymDohC5VXHUwMDEzP/1O6E7ce1x1MDAxMKfRN5tcdTAwMGZo+1x1MDAxYm/lc9F7ezaP5ft93jzc3qhl4dmMekpbfCfChVxcZphcdTAwMDeWhopUu1SN7ZQv5FI6fsFL6E5Ct5RcdTAwMWW6Rlx1MDAxMUVcdTAwMTiTLtLMbSRyqSGKXHUwMDEyXHUwMDEwxFNBd0rWzOtr9n71tfu0V75qXHJcdTAwMGbN4dnTXVp2WyxuXHUwMDFm7J2e9nf0eaNx//gs+WVR5MVuiZFMXHUwMDE4P7ZmYLfOVaZit8Z6VsBroVx1MDAwNG5cdTAwMDdcZn8xiTPOmEdjcUZAxWLsXHUwMDAwXHUwMDEwXHQ82Fx1MDAwZtSxXHUwMDBmZ6kwo1B3mlx1MDAxZXWMgFxyolwiYCdcYouCnbJaMMHz1pe4fyXNsH9D+vK23G9UVnq1p+daf/CvNtx241x1MDAwZXTbXHUwMDAwVzRcdTAwMGbV2WpUq37FXHUwMDEzIL5cdFx1MDAxYSyoUCdWs+JeTD6q9fKuvf3aqP26qf1itPdycFY5ODrJplql1kYz6lStgmtcdTAwMGasWqNcdTAwMTVdqtZcXEB+lsWIpejqXHUwMDA2i8RcdTAwMDXycITtN8iBv1LgQz5z5dtV61VTnYqt/cP3tuzpYuuyXmreXv9ph9RcdTAwMTPfLPfPzP32m+5cdTAwMGVf1zdPXHUwMDFmn7onealsXHUwMDBigDE5qWz300ulsq3CiKDRwKZcdTAwMDQ1sGFcdTAwMDL4NcpTsfil2lNcdTAwMThKUWBGgdL3IXdp4yaj+TxcdTAwMDOauTBcdTAwMDTeXHUwMDAzdUXGJYmMpFDBXHUwMDE4l1xc0alSSFx1MDAxMoxcXE0y7OCQ0lx1MDAxZSm2NzBcdTAwMTjRdOzcLYKiTtCHQUVcdTAwMWS5gpy8Ve/lm5v+7nal9Hx/U1pcdTAwMWRcdTAwMTTXOj1cdTAwMDdcdTAwMTf/kD6TyFx1MDAxNtp4XHUwMDFjaVx1MDAxZeOcKmqDXFxcXFx1MDAxM89qxjB/hVx1MDAxOeLzgHwh21xuz1x1MDAxONg3jFx1MDAxYimB942vMFbUnjJWXHUwMDFiXHUwMDAyfzQ1WrOl8zlcbulcdTAwMTdpw0vs8yeOUKpcdTAwMTSUg1x1MDAxY3bGl3g0Z5eWUkBqvi5pMFx1MDAxZSjlPkshXHUwMDAz/rudRpAtjP+1Mt4yo//4+vf//O38dPQ+xaNcdTAwMTDeouNcdTAwMGKGtHSz3Fx1MDAxZqx3Wq3GXHUwMDAwVnqMd1x1MDAxOVx1MDAxMsaDcm+wXHUwMDA277XRvp98fZ+Jn1x1MDAxZmJEtFx1MDAwZs9avV9cdTAwMDdV3Vx1MDAxZtqNq6fB7oZ591GHv1qd6mgndnq3XHI/I4LdPVx1MDAxY//qiafiXHUwMDE5poGXXHUwMDExXHUwMDBiSlx1MDAxNvTA2Fxi+zhLNTOYN1x1MDAwNWuFJ1x1MDAwMdBcdTAwMGWtc7yjau3qeFxy4fWtojyp18qh/Vxm3/OfXHUwMDAzfDZcdTAwMDJWQa1523lNRVrU48VcdTAwMDY9umaV++dcdTAwMGKiXlx1MDAxZdZ2r25W03niR35cdTAwMDSO6XyWw1JcdTAwMDOSTWhPxEo2XHUwMDBie1x1MDAwMlx1MDAwZkFRMUqfhllSlmRBdpmeslhjga9oZ1x1MDAxMI2baN+eVkpy4Dtj4ZKbxIJXPlx1MDAwYmNcdTAwMDGDvPe2XHUwMDAyIFx1MDAxOdTmwlam9cg77zNcdTAwMWZO0n5tblx1MDAxZL81XHUwMDFl+nsvttRbv6qW2keHYdziJUP5LiY231x1MDAwNTlcdImLoEnlMaGIO1x1MDAxZHe8WVx1MDAxNlx1MDAxYbHVXHUwMDBl6pb5QvZqZu4hlZCCaWdqXHUwMDBi05HUg2n09EqWb6Ki4ZxcdTAwMTBppzI9cqVcdTAwMWWO/YhHWPPOwDCC+jdcdTAwMGa9PXGu22m+3Y9eZJLG3lPFqjrvX1x1MDAxY60yefD4VFxcPVxcO2uHkVx1MDAxZpXSqYyHmcuMg0ZcdTAwMTDhlE5iXHTobamoIPB+xfjsOKVTekIqXCLFZ/qmw1x1MDAxZZHSc1e3LLRM+Fx1MDAwM1r818zZbppoQVxmdVx1MDAxYSPhOPxXxJ1cYlx1MDAxMFx1MDAwNzJH1+LM4bW/464rr57OXlx1MDAwN2dcdTAwMDfdw+3hc2G4rU9vyySH67LH3evj1/p5q6u6e7y5tn2h+d1M101ysW5d8NWNwmmv1FxcO949uy3eNFx1MDAwZX+JTC5LUMxGjnE5k8vSfTcp2L/U3OPAXHUwMDFkqLJwN0AmglJExktcdTAwMTFcclx1MDAxMoJcdTAwMDL516hKrF9cbi1qXG5cdTAwMWRcdTAwMGL8/I+KjVxiN4aT/Vx1MDAxM1x1MDAwNVx1MDAwNpo/XHUwMDBmxlfJXHUwMDEzmVxyy4wxitCpglx1MDAwZlx1MDAxM3dcdTAwMTHavsj9RZbtXHUwMDFiYv+ng07PzfvnmUSXoIaDpkDgpvMxXHUwMDAy1tePTtvDnafubknXu1x1MDAxN1s3hZPz9Fx1MDAxOTmMoSZcdTAwMDczgFjjKmTi2npWXHUwMDFii8WnzupcdTAwMGXMNIjIf8WAhD8nfv5cdTAwMTnvP6zGo5whXHUwMDA0QZmVgkrjXG4oXHUwMDFhXHUwMDEzXHUwMDE5glx1MDAwMDxbiYlcdTAwMDP5af0kbddq62dlVtuXZ2anWL262jlQXHUwMDFiu2m1nWRcdTAwMTLFhSYqp1x1MDAwMJ37blJoO6Yw45spxi3wZlx1MDAxNcyoXHUwMDExmiZcdTAwMDCFSI8ufV3TYuM2XHUwMDBiNjDBULpTUFx1MDAxZD77L0LMXHUwMDE4M0rm6pz/3L2zXHUwMDA151x1MDAxYe27Ju6QOXq7bjuDwWdcdTAwMWaF0Vx1MDAwN1x1MDAwMoovQelcdTAwMDRcdTAwMTVf1Fx1MDAwMvLRgPFcdTAwMDZHfGjOTqajXHUwMDA2qkC4JJ7ihqBcdTAwMTeMgrlcdTAwMTRcdTAwMDI1o3yipsuVJkdccvPAyFx1MDAwNipcZsqWXHUwMDExM/da3p/jXHUwMDFkq2RcdTAwMDC51ISBLFx1MDAxNS5cdTAwMDVoY0gtMfCiKOU5tnFI0oBs+6VENuovx9t7b62LYc3unLeeM9l7mkzYV38swMdAXHUwMDA3cs4x4WCy0UJcdTAwMGVfL4SAknS9aPQlXS/0xHNcdTAwMGI3xqdcdTAwMTeOxHPlXHUwMDE531x1MDAwN/EwXHUwMDFiXHUwMDA0o8hESSp8lUxcYqByXHUwMDE3rV//16KCkdQjzFpcdTAwMDFb2mpcdTAwMDVmtp14JZ7UlCng9UZh8TazrmdcdTAwMWFcYoF+/jxcIkZcdTAwMTm18HhbaGLhhDCCXHUwMDBlK7gvJTnh/vf2uXSZZulcdTAwMDW8lFRCYTZcdTAwMGVWXHUwMDE0U1x1MDAxMYjEXG6LnXmkpYYrXHI2jppm+flcdTAwMDViJ1TcaFmntfskp9qneGRcdTAwMTOCZXRPgceRXHUwMDA0MD9CJ1x1MDAxZcDHKjJfLVx1MDAwNmBjiLnE2+jmd/un3VqlUW6GRFx1MDAxNtxL6GSM+ndL11SM3lx1MDAwMzpcdTAwMGZ2lJGWwUZcbvivXHUwMDAwNp5cdTAwMDFjXHTgyYjljrRcdTAwMWPMkTfwgKTRsFx0hXGk5cyf0f8wm7eaXuUracHwckWwRWRcdTAwMDItvCOsc1B0Kjd3Up78TKy+2+tcZlx1MDAxYrXqyihAvFxiXHR3XHQ8OkjqI+4/XHUwMDFmTt8yx8fnu8flo4dKdUs/vXef5e1Weq+W5J6mQNu5xtSUkFdLXHUwMDEw5mlcdOqHRvQsoVx1MDAxY2RcdTAwMDNqXHUwMDE05nZtyWW7knhY19ywztKuhCCtXHUwMDEzzlx1MDAxNDulolx1MDAwMT9q5COUr5/Jt/P717IqPlxm7M7TwePgfedA3G42dGFcdTAwMGWp7bHXzT+s9dlcdTAwMDRur/RuXkjjsPFw9zRYP71cdTAwMWOenFx1MDAwZXO47s3A3mp++f7wvlWv96+f7KrVt+muXHUwMDFiXCJcdTAwMTihLnhMXHUwMDFiKbJUNcWIJvfbTsM3tPA4mHLIQ1x1MDAxNVx1MDAxMI+gXHUwMDA3kdp4oYTZlVx1MDAxOGw3Qlx1MDAwYsp8XHUwMDFhbek/TFx1MDAxNkh36XlcdTAwMDb2O1x1MDAxMsDrnJIn2n1ILVx1MDAwM1x1MDAwYlx1MDAwYsTP+GbzcyBmcn+HqMZnbvz83IdcdEwjQbtHpPZ/i/Ow99iu3j/Kujw671x1MDAxZV5cdTAwMWQr1d3Yd1hcdTAwMGaR4TOBNiZcdTAwMTjg7j6AwmqPacy7iopcbljiUWLGNpYjKmCNZ4hSgqiRk2NcdTAwMTlGS4D6Q1x1MDAwNqhcdTAwMDM/pIbLcJnt6F7CKTXjUlx1MDAxZYqFVoxPlVx1MDAxODtcdTAwMWTNeD54P2BFullr9lx1MDAxYpdbq7WDzkaJpFWvamN3vXkgt1x1MDAxYXqndvSqjtYqheJqXHUwMDBl6nUsobI02YhcdTAwMDGke5Vp1KtcdTAwMDAoXHRcdTAwMDFcdTAwMTZcImXGalx1MDAxOcSiXHUwMDA0Tq/gXjUnytmTk0pcdTAwMDBcdTAwMWGG6KRcdTAwMTJWUOHC4lLDRsLuMVx1MDAwYuy0wiZgznJYbFlcdTAwMTBcdDtimVTUqlxce1x1MDAxMeZcdTAwMWKkXHUwMDFivdJyXHUwMDA1S8T7i6BsXHUwMDEzNFxcZKzOvY58tO79df3XznbtoLY5ZOvn9Kl1fXu/kUnrKlx1MDAxMNrUXGJn0oq0gHRpXHUwMDAwyCxa60pcdTAwMDanXGKnTEz01/1Culx1MDAxMd7vs1x1MDAxZsdS68bCv5VcdTAwMDH+XG7EMzFEuHx5lLJo415cdIOyeZ7pK/L+mm/T2oa43KtfbF5eXFxWXs6202rdw+b7XuOofrV13lx1MDAxOeyWNpk+WTt9SKd147X5qrVronRW3+s92uKu6NPuXlx1MDAxZbmwl49HXHUwMDA35Y3ru2FXXHUwMDE0Kqv7snjMaVx1MDAxZUZ47izB/VbSsFx1MDAwNFDymjOJU1x1MDAwMJiVIa+/Ylx1MDAwNliCwJq1XGLZwZiHjXFcdTAwMTbL6/9jWEI7g5iQWjMg6M6W3JSKSHJuwVx1MDAxNFx1MDAwN4pBcy1ay5ck3KJYry1cdTAwMDQ/SNDFkfwgtIR8qMHmzend5ntD84I6fJNs9ZLq4m1qaoBNljWOj4AtwMIzJlx1MDAwNPeAOzIuZYRcdLCcMTE7xDvpIc5cdTAwMTiIWYeZPbpcdTAwMTOfwzZkXHUwMDA2YDkx43QqM2A6XHUwMDFlcCtOXpr7cve8dW1qx89H7dbhOpuD9f1H+MXWzvb+w+7B89nZ2vD86NfxQ6Gwt5/DdY+K948nh51ud2u9uvN2Url/uVx1MDAxYTRy4Vx1MDAxN1xcMVx1MDAxYznCcuJcdTAwMTfut52CXyhcIj3DsTFcdTAwMDBmXHUwMDE2KFx1MDAxYVx1MDAxOFx1MDAwYlx1MDAwNZzCM4JpgZrN33V6nFSAWSvSWMlcciZqL7v4ZJI93Vxm9Fx1MDAwMrszgCCJcEJEN7xcdTAwMWNcci9S/vhAPjXxkkqhZ6qJXHUwMDFmqeSV2+dGs+pnXHUwMDA0f45bJCjzcLM95/3nQyziZXBcXJqwoNJTRClr3PWykmOasOBcdTAwMDbroVxyXHUwMDFi76exZ5F5yvBcYmbBlWdHXHSMTFFlfDlsS0+DXHUwMDEz409pc1x1MDAwYqL791ClmVLamUxkoof9qFH/rim9j9PRjvsnVWFXz29Pg/WHrfLG8XD4tpY6d3hstUzV0TPf3GFcdTAwMDdcdTAwMDbwKIS2//hKocXllqZcdTAwMWLvXHUwMDE0XZmuK5CkXHUwMDE2XHUwMDE0tkDdobVcdTAwMWNvro+zwmBGXHUwMDBiaHaC3V6VI1x1MDAxZDljxm28cJ1uXHKTt1xmdlx1MDAwZY25y/l1KHKjIFx1MDAwNVx1MDAxZFx1MDAxM8qA2WbQZ2iE8GdxfsZnmcewlEAoXCL8XHUwMDE11b8lt1CeXHUwMDE4zfNcdTAwMDEoXHUwMDE5je8uLL/n31x1MDAwN/mHXHTsXnpSplx0mO0gYl2dXHUwMDE1WaRcXKbGKlx1MDAwNlx1MDAwNuO3pF7M5vHpw11UXHUwMDA2/2rfTr6sP0fJXHUwMDEyaFBooID7/vOhZPHma1x1MDAxYyVcdTAwMDPp6rGxn4dcdTAwMDXMLCkwXHUwMDAyxC2cIZiWXHUwMDEzZmTwXHUwMDAx/TvB093BRFGPWkIw15dcdTAwMDDxXHUwMDEzdJnzXHUwMDE5XHUwMDBm8/7svFxmZLHS3NlX1ejImi5cdTAwMDArNuSTfKrSzel4WbG5enx/vXdWuN66ejt5KFx1MDAxNkBcdTAwMWJcdTAwMGZ+XCIvK0RCXHUwMDAxjzBcYubBzr6D2XiKajDbXHUwMDA1wXogTlmgTmryrImjPanIWbxXfVqCabVcdTAwMDHTxVx1MDAxOGNcdTAwMDUx7O+YkzJMo/9cdTAwMDRxc8MkXHUwMDE1cdOeYVx1MDAwNPOsXFzNZaTUoFpcdTAwMTRa3cIoXHUwMDA3cZNcdTAwMDSIXXxzmSVvS1x1MDAxMOiD9LxccixcdTAwMWUlmdOVXHUwMDE2TdskZ5yoRWRtXHUwMDFm85NcdTAwMTaItSUwJUdtjuv+82Fta0dv6zVZujrd2Fx1MDAwN5RcdTAwMGXfNmyxzNNH6LTysFtcdTAwMDam5FtcdTAwMWKM0EmtPWVcdTAwMTQoXHUwMDFiXHUwMDFiUZmjuCfByDbuwdnoPseKvnDN4tJVvjKJ7uf06MZguqE4n82Bb8pIdMJcdTAwMGV2QVaWmzlOVD0531l931tcdTAwMWSKl4f3Nju73nkgw6M/nVgzQ/O6WCb50f6fYFx1MDAwN+ZcZvIuXHUwMDA23O6nl0JhK0owQC45jlx1MDAwMjdcIuBcIlx1MDAwN3rlXHUwMDE5zlx1MDAwMZlcZoWzo7hFelx1MDAxNPW10Vx1MDAwNMQ39c3jWIa9krH8kiHkTpjBkDtzhr38s5/CWLajMTZ5T6/ANMtMXHUwMDAzgUPq+lx1MDAxNs8vUtwrQUWG4l7u+88p7lx1MDAxNSvSYuNeXGbsXCLFsbG7s4xWceNxyjnRiHmfXHUwMDBmZVx1MDAxY/dcIlx1MDAxZTd85Dd1dogjXHUwMDFlsUhcdTAwMDKwRYNcdTAwMDTYL0tb4nH+6sZ5XHUwMDE2XHUwMDE3XHUwMDBi8C+4XHUwMDEzqt05NyTUXCLyXHUwMDBi/lx1MDAxMjvIw1ue45jHfvWkfb2zuaVFb+d8u3P8VH+DZ5ZFNY5ki/Lj5lx1MDAwZlx1MDAwNb9cXEDAo1x1MDAxMMbA+GKh9eXmYflcdTAwMDb3XHUwMDA0nWz2SEjQP1x1MDAxMVx1MDAxZkxK5VWJl6tT3fZcdTAwMWZccnl9/bxcdTAwMDX2WrHxcYP9y8ag/lx0nlx1MDAxNIzNXHKSXHUwMDE0jE1g9zNcdKxcdTAwMWXDksDMxpTrM1x1MDAxM1p6gjCFQ8eY8idK+6Z3YJ0s6Fx1MDAwNc2ssD5Gsex/8nlniVx1MDAxMn2YibkpjlVTXHUwMDBlyc2ik6FcdNPA2abzjc8hNLYyXCI/i0DaXHUwMDEyiJI7Mlx1MDAxNrz9fDibPryqv+6X35Q6XHUwMDE1N4X9wVx1MDAwNVx1MDAxMOR6XHUwMDFhTFx1MDAwM1x1MDAxYtNcdTAwMWVnilx1MDAxOSxzxMGGk6CGM1x1MDAxZVx1MDAxOIyGXHUwMDEwISi141391ZRbeNidIM5vusR0XHUwMDAypt/TY5pcdTAwMDKt1lx1MDAwMqPeXHUwMDBlUIc7XHUwMDEw+GZug7FcZq8x90mC6EqYii99oVx1MDAxYcC2OJiO97uEMe24+Zym+8ZWVsVZYVx1MDAxY6wwLEvSmlrLXGJccuawKCNxsFx1MDAxYZDHiIol4J5CXHUwMDFhylx1MDAxOdJLZbjLtVwi8HdQTqXCkWXMzH2Ez8+CuF3PkNJcdTAwMDJmXHUwMDEzV1x1MDAxMbXOxkRjXHUwMDFjhIKyhpM5djI6IKWSPHvbeS8/bFx1MDAxZJ63Xofn/btCtk47TIKZ88dccq6JT0vhaa4sxnY/XHUwMDAzXHUwMDAzSV8vRGLmO65cdTAwMTdGX+L1mPGAlUtcdTAwMWPQJVx1MDAxOF6Ppb1g6Fx1MDAxNc7PpvzdXHUwMDAxdNS3UyhJXHK6fDF+64tcdTAwMDOt/G5cdTAwMDEqPNj9TMDjXHUwMDEyllChjP9KMTFywkFcZmJDZ2XF5GRGsFDBzlx1MDAwMc2p4SNCKmeD2UzNUL9pYON391x0nUBH7H5O0yc0fnMnXX4sXFz+jl9cdTAwMThPsbDZbmXmlcbgbp5cdTAwMWRR3VI8hfXAKPZFwMGtn9mygVx1MDAxOeRgU3goblx1MDAxOHBcdTAwMTL/UPrxXGJy61HpS5ddXHUwMDFhXHUwMDBmmZnFRnpmwbg1mllnulx1MDAxY4+O40j4XHUwMDFhiN9cXMuXcnFcYrRqrW6n01xcqb2Mft1cdTAwMDKYXHUwMDBmXHSsPWg+RC0gXHUwMDFmXHUwMDEz4k6x/UZz51dBXHUwMDFjdMrlt159da1tU5lcdTAwMTBcdTAwMDWhLebxXHUwMDAxpiWY/Vwi1HvQKDWaQU5cdTAwMDTHcZ/hSFx1MDAwZWw10NlWUMXASIR37TJcIjxQucZcblx1MDAwMSakxHzMuU86+GFQ35w9mmM5vFxuIZ358sLEXHUwMDE0UFx1MDAxM0WVVWyOlUx3XHUwMDBm6+T8vVvaeyi1X5+u1a89tnqcocDXwFJcdTAwMDUnfz6YXHUwMDEzjYXR2Vx1MDAxMFxu5sG9v2nKOZFcdTAwMTJzXHUwMDA1YLtwXHUwMDE5zDedPGvDYav5cuYkUuTefilIUcFcdTAwMTLmcaaNsFxmTFx1MDAxNsNcdTAwMDOyXHUwMDEzLFx1MDAxOFx1MDAwZpPPXHQwXHUwMDFl56xkyoWH7WIs/UyzXrKizKJyKz0rQu82wE+7usZEh0motVx1MDAwNIg9tbl2lsNJ51x1MDAwNDbFVGn+v2nRc7/WW6lccmuVZ7jwymC4XGLEKIGLXHUwMDA0iVH0XHUwMDEy8qFGtvNYei2xw+HzzdH29cXmSeNpq5aOXHUwMDFhgWLBXnCoXHUwMDExXHTHmMgkui1VXHUwMDFl02RcdTAwMTTmXHUwMDA0ljveP35mZIW1mHhcdTAwMGamj5VcdTAwMGWjh3hcblg6XHUwMDE4VdQw0Fx0QMCXcI+F+/bMzFxiJ8wo0E/cJVx1MDAwNriMriXi8EI5TqiZXHUwMDFmM3pcIsf7YpXQtZP7Tm/v0FRcdTAwMWHkrftcdTAwMTOZUSFcdTAwMWFcZqPvh2CQJzWKXHUwMDFlzz5f0mRcdTAwMTSzQK7RQ6J9ebCusyZcXE31J4p03PsvXHI1Mpi2+zuTXHUwMDFmOeKk8NSEJFAjyj1cdTAwMGLkkXJiXCK6YyypUYKs3MlcdTAwMTBtlkJxaqy7vDo6i19KhWPKXHUwMDE2k1x1MDAxOX3UuixcYi9KIFwiTl7kWEA+rCheviVcdTAwMTXqXHUwMDE0XHUwMDE0l2DnciU5tzhcdTAwMWRzXHUwMDEy21x1MDAxZlYwwVxiXHUwMDE0c2NbOtCs1bK7TTycd9NSn8jJOWCXXHUwMDEzLal0gZzqyLZWjCjGcVx1MDAwNu5cdTAwMWPjzeZ+U8nV242r3bNnbnu/3oevw9STc1x1MDAxYe/lm5v+7nal9Hx/U1pcdTAwMWRcdTAwMTTXOr08JtzE2zbTXzdeNkx/3Vxuu7nfPnpoXHUwMDBlxNn22l6v3rtjhbV0141lllx1MDAxZlx0QWBIqyxNyWLkUfyNxjuwXHLzXGZcdTAwMThgXHUwMDAytieQXHUwMDBlXHUwMDE57PdAuEcplVx1MDAxYdi7XHUwMDEwPFxcXHUwMDBlzKT0XGZcdTAwMTVKXHUwMDEzylxyXHUwMDFljj4uXG6ojOFMUFxmOFx1MDAxYutzcS/LjVx1MDAwMjLqXCKHOlx1MDAwNKFcdTAwMDT2V3dls6roXHUwMDAx9tiJXVxi6lx1MDAwYm/llPiWdZ9/j1x1MDAwNVx1MDAxNb1P8SiEtuj4eiEoz9+33Gj3XHUwMDFiVb9Ei7OTrKHAXHUwMDAxXHTRVGFcdTAwMDBj0kziQmJXXHUwMDE3w1x1MDAwNHxcZl5NaJlcdTAwMTnrXHUwMDA2Lu/a26+N2q+b2i9Gey9cdTAwMDdnlYOjXHUwMDEz11x1MDAxMtJbepjEwpnluFx1MDAxNzXH4ncrJ1x1MDAxNyGxSy2FlVx1MDAxOLB5meNdLXRZgVs1pzFcbqUxXHUwMDFlllxu4VAjwoBUTMpqRUQ8cVSjnlxcVjNgnTyicowvaWS8iN5Lb1x1MDAxNXLYnVx1MDAwNHOiXFx8kUbGXHUwMDEwNc6wY7n2WEejXHUwMDEwfiObqVx06nk/olx1MDAwNtRcdTAwMTd4mYcpmGB9XHUwMDA1TcHJ286pl7o9etrYP2ltlVx1MDAwZrq93v7NoLixn6qMXHUwMDAwXGKVR8BcdTAwMTbgQLUw4SdAt8Bs90ZeUqs50UKHS4NAsIOE518hL+qoXHKaP4ZjXHRcdTAwMTZcdTAwMGL8/E+it5LBp0OFpoZcdTAwMTHu9OnEpVx1MDAwMSkpcWx3/t1XlFx1MDAxMDOVc6+NyqG7vU71uTInLFx1MDAwZjrdKCBP3HlcdTAwMTC1UbeaXHUwMDBmfvvVyubr2/DlnZ3vXHUwMDFj8ZvW69tL4zFccn6Z0sChiCVcdTAwMDBVqWWwy1xu58ozXGZb9lx1MDAwMNGyxtGwmOhRVaqQ5KNnqyNmvYRvJHwzXGY1x6pcclxmijjD1TEzTrRcIvD25HRhqjyH9ITQ+5lcdTAwMDS30LBcct1jPnitn1Vrg9Nfv542d7fb+uF10KpcdTAwMTd1XHUwMDFhvFxuaz3YXGKCg6lAJM5cYp5cdTAwMDSsNtwjQirMIVx1MDAxMZQ4/Fx1MDAxYkCa4edcdTAwMDZLPY3SrqlcdTAwMDFLvEbiNWJauXt2XHUwMDE5clTiK7jwq1tcdTAwMWKpblx1MDAxNVhEKJBzxSvchUBcdTAwMWKa+XdrVrxcdTAwMWXWXHUwMDA2r53e48rZVWxcdTAwMDeVRYFu3O3mg+Jhe7e6c7T5esIrr5cvdufm7VSepVx1MDAwZZvg/DpPaiWYXHUwMDE4zVx1MDAwNFx1MDAwZY4g4tqzQNZcZqhmkP2Onil/bFx1MDAwNNF/3o2On+6crKdcdTAwMDczw9RFXHUwMDBiXHUwMDBm2236xsVKXHUwMDAwylTJefY1e9e/boe1Qu3oRFx1MDAwZa5cdTAwMWGrL/d3XHUwMDA3uvdPXHUwMDFk6DPyyUrsOZNT7MH99FKoZkol6F5jrcLkXHUwMDE1fzOZXHUwMDBmPGvtaa2lZTjBQDvwXGZUWqrlVOEp0dzIoJrh/WBHcLcji8nI1kaSXHUwMDE4qYjWecdcdTAwMTQy79+Qbv5cdTAwMTiIM0pcdTAwMTDoLURBTIJudFx1MDAwZvRcdN1/Pno6XqbFhVx1MDAxM7XQnjGCiM8pgeNcdTAwMTf/gWmlPK6p4Fx1MDAxNkQ8XHUwMDE1YfOYg3lccl/FXG6/qO7x2H5cdTAwMTh+gVx1MDAwMlx1MDAwM1x1MDAxYussXHUwMDE0WVx1MDAxNsPEXHUwMDAz/cFccvQsKZ/wvpB4uVwiijbcuvTLXHUwMDExXHUwMDA2elx1MDAxZURcdTAwMDfjc2xstnfYXHUwMDFhntZ2LlfF8Pj2107lqFG6fvhe3fj141xc45WRSMCjXHUwMDEwXHUwMDA2wfh6oSUudvd4UPFUoLuMMYzN0omn5IFcdTAwMTQxIDaQXHIwYFxuLC6vM1XAMl7OTrlcdTAwMDZmcEaiJPA/nHijXHUwMDAy8crJ00zFrGFcdTAwMDHjlW5IpSB4loiP1rNcdTAwMTRenFYkUPMspPFcdTAwMDTOfSPKXHUwMDAw0XNcdTAwMTA8Kj38ulx1MDAxMihDuKRcdTAwMGJB8H6Y9H9MT/Mkx1x1MDAxMSHSOlx1MDAxM/tjOiYhcrFPTu4kj/jLXHSm7oNWq64sTLf5XHUwMDA0ZuXuhFx1MDAxNl5APjQv3sSMo3mYXHUwMDEx5pFIV4yk1Fx1MDAxM4JYanFso1x1MDAwZVx1MDAwZlx0mlBurva1npFWj0pwlZZELLvXxoO8OTPFo8zg++AyXCJXIXLUXHUwMDA0drLTgsipTLzpON7NQVesdWWn82B2KtWr2+Lx9fXqT+R4XHUwMDA1N1x1MDAwZfBcYiFgXHUwMDFl/O57uJGSXFwrZjg8dqXZRLNcdTAwMTlgf1xcKFx1MDAwNiahXHUwMDFhdUiILdv5Y6NcdTAwMWI9XHUwMDA1b1x1MDAwMvaMXHUwMDExXHUwMDE0c1x1MDAxOVWgXFx78qxcYneEWmh251x1MDAwNlNcdTAwMWF2R4lncdS2hTdoVMB7J1x08bDgT1x1MDAxYqss8YU4/OO4XHUwMDE15pwqK1xmW5Rx3D9M7rcyeOQx30hcdTAwMWFnS1x1MDAxYlx1MDAxMSndKdfSWqLlVCZ8XHUwMDEyuVx1MDAxYntupp4ktDjULoFNuVx1MDAwN1x0fVx1MDAwZrHbo4XVs2pzq7hbroqTs/6wTkwnQ5yNWlx1MDAwZpRcdTAwMGVcdTAwMTjdXHUwMDFjXGI404G2mGDSeVJgZlx1MDAwNXzSP4BmXHUwMDE5aMtcdNad9LBcdTAwMDaKLVx0qFx1MDAxZXeWXHUwMDBij8lS04ZJTDScqtHtdMTt6Lr7/rBXkIXm4bDG+o+t9/13lTYg1t+25f6xLax2e+ZBmMdu//ClnEOgba1cXN8+f9+/WLtqP5e2zuuF8q+blNdNJpp5XHUwMDBlXHUwMDEwcj+9XHUwMDE0mlx1MDAxYdi79Vx1MDAxOCdcdTAwMDbTXHUwMDA3LZVcdTAwMDFrjUrlXHQqpTSGXHUwMDEzysN4tnxcdTAwMDG70f9cdTAwMTgwd9OD2WhcdTAwMDXGXHUwMDAwcVx1MDAxNlx1MDAxMVNBXCKD5tRqwLHi7FuaVucwQOgjTrVcYlo6QTW651x1MDAwN1x1MDAwNW8/XHUwMDFmLd15Kd5e7anV7Zvj1v1pp7JRbbX30mtpRoWncHacZIQrXHUwMDEzXHUwMDAwtVx1MDAxNdxcdTAwMDOTXlFFMaFxgXT0P1xi2llcdTAwMDb9XHRAXHUwMDEzR9rkwrbS0XqaXHSOaMy5Ib0hbJR3kY9qap08Pj9un11tXHUwMDE0Szs7xy8321vP9dNcZltcdTAwMTlsQYrjSrlcdTAwMTKUhrYyXHUwMDA2jdFQ1JpcdTAwMWK5SFv5XHUwMDFmQjczzLijXHUwMDA059VIa1xcoWCgXHUwMDE5MTPugFtcYk3m6Ca8VO9rXHUwMDE3jWalwV51a6evXHUwMDBlN1x1MDAwNlx1MDAxMzOuYlkhey80K+qyzl80u6vdXFzQk8uN61x1MDAxY9jmTrN0ctI9PjleeypccsWgdVs4vK7mwzbzhbT76aVhm4xyXHUwMDBmXHUwMDFi1KBZXCIoZ5NhP0VcYvb9XHUwMDEyIFx1MDAwZoV0tjpcdTAwMTbS00A3XHUwMDA1XHUwMDExKC6pXFyIqN+PXHUwMDAxc8QgOzfdlFx1MDAxNN+Ec36CXG5blGMsS0KxP3K+XHUwMDExvyn2b4huNpBcdTAwMWRcdTAwMDJ/W1x1MDAxOVxmXHUwMDE3KbkrQT9cdTAwMDZZZ+wq8iGfN1x1MDAwM3ur+eX7w/tWvd6/frKrVqebmmLAXHUwMDFjNJRgeI+ExkwrSqSngHCCXHUwMDE2dzem0oZ5REaWXHUwMDFmwy7wJFdgalx1MDAxYaA8xjdcXGvpXHUwMDAxdsB9VWTR3Vx1MDAxY+xLoYXTVWRJdOc+XG5vXHUwMDA1bH86x5zs17NzfV6yXHUwMDA3V8dXw1Lram11lZ5VsilDbaT48zG+iU9j2FxcWyVcdTAwMDBcdTAwMDR6NGndJH294MRL3tcqjFrtgsa1OK3QWJs8J0VcdJz4gO1cdTAwMTlcdTAwMTglxlxinvZ6oTc3h36ELXN8fL57XFw+eqhUt/TTe/dZ3m75tp1/gFxuUFx1MDAxNqG51Vx1MDAxY2guPFnr61x1MDAxM/I5P0X6v1x1MDAxONvFQlx1MDAxMMqBMIOKxaFcdTAwMTZjT1xuXHUwMDFlxNPYXHUwMDFiQFx1MDAwMZfW+Fx1MDAxZYXz/SVcck35saNRYrZvqoEhkXs56dLJRD3LWJSpb2OmXHUwMDE1xuBrnuNQ3EI6hY2ghSex/JnBXHUwMDFmSY2dTFx1MDAxM1dcZvOHwIRcdTAwMDA6KPy99n2jXHUwMDE0gVx1MDAxYXBOmTXG3Vx1MDAwMmFcdTAwMTk6TiBcdTAwMGUyPXFcdTAwMDB9ykCVKldqUHi84tgpzbAsXHUwMDA37IT8rIS85qM+34K2WZDGllx0TDyUXHUwMDEyXHUwMDE4vvd8LILdh3L5tH37tvFrc/dlt3S0rTda6d3RXHUwMDA1xrSHrmaKXHUwMDEzTjlcclx1MDAxNVqDXHUwMDEw9HBwkaZuTC9cdTAwMWTSOYDaZrBcdTAwMDYoYVIpTpxcdTAwMGVpf517sEJcdTAwMTOzuECZ5dvOXHUwMDFmO23DXHUwMDE2yVx1MDAwMuuYvdx62NywN1x1MDAxN+L4tLx/XHUwMDA0luhLlej0/uhcdTAwMDJcdTAwMTPUU5aBXHUwMDExyzg3jr0ssVx1MDAwN4/lkdO6llx1MDAxZenZ9vFq+n0sLfJrXHUwMDFinj8xkuQ8MrNJw8uTXFyKOXajb+j3XmWtdrXaq96WXHUwMDFi70dnW7+6jYzd6PPDiPtuUpA3XHUwMDEw9UBsOVVcdTAwMDY2v1x1MDAxM1x1MDAxZZZ4RsHtwmtxxlx1MDAxZVx1MDAwNfFASZjFaEr+Y0CxlkG4W81GtqXTtetLwlxyu3ZBRVNFSK4z7HDnMlx1MDAxMJczkbZerdVcdTAwMTnUXHUwMDE2zbFcdTAwMWKvaILkLWZccvmQOPa4e338Wj9vdVV3jzfXti80v1x1MDAwYoPa4dbFPsBcdTAwMWO4NbwtYO5EXHUwMDA28nnBXHUwMDAw8Fx1MDAxOKHY2opKQ31cdTAwMTmjv1FtNHa+MsJcdTAwMTj0XHUwMDA3XHUwMDE5n1pcdTAwMWOXdWjtXHUwMDAxNYTfYrmx/r6eSyPNXHT5LMOwMVx1MDAxM1x1MDAxYuw0J+JtdLNJoIHwRuZcdTAwMTmXbbxePLxsl19M44asllx1MDAwNd1+/PXQzJRVR1xmJp1PI0y+0bVcdTAwMWLa20lfL0RcIibpeqEnlFtcdTAwMDXInipW1Xn/4miVyYPHp+Lq4dpZ2+UoJVx1MDAxZY5HJURrXHUwMDBldlx1MDAwNEhcdTAwMDffOJYsblLicaRvVlEujOaaTYx1wbnSsEGJIFx1MDAwNrhcdTAwMWNWkrhcdTAwMWVqpsHSM7uIgdtoQlxy51RQqYW2NLRy5lx0ylxm9o80YFx1MDAxNqdcdTAwMWOwTVx1MDAwM/7iwHxcdTAwMWJcdMuHXHUwMDA3pVx1MDAwND5zQqd5XHUwMDBlc/JcdTAwMTWH953fp1x1MDAxOPydwXNxjNUpOFIwViG4R6iK0m2CUk+CjFx1MDAxNNiTVVx1MDAwYlx1MDAxYdZt2ngusiqWTsZ4/ZVh5DImsmtcdTAwMTCFTspcdTAwMWE9b9VcboZBKJV7t0ZcdTAwMDam1kzd3yq/5fG/2lx1MDAwMIJBbVx1MDAxMehqXHUwMDAyPVxm0tXoJeTDVq+P125b94fnt42d3ZfTY13S9NzROioqbVApT2BlocbO55JcdTAwMDdgLTCVQCiuXHUwMDA0U6A4wr1m3PYn5Vx1MDAxZfdcdTAwMWZcdTAwMGJlji5Qa8fViMGhXHUwMDE5xulcYs5xoI5cco2k+Fx1MDAwYlx1MDAxM0mibVTCLVx1MDAwN9zLOVauXHUwMDE42S/WS2+dq1x1MDAxZFwiXHUwMDBmWu9PtNTv0rQ5f098s9w/M/fbb7o7fF3fPH186p5M/pbpK0y0YMbni53JJSSvns5eXHUwMDA3Z1x1MDAwN93D7eFzYbitT2/LJJX1qIXEiJ6ijDn6XHUwMDAxKKYxV8BKwzB2XHUwMDEz1q+KWY/huGOjpJlI21x1MDAxOCdcdTAwMDZcdTAwMTGLxItjocoomL5UvfHwjJj06fahKrBAdMRcXCtcdTAwMTWdXGJowWrR8ELm2OBJXHUwMDFmXtVf98tvSp2Km8L+4IIxUc9ek5VcdTAwMDUx32M9Ru752b9dXGKBJelyTCrPXHUwMDEyeDzY1lxcMqrSXi70uHMzRdeO3tZrsnR1urEvlFx1MDAxZb5t2GKZ+1xysuyV/GhiccVcdTAwMDX2XHUwMDFj4qN+XHUwMDAzkzZcdTAwMTbYodjnUIJFK0BcdTAwMDHZmbtcdTAwMTGY9ZPSw35/0Hig95uN992Tw83dzsxroNZ/qPBcdTAwMWFwQlxmXHUwMDA3I1KBvTOfZlPfnTxcdTAwMTS73f1wSZNZk3CxmO0+z+SaeNZcdTAwMTA/0816RGujsUdcXGi8pLBYvm2NVtSAQlx1MDAwZYdnmFx1MDAxMlx1MDAxZVx1MDAxOMI4KIhgcVx1MDAxOXVoY6kl7EOuNTwmYbXPvbhcYjR5ofRwxEQ3d4Yu2J5cdTAwMTL7oDtcdTAwMTWxiI7JXHUwMDBiXG5cdTAwMDbrlF7c2NltICynitrkO7tNeNZcdTAwMWEw8ih6wPyWXlx1MDAwZV8vhPZy4vVcIlx1MDAwMZJ0ve/Tlt8wh41cdTAwMDbGsFx1MDAwNVx1MDAxY5KgaLCLXHUwMDEyTkVcdTAwMDA5SUy4Q1FGZXlhjraKw7OX2+HBoNe9vHkoXGaKXHUwMDE3sy1cdTAwMDGTcCe1pVxyulU1alx1MDAwMK04XHUwMDEz0lx1MDAxMPHP0JbxXHUwMDFiPqu6TLpazIafp750m+ghfelcdTAwMTjJJbjHiVx1MDAxZHWpZDgmbUJfXG5tZKxcdTAwMWKJ4sguPKTBXHUwMDE2ucKMQ+fLXHQhyfoxg5lcbtamwmJ2l79ImuhcdTAwMGV1XHUwMDFju3NxPk31ysRtzO57XHUwMDE578ZPXHUwMDBm60VcdTAwMTlcdTAwMWV4edD545NBknyxQdew48bz8Vx0x5tMST5hZSd8wmKy9lRcdTAwMTjst8fB9rOcU+tqTElcYo7Q/DqEw0VMXHT11PhYNPK7SODeSVx1MDAwZm7sQFx1MDAwM7a3cII77Jn6XHJu0N5EXHUwMDExNsfKtMdfrW75vW7UTamxQW+blb3NRietKzjewzrx+zOVlVx1MDAxZrfpIV/f26Xl3klt96x3e/J8fJCXiznPJkbup5dCS2tjPS7pXHUwMDE3siebklx0S7Rnv7S0Y45cdTAwMTelwnNcdTAwMTWcLljC4UKhXHUwMDE3XHUwMDA0YFx1MDAxNuuVYdklJ6FY7cinXHUwMDEwOXdcdTAwMWNcdTAwMDe3XHUwMDEwQuVUQ0Li9fPs7YvgXlx1MDAxYndvK+V2daWPStSlpedcdTAwMWPDTVCQQUVcdTAwMWS5gnzUdbzRlqSugW1PhHBcdTAwMDPU2yjmKSNccpBq+IQj0X6prnNcdTAwMDX8QVx1MDAxNrBrXHUwMDBl9lx1MDAxMjGhMO1fo3hSXHUwMDE02lx1MDAxOWBcdTAwMWRDhHPMN6S9puXnTVp82bmrPvb0kOzXdtIq1s290rt5IY3DxsPd02D99HJ4cjrMQWH/mJiw++mlUNhwXHUwMDBiXHUwMDFlYO9cdTAwMGLbgaAwaGw+yrqyhjH0xIc1tlLOrKulxo5cdTAwMDRwMT2AXHJWnnN/XHUwMDBmXHUwMDE337CH6LahXHUwMDFjXGZtsMOn61x1MDAxYVx1MDAxYbNlKfZcIp1polevVqhccmuV50FtpOw+9N5cIijsXHUwMDA0XHUwMDE1XHUwMDE5rlx1MDAxMYhdR07z62PFWlxccIlROaGyyWR4SWDjQZ/OXHUwMDBlo5pcdTAwMTmPa1wiXHUwMDA0amL/2IBxVqW2XHUwMDFlg80pXGJcdTAwMTM4XGJyTOBcdTAwMTZcdTAwMDHui1x1MDAxNFs6zKCvNfZ+XHUwMDA1+yjkJsMnXHUwMDFlXHUwMDFkWVx1MDAwMrlNMXFn/JbzIedcdTAwMGLTn6VgpacwvEGEXHUwMDE2XHUwMDA09iTL8euhrZx0Oc5GiVJcdTAwMTi5x7FcdTAwMDJGp7xaiFx1MDAxM6RcZlx1MDAxYn1TzIWRiUNN5rZgV1xmzSjHXHUwMDA0eYlcdTAwMTZaaFx1MDAxZHnn7k+3XG5BuEb3v+FUXG7NaWBcdTAwMTVwkiimpLVcdTAwMTg0kPOZ+/DdoaP4/Zw1dJR0tehcdTAwMWSde+To6+RcZrMyisOLtd1cdTAwMTJr3F2eVtrvh5XzzsH1dVhrRlx1MDAxOLtaXHUwMDExz2LBOKBDXHUwMDE464tcdTAwMTN9tOEm0tOCXG54VoJcdTAwMTPrXGI0XHThXHTf4UrLkJ70XHUwMDFkau7d036M6jzNoDpBVElilZMqU1x1MDAxZenYMtxcdTAwMWFcdTAwMDIkaFx1MDAxYdU5panL+NFNr1TaljVuVJ3x1V6nkzpNeVx1MDAwNlx1MDAxZnLsdWdo3Fx1MDAxZlx1MDAwMv63+qbdTy+Fqas1XHUwMDAznVx1MDAwN5zYclBcdMI3XHUwMDBm+XNgxoSl61x1MDAxOJjBmMd4fIf9+ddcdTAwMWH9XHUwMDE4LJ+lx7I2XHUwMDFmvVxmXT5qoCNRUFx1MDAxZblUmKXT5DnPyUWNzt3y4LlX+1dcdTAwMWLNxo/SnUUwf1x1MDAxM5RmhL/6azkrrtXk1Fx1MDAwM3VfXFxcdTAwMWS2Ll7ubuvHlFZOXHUwMDFmS7XuU1x1MDAwNnVuPNDWVsLO0JSR4JRcdTAwMWPtXHUwMDBmSFGHIbxU5/mJgPNcZurcXGJAXHUwMDFlcXuuqYrugspcZtNcdTAwMTbsgXnq87XykSpcdTAwMWSS5v3d4Ub98eq0f9nc/sdccrZxrzKN3kVcdTAwMTfzaFx1MDAwMFx1MDAxMuhYSoN9aKhJYtREJKZuLfVuJOgyxIYtJ0RaXHUwMDExnlx1MDAwZjDCXFzM+DmhqGLTRYu+da5Nq9x7XFz5mFx1MDAwZVPu/6v9Mc1tXHUwMDEx1G2CUlx1MDAwYqrbiXWsXHUwMDA0l5GPno03LGJ7jVx1MDAwYuUpXHUwMDAyW4dcdTAwMWKrwZZcbsSH4Vx1MDAxZKIvyeKMQuNrT/lVyeBcdTAwMTEws3GwXHUwMDE1/MFJlGF4w1x1MDAwZfOY/n3A55Y6Nlxu7pfp4c41zvRcdTAwMTTWpWJNdCbIqGOoVmqOweG3/cfDU9EsP1xcXHUwMDBlds6fXG5PXHUwMDBmZXk4RTnhVLIk31x1MDAxYYmovY5HeJePL1x1MDAxN1phStfxN9S2XHUwMDExT1x1MDAxMlwiKI4qXHUwMDE1oz9cdTAwMTNcdTAwMWVXbFx1MDAwNuOvbaMzV1x1MDAxY8SbJr4lNNr9RrWWclxyinyMcfv8e6KrXHUwMDBm8TDZ6eugxMSsYVx1MDAwMadcdTAwMDW74ZKCqlx1MDAxOVx1MDAwMq+PXHRmKLrFzGReriScepZ/XHUwMDA1XHLDxpLlPyF5b6GkdVx1MDAwNnammZJCulx1MDAxZJw8ulx1MDAwZoPB7o5MqLxjg3nwM/RcdTAwMWbUqlx1MDAxZsxmXHUwMDExaFlcdTAwMDJcdTAwMDdcbnV5dt5+PnQs3lx1MDAxZlx1MDAxYkfHcOS3UVhcdTAwMTOLul34ylx1MDAxMz7sLc49XHUwMDEx5+dkhHtcdTAwMWNcdTAwMDdcdTAwMDeD4aZcdTAwMTjY4Vx1MDAwZZ9cdTAwMDfx6Fg+Ylx1MDAxM1x1MDAxZT2+zlx1MDAxMuOTXHUwMDE4v01cdTAwMGZxi4lYVihnXGaDRDZxZ2B9cXhTc2zwcLn+S5FimZnyL1K/K9xcdTAwMWOR+lZG31x1MDAwNIPFLkDVauRuXHUwMDFmfT28z2fnZKlcdE22KlDjYzRiMj/CXHUwMDEz1owrLI1k4d5cdTAwMTBcdTAwMTlJWfyE1qnWMJrFQo1SmltcdTAwMWNbhZ1xZeBlaDKSWZIwpVx1MDAxNCxkLrzsz/TRcyMsXHUwMDA1iYOnXHUwMDAz7Fx1MDAxNpNcIrC0yldcdTAwMTH8Kf9cdTAwMTnIf/Vb/jtKMOC8SXC3LVx1MDAxOV20tDdcdTAwMTmc3Fx1MDAxNvhcdTAwMTNn4ZHRIylcdTAwMTnJ6Fx1MDAwNFx1MDAxYYlC2m+IczFcdTAwMWM3MFx1MDAwYqP7aka3OKQugUlFttP7XHUwMDFlXlx1MDAxN+/vj+N1XHUwMDE0kVx1MDAxOVx1MDAxM8tiXHUwMDEzqdphYMNcdTAwMTc84lx1MDAwYlT5Ulx1MDAxZcbA9ihIVyOpklxuR3vKuTfw+jFAr6bHueFSXHUwMDFi5lx1MDAwN6zP0Vx1MDAxNpnUSYnFuIhic1xmZYFcdTAwMDJud+vF7ePq6+N1pXm0W3ls7mSkdZLxXHUwMDA1SFx1MDAxOI3e7XiE9/nstC41Jcrkp8IsTWO5pvC3XHUwMDA0ilx1MDAxZmgkZXD2nbVg1Vx1MDAxMdgpPM5RlYrXxVx1MDAwNyam5aaTR2DAniQk3Hxlof1rbpSkoWaaeVx1MDAxNk1zXHUwMDAxRF0oXHUwMDFlNM2tQOZmLcEh62HL3EqP41x1MDAwM+RcdTAwMDQuXHUwMDAxXCLFkZa/ZGbRXHUwMDAyO0NcdTAwMTNcdTAwMGLKsFxyJmHhvMFcdTAwMTFcdTAwMDGLpmbUMsNcdTAwMTmdpvAmiZrBflx1MDAxOW+JKajZR9hwcXhZXHUwMDAyXHUwMDEzXG7yMvft5+Rsu9ZnlU5la+O0zp5f+Va9NFxc42lcdTAwMDBdXHUwMDAwXHUwMDAzybOUwXZcdTAwMDExzJk2waaqVHnSMsyuN0S6slx1MDAxYqRaXHUwMDBlUp9cdTAwMWXUtSyl79wwLq2/ta2vuoZF5lx1MDAxNFlcdTAwMDIv0FiVdzWdUVx1MDAwNnT2LJD+XHUwMDFjM/hcdTAwMWJcdTAwMWGTXW+/XHUwMDEx2TEtaiZuP2Isovt+84Fyb7/6i8uDm/Pj4+fHu7PSTWnt4T11umBcdTAwMDFMJ49YYbBcdTAwMGVcdTAwMGKsLFx1MDAxZDSypFbYlTImW0k5zCpKl+7xKFx1MDAwNN9lSVCSiimqXe5xySNcdTAwMWIgM7hNZY3mU1x1MDAwMfiLmmYypHZPiuur5nqHlHbrtd2n0q487W6mzVx0PDpcdTAwMTlcdTAwMWPp1Wq5em2GulQ52t+hvlx1MDAwMVx1MDAwMFx1MDAxM78/e06gpYLkVXbuXmVcdTAwMWG9XHQ86aOmiGtmOdeBYVx1MDAxZoomJVx1MDAwNVLqcYHObJxcdTAwMDXib1x1MDAwN7fMXHRMxtx9hiQhTjnHXCI7XHUwMDE35qJzXHUwMDAy0ZFcYsLSToe5uO2LfumZePBn7vpg2Mes9VFWYOduIebUJeiuiCT8z4WsXHUwMDA015GPPo2XRfFcco5ccvVcdTAwMDSxkoLlQqny91x1MDAxYvlcdTAwMDC5XHUwMDAwXHUwMDE5XHUwMDAwtjSaw1x1MDAxM/MpvzyWUnpcdTAwMDa+rVx0UDc8XHUwMDFj3Fh5XHUwMDE0LS5BlVx1MDAwMLPZ0rFkW4J+XHUwMDEy9HU36MOjQH7XyIZGgeCQL6NxXHUwMDEwmENcdTAwMThoXHUwMDFlbVx1MDAxNqNHXHUwMDFhOPRUOSixXGZaKk2nilfkXHUwMDFiQ47cp3hcdTAwMTRCW3R8vZC6ztxqOOhcckt28cVLmZWpQ7f+LsShyK0kzFx1MDAxMCok/C1cdTAwMThzPIKFdvhcdTAwMTXWTnZu7b16ezq31b3asX57rZuwXHUwMDEwRLlcdTAwMWGQgZJ6Rmqw7bXG1Fx1MDAwZSZCMlDBXHUwMDA3mMQ29sB2wkRcdTAwMDeHsvhcdTAwMGXjSMah0njGdyxjNlFcIvAhg7OAMqvQV+ByXHUwMDE2RHfOklx1MDAxNIzGKcuP4lx1MDAxZICwdzRZgFxm5qj9iFx1MDAwN/Wo71x1MDAwMObIZrpcXIHKydM8Q5gmWjCGxWlaaTNxrttpvt2PtkOS+Pi1zd+ei7uXW7vHezV+d1wibtfIelh8RJUwSjZKjFx1MDAxMUiiXGKxk+VcdTAwMTXSXGJgcOPyijH/+ZIgXCJZhLDAg865m88/ZOD9c3rxgdlJRFx0ZyNcdTAwMWbGoplcdTAwMTJcdTAwMGX+sCBAckzl+9iD7dYu3ajtbD5cZt5cdTAwMGbqr5fHd3el0nE+Plx1MDAwNUPAfvf7tmfyKbxUNnZcdTAwMGZcdTAwMGa2+7S42a3emap9Z/379GDR2lx1MDAwM1x1MDAwM5FcdTAwMDNcdTAwMTQ0Re1cdTAwMWFcdTAwMDCLmqj3dVx1MDAxNCMtwZJcdTAwMTNYXjLoWrD/rMX4h1x1MDAwYi0mXHUwMDFhLVxmm1GTuWZIvD9cdTAwMTWPro+vb9RatbT7Zovv2y9MpXXsbZSFOHu6W2eHN7vrsrB7ZouXl7mB0Fx1MDAxMn+W/0wgdK8yXHUwMDA0QldcdTAwMDWJXHUwMDE4XHLExlZMUlx1MDAwYlx1MDAxNXCfm1FcdTAwMDA8XHUwMDA2f1x1MDAxOFx1MDAwMudUKczLsFx1MDAwMrtcdTAwMGU78JezY+9cdTAwMWZcdTAwMDK41yzkXHUwMDE2bTTC3JW+PLKPO5i32E6QTFXqm9RoznLfi53Cr9euXHJeO73HlXKlXHUwMDAyv3mwclxi/4qa2TDHgFiSQlx1MDAwYnr1XHUwMDEyl5FTXHUwMDFlYveu0lo9q91s313dVO9/Vaqvu+lbQlx1MDAxYmo97FxyyFxmXHUwMDA2VP2sc/RcdTAwMTQtkVx1MDAxZahanOkhgLo6eukoXHUwMDA3ruXcXHUwMDFk9j9sWugwPcJcdTAwMDUxXHUwMDFj88FcXFx1MDAwMJfRU3sts5Kjg2Z+XG51+Pwm66fNTm1b8NutXHL+q3N2t5FJ8WVcdTAwMTZcdTAwMWQxsHDfTVx1MDAxYcUniGcldlx1MDAxZsE5rf6xx1x1MDAxZohgJFx1MDAwMVx1MDAxMYx5VGK1qWSj0uDv13v/OHy8ZcFcdTAwMDcgROtw9i2+rOhucVxmQ2FSTzXIJM9cdTAwMWRcdTAwMWNWfp1Brb9Sfik3muXbZu3/LkI4K0HFhFx1MDAxNF/kXHUwMDEycmoktbN5/lbn5o1u84t68WFv83T9KJPGw9k3XHUwMDA2RCSS06DGk9ajRnLGOFxigaXGy1x00e/pXHUwMDExzSUlSmvm8riomJRNK1x1MDAxOHbin65X+nQqb/3qslB73u8+Ny+ee1uFjZd+6+Xlj6k8992kUXnceopcdTAwMTjsgiw06K5AoZlVXCJcdTAwMDFcdTAwMTJcZlhcIpaaUYbNXHUwMDA2XGZdiIaKP1x1MDAwYiFrJD1CXHUwMDE0Z1xuRZiLXHUwMDEzKlx1MDAxYm30XHUwMDE5plx1MDAxOLzgqUbzfa/W+92CfzD8V1x1MDAxYpRP/7nVaN+vjFx1MDAxNMlcIijAXHUwMDA0jVx1MDAxM1SA49WsRC4mp6SO1vFB66zzWtsqkmbrqvX2LtZu06tCnKWpOfpvjFxiXHUwMDE5f1x1MDAxOKJcdTAwMDCqSyjTJMr4XHUwMDAzwYB9YyhcdTAwMTOT016XmvH3nSXinqbHPTxmTlxyMWGzXHUwMDBmTmpcdTAwMTFcdHypqbGKqTmO7etcdTAwMWPf1GuD97vbzqF9ouywXXztNP6YZnTfTVx1MDAxYc1omCeFxIa0loBBLVx1MDAwM1xiYVwiXHUwMDFlIYJ6eoRcZsY1UUQuRFOdXHUwMDFmXHUwMDA2XHUwMDEwlqUwXHUwMDAwS7KUdXJHI6K5I2GS4KidhVOMo1xm+0VQgVx0msZZ5JO3tnswj+Z+6+h9eH4ht9/JLitcdTAwMWNVXHUwMDFkKYyR2o6AtlJW4TBcdTAwMDCHtlNcdTAwMDQ7nnFE+9LVmVx1MDAxYnp5evRiTlwiwzxyp+XHo1OU4Y1Zoe08g4dntPgqXq6fN+RBofXwTHv3j+yPqTf33aRRb9p4XHUwMDFj9Fx1MDAxYUFWoTVcckJcdTAwMDInzcdBQoDdiHaf4Fx1MDAxZuJzITqM/DCAiFxm6s1cdTAwMTBNtWRcdTAwMTH8L1x1MDAxMiCCWKop41PlonyrevuoI1tcdTAwMDT9lqBb3Fx1MDAwNXB5K7jH1ba+P33Y2CncnVZ18XhcdTAwMDfAuZZawTGLM5dBtUmOXHUwMDA1kjSUMiM89TWN3bgq3ibT/lx1MDAxY15cdTAwMWN/SHmZkzpcdGSZXHUwMDFlyJowXHUwMDAzXHUwMDAwcjpwWHTDIClcdTAwMTW8VS7n2Fx1MDAxZa6/VT9pmiq/Wd9rVY5cdTAwMGLdg1LlZiP1iJvTnbvq9f7D40tth562X6/fa6w2+VumXHUwMDFhcTNDXG5cXOx1vzerXHUwMDA3/as5ZfW430pcboXPmfU0dlx1MDAxZZJMXHUwMDAw0+I0kNZjqY2XXHUwMDExXHUwMDE2ODSP9/Qus3rc4iFLh1hcdTAwMDS5cJuxVEbWx+JMXHUwMDA2rHDPvWtcdTAwMDXsXsa4ybJ7Q6q+0un0qqBC55XJk6DvXHUwMDEzVG24mZjj7vNR+uy90Kyoyzp/0eyudnNBTy43XHUwMDFjY+5cXFxyYo3wiDBcdTAwMDLViYX9XHUwMDEyXHUwMDE4cWdccvW4NJZcdTAwMTJcdTAwMGJvT5vwzGdGiadAo2CTQPw/V89cbupcdTAwMTlNXGK2tNFMUb9cdTAwMGJsyeddONdumGeozuNUw/NcdTAwMGXnlo8sXHUwMDAwXHUwMDFh6eWFl8lQKps5XHUwMDA2QIu/Xq+a95ub12fdzf75xq/9zfvXjHawVWDRTCNYcq2GoYx6lmmujCRcdTAwMWFcdTAwMWXhZHsuQDC1o3FE6IFcdTAwMTfG30fefb1oZH1cXC+AqfHlQk9sXHUwMDBlhYStk8fnx+2zq41iaWfn+OVme+u5fup73d/UXHUwMDAzVlx1MDAxOU1Hllxujnw19oeVXHUwMDEyurd+XG5cdTAwMGVGiTSYWi2JtJppXHUwMDE2MNOwtVx1MDAxOLZcdTAwMDSzoMuFL+3P3ztsznnV/zhcdTAwMTmdobUrlnzCNlYhsYuvXCIy/Vx1MDAxMrhcdTAwMTLjhE6VfVx1MDAxOS8slS9le/pAe3VlMFxcXHUwMDA0XHUwMDE2lsB9XCJcdTAwMDLrk3efXHUwMDBmXHUwMDBi22mWTk66xyfHa0+loVx1MDAxOLRuXHUwMDBih9fVVCyMjrr9KYGt/lx1MDAwNKWBxmFcblxmP1x1MDAwZvSytiDpfDJwgoNJZYUlTFLQ/NZhUlx1MDAxMc+CZYbtYi0oKGXM3LtcdTAwMTD9MIDbmUlcdTAwMTiYUsLgk1x1MDAwZTXrXHUwMDFmoUREXHUwMDFiYdhcZlx1MDAwN7n3XHUwMDFjU6/p3mPhSTyuXHUwMDE2blx1MDAwYrXOUaXVaNw2ij+Rhlx1MDAxNcBkUVJrTEjhOkBcdTAwMTgstk5cdTAwMDW9XHUwMDA3llxmjifTiVx1MDAxN4uG1ug0UFx1MDAxNKDTXHUwMDEyJ2BhU19D8m3osFhsSzNCsY0umFx1MDAxNpprlbq/7PQ11vOhYe6tn4qGaelJYTRsKKDnSk5az1hU5SG9iuJhXHUwMDFh+0VcdTAwMDIxXHUwMDEwXHUwMDE4WEGSv+RhmcX0aobYl1x1MDAxNYqhryPUseGvuCF3oypcdTAwMTjLRb5ccmvyYGKfvVx1MDAxM1x1MDAxN4OHJbCfiFx1MDAxNq75s7D4iEFcdTAwMWNcdTAwMGJcdTAwMTMqdliSsVx1MDAxM33oXHUwMDFj9eLSTkx/drCwvHM9/iGO7bX0IDaSMdA+zjFcdTAwMTk0XHUwMDE0XGb7jWGjQKdcdH/i+bdcdTAwMTMqoXb3Tlx1MDAxZvZeVy9cdTAwMWV2LmvPXHUwMDAzvl5cdTAwMWM8Z1x1MDAwYvdIK9lU7e1yJVRilo9cdTAwMTeCM9NF0vcpxpd8h/zOy8lcdTAwMTT9vFwiQFx1MDAxZPp+6IWmJHPxYYSpyFx1MDAxY46AXHUwMDEylIPBiDaIMCzA5Fx1MDAxOIcz6vNvoc3Mo1x1MDAwMupcdTAwMDfN1cvKRXlna2O3/txeK+1dv864XHUwMDA2JKSMUatR1mJLQGtsYFpcdTAwMDCljCjk8yCzcVx1MDAxOSxmXHUwMDE5c1x1MDAxYVx1MDAwMVx1MDAxNdV35VPWTbQ0+rinwLKDXHUwMDAwit3zXHUwMDEzu/ZjXHUwMDE5ocvFXHUwMDAyIOnqMeIqNN9qUlx1MDAwNFx1MDAwNVx1MDAwNlxcJXFwt7RMwcGFklx1MDAxZcGtbMHSNtTfo+uzJVx1MDAxMvO1RFxu+04wY4XPufDoXHUwMDFmorXX02ttNLYpdavtmJxqkEyMgzLMP1x1MDAxOI3D0ESWzsch8v1Rovp/Vj4u3V+56/T+1f7dqIEuXHUwMDAyI09gwu6a24lcdTAwMDWtONaTXHUwMDBmS4/Pv5mBpVtuPUKU1Fx1MDAwNrsxuVx1MDAxYclcdTAwMWHpKd/haOpcdTAwMDRGPWw7UMNYt6FcdF02dXLBfyM9/LlkWlIhnbNMdST+XHUwMDA11ioxMcdg9OHdXHUwMDBikZv7g6tSrXhSvFx1MDAxZfzaXW/1vrf92feQdjBTPyY3flx1MDAxZVTk+PVcdTAwMTA+XHUwMDEySXRcdTAwMTTmXHUwMDEyrvaDKLXUjDLY41ZcdTAwMWFC1ax8Or5LZC58XHUwMDFhZGNwgJggXHUwMDA0O+h/LJKFp9v+c1x1MDAwNqq6cZ6CbkqiPEYoM1x1MDAwNvaqJv6Skk/1wz2ObNQqeIT+3o1LwjmzxtlMr3GUUURPzEv093SJJJyjiZlKTJX9XHUwMDE45+v9ZrrJXHUwMDE2gW4mULosdJPlTTfj07JnoZtcdTAwMDZ+M0hcdTAwMDNCXHUwMDEw8iyc7cwpfuDroK4momrCXHUwMDA05z7Cs0T/XHUwMDE4/VtcdTAwMTlcIj04XHUwMDE3XHUwMDE5p1a7qiNcdTAwMDRcdMXjx5NKXHUwMDE0vGF0J82Pcsq6IHZ35+pye725073cv673t00mylx0XHUwMDBiXHUwMDEy0yVW50o5pfWEXHUwMDFkXHUwMDFmgclcdTAwMDczfp2jXHUwMDE3h3CrXHUwMDAxQkxYQ2TS9ajlnp2A1XdeL1x1MDAwNOGky0XLhYTL/SBObKyyXHUwMDAwRS24UmD8xVx1MDAxMcpUrDi+e+Z3sWJlLLdEXHUwMDAzN+bCUId5smhO5olcdTAwMTBFhJM5XHUwMDFlbX64pvExJ10tXHUwMDA2bVx1MDAxM07mv+NcdTAwMTeWxns+260krjReXG7ktlKRYqWz3crMK41cdTAwMTZR84xcdTAwMWK4tWdcdTAwMWFDTmn0XHUwMDEzXHUwMDFhXHUwMDAxXHUwMDAyjDPlq6NcdTAwMWOl7nCw85TmTEhYnH9Y0dKMm5nIZZm9TFx1MDAwNZA4K4mzd23M6Fx1MDAwNG3AlNO5N+f8ZjvucFx1MDAxMey4XHUwMDA0WymLXHUwMDFkdziDXHUwMDFk91x1MDAxZp9C469yt3s6gEt/XHUwMDExg79eXHUwMDFhtde1aHj8x6ewQHjURjm7//6Pf/9/zVxygrsifQ==RPCsubmit proven txbasic requestverificationverify tx proofquery stateStoreinflight stateproxied queryverify stateinflight transactionsinflight batchesbatch builderselectbatchprovenbatchblock builderselect blockmempool eventsuser executed txuser proven txUserBlock producermempoolNetwork TX builderbatch proversselected batchproven batchblock proverinternal tx proverssubmit txremote tx proverscommittedstateValidatorsign blockverify and signre-execute and verifyverify signatureand commitmark block asprovensigned blockcommitted blockproven blocksubmit proven batchverify txs andproofsnetwork account N actornotes available?execute txconsuming notesprovesubmitcoordinatorexecuted txproven txnotes & updates foraccount 1notes & updates foraccount 2notes & updates foraccount N \ No newline at end of file diff --git a/docs/internal/src/assets/operator_architecture.svg b/docs/internal/src/assets/operator_architecture.svg deleted file mode 100644 index 961c58faa3..0000000000 --- a/docs/internal/src/assets/operator_architecture.svg +++ /dev/null @@ -1,4 +0,0 @@ - - -eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1dWXPiSNZ971/hqHlcdTAwMWRycl/mzftWhfFu19dcdTAwMTNcdTAwMGVcZlx1MDAxOFPGgFmMYaL/+3cv2CBQSlxiUNluXHUwMDBmVHRHXHUwMDE1XHUwMDAykZLynHtu5l3++8fa2rd2r1H69u+1b6WXQr5aKTbz3W//xPefS81WpV6DQ3zw71a90yxcZj553243Wv/+17/yjVx1MDAwNlx1MDAxOX+LXHUwMDE06o/Db5aqpcdSrd2Cz/5cdTAwMWb8e23tv4P/w5FKXHUwMDExv9882eLHpYuGeTgo9be3M/eb2YfTwVdcdTAwMDdcdTAwMWZ6XHUwMDFiULNUaOdr5WppfOhcdTAwMDXezzDtJIH/qFx1MDAxNdRcdTAwMTiljVx1MDAxYVx1MDAxZO/Bcam0IEI4J5zWXFxYOzrarVx1MDAxNNv3eD1MXHUwMDEzpfn4a/elSvm+XHJHmLGEwlx1MDAxMfr2cqPPXGZH8u81Onqn1W7WXHUwMDFmSpv1ar2Jw/1cdTAwMDcr4Z/xYG/zhYdys96pXHUwMDE1R59pN/O1ViPfhLsz/txdpVo9bfdcdTAwMDZnh7tcZnfz29RvXFy+XHUwMDBlnE29XHUwMDFm9S340fJ9rdRqTXyn3shcdTAwMTcqbbxFjI6vXHUwMDAyR9jYL1x1MDAwZVx1MDAxZdZ/xmNq5lx1MDAxZkv7+LRqnWp19HalVizhM/h2q+tcdTAwMTM/Vyu+/tzbo1x1MDAxZT9H8frOX+PBl0p4ZiaUMfCA5Pgmj+ecXHUwMDEwYvrdbL02mH+cXHUwMDFhTqk0xozH1dqCSddcdTAwMWWc9i5fbZXGXHUwMDBmXHUwMDAxx7Y9PSGDk3JizrVLL+MnXHUwMDEzmLInt1vC3j5eZJ9cdTAwMGV2X07V9/Wd68fut9Hn/nr92/hcdTAwMDZ2XHUwMDFhxfxwPMxI5oQ0XHUwMDFh5ipcdTAwMWZcdTAwMWSvVmpcdTAwMGbTd7daLzyML+GPwE2bXHUwMDAykH80IVx1MDAwME1cXMxcdTAwMWJ2XHUwMDE0gMNauHlcbuDB6Fx1MDAxNHZcZkBDUi4jscOEIE4xa1x1MDAxZFx1MDAxNZZa5WRcdTAwMThFnK5QXHUwMDEzgZqGXHUwMDFmNVx1MDAxM1x1MDAxZn+FXHUwMDA3Z4YzYcM4wKFYXHUwMDE2hVx1MDAwZabhXHUwMDE5Sk2tW1x1MDAwNFx1MDAxZGlO4PF8xHlcYpe//dIuNWv56lqr1HyuXHUwMDE0Sq3A06zX2qeV/vBcdTAwMDIm3t3JP1aqePvVxPnWq5Uy3olvXHUwMDA1XHUwMDE4eqn5LXg72lx1MDAxNbBJo1x1MDAwZtzW2+1XyzT4QFx1MDAwMU6ar9RKzf0kxqjerJQrMOaz2deQ77TrJ6XW8CrazU4peK9KeyNcdTAwMWJDuIrB9vHlRrnzcNzd46Xb2l3m9rZePXxcbmNcdTAwMWJPOY1tITWxVnEntaHM6fHMXHUwMDE5YJsqTpy2kjLO4WHaMXLfwG1cdDdCXG7jsYzaOVwiXHUwMDFjXHJazVSw/Y+7wesjce2Wh/VTclhcdTAwMGJONXNKy+CMfYO1iTR6TDFBXHUwMDA1oDB9WFx1MDAwM4/L4FRNXG7rRr0ybVPHf1tcdTAwMWJPi8E/Rn//zz+9n86E5lx1MDAxZb7Gs278/ZCpreZb7c3642OlXHJcdTAwMTeWw0GFbn0732xvwPOq1MrTx0q1YsSRwbfWm816976UXHUwMDBmPX34XuSxRr3aK1x1MDAwZp7oLEtOj1x1MDAwZa73W81f27ubL3m6TW/PnvaP5pDCXHUwMDAwZsJB6irDfJBcdTAwMDdcdTAwMWJCmFx1MDAwNDttzVx1MDAwMPIhxDtBrFDOXHUwMDBieW5cdTAwMTlx0nFmjXT4XHUwMDFhz5KvXHUwMDAy/TRMejM59lx1MDAwMcKaWSetXHUwMDBm/IJF23SUXHUwMDAxzoo0XHUwMDE173D+5XPPlVx1MDAxYvuw3ji5/9H5dXNeOm5vNCdcdTAwMDXw21x1MDAxY8zjbFx1MDAwZkjef/rPO0tJ7/063toq0/Of33uV5+7Zy0OeuufZp1x1MDAxZH75PPOYr5+fZLv9851dtpHJ5V86dqnhXHUwMDBlz3uXzb6U+4W9evbC6MPczcND76iXwnl/Xl6f7Gycnfeu1Pn9WVlU2J14SuG8p9ubXHUwMDE3/Kad/fm0337eqj0/PuV6xVx1MDAxNM57eLBrMzcv5e7R5Vx1MDAwZlHKnJ/x+4ZK4byslz/IXHUwMDE1lap2vre2u7aYK7bLuymct7qXPT2612ePnZPDrWJnc6/Lm1x070PIjExZRnCBJNdiXHUwMDFly1x1MDAxOMPz/lmfyGNcdTAwMTPA0dKBs1xmLFx1MDAwZTyvpime83iKN2BZmXNcdTAwMWOYXFxcdTAwMWJlXHUwMDAynsOI6VXKXHUwMDBl21x1MDAxN2H2VnJml4pR7uCPh9i5iiZ2qlx1MDAxNXPglC9E7DGqXHUwMDBlXFxAI9VcdTAwMWNzN+Ssfa/ni3/WbvPVPFxmNWDjXHUwMDAynlrAz0/FU2vXXHUwMDAz7vGUmzZDKE27aTj6tfDg03HR4o3mXHUwMDE040yh2UhBlGRK+n00blxyUdRILVwi0CxcdTAwMDR8XHUwMDAw/Fx1MDAwN8GGq5TjXHQ3QrMgwlxiXHUwMDAzQlx1MDAwM1jB4Fx1MDAwZo1/46ugO1x1MDAwNZet7Vx1MDAwN3d4/ZK/vlx1MDAxM1q/5Fx1MDAxY1SZVkpcdTAwMDdn9CvmlYvEvIOnXHUwMDBm1oWNn0pcdTAwMWGQfzVXXHUwMDBiQT5VRy5yguIrXHUwMDEznpvj84UscoqOXeCxvu5OJKGVXHUwMDAxsVx1MDAxNTp4ayhRYD6lXHUwMDE11mmp4Ops4EPlfFx1MDAwM8FLXGbnWo2d1b8mx5nUlfzWblamnL1S9bbeTSQ0XFy++Ou+dcrV5pbKdFx1MDAxZjNCybN6mJtcIlx1MDAxZEqmXHUwMDE4kdRcdTAwMWHBvVx1MDAwZSVz4FByq5SRxu9QSkKn1olGxGRcdTAwMWPhUtNcdG9zxUgjRurM4UhS6lx1MDAxY9xjOf7VgCPp1PS7I+7RWlvu2EKLSH4hPsvfu/l+eLXJvqv129rDXcm52nnN8Ll0uJYgcsfzcClcdTAwMWTuXHUwMDFmTVwiXHUwMDFkXHUwMDBlfEZcdTAwMDBcdTAwMTFcZu+ftDyswyWoXHUwMDA2pqlcdTAwMTTIXHUwMDE0YWRIVFx1MDAxNfDUwJF33K/DV4urPlxcPM+BXHUwMDBiMC5gdaz0LbDA3Y/U4WCTwbuyMl1cdTAwMWS+wNxccunwk9zmu4jvx0qxXHUwMDE4NFx1MDAwN1P6e4ZdmdbfXHUwMDEzo05HdW9lq4V18ySK6q6S3c11nsBmy+SWXHI8aLA+1IBLzz2GTShHmJDgOFk/fFx1MDAxOYdcdTAwMGaoQGzA+Fx1MDAwNCP82lx1MDAxNX49+O0mx69yIFx1MDAxZoFCffCVzky/O3ajXHUwMDE1XHUwMDEzXHUwMDAymHd8XHUwMDA1v92u5Z5y1bx8PKg/XHUwMDFk3+V0+fqht3+ReFx1MDAxZDP7Yvt78uKqdl1cdTAwMTPlp++n99+Pa9lcdTAwMTTWw84vj8p5/tg+uDrf29+93C00Ly76KZw3W9y9VqJ7dFTey3V6h3Inw053UjhvvZwtXHUwMDE2XHUwMDFm9jKX94rnmmXZ67DL41x1MDAxNM7b7WbM9Xd6dbi/n1x1MDAxNUeZy7ueeDpIYV1QcaZcdTAwMTmGR8iUXCI5/LMoiVx1MDAxZVx1MDAwMVx1MDAxMVx1MDAwMb5cdTAwMTRcdTAwMDc5zVx1MDAxNafgmkzxmdFEx/GZNVx1MDAwNCxcdTAwMTQ4o1pcdTAwMGJcdTAwMDOUOFx1MDAwNtdXXlx1MDAxNkyBzl7mXHThYFJYwY1viUDwyM1eeGaOmlRDOHDmgluH3usyamRcdTAwMDOP/1nLNevFznstXHUwMDBizlAmM3TBtDJcdTAwMTlcXMFa+Fx1MDAwMtJcdTAwMTEp/f3ds/Vcct5061l1cFY6XHUwMDEzj7XaQXKRMnC/mUL3wvlUXG5u98JRaVxchJOxcr9cdTAwMTfFddBccpzpZnDUKHCTfcv9gkW634xcdTAwMTnqrLaLXHUwMDAxezGdsrVO2/3dbuGJ9ot7N+vHt7nb0+pcXP63dFpcdTAwMDVcdTAwMThsKXvnXHUwMDFmTVx1MDAxMntcdTAwMDf+tySOKW7B01x1MDAxNpZPXHUwMDAxg1uiXHJcdTAwMWPV3L9uXHUwMDBl3rdcdTAwMDLv2ygmXHKXRjBf3OJcblx1MDAxN1x1MDAxZVxciDlim1x1MDAxY9hcdTAwMTgupPXAgoWN4Fx1MDAxYiwsRlx1MDAxNmpcdTAwMTAj6Zm7YWiTcmq5iMVP4nzPsCrv4Hzv5GVh29W2Xurm8Ojmtnd/dVm6SVx1MDAxOHHsXGbhjGpHgTGDS4/DZTMtXHSLXHUwMDAzLnOUSOGsklx1MDAxNP6TaoXb4ZBm4lbNYc+M4NRK5l1O5jJk5d6AqyjnXFwzm/ZWloWpwE1wos5cdTAwMGLcZqNA2qVWu1ZqXHUwMDEzXHUwMDAwV6lGKvV3QXLMXHUwMDFl9sT4pzFcdTAwMWI/4HRA3MrXr/ZcdTAwMWF721x1MDAwZv1fxf7d1U++U6mo5OLUMfA4XHUwMDAxpVx1MDAxMeHFmFVcdTAwMDPqkkYjebWCtiCU9Vx1MDAxY1BcdTAwMDYpJJAvfVBWMibEkEsnXHJdLFx1MDAxMmUxadr4sStcdTAwMWXqz7/uMs3W6Wlm09Q2srmPXkJTL9tcdTAwMDUmLlxmP7t92t/pt8/7XHUwMDE3ZzzZeWOl9GDpiHFl05LS/ruXxCRbI4mxUisluFx1MDAwNp2kp4AsZ1x1MDAwMFmBk2lASzPBMH48XHUwMDEwXHUwMDAwsTLJsTg2yXFcZlrHgHHl3lxyXlx1MDAxM71cdTAwMTJcdTAwMGXe/SD2XCJdLVxyJtnCqZdcbig7bdebpXexwTPU9FxmMzhtmafGnY4pvixdXlxu2fm1l+uXbp9OSoe10+plMj1cco+DXGJcZsCQ4OBINVx1MDAwNV5M8oEnZsVrOGg4yUdpXCJcckZcdTAwMGYw3DZxcrVcdTAwMGY9XHUwMDFj0kz02uToVVZcbuGMNzSMRW9DS0ZccrOKpq2njaQs8JxcdTAwMTdcdTAwMDBvrnNbrVx1MDAxND61gp5cdTAwMWViSlx1MDAwYrrXXHUwMDFifVa6zD5v25dcdTAwMWWzN1uFi41MMqBcbsFcYnPGaInuLZteskKkgmhcdTAwMWV5vmGkXHUwMDFhQzDtZlx1MDAxMHJm/Wl5K6T6kOqSI1x1MDAxNWhQaHg4viUrYULW91xyqcZyaUE8pVx1MDAxZC9ihJWByPVcdTAwMDWQul9cdTAwMWImqH5qrIZcdTAwMDeZXHUwMDBlWnefLCvou9t24+nutP24ccp4dX2e7Vx1MDAxN62JdVx1MDAwMmnY5+IqxVxieFaMOeePflxcubiLQnY9OWRcdTAwMTmlnFHwX3za2PHI1SqBUfdUL1x1MDAxNuP1emBOXHUwMDBm90dJXbv6xf2jfX7sn1x1MDAxZVxc986vjlx1MDAxM3u4SySlvf4tTs/jPmBKnqj/Klx1MDAxM9lIJlx1MDAxNPia0imutGUhNavsXGbAXHUwMDAxXlx1MDAxNYhZXHT+qLIuXHUwMDE497cykbF425hjV0eAelHWu9lpdMyKXHUwMDEyzDP4o1x1MDAxNlxuykpz6oZs5E6+Uyi138VCzvBFZ1x1MDAxOKxpuzk98HSsZnyW6qx8JoX1ZDh3mnIqxFx1MDAxOKLDfVmMulx1MDAxNJZppVx1MDAxNVx1MDAxN2GJK6Qg1GLuolx1MDAxNGxcIsF5jF9iuYBcdTAwMTNcdTAwMDPEuaOW6lx1MDAxNZ49eN7043mOdCajpFx1MDAxMcoxb6ySilTCXHUwMDFjyFx1MDAxOWwqtWnDfJDPtFx1MDAxMMzTzWeKmqH4yoQn5/h8ISP8mfKZMpRcYm2wpojkXHUwMDE0Y1xigVNcdTAwMDNcdTAwMWZcdTAwMWJmNLHwXFx5t0Sm7P7L0XFr52kj89J6eX6q9i6LP34kXHUwMDEzXHUwMDE1hjFcdTAwMTRcdTAwMTVWWVxy16emIyM5JVx1MDAxMmYst8xLSZwr0PDOcqqU1iBcdTAwMTg9KZYrTeHjoK05XCIjXHLHwFRvXHUwMDFkXGZcdTAwMWHyxcfxU+CcKblgXHUwMDFkjNhccmelzFJcdTAwMWLOd1x1MDAwM8v899pznjnmdORFfLGKWfKCXHUwMDExyozBfFmAspmOh9ScUKqdZkxyzsZIXHUwMDFkp0uDQ1x1MDAxZlHfRlx1MDAxMMVcdTAwMTmmoVqnwNJcdTAwMDSXXVegXHUwMDFlgXp7eWFhtbMuWJAhkNTBXCKFXHUwMDA1Y1x1MDAwNlxmrmGBMlx1MDAwNl9LWISmJr4y4Vn591FcdTAwMTRcdTAwMTg+XHUwMDAwXHUwMDBmjCthuFx1MDAxYS9oj/RcdTAwMDTHXHUwMDBipoyCr8A0XHUwMDA1j91TXHUwMDFi9P1cdTAwMDTGdnvjeqPPb3LPNb6VLz02mvvbXHRcdTAwMDVcdTAwMDbWXFzR2lx1MDAxOO1A/LrAitKQlYRcdTAwMDFZaOBwxFx1MDAwNjqHR1x1MDAwZlLbwlRcdTAwMDRkOG08y4QrieFjo53kXHUwMDEyQ4IyZ456d9C5idRcdTAwMTjWYnRTMGkzJYlcdTAwMDFkxpbagyu9NOBZlpp/L5GRYNTpyFxm2egpd9b63jtSe8d71eNm/1b1w3iOriotwNHTXHUwMDAwSH/lXHUwMDAz8IyJ4ehORKVexJTSg+8qsGdgzlao9qB6f46ddUOlXHUwMDEz/lxuejq6Kq7EMtPGLrZfNzJjc639l+3zuaCbzcLPzMGR2OKKO73/XHUwMDBlhdhCXCLht5ai9l9lXCIrqrkkUoE2tNRRXHUwMDFkXFzXXHUwMDFiXHUwMDAwXHUwMDBlXHUwMDBmO4P7s1GFzTTBpFx1MDAwMZxcdTAwMGVcdTAwMTS8dbFcbmRcdTAwMTlcdTAwMGVpJtxcdTAwMGWSw41ZyzU8JG9cdTAwMDajltHlalFka1xyTzdVM5pGXHUwMDE16jeD9Fx1MDAxOZb/Z9isKFuatuksV/mP9cNSp3O1e7N72MlcdTAwMTaz3aeLebbNrSVCOoXBS9IwN1x1MDAxNenCOCOOSVxmKGU0qKxG2+bGXHUwMDEwv4vuNFx1MDAwMb6nuHWL32RcdTAwMWU8XHUwMDEzdChcZmapScbgt1x1MDAwM2Vq/6drzZsjP8zn6NAgKFx1MDAwNpJcdTAwMDdWhic897hcdTAwMTL0lCpOaerlan9X/df061x1MDAxYsxSXHUwMDA3zVx1MDAxZvZmm9Wvv7+Yo6fde7F3Vrc3nyMyQINcdTAwMTROSVx1MDAxZPivMpE6XHUwMDAwJ5tQQWFcdTAwMTKB5p5og/DKKXpcdTAwMDanSEmMUFx1MDAxNr6umeCWeZbxXHUwMDAz+U5/Mzop1nHF55355FwiuW5cdTAwMTDcXHUwMDE5ZbVcdTAwMGWlQk8qmGnfm1x1MDAxYYtcdTAwMDVRU49/pZJcdTAwMDW6zCwgXHUwMDFh9sBcdTAwMTT30XZXq721XHUwMDE2WPhS8c/aSW5zrdGsP4ND23yfXlx1MDAxNnFcdTAwMTVSZ1x1MDAxOPFpLeG5orWIXHUwMDBiSkdk6PXWy0a/dlcsZ1x1MDAwZmljU9ZcdTAwMWIv9/N0fWKKY1xyRiypwT1dn1x1MDAwNOWgQZh13FgjXHUwMDAyXHUwMDE59qvSXGJLI/80OfC1VYIqqn1da0R0JilcdTAwMDc3Qzn+ju75nr10ZbvRO3xcdTAwMTAnldr3w/7Oz9zVfG70vOnlMeDwjyaZoWSCXHUwMDE4jiVcdTAwMWG0tsHsstfdboe73W/N0MKwwIZPwjlcdTAwMGLaXHUwMDFkwGWCkdArNzpcdTAwMTZcdTAwMTVnc7jRXHUwMDFja/NqXHUwMDEzdpi/XHJcImhcImGBXHUwMDE1Q2C2pVx1MDAxY0OXRmlcdTAwMDRCyGfwoGdYlWmrNzHqdOxavE9cdTAwMTK/vY31fISw8EiGKdXjx/xaXHJcXKNcdTAwMDbmRjBcdTAwMGXzIFxmXs44MfBdXHUwMDA3/IlBsNIjcuFcdTAwMTeMXHUwMDAzw2hcdTAwMDVcdTAwMDVcdTAwMTHkWCCe5VN3ZyvmW/fzSFxcvTyir5IjmlOnMWg5nFGNclwipuK/tlx1MDAwMlx1MDAwM+1SLnZcdTAwMDJcdTAwMTRusL/DXCKITnVXO3pG4is8XHUwMDE3x+dcdTAwMGJcdTAwMTncuTe3ly/yXHUwMDFkeC8qXHUwMDAyLtGmebxcdTAwMTZcdTAwMGb+XCLBgoKCYVx1MDAxMy5upWJWJ1x1MDAxOMP7bZLHrzTMXCI3Q4DWXHUwMDAwJOCkY77UlDKRjGhF2aAwXGajY8SMyE1cblx1MDAwMlLfXG4nXHUwMDA05zRYRW+sTDglXG6DTy1cdTAwMTVcXJpgKZJcdTAwMTW5TZLbz6VcdTAwMDN5UIywiWYlgaxWXHUwMDFkk1x0YFx1MDAwNGhTo1PfU3dcdTAwMDYwM55WXHUwMDFmxXmg7ohQ3FxmdLR2kk9wnuRcdTAwMDSLLFx1MDAxOGNcdTAwMWR10s0k0MhZPzhcdTAwMWGa75+cQDUwPrVcdTAwMGUuXHUwMDFkSUBSvVx1MDAxOKHG50msTUQhgWyyIJkwhEo5oWk4rplcdTAwMTOlsCBcdTAwMTVcdTAwMTdOwVNTXHUwMDFmybDxO72z5SNcdTAwMTbhsNZb01pqmCxcdTAwMWFcdTAwMGVYRv01rY0gblAxffjinjgkpjThXHUwMDEyq+Y5LrkwX5RgzfJcdTAwMDSbX5pgse4rcGW4XHUwMDBi6MBcdTAwMDJEV69XXHUwMDE458SZSp1flWNzdUD7PfwqsOeItFRcdIkkYidcdTAwMTnRYMFU9JHh3oEon0mwXHUwMDE5hoGXjFx1MDAwM1GDKVx1MDAxM4aJiZSO0Hz/5Fx1MDAwNLugQo3feZ5UqLiealxyc8xcdTAwMTmmg+Gqb0MgwOzgvKpcdTAwMDHHayE+MqxzvVI9zOzwl+1GvXCzeXj+424z61x0XHUwMDAz81x1MDAxNmzA5ltOSVx1MDAwNWxcdPZhaoVcdTAwMTmMu1x1MDAwNsFcdTAwMWHTXHUwMDFiXHUwMDFkd6uNUlHx5jLlVbTPQp1p7C1cdTAwMTWTe96MO9RHOtxcdTAwMGVgQIXRvW+ExoJccilHpGBxXHUwMDE1R5cr2bD9kn9sVEtrlVx1MDAxYdwoeFx1MDAxOJ1Cu1x1MDAxM1EqSbzfhlJsVOesIafUaG89c0jN7f7l5nrzR7t+eSy08mSORG9cdTAwMTlcdTAwMTlJNFx1MDAwNo9p69syMiifxoD2rI2vyjksiOfSXHUwMDFjeFx1MDAwNvVcbrdfeveKrYhJXHUwMDA2c9ZcdTAwMTlcdE9vXHUwMDExQI9s2Fxcm0ZX51fdnev8Ra6g9lvH992d5+puYb7oXG6FXHUwMDAxXHUwMDE2c7BFXGY8/KNJZOqYttjMSoJNkzC9p4rygl9cdTAwMTm0dFx1MDAxZWAwXHUwMDA1joPDXHUwMDE0XHUwMDE17sDb+lx1MDAxZmkqm1x1MDAwMjDuklx1MDAwM4NcdTAwMGbLzEtcdTAwMWJcdTAwMTL+6Fx1MDAwZURnZFs4JrVx6e5cdTAwMTktMHVDhu5cZlx1MDAxNUu+0IbB/lnDQIPPXHUwMDExhTnDzEzbvtirSKnq2IbLle9cdTAwMWKFg1x1MDAwMq90tuV67eCEeXKTXCJcclx1MDAxZqVcdTAwMTZ+gEdcdTAwMTk+7Vx1MDAxYzGYVDnE91hIrVxm37L4Ls9h+FxmV0xST9ZcdTAwMDJcdTAwMGXFRMZKMINcdTAwMWRcZrBcdTAwMGLp+1x1MDAxOb7+82G2eXL6/JC9KZxtPVx1MDAxNzi/3/75YYbPP5okhs/hilx1MDAxN5VKYMFcdTAwMTLBQoqQXHUwMDE5wuOAoSmR2MSR+l28lc3zYeI+OSaMXHUwMDEyXHUwMDEyXHUwMDFkcJ9cdTAwMTY04e7qozhcdKAqxcUnNHlcdTAwMWL5duH+M1x1MDAxObtcdTAwMTmmJdQuXHTHv/Z7rNyx/dXrdfP32Xz1VjaOrnqVXHUwMDA3ypJbOSspwaWaSO9OXHUwMDEwzuJcIp9WRm5BQFfmMHJMYq1Mp31cIpZRXHUwMDE27d4pimuOPM0kgllWrlTJP12c1C7sfpHy7nWt8Vwic/bDrJx/NEmsnKVcdTAwMWFcdTAwMTfFI61cdTAwMWOnxMQhY2XlXHUwMDE2XHUwMDAwxa85QKFgciuqqU/52eglTEa5hS+yxeJkf6+dXHUwMDFiNlx1MDAwNvw8dm6GcfG3XHUwMDA1/D12bqvb3Wjv72RPzMX1IbN0/9fm2VlyO4dVj1xiY5wq5vfmjFx1MDAwNE1cdTAwMWKzL6FwndP6kMzk32Q791x1MDAwM9LnXHUwMDFlkm7nRqbPMVxmf1x1MDAwM/PndfBUeJc3sJ9rwINcdTAwMTc0RVx1MDAwN2/pxLF/xp33d/VcdTAwMWSu7mVPj+712WPn5HCr2Nnc6/JmMYXztphq7Vx1MDAxNHdfNjZcdTAwMWVcdTAwMWaL21x1MDAxN1V6/7hXTXbeWFxyMGzyYmVgj3o5XHLAevmDXFxRqWrne2u7a4u5Yru8mzA6xFxiSpxcdTAwMTC4PcWpZtNl8DAuSTDJmFx1MDAwMFx1MDAwMlx09IBcdTAwMTntZnJJhOZmkNArYDZ6ausyrGCPXUmVlcroQFDrSlx1MDAxOIyIpJqUSKJcdTAwMDPvpGNcbuid+9xixaNcdTAwMDNDwFx0cpTxxZo6/VxySmhFTlF8ZUKzc3y+XHUwMDEwij9TJS1KXHUwMDE4jFx1MDAxYjtDOFx1MDAwMcN3elx1MDAxY9W69lx1MDAxNnOhXGI3XGZ0m1aWXG6pw2F171x1MDAxN3Gxa+0+z262XHUwMDBlOrdN11CFcr32azORn8JcZmdEUCxcIqxcdTAwMDU8pOlcdTAwMTZcdTAwMTngm1x1MDAxMCqBpFx1MDAxY8VCpYFcdTAwMDCKUYywXHUwMDAy7YVcdTAwMDFullxyqnmKVfbScEgzWelxXHUwMDFlXHUwMDFm3qH65N7AX1x1MDAxMd63XHUwMDFkXHUwMDE13KcgfThLvZ1cclx1MDAwMJ0u5620XzJDqf/3qqWVZNjpuCzxwmtWVConhjvskO1LapLUYlx1MDAxZmdQuFx1MDAxYYuuepKapMbNZW2A4Fx1MDAxY1x1MDAxM8x6XHUwMDFjXHUwMDE37jThWLrYUudcdTAwMDR87ovGVqVcdTAwMTD3X1tefmCkgKLOpz5cdTAwMDDdoVJcXONSekpwoI3UXHUwMDBieFx1MDAxYaXkx6tcdTAwMGaGXHJPjZOOWzlcZnxcZn5dXHUwMDBiXHUwMDAyw+VcdTAwMDLUt9bUcTnrdNHTfnA0NOHTXHUwMDE0M78jLlx1MDAxNcRcdTAwMTcoXHUwMDE3I7GFlTOGL1x1MDAxOPlcdTAwMWa/eFx1MDAxMlx1MDAxOEKGXHUwMDEyreFJgCDkXGZ72/FwXHUwMDAxUvCIwCGXXHUwMDFh01x1MDAxMVxmxXanXHUwMDFmqJviO5jGcqxVnMBcdTAwMTTAXHUwMDFjOqXgLk/XQlAqPlKVXHUwMDEx9FXBOktuqaaBXHUwMDEwqnG0qiNcdTAwMGWblmnMaGEuIKw+NcHOKaRSXGL7by7Nr5piVTPqXVxy5uGe3G/0qo1cdTAwMDXN7NKsj4zrXHUwMDE31Fx1MDAxYWw49/H0XHUwMDFhNUfxlVx0Tc/3oMOtbLWwbp5EUd1Vsru5zlx1MDAxM/CijOBcImqEsIZyXHUwMDFj5SCQK0xGRFrsXHUwMDBmiMlcdTAwMWLo4NgwXHUwMDE5JWLI+H6mXHUwMDEzfiVcdTAwMTVcdTAwMWHzKFx1MDAxODdgSpzR4dQoSahcdTAwMDV/edCvJWJQ78eQ8cuKM1x1MDAxOFJcdTAwMTGjcL1cdTAwMWPbQEg2VVx1MDAxNlx1MDAwM3xJwsCjZNiFwJdcdTAwMWFFiXRot1x1MDAxZNpcbkYtXHUwMDE3Plx1MDAxNcotUSBGXHUwMDE0uulKikD7qVx1MDAxNUlOkGR7eVx1MDAxMao0aFBhmbdcdTAwMTidil5Mp0I5zlx1MDAxNUvRXHUwMDBiXHUwMDFk0+RCXHUwMDFkkVOlyZiJiq/wXHUwMDE0/VRESVx0SDUgSCGAlFx1MDAxNFx1MDAwNVx1MDAxMZ0k/z1d4UhcdHY401i2XGJ8XHUwMDE1LYVHOMrBXHUwMDA22keKxdhcboXxXHUwMDBlOWUge7EnODhPRunpQrsgXHUwMDEwXHSIXHUwMDBm50BcdTAwMTebYDGaUeUsRsDQ4nqjRi3PPGtsmIfqrEGZL1x1MDAxNKeB3JxcdTAwMTVcdTAwMTFOXHUwMDEw4fPyRFxik1VgN2CvXFyMXHRcdTAwMWK11kkrxFclwshZiq9MaIK+XHUwMDA3XHLOUVx1MDAwNlx1MDAwNJ4o+K1aweBpMFx1MDAxNiOgXHUwMDE2lVx1MDAwNlx1MDAxZIxcdTAwMTeHYLXh+ZGMXHUwMDE1kzPzoG4m1Vj8mdLAdFtcdTAwMWLvQTDMXHUwMDFlMWyQRlx1MDAwYvf3XHUwMDAz+VGVy13a0Y1cIn88t9dcdTAwMTeX1/Sg6MmFieRHKkDrSoF7ZW46jZ5zglkujltkT0+pUSpcYvOVXHUwMDE1pESnXvH/61Bhb2kqdEZyQ1x1MDAxZPcxYTByLVxcj1x1MDAwNFtKXHUwMDA1XHUwMDFmZUpcdTAwMWJcdTAwMTPgz4mPZ8Kp+YivzHAqpkl60Vx1MDAxYqDhb71cdTAwMGJcdTAwMDXk9N3uyU3ntnR1UN7e7fZE5XDnLjFcdTAwMDUwXCKpjaRcdTAwMDAqicRQyS9NXHUwMDAx81x1MDAxNlx1MDAxNU6BXHUwMDAz+inIIWe0kqBtfSRcdTAwMTCdJceU1Fx1MDAxOE3H096dWJHAR5LASbVcXOQ/mtnur9rhXHUwMDEx7Z10Ls7yx0lJgDqCScWUamq4tVNZsVxcgrfqNFx1MDAxN05orJ/5NUng3XWApcvrXHUwMDAwXHUwMDA2ytlcbn911fCy+tgjwrZcdTAwMTJ2sVx1MDAwNPJcdTAwMTVcdTAwMDN8Ulx1MDAwNmjcXFzWj93V7e3J0e5Gj/aeLzZ+eKKtI2WAZlxcYOt6KUMrJVxcaeKUdlxcaPB8lWdf7SswwPvLXHUwMDAwy5aXXHUwMDAxUlHJqPZcdTAwMDYpXGJcdTAwMWItXHUwMDAzgNZcdTAwMTW8+IpcdTAwMDS+XHUwMDEyXHRk3fFJQ1x1MDAxNZ9+tjbyRXq5aU4v+zyxXGbA2lx1MDAxOFx1MDAwMH+MkcZQsylcdTAwMTIwnDClmOPKYora1ySB95dcdTAwMDF8eVx1MDAwZXCOKuy56nNcdTAwMDVkTL6F1FRcdTAwMWGTcpH1XHUwMDE1XHUwMDA1fCxcdTAwMDX0e+r5olf9SVx1MDAxZlx1MDAxZlx1MDAwZU8vzEm2ITY9YclRWVdcdTAwMWF7/CqsZlx1MDAwN/9pRqd6/FxuLYKVXHUwMDAyVtnFqUUmWzFHZLJcdTAwMDEthkWJvVx1MDAxOVbR9Vx1MDAwMpiQWihcdTAwMTmstPnbk4srd3u75afT40r+7vy03OxsXHUwMDE1XHUwMDBl69dJXHUwMDEzlurlbLH4sJe5vFc81yzLXoddXHUwMDFlT/7KQolQ3W7GXFx/p1eH+/tZcZS5vOuJp4NcdTAwMTTOq162XHUwMDBiTFxcXHUwMDE4fnb7tL/Tb5/3L854XG7n/f2JW/PwdVxm+/ifdoh9PElcdTAwMTGaKYKKXHUwMDE0XHUwMDAziIxcdTAwMTFsumy6pcHaPVx1MDAxMbW5MOpSO2m55MxcdTAwMTPct8rg9lx1MDAxMY9MTjxW4611Xt6RMlx1MDAxNKIy4lx1MDAxZMyGsFLJlPdgwfHBUKdFpMZbSkS21O7Wm1x1MDAwZmtnL3/WNjqVavFzpHHPsOLTXHUwMDE5XHUwMDEysVeRTmpEPFx1MDAxNce6XHUwMDE2xlx1MDAwMrQtVdRcdTAwMGU1xXSvJsuJXHUwMDEy2ljMyFxmXHUwMDA2nI2grVx1MDAxY7GDiqOvL5+PwSUmVjhcdTAwMTCfXG7DaeTXXGbcTSHD26o5hFx1MDAwNipccmqd8tbii6nMbSXlTqVZmXuId2OCjXw+zLWImpCDb4en4vh8IWv8IUG6WGlKU8rBUaNWXHUwMDE5T+xcdTAwMTmR8MydktiuRHIpXHUwMDE3zGKIp7G1YNiFkjDZNOYmWLAxKjwmQZSxjH9kNNpBu3K5v311scvbx/r7zuWPVqvrSV3wqFx1MDAxYmP0XHUwMDA0XHUwMDA1htwqXHUwMDFlVDeeXHUwMDE1VjHG/ThcdTAwMGU3ZTnzhTjOzMFxyiqmWHBJK5hlXHUwMDFlxXFCUFxmXHUwMDAyc4vso05cZiOsxynMh4Xau7yJmsfSY6Ner65cck+dSo/WaumuXHUwMDFkI2dcdTAwMTZN7YxcdTAwMWNpOqIl3s+LXHUwMDE1LUBDXHUwMDAxxNLpXHUwMDFjbak40do4inwlXHUwMDAzNmnco1x1MDAwZXNcbjRcdTAwMWNcdTAwMDSW1TbQMG2ca2SIwO8jrU2Emq3gvDZcdGfrh/NcdTAwMWOLpMCuXHUwMDAwqkBXnGAyp4hcXCNVXHUwMDFhbDk3iyyRxoN84Ll8fDJnhnFDsHUt11x1MDAxYYthOT6hZXDrnzolXHUwMDFjeOWOazXzdJGzfnDUXHUwMDEw5oDdsLFcdTAwMDa1uOHwXHUwMDFlwiixXHUwMDA2XHUwMDAxYYSNX1x1MDAxOHiqg/VcdTAwMDPjfHlCXHUwMDAy+7ZhWydquHK/P1x1MDAxY1x1MDAxNcSkhLtcdEPCsu9GhoP0XHUwMDE5XHUwMDExcFRITDelXHUwMDFjV3M+UFwircvj5lnt0pby9rmw3n583tXZm2RcdTAwMTJJxUokqVm8RHKKUFxyL4Csk1hgeCWY5mBYl1xcMGmhnPR3a1x1MDAxMtFBJ1RTJfhiva5nUCny11KLQLXX5ZP2y2fWSt5RpqOT4tetZyzuyDjYcjXLs3G+5r30a2bSpIHUjaW1kMb0T8a9zeqjM1xuXHUwMDFkXHUwMDE1WGRgob2jXHUwMDE5XHUwMDBlj1x1MDAwMDXw8VqI0YncmVHlhugvuMkuv/RdsmtcdTAwMTLnPWdcdTAwMDZ1ooXBmrdMO4zf9GhcdTAwMDfJXHUwMDAwmoPm6ExcbmXDVVx1MDAxZNNe6GEwXHUwMDE2jtnWmPzorPNliI92j+HlqPvINZ+NRv/5+qbSyOmfrX7/6fKgdiN+JFx1MDAxMzTaxjKjXHUwMDE28cxoLPH2U1uJmChq3EwuYlxm+iCce1x1MDAxN31YuFx1MDAwZf+4+5K2VDK1UFx1MDAxZvNcdTAwMTksqLilS8mYwlx1MDAxYr38WVx1MDAwM3y0/a3UPoeWiVx1MDAxZWo6giZ+wzx+4Vx1MDAwN1x1MDAxY404P0TReNhK51uqVauKw1Go3Vo+XHUwMDAyjlPKXHUwMDA0N85fbT+62D6XXHUwMDA2i1x1MDAxNS9UKHRcdTAwMDaa562G+3s0TSYwXHUwMDFi8Vx1MDAwNfPwPTTKXFxrLljGQWtcdHpgkKdcdTAwMWSSXHUwMDAznIz22Vx1MDAxY3bU9lSvSr2kXHUwMDE2XGInM+g3hKGVNqybuCXvVlx1MDAxOOGP10f0Ld9onFx1MDAwZcjy7Vx1MDAxYb89V0rdjei4lD9ex4bEUFx1MDAxYWDqrz/++n9cdTAwMWFBYUUifQ==External servicesLoadbalancerRPCBlockProducerRPCrpc.testnet.miden.ioStorePublicInternalFaucetfaucet.testnet.miden.ioexplorer.testnet.miden.ioexplorerHorizontally scaledRPC providers...Example infrastructureTransactionproverBatchproverBlockprovertx-prover.testnet.miden.ioNetwork TxBuildermempool updatesnetwork txscommittedstate \ No newline at end of file diff --git a/docs/internal/src/assets/workspace_tree.svg b/docs/internal/src/assets/workspace_tree.svg deleted file mode 100644 index 016e69a633..0000000000 --- a/docs/internal/src/assets/workspace_tree.svg +++ /dev/null @@ -1,4 +0,0 @@ - - -eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1cXFlT20hcdTAwMTd9z6+g+F5cdTAwMDdN78u8sS9J2MP2zVRKWMJcdTAwMTbYkpFlXGaZyn+f2zJYsjYvXGJCpaypmlwiUku+3brnnHu7b+vfT0tLy9FT113+a2nZfWzYbc9cdO3B8lx1MDAxZub8g1x1MDAxYva8wIdLJP53L+iHjbhlK4q6vb/+/NPudq3kLqtcdTAwMTF0hne6bbfj+lFcdTAwMGba/lx1MDAxZv69tPRv/H+44jnmfrt72nWf9qLPXHUwMDA3XHUwMDE33e1cdTAwMWLXsa/Do5X41rjRi0Gh24hsv9l2k0uPcH5FIGJJglx1MDAxOVx1MDAxMYpILiRcdTAwMWZdfoLLnFwibWlEXHUwMDE41UJcdTAwMTCq9OjqwHOiXHUwMDE2tMBEW5hcdTAwMGLC0fNBR01artdsRdBGJVx1MDAwZlx1MDAxZJrw11x1MDAxMlx1MDAxYZ3pRWFw565cdTAwMDftIDR2/lx1MDAwZrvmv8TKa7tx11xmg77vJG1u4iNpc+O12yfRU/xkXHUwMDE4Wlx1MDAxOMLlzPPPX8zNnC+7XHUwMDBifrDZ8t1eb+yeoGs3vMhcZoxOOmCM6+468Vx1MDAwYvonMSm0O+6ueUN+v91cdTAwMWWd9nzHNeO+fC22x37Nd55/bax5z3WdeJBcdTAwMTlnhFx1MDAxMiZGV1x1MDAxMoeCkc+e3Vx1MDAwZvzYubBcdTAwMTJaXHUwMDEzTVx1MDAwNUlcZuhtgEdF8WNv7HbPTVx1MDAwNtpcdTAwMTixmfW2tMeNOVTkPkajXHUwMDFlpPyxtXaI2ofR3e6mdKR/7j8+9Fx1MDAwNuvLo3Y//yh+7PDm3ire3rn8et/yzvd31lvisDPY/D7+Ky+/b4dhMEg99/mv5Fxy9LuOPewnllxmK6koZUgkY9X2/LvseLeDxl0yNJ9SXHUwMDA2Z1BX3MtcdTAwMWPqxlx1MDAwNmlcYjguscUoJZJcdTAwMTDCOVx1MDAxNjJcdTAwMDM4Olx1MDAwMXBCWFxcas2Q1FxcacVYXHUwMDFlb2SBt1x1MDAwMrztTI83RrhcdTAwMTZIcFZcdTAwMDA3cKIyuIGHUc2FXHUwMDEw86CtwnFcdKaCq1x1MDAxOVx1MDAxYzfxQ+N/0Pstu99wU554XHUwMDEz+NGJ98NcdTAwMThN0NjZLbvjtWNPXHUwMDFje8hq22ua3i83wF43XFxOXHUwMDBmQeSBao1cdTAwMWF0PMdJq0xcdTAwMDNcdTAwMWVqe75cdTAwMWLuTiNXQeg1Pd9un5ZcdTAwMThu96Pg2O1cck2Pwr6bXHUwMDFlXHUwMDE1d+dcdTAwMDVcdTAwMDHYXCK8XHUwMDAyvWxD76w621x1MDAxYmtXXHUwMDE3q963jZuHXtd1ptdMxaTFZIVmUlx1MDAwYlx1MDAxMflcdTAwMDLhXHUwMDA04Fx1MDAwYs18LYZ3Z9FMzSCwUWlPfYYwkzJ7dqSYWCosQG5cdTAwMTNb31xcMTtP97x/1du9WD9k+P76fHBPXHUwMDBlrqdVzKfdjfD7gX7avXSO+fnKw8n64PPOdIpZ+dzPbO389sfBXHUwMDBmPOh1dptHzYPv3/drU2JcIjRLXHTXq5S4ePSmUWKFhCVAiTVBXHUwMDAya0ayMKZcdTAwMTNgXHUwMDFjs4DWXG4xoVJcdTAwMWOwkOFKXGLvzVx1MDAwMGGqmYZgSOdcdTAwMTTX6DAhZSAmXGJhjbhIgb8mIUacMjyD3+aEeD9w3I8gw1x1MDAxM1x1MDAxNDArw+Nm1yPCZM+/v7tEO0H40FljX44j3e99nl6EIfq1XHUwMDE0XHUwMDEzZVwizDTktVIuXHUwMDEy1+H7rFx1MDAxM8Ffp0ewwlxmXHUwMDEzqlVR3kpUTptHKiwpRlx1MDAwMt7wXFyR9HwqfNE/vrv88rjd31xiwvVz/8z+7GytzaZqSEHsUI+qXHUwMDE1WzONqmlcbrJEQGIlZURcIkbGccExskhcdTAwMTUulLAo1lx1MDAxYV5cdTAwMDBcdTAwMDX1o0X5ZSo4WsBiXHUwMDA0i/3pYWFSXHUwMDA0znCRrOFU1JBFXHUwMDA1eJlmb1wiazM6bk7W1sz1v/3DMHAgX1x1MDAwYj+Cwk2Ql6zCxT1YynegXHUwMDFlrfNuKHnyVVdcdTAwMDTbh0fbrTZpfuHR9FqnkKzWOmaxxSTty5utXHUwMDEz1Fx1MDAwN9ODXHUwMDFhXHUwMDEyXHUwMDAxLDWnhVx1MDAxOSfT2bPJXHUwMDFjLcNSIPWeXHUwMDE5py02Ns5WNzna37389rDPXHUwMDFmoofByrRax4dcdTAwMTOXmqFZXCLhXG5wXHUwMDE0WzON1kmJLIpcdTAwMTRRkilGUWqObKh1ZFx1MDAwMi44s1x1MDAxMGRwXHUwMDEyU0yZXHUwMDE0XFzkYbFI4opgcTjDXFyq5EhcdCVcbkPAfGaXwEJTJECXalx1MDAxNLv5PDcndidRXHUwMDEwfogkboKqZCUuY3c9ynbUvjl19OGpaovrtvTPTu4vXHUwMDFjb4blR4EnZXGYk8VU6vCF1lx04ZNcdTAwMTnmYVx1MDAxMCZMYo5yMy5cdTAwMDbfvDyN44aVJUPvmMa198VX3DknXHUwMDBmq9tX67p73fLo9sWvXn5cdTAwMWNcdTAwMTJcdTAwMGZcdTAwMTFcdTAwMTjPsopTgbriXk4jmVx1MDAwMkMoqUEyqVwiXHUwMDA0a8HGXHUwMDAxXHUwMDA3llZcdTAwMDOOXHUwMDE5PtBaQFKvXHSXXG7n8baQzFwivJ1OjzeCzZw0YYXL/USX4o1IZlx1MDAxMEfZPHCr03Nzknl8uP5cdTAwMTFcdTAwMDRzglhlXHUwMDA1c8zqeuTye6SO7txcdTAwMGIsv+701vvnZ61cdTAwMWS3WVA3UL7yWDnpKbFKT+4s5LI++H6bQS6lVlx1MDAwMutCtaSkNFx1MDAxMdRcdTAwMDRIXHUwMDE1XHT8jnmg39JcdTAwMWR8dSld4ZydXHUwMDFkba1+OZBcdTAwMDK/w1xuYeVz9fWj5+3rrb67s9J4jC5a+LB/UoNcYtdeXHUwMDAzVDx604iwmc8h2Igwx1x1MDAxMpFUfDSEMZtcdTAwMDBjxkHDjVxiU6nAZchi8XFo0kRcdTAwMTSfzSDCiFxiKSFcdTAwMTIqnKVcdTAwMTWlXCKsiFx1MDAxNlxcgGTWJ8LDSVqIylJ8P4dcYn+LvHbvI8jwXHUwMDA0XHUwMDExzMpwxu56hPjpXCJyLlr8cnswOOvpq69cdTAwMDdcdTAwMWTpXHUwMDBlZshbKa8qm5VcdTAwMTBGL2Zk31x1MDAwNMLnM1xisSBEXGKBRKFcdTAwMTKr8rknjjRWXHUwMDE0v+fyo33q2WdcdTAwMGZcdTAwMTf3W61cdTAwMWbN1sr+/q1e5edTS+be3sn2/tFaXHUwMDBmXHUwMDFkiOPOce/cd1x1MDAwNqguyWSIXHRRU95a3MtpJJNrZWFcdTAwMDHhXHUwMDEzUvGSciZvhURl8lQv1VqqeFGTLup1nk2aiLeLXHUwMDE58EYoMaVQhZEvqVjYRFjDe8F6rtD3TTWzXHUwMDFiXHUwMDA2UfBcdTAwMTE0c4JeZTUzY3c9mon2NndcdTAwMGb3XHUwMDAy7Fx1MDAxZWxdXHUwMDEye3s7XFxVx1x1MDAwNUVcdGWaiVx1MDAxMYS9glGKcPFkL2BUJHNPXHTljErfTcmPZHp05DGMOejySFTHdLVcdTAwMTZMR6Ht97p2XGJv81fiXHUwMDFho9dcdTAwMDP7slx1MDAxONiJvLy8UPp85mdi/HNcdTAwMTmDkIAwwlxu4Y4q4E45RUrMXHUwMDA39ypcdTAwMWR8q2LYV6Skb6XbJc+dXHUwMDE0Zkgq2bq/vv/5dmflylx1MDAxYlxc0yC825xtXHUwMDFhXHUwMDFi1JPWXHUwMDE0XHUwMDBlXHUwMDE0WzNNOCBcdLU0uFx1MDAxZThcdTAwMTFcIkqgXGaVcMXT4UCeSnS8XHSHQl84gsa6aFx1MDAxN03NVU6/XHUwMDExdVxcTVx1MDAxZlx1MDAxM0gkgO5xcVx1MDAxNk1zXHUwMDFibJJcdTAwMTBcdTAwMWNkgs5cdTAwMTWAj1nxevfNhVx1MDAwNOtBp1x1MDAxYvjxj1x1MDAxNcVcdTAwMDWpQqpa4oLrIIqe94LGXHIyccFcdTAwMDRNzsZcdTAwMDVFxteUUFdyb4bHMmCWwlx1MDAxMoBjJmhcXIufaPawXHUwMDEwX2mLJ4X4eTBcdTAwMTNcdTAwMGJpXHLhI1x1MDAwNJ5cdTAwMTJcdTAwMWNcdTAwMDel4s9cdTAwMTGaXHUwMDA1s5hcdTAwMTSQ9LF4Y4iuOdb/jcBtT1x1MDAxYlx1MDAxN5CyuFx1MDAwMCOuQeJcdTAwMGJWqmJaLVx1MDAwNT3QNqTrhNde8jE35ruBl1xyO5K/llx1MDAxMpeJ/zH6+58/XG5br5T7qTlyXHUwMDFlmjwvp8ptu1x1MDAxN1x1MDAwMZQ7Xlx1MDAwNFx1MDAxZD00RuboN7LDaFxyXqvnN8ff3vOG8mn2XHUwMDAxxFx1MDAxY9bom1x1MDAwMVhBYLykRCqwXHUwMDBlIGqWKlLNmnY3poh06K3RqFx1MDAwZomLuL4z2apqUlx1MDAxYreKYlx1MDAxOFRcdTAwMGVcdTAwMTZx+G3Jkz2bI6tcYmRcdTAwMDR5VzXjs2rIqOXaOTiAlelrWdZy29fBYKpcYqd6kaSSXHUwMDE1zS5DjlVZtkQlT29wyLNcIjXbm4qWXHUwMDA2qFwiVt1zXHUwMDFkv1x1MDAxMf9dv5r/XHUwMDA0IIVQgou2XHUwMDBmY5lb1Fx1MDAxZsU8lEA8rJSaa7d+1TSIQlxcJKL6y/gv45HmwIK8hFx1MDAxNVV3XG4rtcPOXHUwMDFjQ1x1MDAxZv5QXGZcdFx1MDAwNMkxXHUwMDEzSnHGXHUwMDE1pLgqqTJMXGJyPj6sXjBcdTAwMTnnQ86wpphcbiW1RCxZPFx1MDAxY9nAoYmZpftVlFhdZFVJiZxcdTAwMTHge1x1MDAwMXQoITmQODNcdMzAv1BloKgspokgjGmMXHUwMDExXHUwMDA0m1x1MDAwNeyokJWaYkrXny6IcpwoXHUwMDFirydKriDiXHUwMDExxfPF5XFcItBcdTAwMWFEXHUwMDFmtE6efKlzYnSu6eKa48RSNzVHzkHfg1x1MDAwNas/2zDOQFgxIc1SjYnMmCR5XG7C2MIk6Vx1MDAwM9Lz0WJ1OddcdTAwMTg1Y4K4woxQKlx1MDAwNUNcYlx1MDAxN9FcIk1NXHUwMDFjXHUwMDE3xq7vRJDVs43VmbSCoFx1MDAxMFJcYlx1MDAwNNFcdTAwMDdcdTAwMDGmTKBcdTAwMWZcdTAwMTOkwMRikF4oXCJMNW+Cn2RZ2sLArVx1MDAxOJtpMaHSXHUwMDEwXHUwMDFiXHUwMDExpKZcdTAwMTalwjRjklx1MDAwMtEmv7EgyHGCdF6fSWNiUkVV/CWaiukz8F/JIdasnVwiP0YqXe6n5sh56Hsw5NQ5K6SskOczgrSKXHUwMDBifbnOk5FKgtu3XG5cdTAwMTWRZUrLTJlcdTAwMDPDWFxiSFhUzlxmQ1x1MDAxN8lqW1x1MDAxMU+/XHUwMDEzJZ45p97Z5lO4s3J8uXrFelxy70fE8pRo5iuzhTqIWbh8zZEyXj23yGnBKiNcdTAwMTHaWiTQZbTnvZr2pNlKqXjhXHUwMDA26XTpRob1XHUwMDA03CRY3dkzXHUwMDE1TPBcdTAwMGZcdTAwMTBcdTAwMTVigbJZ8FxuRtxKUrnye1NebFx1MDAwZSyoScTrpMUs7kuuzMRcdTAwMTdj17pB+6lcdTAwMTl7wMT4qXJVtTrBVNhcdTAwMTKQXFwwU4cgXHUwMDE5z3ycT0DgakhTUqmVLijMXHUwMDA13kZcZljGRO1cdTAwMWH4JuGTpEJcdTAwMDFeXHUwMDE51YhA7iMoQ5gv4qcyXCK5rWElwrA64YU7abAq/1xmmCn+o0ih3zR+Wil1VHPkXfRDXHUwMDA1UGbSXHUwMDFmca5cdTAwMThcdTAwMDJao9zsYkux4nPoXHUwMDAyqj9vXGJVXWqVMYRcdTAwMTPFhVx1MDAwNlx1MDAxZlx1MDAwM+lcdTAwMDHVyq8+cCtV5ftL88q9x6fDe9c5woNvT7eis7Fy2mdcdTAwMDV5ZdF+XHUwMDA1rKqWXCKw1tWFW+BJXHUwMDE2vCZcdFKlzTb799iw8Fx1MDAxYvFge5ZdXHUwMDBiXG6iXHUwMDE2iFiKXGKv/INLVCGlmZA17lx1MDAxY1x1MDAxY9ZcdTAwMGVTrlJlenNUW5xcdTAwMDfhXHUwMDFkvMaGu+S4XYCC6zeelqLQrWVcdTAwMDN+271JOUeu9FwiXG66ZXVcdTAwMTdjXchcdTAwMTZZTGFzPTVcdTAwMTfrnq+um3fdxplq3eCVPtm4dGb4rFxmx9LSXHUwMDE4fFx1MDAwNXFmcqTc5nuUzo1cdTAwMTa7XHTrK6pcdTAwMGVmQLSE6IVcdTAwMTZ+QS39XHUwMDFki1xmoM1GYKzQu374+1x1MDAxYu3cSXHTdE/Xd24vb69cdTAwMGY8XHUwMDFl/bJcdTAwMWTyxdZMI3bcXHUwMDA0+YhcdTAwMTJTpCEkXHUwMDExLFx1MDAwN1x1MDAwYj5cdTAwMDFcdTAwMTaYW3pCcaFcXMCiXHUwMDAwXHUwMDE23elhgcH5hSZUXHUwMDE2zVx1MDAxMVx1MDAxMFG6y5YgJDhGsv5N8kCE8lWFhftuNFx1MDAwMOX42z81oYvdiMDsv/21vtd2PsYn1SbITe6jocP+LKW6s5TrzcxC+OmZQpbtbvckgncwiuSXXHUwMDFmPHewVo6NT89cXGGw4caJ5M9PP/9cdTAwMDPDXHUwMDE1Zk4ifQ==FaucetNodeBlockProducerStoreRPCUtilsprotoComponentsWorkspace dependency treeNetworkTransactionBuilder \ No newline at end of file diff --git a/docs/internal/src/block-producer.md b/docs/internal/src/block-producer.md index 26f82e9043..fc7e01b0ce 100644 --- a/docs/internal/src/block-producer.md +++ b/docs/internal/src/block-producer.md @@ -1,11 +1,9 @@ # Block Producer Component The block-producer is responsible for ordering transactions into batches, and batches into blocks, and creating the -proofs for these. Proving is usually outsourced to a remote prover but can be done locally if throughput isn't +proofs for batches. Proving is usually outsourced to a remote prover but can be done locally if throughput isn't essential, e.g. for test purposes on a local node. -It hosts a single gRPC endpoint to which the RPC component can forward new transactions. - The core of the block-producer revolves around the mempool which forms a DAG of all in-flight transactions and batches. It also ensures all invariants of the transactions are upheld e.g. account's current state matches the transaction's initial state, that all input notes are valid and unconsumed and that the transaction hasn't expired. @@ -17,8 +15,22 @@ the mempool where it can be included in a block. ## Block production -Proven batches are selected from the mempool periodically to form the next block. The block is then proven and committed -to the store. At this point all transactions and batches in the block are removed from the mempool as committed. +Proven batches are selected from the mempool periodically to form the next block. The block is then built and sent to the +validator for verification and signing. +This signed block is then submitted to the store where it gets proven and committed. Proof +generation in production is typically +outsourced to a remote machine with appropriate resources. For convenience, +it is also possible to perform proving in-process. This is useful when running a local node for test purposes. + +Once the block is committed, all transactions and batches in the block are marked in the mempool as committed. + +## Mempool data pruning + +The mempool keeps the `N` most recent blocks locally, to allow incoming transactions a grace period so we can verify their +state against the store, and the local state deltas in the mempool. Without this overlap, we would constantly be racing +transaction check against the store with newly committed blocks. + +After each now block, the `N+1`th oldest block and its batches and transactionsa are pruned from the mempool state. ## Transaction lifecycle @@ -42,5 +54,5 @@ above lifecycle (which effectively shows the happy path). This can occur if: - The transaction expires before being included in a block. - Any parent transaction is dropped (which will revert the state, invalidating child transactions). -- It causes proving or any part of block/batch creation to fail. This is a fail-safe against unforeseen bugs, removing +- It causes proving or any part of block/batch creation to fail repeatedly. This is a fail-safe against unforeseen bugs, removing problematic (but potentially valid) transactions from the mempool to prevent outages. diff --git a/docs/internal/src/codebase.md b/docs/internal/src/codebase.md index 1a31c87f0d..09ae97fba9 100644 --- a/docs/internal/src/codebase.md +++ b/docs/internal/src/codebase.md @@ -3,26 +3,13 @@ The code is organised using a Rust workspace with separate crates for the node and remote prover binaries, a crate for each node component, a couple of gRPC-related codegen crates, and a catch-all utilities crate. -The primary artifacts are the node and remote prover binaries. The library crates are not intended for external usage, but +The primary execution artifacts are the node and remote prover binaries. The library crates are not intended for external usage, but instead simply serve to enforce code organisation and decoupling. -| Crate | Description | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `node` | The node executable. Configure and run the node and its components. | -| `remote-prover` | Remote prover executables. Includes workers and proxies. | -| `remote-prover-client` | Remote prover client implementation. | -| `block-producer` | Block-producer component implementation. | -| `store` | Store component implementation. | -| `ntx-builder` | Network transaction builder component implementation. | -| `rpc` | RPC component implementation. | -| `proto` | Contains and exports all protobuf definitions. | -| `rpc-proto` | Contains the RPC protobuf definitions. Currently this is an awkward clone of `proto` because we re-use the definitions from the internal protobuf types. | -| `utils` | Variety of utility functionality. | -| `test-macro` | Provides a procedural macro to enable tracing in tests. | - ---- +We have a top-level `proto` crate, which contains the external and internal gRPC and protobuf schemas. It also exposes the +`tonic`/`prost` file descriptors for each gRPC service for convenience. We then have an internal `proto` crate in `./crates`, +which uses the above file descriptors to generate the actual service traits, and also defines some domain objects and other gRPC +shared utilities and definitions. > [!NOTE] > [`miden-protocol`](https://github.com/0xMiden/miden-protocol) is an important dependency which > contains the core Miden protocol definitions e.g. accounts, notes, transactions etc. - -[![workspace dependency tree](assets/workspace_tree.svg)](assets/workspace_tree.svg) diff --git a/docs/internal/src/components.md b/docs/internal/src/components.md index 9253fed98f..e427c1083f 100644 --- a/docs/internal/src/components.md +++ b/docs/internal/src/components.md @@ -1,6 +1,6 @@ # Node components -The node is split into three distinct components that communicate via gRPC. See the +The node is split into five distinct components that communicate via gRPC. See the [Operator guide#architecture](https://0xmiden.github.io/miden-docs/miden-node/operator/architecture) chapter for an overview of each component. The following sections will describe the inner architecture of each component. diff --git a/docs/internal/src/ntx-builder.md b/docs/internal/src/ntx-builder.md index 27be99a0b7..bc9c3cfbe9 100644 --- a/docs/internal/src/ntx-builder.md +++ b/docs/internal/src/ntx-builder.md @@ -8,13 +8,13 @@ Network accounts are a special type of fully public account which contains no au whose state can therefore be updated by anyone (in theory). Such accounts are required when publicly mutable state is needed. -The issue with publicly mutable state is that transactions against an account must be sequential +An issue with publicly mutable state is that transactions against an account must be sequential and require the previous account commitment in order to create the transaction proof. This conflicts with Miden's client side proving and concurrency model since users would race each other to submit transactions against such an account. -Instead the solution is to have the network be responsible for driving the account state forward, -and users can interact with the account using notes. Notes don't require a specific ordering and +Instead our solution is to have the network be responsible for driving the account state forward, +and users can interact with the account only indirectly using notes. Notes don't require a specific ordering and can be created concurrently without worrying about conflicts. We call these network notes and they always target a specific network account. @@ -53,9 +53,8 @@ argument (default: 5 minutes). Deactivated actors are re-spawned when new notes targeting their account are detected by the coordinator (via the `send_targeted` path). -If an actor repeatedly crashes (shuts down due to a database error), its crash count is tracked by -the coordinator. Once the count reaches the configurable threshold, the account is **deactivated** -and no new actor will be spawned for it. This prevents resource exhaustion from a persistently +Each actors crash count is tracked, and once the count reaches a configurable threshold, the account is +**deactivated** and no new actor will be spawned for it. This prevents resource exhaustion from a persistently failing account. The threshold is configurable via the `--ntx-builder.max-account-crashes` CLI argument (default: 10). diff --git a/docs/internal/src/oddities.md b/docs/internal/src/oddities.md index b7b65e2173..24bca25f8a 100644 --- a/docs/internal/src/oddities.md +++ b/docs/internal/src/oddities.md @@ -13,3 +13,10 @@ between the chain MMR and the block hash: To work-around this the inclusion of a block hash in the chain MMR is delayed by one block. Or put differently, block `N` is responsible for inserting block `N-1` into the chain MMR. This does _not_ break blockchain linkage because the block header (and therefore hash) still includes the previous block's hash. + +## Crate: `rocksdb-cxx-linkage-fix` + +This crate is used to ensure that statically linking the `rocksdb` library works as intended. + +More information can be found in the crate's doc comments, but this crate is required for now to be included +as part of the `build.rs` in the large SMT crate which depends on `rocksdb`. diff --git a/docs/internal/src/rpc.md b/docs/internal/src/rpc.md index 3870dedfe4..361fe8927e 100644 --- a/docs/internal/src/rpc.md +++ b/docs/internal/src/rpc.md @@ -8,16 +8,19 @@ get rejected _before_ reaching the store and block-producer, reducing their load the proofs of submitting transactions. This allows the block-producer to skip proof verification (it trusts the RPC component), reducing the load in this critical component. -## RPC Versioning +## RPC Versioning and the HTTP `ACCEPT` header -The RPC server enforces version requirements against connecting clients that provide the HTTP ACCEPT header. When this header is provided, its corresponding value must follow this format: `application/vnd.miden.0.9.0+grpc`. +The RPC component allows clients to negotiate their desired Miden RPC version using the well-known HTTP `ACCEPT` header, using the following format: -If there is a mismatch in version, clients will encounter an error while executing gRPC requests against the RPC server with the following details: +```sh +application/vnd.miden; version=; genesis= +``` -- gRPC status code: 3 (Invalid Argument) -- gRPC message: Missing required ACCEPT header +The `version` lets the client specify their supported version and the server will attempt to comply if it can. At this early stage, only client versions which are semver compatible with the +server version are likely to be accepted i.e. the server in all likely only supports a _single version_. -The server will reject any version that does not have the same major and minor version to it. This behaviour will change after v1.0.0., at which point only the major version will be taken into account. +The `genesis` property is intended to let the client confirm they are on the correct network, by specifying the network's genesis commitment. This guards against operating on the wrong network, +as well as against network resets. ## Query limits (`GetLimits`) diff --git a/docs/internal/src/store.md b/docs/internal/src/store.md index 67ad1d4d9b..90bef64e68 100644 --- a/docs/internal/src/store.md +++ b/docs/internal/src/store.md @@ -10,10 +10,6 @@ information is always read from disk. We will need to revisit this in the future We have database migration support in place but don't actively use it yet. There is only the latest schema, and we reset chain state (aka nuke the existing database) on each release. -Note that the migration logic includes both a schema number _and_ a hash based on the sql schema. These are both checked -on node startup to ensure that any existing database matches the expected schema. If you're seeing database failures on -startup its likely that you created the database _before_ making schema changes resulting in different schema hashes. - ## RocksDB tree storage The account and nullifier trees are persisted in separate RocksDB instances under @@ -27,7 +23,6 @@ bits vary by depth (8.0–12.0) and memtables are 128 MiB per column family. See full fixed configuration. Runtime-tuneable parameters are documented in the [operator usage guide](https://github.com/0xMiden/node/blob/next/docs/external/src/operator/usage.md#rocksdb-tuning). - ## Architecture The store consists mainly of a gRPC server which answers requests from the RPC and block-producer components, as well as diff --git a/docs/internal/src/validator.md b/docs/internal/src/validator.md new file mode 100644 index 0000000000..336400ae04 --- /dev/null +++ b/docs/internal/src/validator.md @@ -0,0 +1,27 @@ +# Validator Component + +The validator is responsible for verifying each new block and signing it if correct. + +This signature is required _before_ a block may be committed on chain, and thus acts as an +independent safe guard. + +The validator is therefore run completely separate from the main node operations, and is operated +by a separate entity. The validator's public key is published (or at least will be for `mainnet`). + +## Dual purpose: training wheels + +The validator has a 2nd purpose while Miden is maturing. To prevent private state from being lost, and to guard +from potential bugs in the VM/cryptography primitives, Miden will launch with training wheels. Notably, we will +require users to _include_ the private input data along with their transactions. This means users will have privacy +on the _network_ but not from the network operator. + +As part of the transaction submission process, each transaction, its proof, and private inputs, are sent to the validator, +which re-executes the transaction, thereby verifying it and its proof are correct. This also lets us store the private data +as part of our training wheels. + +## Block verification + +The validator ensures that each new block is sequential with the previously signed block. i.e. `header.parent_commitment == last_block.commitment`. +It also checks that the block contains only transactions that it has previously seen and verified. + +Once verified, the block is signed and returned to the sender. diff --git a/proto/proto/types/transaction.proto b/proto/proto/types/transaction.proto index 66504d67a1..e133bcdc0b 100644 --- a/proto/proto/types/transaction.proto +++ b/proto/proto/types/transaction.proto @@ -8,18 +8,39 @@ import "types/primitives.proto"; // TRANSACTION // ================================================================================================ -// Submits proven transaction to the Miden network. +// A proven transaction. +// +// Note that we currently require full transaction transparency for the network operator. +// This is a temporary measure while Miden stabilizes its protocol and proof systems. To +// this end, a transaction submission includes its **private** inputs which the operator +// can use to verify the transaction execution and proofs are correct. +// +// This means the transaction is _not_ private wrt the network operator (but it is still +// private onchain). This requirement will be lifted as Miden matures. message ProvenTransaction { // The transaction proof. // // Encoded using [miden_protocol::transaction::ProvenTransaction::to_bytes]. bytes transaction = 1; - // The inputs used for the transaction proof. + // The private inputs used for the transaction proof. // // Encoded using [miden_protocol::transaction::TransactionInputs::to_bytes]. + // + // Transactions missing this field will be rejected as per the message description. optional bytes transaction_inputs = 2; } +// A proven batch of transactions. +// +// Note that we currently require full transaction transparency for the network operator. +// This is a temporary measure while Miden stabilizes its protocol and proof systems. To +// this end, each transaction includes its **private** inputs which the operator can use +// to verify the transaction execution and proofs are correct. +// +// This means the transaction is _not_ private wrt the network operator (but it is still +// private onchain). This requirement will be lifted as Miden matures. +// +// In addition, in order to verify the batch itself, we also require the proposed batch. message TransactionBatch { // The batch proof. // @@ -28,12 +49,16 @@ message TransactionBatch { // The batch contents of the given proof. // // Encoded using [miden_protocol::batch::ProposedBatch::to_bytes]. + // + // Batches missing this field will be rejected as per the message description. optional bytes proposed_batch = 2; // The transaction inputs for each transaction in the batch. // // Must match the transaction ordering in the batch. // // Encoded using [miden_protocol::transaction::TransactionInputs::to_bytes]. + // + // Batch will be rejected if any transaction's input is missing as per the method description. repeated bytes transaction_inputs = 3; } From 5d6772f138e24588660fb88f2e9f49f25f09be9e Mon Sep 17 00:00:00 2001 From: KOVACS Krisztian Date: Thu, 21 May 2026 11:14:50 +0200 Subject: [PATCH 08/10] refactor(mempool): make mempool lock poisonable (#2103) --- .../block-producer/src/batch_builder/mod.rs | 77 ++++++++++++------- .../block-producer/src/block_builder/mod.rs | 33 ++++---- crates/block-producer/src/errors.rs | 11 +++ crates/block-producer/src/mempool/mod.rs | 18 +++-- crates/block-producer/src/mempool/tests.rs | 14 ++++ crates/block-producer/src/server/mod.rs | 36 +++++++-- 6 files changed, 132 insertions(+), 57 deletions(-) diff --git a/crates/block-producer/src/batch_builder/mod.rs b/crates/block-producer/src/batch_builder/mod.rs index c93fadf4ca..a18024cfdb 100644 --- a/crates/block-producer/src/batch_builder/mod.rs +++ b/crates/block-producer/src/batch_builder/mod.rs @@ -3,8 +3,7 @@ use std::ops::Deref; use std::sync::Arc; use std::time::Duration; -use futures::never::Never; -use futures::{FutureExt, TryFutureExt}; +use futures::TryFutureExt; use miden_node_proto::domain::batch::BatchInputs; use miden_node_utils::spawn::spawn_blocking_in_current_span; use miden_node_utils::tracing::OpenTelemetrySpanExt; @@ -40,7 +39,7 @@ pub struct BatchBuilder { /// state and are immediately available for a new batch building job. /// /// See also: [`BatchBuilder::wait_for_available_worker`]. - worker_pool: JoinSet<()>, + worker_pool: JoinSet>, batch_interval: Duration, /// The batch prover to use. /// @@ -69,7 +68,7 @@ impl BatchBuilder { // It is important that the worker pool is filled to capacity with ready workers. See // `Self::worker_pool` and `Self::wait_for_available_worker` for more context. - let worker_pool = std::iter::repeat_n(std::future::ready(()), num_workers.get()).collect(); + let worker_pool = (0..num_workers.get()).map(|_| std::future::ready(Ok(()))).collect(); Self { batch_interval, @@ -85,7 +84,7 @@ impl BatchBuilder { /// A pool of batch-proving workers is spawned, which are fed new batch jobs periodically. /// A batch is skipped if there are no available workers, or if there are no transactions /// available to batch. - pub async fn run(mut self, mempool: SharedMempool) { + pub async fn run(mut self, mempool: SharedMempool) -> anyhow::Result<()> { assert!( self.failure_rate < 1.0 && self.failure_rate.is_sign_positive(), "Failure rate must be a percentage" @@ -99,15 +98,15 @@ impl BatchBuilder { loop { interval.tick().await; - self.build_batch(mempool.clone()).await; + self.build_batch(mempool.clone()).await?; } } #[instrument(parent = None, target = COMPONENT, name = "batch_builder.build_batch", skip_all)] - async fn build_batch(&mut self, mempool: SharedMempool) { + async fn build_batch(&mut self, mempool: SharedMempool) -> Result<(), BuildBatchError> { Span::current().set_attribute("workers.count", self.worker_pool.len()); - self.wait_for_available_worker().await; + self.wait_for_available_worker().await?; let job = BatchJob { failure_rate: self.failure_rate, @@ -118,6 +117,8 @@ impl BatchBuilder { self.worker_pool .spawn(async move { job.build_batch().await }.instrument(tracing::Span::current())); + + Ok(()) } /// Waits for a new batch building worker to become available. @@ -132,13 +133,16 @@ impl BatchBuilder { /// design was chosen instead as it removes this branching logic by "always" having the pool /// at max capacity. Instead completed workers wait to be culled by this function. #[instrument(target = COMPONENT, name = "batch_builder.wait_for_available_worker", skip_all)] - async fn wait_for_available_worker(&mut self) { + async fn wait_for_available_worker(&mut self) -> Result<(), BuildBatchError> { // We must crash here because otherwise we have a batch that has been selected from the // mempool, but which is now in limbo. This effectively corrupts the mempool. - if let Err(crash) = self.worker_pool.join_next().await.expect("worker pool is never empty") - { - tracing::error!(message=%crash, "Batch worker pool panic'd"); - panic!("Batch worker pool panic: {crash}"); + match self.worker_pool.join_next().await.expect("worker pool is never empty") { + Ok(Ok(())) => Ok(()), + Ok(Err(err)) => Err(err), + Err(crash) => { + tracing::error!(message=%crash, "Batch worker pool panic'd"); + panic!("Batch worker pool panic: {crash}"); + }, } } } @@ -151,7 +155,7 @@ impl BatchBuilder { /// It is entirely self-contained and performs the full batch creation flow, from selecting the /// batch from the [`Mempool`] up to and including submitting the results back to the [`Mempool`]. /// -/// Errors are also handled internally and are not propagated up. +/// Recoverable errors are handled internally. Mempool poison is propagated as a fatal error. struct BatchJob { /// Simulated block failure rate as a percentage. /// @@ -163,17 +167,18 @@ struct BatchJob { } impl BatchJob { - async fn build_batch(&self) { - let Some(batch) = self.select_batch().instrument(Span::current()).await else { + async fn build_batch(&self) -> Result<(), BuildBatchError> { + let Some(batch) = self.select_batch()? else { tracing::info!("No transactions available."); - return; + return Ok(()); }; batch.inject_telemetry(); let batch_id = batch.id(); - self.get_batch_inputs(batch) - .and_then(|(txs, inputs)| Self::propose_batch(txs, inputs) ) + let result = self + .get_batch_inputs(batch) + .and_then(|(txs, inputs)| Self::propose_batch(txs, inputs)) .inspect_ok(TelemetryInjectorExt::inject_telemetry) .and_then(|proposed| self.prove_batch(proposed)) @@ -181,19 +186,25 @@ impl BatchJob { // called. The system cannot handle errors after it considers the process complete // (which makes sense). .and_then(|x| self.inject_failure(x)) - .and_then(|proven_batch| async { self.commit_batch(proven_batch).await; Ok(()) }) + .and_then(|proven_batch| async { self.commit_batch(proven_batch) }) // Handle errors by propagating the error to the root span and rolling back the batch. .inspect_err(|err| Span::current().set_error(err)) - .or_else(|_err| self.rollback_batch(batch_id).never_error()) - // Error has been handled, this is just type manipulation to remove the result wrapper. - .unwrap_or_else(|_: Never| ()) .instrument(Span::current()) .await; + + match result { + Ok(()) => Ok(()), + Err(err @ BuildBatchError::MempoolPoisoned(_)) => Err(err), + Err(_) => { + self.rollback_batch(batch_id)?; + Ok(()) + }, + } } #[instrument(target = COMPONENT, name = "batch_builder.select_batch", skip_all)] - async fn select_batch(&self) -> Option { - self.mempool.lock().await.select_batch() + fn select_batch(&self) -> Result, BuildBatchError> { + Ok(self.mempool.lock().map_err(BuildBatchError::MempoolPoisoned)?.select_batch()) } #[instrument(target = COMPONENT, name = "batch_builder.get_batch_inputs", skip_all, err)] @@ -286,13 +297,21 @@ impl BatchJob { } #[instrument(target = COMPONENT, name = "batch_builder.commit_batch", skip_all)] - async fn commit_batch(&self, batch: Arc) { - self.mempool.lock().await.commit_batch(batch); + fn commit_batch(&self, batch: Arc) -> Result<(), BuildBatchError> { + self.mempool + .lock() + .map_err(BuildBatchError::MempoolPoisoned)? + .commit_batch(batch); + Ok(()) } #[instrument(target = COMPONENT, name = "batch_builder.rollback_batch", skip_all)] - async fn rollback_batch(&self, batch_id: BatchId) { - self.mempool.lock().await.rollback_batch(batch_id); + fn rollback_batch(&self, batch_id: BatchId) -> Result<(), BuildBatchError> { + self.mempool + .lock() + .map_err(BuildBatchError::MempoolPoisoned)? + .rollback_batch(batch_id); + Ok(()) } } diff --git a/crates/block-producer/src/block_builder/mod.rs b/crates/block-producer/src/block_builder/mod.rs index 08ebd38f5d..00611ebcd2 100644 --- a/crates/block-producer/src/block_builder/mod.rs +++ b/crates/block-producer/src/block_builder/mod.rs @@ -2,7 +2,6 @@ use std::ops::Deref; use std::sync::Arc; use anyhow::Context; -use futures::FutureExt; use miden_node_utils::spawn::spawn_blocking_in_current_span; use miden_node_utils::tracing::OpenTelemetrySpanExt; use miden_protocol::batch::{OrderedBatches, ProvenBatch}; @@ -82,12 +81,16 @@ impl BlockBuilder { // Exit if a fatal error occurred. // // No need for error logging since this is handled inside the function. - if let err @ Err(BuildBlockError::Desync { local_chain_tip, .. }) = - self.build_block(&mempool).await - { - return err.with_context(|| { - format!("fatal error while building block {}", local_chain_tip.child()) - }); + match self.build_block(&mempool).await { + Err(err @ BuildBlockError::Desync { local_chain_tip, .. }) => { + return Err(err).with_context(|| { + format!("fatal error while building block {}", local_chain_tip.child()) + }); + }, + Err(err @ BuildBlockError::MempoolPoisoned(_)) => { + return Err(err).context("fatal error while accessing mempool"); + }, + Err(_) | Ok(()) => {}, } } } @@ -107,7 +110,8 @@ impl BlockBuilder { async fn build_block(&self, mempool: &SharedMempool) -> Result<(), BuildBlockError> { use futures::TryFutureExt; - let selected = Self::select_block(mempool).inspect(SelectedBlock::inject_telemetry).await; + let selected = Self::select_block(mempool)?; + selected.inject_telemetry(); let block_num = selected.block_number; self.get_block_inputs(selected) @@ -121,15 +125,15 @@ impl BlockBuilder { // Handle errors by propagating the error to the root span and rolling back the block. .inspect_err(|err| Span::current().set_error(err)) .or_else(|err| async { - self.rollback_block(mempool, block_num).await; + Self::rollback_block(mempool, block_num)?; Err(err) }) .await } #[instrument(target = COMPONENT, name = "block_builder.select_block", skip_all)] - async fn select_block(mempool: &SharedMempool) -> SelectedBlock { - mempool.lock().await.select_block() + fn select_block(mempool: &SharedMempool) -> Result { + Ok(mempool.lock().map_err(BuildBlockError::MempoolPoisoned)?.select_block()) } /// Fetches block inputs from the store for the [`SelectedBlock`]. @@ -267,14 +271,15 @@ impl BlockBuilder { .map_err(BuildBlockError::StoreApplyBlockFailed)?; let (header, ..) = signed_block.into_parts(); - mempool.lock().await.commit_block(header); + mempool.lock().map_err(BuildBlockError::MempoolPoisoned)?.commit_block(header); Ok(()) } #[instrument(target = COMPONENT, name = "block_builder.rollback_block", skip_all)] - async fn rollback_block(&self, mempool: &SharedMempool, block: BlockNumber) { - mempool.lock().await.rollback_block(block); + fn rollback_block(mempool: &SharedMempool, block: BlockNumber) -> Result<(), BuildBlockError> { + mempool.lock().map_err(BuildBlockError::MempoolPoisoned)?.rollback_block(block); + Ok(()) } } diff --git a/crates/block-producer/src/errors.rs b/crates/block-producer/src/errors.rs index d32550d9c1..862a1a2ce9 100644 --- a/crates/block-producer/src/errors.rs +++ b/crates/block-producer/src/errors.rs @@ -12,6 +12,7 @@ use miden_remote_prover_client::RemoteProverClientError; use thiserror::Error; use tokio::task::JoinError; +use crate::mempool::MempoolPoisonError; use crate::validator::ValidatorError; // Block-producer errors @@ -72,6 +73,10 @@ pub enum MempoolSubmissionError { #[error("the mempool is at capacity")] CapacityExceeded, + + #[error("mempool lock is poisoned")] + #[grpc(internal)] + MempoolPoisoned(#[source] MempoolPoisonError), } // Mempool submission conflicts with current state @@ -123,6 +128,9 @@ pub enum BuildBatchError { #[error("batch proof security level is too low: {0} < {1}")] SecurityLevelTooLow(u32, u32), + + #[error("mempool lock is poisoned")] + MempoolPoisoned(#[source] MempoolPoisonError), } // Block building errors @@ -148,6 +156,9 @@ pub enum BuildBlockError { #[error("block signature is invalid")] InvalidSignature, + #[error("mempool lock is poisoned")] + MempoolPoisoned(#[source] MempoolPoisonError), + /// We sometimes randomly inject errors into the batch building process to test our failure /// responses. diff --git a/crates/block-producer/src/mempool/mod.rs b/crates/block-producer/src/mempool/mod.rs index 9395a41114..9ec79c417b 100644 --- a/crates/block-producer/src/mempool/mod.rs +++ b/crates/block-producer/src/mempool/mod.rs @@ -52,7 +52,7 @@ //! transactions even if the store and block producer momentarily disagree on the chain tip. use std::collections::{HashSet, VecDeque}; use std::num::NonZeroUsize; -use std::sync::Arc; +use std::sync::{Arc, LockResult, Mutex, MutexGuard}; use miden_node_proto::domain::mempool::MempoolEvent; use miden_node_utils::ErrorReport; @@ -60,7 +60,8 @@ use miden_protocol::batch::{BatchId, ProvenBatch}; use miden_protocol::block::{BlockHeader, BlockNumber}; use miden_protocol::transaction::{TransactionHeader, TransactionId}; use subscription::SubscriptionProvider; -use tokio::sync::{Mutex, MutexGuard, mpsc}; +use thiserror::Error; +use tokio::sync::mpsc; use tracing::instrument; use crate::block_builder::SelectedBlock; @@ -90,6 +91,10 @@ mod tests; #[derive(Clone)] pub struct SharedMempool(Arc>); +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] +#[error("shared mempool lock is poisoned")] +pub struct MempoolPoisonError; + #[derive(Debug, Clone, PartialEq)] pub struct MempoolConfig { /// The constraints each proposed block must adhere to. @@ -147,13 +152,14 @@ impl Default for MempoolConfig { // ================================================================================================ impl SharedMempool { - /// Acquires an asynchronous lock on the underlying [`Mempool`]. + /// Acquires a lock on the underlying [`Mempool`]. /// /// Callers should minimise the amount of work performed while holding the lock to reduce /// contention with other subsystems that need to access the pool. - #[instrument(target = COMPONENT, name = "mempool.lock", skip_all)] - pub async fn lock(&self) -> MutexGuard<'_, Mempool> { - self.0.lock().await + #[instrument(target = COMPONENT, name = "mempool.lock", skip_all, err)] + pub fn lock(&self) -> Result, MempoolPoisonError> { + let result: LockResult> = self.0.lock(); + result.map_err(|_| MempoolPoisonError) } } diff --git a/crates/block-producer/src/mempool/tests.rs b/crates/block-producer/src/mempool/tests.rs index e1b38b0108..4a835d0d4c 100644 --- a/crates/block-producer/src/mempool/tests.rs +++ b/crates/block-producer/src/mempool/tests.rs @@ -13,6 +13,20 @@ use crate::test_utils::batch::TransactionBatchConstructor; mod add_transaction; mod add_user_batch; +#[test] +fn shared_mempool_lock_is_poisoned_after_panic() { + let mempool = Mempool::shared(BlockNumber::GENESIS, MempoolConfig::default()); + let poisoned = mempool.clone(); + + let _ = std::thread::spawn(move || { + let _guard = poisoned.lock().expect("fresh mempool lock should not be poisoned"); + panic!("poison shared mempool lock"); + }) + .join(); + + assert!(matches!(mempool.lock(), Err(MempoolPoisonError))); +} + impl Mempool { /// Returns an empty [`Mempool`] and a perfect clone intended for use as the Unit Under Test and /// the reference instance. diff --git a/crates/block-producer/src/server/mod.rs b/crates/block-producer/src/server/mod.rs index 853875b780..0e9876d59d 100644 --- a/crates/block-producer/src/server/mod.rs +++ b/crates/block-producer/src/server/mod.rs @@ -157,10 +157,7 @@ impl BlockProducer { let batch_builder_id = tasks .spawn({ let mempool = mempool.clone(); - async { - batch_builder.run(mempool).await; - Ok(()) - } + async { batch_builder.run(mempool).await } }) .id(); let block_builder_id = tasks @@ -272,7 +269,10 @@ impl BlockProducerRpcServer { interval.tick().await; let (chain_tip, unbatched_transactions, proposed_batches, proven_batches) = { - let mempool = mempool.lock().await; + let Ok(mempool) = mempool.lock() else { + tracing::error!("mempool lock poisoned, stopping mempool stats updater"); + return; + }; ( mempool.chain_tip(), mempool.unbatched_transactions_count() as u64, @@ -301,6 +301,7 @@ impl BlockProducerRpcServer { skip_all, err )] + #[expect(clippy::let_and_return)] async fn submit_proven_tx( &self, request: proto::transaction::ProvenTransaction, @@ -336,7 +337,14 @@ impl BlockProducerRpcServer { .map(Arc::new) .map_err(MempoolSubmissionError::StateConflict)?; - self.mempool.lock().await.lock().await.add_transaction(tx).map(Into::into) + let shared_mempool = self.mempool.lock().await; + // We need the let binding here to avoid E0597 `shared_mempool` does not live long enough + let result = shared_mempool + .lock() + .map_err(MempoolSubmissionError::MempoolPoisoned)? + .add_transaction(tx) + .map(Into::into); + result } #[instrument( @@ -345,6 +353,7 @@ impl BlockProducerRpcServer { skip_all, err )] + #[expect(clippy::let_and_return)] async fn submit_proven_tx_batch( &self, request: proto::transaction::TransactionBatch, @@ -374,7 +383,14 @@ impl BlockProducerRpcServer { txs.push(tx); } - self.mempool.lock().await.lock().await.add_user_batch(&txs).map(Into::into) + let shared_mempool = self.mempool.lock().await; + // We need the let binding here to avoid E0597 `shared_mempool` does not live long enough + let result = shared_mempool + .lock() + .map_err(MempoolSubmissionError::MempoolPoisoned)? + .add_user_batch(&txs) + .map(Into::into); + result } } @@ -422,7 +438,11 @@ impl api_server::Api for BlockProducerRpcServer { &self, _request: tonic::Request<()>, ) -> Result, tonic::Status> { - let subscription = self.mempool.lock().await.lock().await.subscribe(); + let shared_mempool = self.mempool.lock().await; + let subscription = shared_mempool + .lock() + .map_err(|err| tonic::Status::internal(err.to_string()))? + .subscribe(); let subscription = ReceiverStream::new(subscription); Ok(tonic::Response::new(MempoolEventSubscription { inner: subscription })) From 4abe27eea849db79c9b242e4a41d2876c4f28bcc Mon Sep 17 00:00:00 2001 From: OllieDev Date: Thu, 21 May 2026 12:04:57 +0100 Subject: [PATCH 09/10] fix(store): move local block proving off async runtime (#2108) --- CHANGELOG.md | 1 + crates/store/src/server/block_prover_client.rs | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e18a81ad..3d4214ab99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Replaced the network monitor's JavaScript dashboard with a server-rendered Maud + HTMX frontend ([#2024](https://github.com/0xMiden/node/pull/2024)). - [BREAKING] Removed `CheckNullifiers` endpoint ([#2049](https://github.com/0xMiden/node/pull/2049)). - Replaced blocking-in-async operations in the validator, remote prover, and ntx-builder with `spawn_blocking` to avoid starving the Tokio runtime ([#2041](https://github.com/0xMiden/node/pull/2041)). +- Replaced local store block proving with `spawn_blocking` to avoid starving the Tokio runtime ([#1976](https://github.com/0xMiden/node/issues/1976)). - Implemented persistent RocksDB backend for `AccountStateForest`, improving startup time ([#2020](https://github.com/0xMiden/node/pull/2020)). - [BREAKING] Replaced binding URL env vars and CLI flags with listen socket addresses ([#2054](https://github.com/0xMiden/node/pull/2054)). - [BREAKING] `BlockRange.block_to` is now required for all RPC endpoints ([#2056](https://github.com/0xMiden/node/pull/2056)). diff --git a/crates/store/src/server/block_prover_client.rs b/crates/store/src/server/block_prover_client.rs index 5af15ac433..f34bba6851 100644 --- a/crates/store/src/server/block_prover_client.rs +++ b/crates/store/src/server/block_prover_client.rs @@ -1,4 +1,5 @@ use miden_block_prover::{BlockProverError, LocalBlockProver}; +use miden_node_utils::spawn::spawn_blocking_in_current_span; use miden_protocol::batch::OrderedBatches; use miden_protocol::block::{BlockHeader, BlockInputs, BlockProof}; use miden_remote_prover_client::{RemoteBlockProver, RemoteProverClientError}; @@ -12,6 +13,8 @@ pub enum StoreProverError { LocalProvingFailed(#[source] BlockProverError), #[error("remote proving failed")] RemoteProvingFailed(#[source] RemoteProverClientError), + #[error("local proving task join error")] + LocalProvingTaskJoin(#[source] tokio::task::JoinError), } // BLOCK PROVER @@ -43,9 +46,18 @@ impl BlockProver { block_header: &BlockHeader, ) -> Result { match self { - Self::Local(prover) => Ok(prover - .prove(tx_batches, block_header, block_inputs) - .map_err(StoreProverError::LocalProvingFailed)?), + Self::Local(prover) => { + let prover = prover.clone(); + let block_header = block_header.clone(); + + spawn_blocking_in_current_span(move || { + prover + .prove(tx_batches, &block_header, block_inputs) + .map_err(StoreProverError::LocalProvingFailed) + }) + .await + .map_err(StoreProverError::LocalProvingTaskJoin)? + }, Self::Remote(prover) => Ok(prover .prove(tx_batches, block_header, block_inputs) .await From c5864e6e58f069b8eb3e85c423bf210287770134 Mon Sep 17 00:00:00 2001 From: SantiagoPittella Date: Thu, 21 May 2026 10:02:53 -0300 Subject: [PATCH 10/10] fix: lint --- bin/ntx-builder/src/builder.rs | 5 +-- bin/ntx-builder/src/db/mod.rs | 4 +- bin/ntx-builder/src/db/models/conv.rs | 3 +- bin/ntx-builder/src/lib.rs | 64 +++++++++++++-------------- bin/ntx-builder/src/test_utils.rs | 1 - crates/store/src/db/mod.rs | 2 +- 6 files changed, 35 insertions(+), 44 deletions(-) diff --git a/bin/ntx-builder/src/builder.rs b/bin/ntx-builder/src/builder.rs index b61136861d..be5cf0b6a2 100644 --- a/bin/ntx-builder/src/builder.rs +++ b/bin/ntx-builder/src/builder.rs @@ -178,9 +178,7 @@ impl NetworkTransactionBuilder { // Compute the chain MMR that will result from advancing to this block, then persist it // atomically with the block effects so the DB stays consistent across restarts. - let next_mmr = self - .chain_state - .next_chain_mmr(&header, self.config.max_block_count); + let next_mmr = self.chain_state.next_chain_mmr(&header, self.config.max_block_count); let result = self .coordinator @@ -234,4 +232,3 @@ impl NetworkTransactionBuilder { Ok(()) } } - diff --git a/bin/ntx-builder/src/db/mod.rs b/bin/ntx-builder/src/db/mod.rs index c66939a2df..c9b9abe994 100644 --- a/bin/ntx-builder/src/db/mod.rs +++ b/bin/ntx-builder/src/db/mod.rs @@ -173,9 +173,7 @@ impl Db { /// Reads the singleton chain state row, returning the last synced block number, its header, /// and the persisted chain MMR if any block has been applied locally. - pub async fn get_chain_state( - &self, - ) -> Result> { + pub async fn get_chain_state(&self) -> Result> { self.inner.query("get_chain_state", queries::select_chain_state).await } diff --git a/bin/ntx-builder/src/db/models/conv.rs b/bin/ntx-builder/src/db/models/conv.rs index b7b6e3b946..c25c3c762e 100644 --- a/bin/ntx-builder/src/db/models/conv.rs +++ b/bin/ntx-builder/src/db/models/conv.rs @@ -80,6 +80,5 @@ pub fn partial_mmr_to_bytes(mmr: &PartialMmr) -> Vec { } pub fn partial_mmr_from_bytes(bytes: &[u8]) -> Result { - PartialMmr::read_from_bytes(bytes) - .map_err(|e| DatabaseError::deserialization("partial mmr", e)) + PartialMmr::read_from_bytes(bytes).map_err(|e| DatabaseError::deserialization("partial mmr", e)) } diff --git a/bin/ntx-builder/src/lib.rs b/bin/ntx-builder/src/lib.rs index 3d3ada8d16..e3c5b12970 100644 --- a/bin/ntx-builder/src/lib.rs +++ b/bin/ntx-builder/src/lib.rs @@ -303,7 +303,7 @@ impl NtxBuilderConfig { let block_from = stored_chain_state .as_ref() - .map_or(BlockNumber::GENESIS, |(num, _, _)| num.child()); + .map_or(BlockNumber::GENESIS, |(num, ..)| num.child()); tracing::info!( %block_from, @@ -318,37 +318,36 @@ impl NtxBuilderConfig { .context("failed to subscribe to committed blocks")?; let mut block_stream: BlockStream = Box::pin(block_stream_inner); - let (chain_state, last_applied_block) = if let Some((block_num, header, mmr)) = - stored_chain_state - { - let cs = Arc::new(SharedChainState::new(header, mmr)); - (cs, block_num) - } else { - // Fresh DB: consume the genesis block from the subscription, apply it with an empty - // chain MMR (the MMR for tip=GENESIS has no leaves by the one-block-lag convention), - // and bootstrap the in-memory chain state. - let genesis = block_stream - .next() - .await - .context("block stream ended before delivering the genesis block")? - .context("block stream failed before delivering the genesis block")?; - let genesis_header = genesis.header().clone(); - anyhow::ensure!( - genesis_header.block_num() == BlockNumber::GENESIS, - "expected genesis block from subscription but got block {}", - genesis_header.block_num() - ); - - let effects = CommittedBlockEffects::from_signed_block(&genesis); - db.apply_committed_block(effects, PartialMmr::default()) - .await - .context("failed to apply genesis block during bootstrap")?; - - let cs = Arc::new(SharedChainState::from_state(ChainState::bootstrap_genesis( - genesis_header, - ))); - (cs, BlockNumber::GENESIS) - }; + let (chain_state, last_applied_block) = + if let Some((block_num, header, mmr)) = stored_chain_state { + let cs = Arc::new(SharedChainState::new(header, mmr)); + (cs, block_num) + } else { + // Fresh DB: consume the genesis block from the subscription, apply it with an empty + // chain MMR (the MMR for tip=GENESIS has no leaves by the one-block-lag + // convention), and bootstrap the in-memory chain state. + let genesis = block_stream + .next() + .await + .context("block stream ended before delivering the genesis block")? + .context("block stream failed before delivering the genesis block")?; + let genesis_header = genesis.header().clone(); + anyhow::ensure!( + genesis_header.block_num() == BlockNumber::GENESIS, + "expected genesis block from subscription but got block {}", + genesis_header.block_num() + ); + + let effects = CommittedBlockEffects::from_signed_block(&genesis); + db.apply_committed_block(effects, PartialMmr::default()) + .await + .context("failed to apply genesis block during bootstrap")?; + + let cs = Arc::new(SharedChainState::from_state(ChainState::bootstrap_genesis( + genesis_header, + ))); + (cs, BlockNumber::GENESIS) + }; let (request_tx, actor_request_rx) = mpsc::channel(1); @@ -387,4 +386,3 @@ impl NtxBuilderConfig { )) } } - diff --git a/bin/ntx-builder/src/test_utils.rs b/bin/ntx-builder/src/test_utils.rs index b66e557fdd..c1fd3e27ca 100644 --- a/bin/ntx-builder/src/test_utils.rs +++ b/bin/ntx-builder/src/test_utils.rs @@ -52,4 +52,3 @@ pub fn mock_single_target_note( pub fn mock_block_header(block_num: BlockNumber) -> miden_protocol::block::BlockHeader { miden_protocol::block::BlockHeader::mock(block_num, None, None, &[], Word::default()) } - diff --git a/crates/store/src/db/mod.rs b/crates/store/src/db/mod.rs index 6e1632d04f..4eb7365ec4 100644 --- a/crates/store/src/db/mod.rs +++ b/crates/store/src/db/mod.rs @@ -33,6 +33,7 @@ use tracing::{info, instrument}; use crate::COMPONENT; use crate::db::migrations::apply_migrations; use crate::db::models::conv::SqlTypeConvert; +use crate::db::models::queries; pub use crate::db::models::queries::{ AccountCommitmentsPage, NullifiersPage, @@ -40,7 +41,6 @@ pub use crate::db::models::queries::{ PublicAccountStateRootsPage, }; use crate::db::models::queries::{BlockHeaderCommitment, StorageMapValuesPage}; -use crate::db::models::queries; use crate::errors::{DatabaseError, NoteSyncError}; use crate::genesis::GenesisBlock;