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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions bin/node/src/commands/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,11 @@ async fn read_bootstrap_genesis_block(
genesis_block_file: Option<&Path>,
network: Option<OfficialNetwork>,
) -> anyhow::Result<GenesisBlock> {
let signed_block = match (genesis_block_file, network) {
(Some(path), None) => read_genesis_block(path)?,
(None, Some(network)) => fetch_genesis_block(network).await?,
match (genesis_block_file, network) {
(Some(path), None) => read_genesis_block(path),
(None, Some(network)) => fetch_genesis_block(network).await,
_ => unreachable!("clap requires exactly one genesis block source"),
};
GenesisBlock::try_from(signed_block)
}
}

// MIGRATE
Expand Down
1 change: 1 addition & 0 deletions bin/ntx-builder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ build-rs = { workspace = true }
miden-node-db = { workspace = true }

[dev-dependencies]
fs-err = { workspace = true }
miden-node-utils = { features = ["testing"], workspace = true }
miden-protocol = { default-features = true, features = ["testing"], workspace = true }
miden-standards = { features = ["testing"], workspace = true }
Expand Down
9 changes: 4 additions & 5 deletions bin/ntx-builder/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,9 @@ async fn read_bootstrap_genesis_block(
genesis_block_file: Option<&Path>,
network: Option<OfficialNetwork>,
) -> anyhow::Result<GenesisBlock> {
let signed_block = match (genesis_block_file, network) {
(Some(path), None) => read_genesis_block(path)?,
(None, Some(network)) => fetch_genesis_block(network).await?,
match (genesis_block_file, network) {
(Some(path), None) => read_genesis_block(path),
(None, Some(network)) => fetch_genesis_block(network).await,
_ => unreachable!("clap requires exactly one genesis block source"),
};
GenesisBlock::try_from(signed_block)
}
}
32 changes: 26 additions & 6 deletions bin/ntx-builder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,33 @@ pub fn migrate(database_filepath: impl AsRef<Path>) -> anyhow::Result<()> {

#[cfg(test)]
mod bootstrap_tests {
use miden_node_store::genesis::GenesisBlock;
use miden_node_store::genesis::{GenesisBlock, GenesisState};
use miden_node_utils::fee::{test_fee_params, test_protocol_config};
use miden_protocol::block::{BlockSignatures, SignedBlock};
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey;

#[test]
fn genesis_block_accepts_unsigned_block() {
let block = crate::test_utils::mock_genesis_block();
GenesisBlock::try_from(block).expect("unsigned genesis block should validate");
#[tokio::test]
async fn bootstrap_accepts_genesis_artifact_with_protocol_config() {
use miden_node_utils::genesis::read_genesis_block;
use miden_protocol::utils::serde::Serializable;

let genesis = GenesisState::new(
Vec::new(),
test_fee_params(),
0,
crate::test_utils::mock_genesis_block().header().validator_config().clone(),
test_protocol_config(),
)
.into_block()
.unwrap();
let root = tempfile::tempdir().unwrap();
let path = root.path().join("genesis.dat");
fs_err::write(&path, genesis.to_bytes()).unwrap();
let decoded = read_genesis_block(&path).unwrap();
assert_eq!(decoded.protocol_config(), genesis.protocol_config());
let database_path = root.path().join("ntx.sqlite3");
super::bootstrap(database_path.clone(), &decoded).await.unwrap();
assert!(database_path.is_file());
}

#[test]
Expand All @@ -77,7 +96,8 @@ mod bootstrap_tests {
let signatures = BlockSignatures::new(vec![signature]).unwrap();
let block = SignedBlock::new_unchecked(header, body, signatures);

let err = GenesisBlock::try_from(block).expect_err("signed genesis block should fail");
let err = GenesisBlock::new(block, test_protocol_config())
.expect_err("signed genesis block should fail");

assert!(err.to_string().contains("must not carry signatures"), "unexpected error: {err}");
}
Expand Down
54 changes: 47 additions & 7 deletions bin/validator/src/commands/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use std::path::Path;

use anyhow::Context;
use miden_node_store::BlockStore;
use miden_node_store::genesis::GenesisBlock;
use miden_node_tracing::info;
use miden_node_utils::fs::ensure_empty_directory;
use miden_node_utils::genesis::read_genesis_block;
Expand All @@ -13,8 +12,8 @@ use miden_validator::DataDirectory;
/// produced by the `genesis` command.
///
/// The genesis block is the chain's trust root and carries no signatures; it must come from a
/// trusted source. This command verifies the block (via [`GenesisBlock::try_from`]) and persists
/// it as the chain tip.
/// trusted source. This command verifies the block and its protocol configuration. It persists
/// the block as the chain tip.
pub async fn bootstrap(
data_directory: &Path,
sqlite_connection_pool_size: NonZeroUsize,
Expand All @@ -33,10 +32,8 @@ pub async fn bootstrap(
let dirs = DataDirectory::load(data_directory.to_path_buf())
.context("failed to load the data directory")?;

let signed_block =
read_genesis_block(genesis_block_file).context("failed to read genesis block file")?;
let genesis_block =
GenesisBlock::try_from(signed_block).context("genesis block validation failed")?;
read_genesis_block(genesis_block_file).context("failed to read genesis block file")?;
let genesis_commitment = genesis_block.inner().header().commitment();

let _ = BlockStore::bootstrap(dirs.block_store_dir(), &genesis_block)?;
Expand All @@ -62,8 +59,9 @@ pub async fn bootstrap(

#[cfg(test)]
mod tests {
use miden_protocol::block::BlockNumber;
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey;
use miden_protocol::utils::serde::Deserializable;
use miden_protocol::utils::serde::{Deserializable, Serializable};

use super::*;

Expand Down Expand Up @@ -95,6 +93,24 @@ mod tests {

assert!(genesis_directory.join("genesis.dat").is_file());
assert!(data_directory.join("validator.sqlite3").is_file());

let genesis = read_genesis_block(&genesis_directory.join("genesis.dat")).unwrap();
let config = genesis.protocol_config().clone();
let commitment = genesis.inner().header().protocol_config_commitment();
let block_bytes = genesis.inner().to_bytes();
assert_eq!(config.to_commitment(), commitment);
let node_directory = root.path().join("node");
fs_err::create_dir(&node_directory).unwrap();
miden_node_store::State::bootstrap(genesis, &node_directory).unwrap();
let directories = miden_node_store::DataDirectory::load(node_directory).unwrap();
let block_store = BlockStore::load(directories.block_store_dir()).unwrap();
assert_eq!(block_store.load_block(BlockNumber::GENESIS).await.unwrap(), Some(block_bytes));
let db = miden_node_store::Db::load(directories.database_path()).await.unwrap();
assert_eq!(
db.select_protocol_config_by_commitment(commitment).await.unwrap(),
Some(config)
);

assert!(
fs_err::read_dir(&accounts_directory)
.expect("accounts directory should be readable")
Expand All @@ -107,4 +123,28 @@ mod tests {
"genesis should write the generated native faucet account file",
);
}

#[tokio::test]
async fn bootstrap_rejects_block_only_genesis_before_creating_database() {
use miden_node_store::genesis::GenesisState;
use miden_node_utils::fee::{test_fee_params, test_protocol_config};
use miden_protocol::block::ValidatorConfig;

let root = tempfile::tempdir().unwrap();
let key = SigningKey::read_from_bytes(&[7; 32]).unwrap().public_key();
let genesis = GenesisState::new(
Vec::new(),
test_fee_params(),
0,
ValidatorConfig::new(vec![key], 1).unwrap(),
test_protocol_config(),
)
.into_block()
.unwrap();
let path = root.path().join("genesis.dat");
fs_err::write(&path, genesis.inner().to_bytes()).unwrap();
let data_directory = root.path().join("data");
assert!(bootstrap(&data_directory, NonZeroUsize::new(2).unwrap(), &path).await.is_err());
assert!(!data_directory.join("validator.sqlite3").exists());
}
}
2 changes: 1 addition & 1 deletion bin/validator/src/commands/dkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1570,7 +1570,7 @@ fn take_scalar(bytes: &mut &[u8]) -> anyhow::Result<StorageScalar> {

/// Reads and validates the trusted genesis block used by the ceremony.
fn read_trusted_genesis(path: &Path) -> anyhow::Result<GenesisBlock> {
GenesisBlock::try_from(read_genesis_block(path)?).context("failed to validate genesis block")
read_genesis_block(path).context("failed to validate genesis block")
}

/// Commits a validator signature to one genesis-bound DKG identity registration.
Expand Down
3 changes: 1 addition & 2 deletions bin/validator/src/commands/dkg/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ fn write_genesis_with_validator_count(
Some(&config_path),
validators,
)?;
let genesis =
GenesisBlock::try_from(read_genesis_block(&genesis_directory.join("genesis.dat"))?)?;
let genesis = read_genesis_block(&genesis_directory.join("genesis.dat"))?;
Ok(TestGenesis {
path: genesis_directory.join("genesis.dat"),
signing_keys,
Expand Down
2 changes: 1 addition & 1 deletion bin/validator/src/commands/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ pub fn generate(
let genesis_block = genesis_state.into_block().context("failed to build the genesis block")?;

let genesis_block_path = genesis_block_directory.join(GENESIS_BLOCK_FILE_NAME);
fs_err::write(&genesis_block_path, genesis_block.inner().to_bytes())
fs_err::write(&genesis_block_path, genesis_block.to_bytes())
.context("failed to write genesis block")?;

println!("Genesis block written to {}.", genesis_block_path.display());
Expand Down
45 changes: 33 additions & 12 deletions crates/rpc/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ use miden_protocol::block::{
ValidatorConfig,
};
use miden_protocol::note::NoteType;
use miden_protocol::protocol_config::ProtocolConfig;
use miden_protocol::testing::account_id::{ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_SENDER};
use miden_protocol::testing::noop_auth_component::NoopAuthComponent;
use miden_protocol::transaction::{
Expand Down Expand Up @@ -150,9 +151,13 @@ impl TestStore {
}
}

async fn start_from_mock_genesis(genesis_block: &ProvenBlock) -> Self {
async fn start_from_mock_genesis(
genesis_block: &ProvenBlock,
protocol_config: &ProtocolConfig,
) -> Self {
let data_directory = new_tempdir();
let genesis_commitment = Self::bootstrap_from_mock_genesis(&data_directory, genesis_block);
let genesis_commitment =
Self::bootstrap_from_mock_genesis(&data_directory, genesis_block, protocol_config);
let (state, ..) = State::for_tests(&data_directory).await;
Self {
state,
Expand Down Expand Up @@ -183,15 +188,19 @@ impl TestStore {
genesis_commitment
}

fn bootstrap_from_mock_genesis(path: &std::path::Path, genesis_block: &ProvenBlock) -> Word {
fn bootstrap_from_mock_genesis(
path: &std::path::Path,
genesis_block: &ProvenBlock,
protocol_config: &ProtocolConfig,
) -> Word {
let signatures = BlockSignatures::new(Vec::new()).unwrap();
let signed_block = SignedBlock::new(
genesis_block.header().clone(),
genesis_block.body().clone(),
signatures,
)
.expect("mock genesis header and body should be consistent");
let genesis_block = GenesisBlock::try_from(signed_block)
let genesis_block = GenesisBlock::new(signed_block, protocol_config.clone())
.expect("mock genesis should become a store genesis block after stripping signatures");
let genesis_commitment = genesis_block.inner().header().commitment();

Expand Down Expand Up @@ -326,6 +335,7 @@ fn replace_transaction_proof(
struct ValidBatchFixture {
request: proto::submission::TransactionBatch,
genesis_block: ProvenBlock,
protocol_config: ProtocolConfig,
}

async fn build_valid_batch_fixture() -> ValidBatchFixture {
Expand All @@ -349,6 +359,7 @@ async fn build_valid_batch_fixture() -> ValidBatchFixture {
.unwrap();
let mock_chain = mock_chain_builder.build().unwrap();
let genesis_block = mock_chain.latest_block();
let protocol_config = mock_chain.protocol_config().clone();

let tx_context = mock_chain
.build_transaction(account.id())
Expand Down Expand Up @@ -388,7 +399,7 @@ async fn build_valid_batch_fixture() -> ValidBatchFixture {
sealed_transaction_inputs: vec![proto::submission::SealedTransactionInputs::default()],
};

ValidBatchFixture { request, genesis_block }
ValidBatchFixture { request, genesis_block, protocol_config }
}

fn assert_beyond_tip(status: &tonic::Status, endpoint: &str) {
Expand Down Expand Up @@ -687,7 +698,10 @@ async fn rpc_server_rejects_invalid_deferred_transaction_proofs() {
async fn rpc_server_forwards_valid_deferred_proofs_and_rejects_missing_witnesses() {
let fixture = deferred_transaction_fixture().await;
let data_directory = new_tempdir();
State::bootstrap(fixture.genesis.clone().try_into().unwrap(), &data_directory).unwrap();
let genesis =
GenesisBlock::new(fixture.genesis.clone(), fixture.inputs.protocol_config().clone())
.unwrap();
State::bootstrap(genesis, &data_directory).unwrap();
let (state, ..) = State::for_tests(&data_directory).await;
let submissions = Arc::new(std::sync::Mutex::new(Vec::new()));
let (validator, _, _, _guard) =
Expand Down Expand Up @@ -928,16 +942,22 @@ async fn start_source_rpc(
async fn start_source_rpc_with_genesis(
ntx_builder: NtxBuilderClient,
validator: ValidatorClient,
genesis_block: Option<&ProvenBlock>,
genesis_block: Option<(&ProvenBlock, &ProtocolConfig)>,
) -> (RpcClient, TestStore, TestServerGuard) {
let store = match genesis_block {
Some(genesis_block) => TestStore::start_from_mock_genesis(genesis_block).await,
Some((genesis_block, protocol_config)) => {
TestStore::start_from_mock_genesis(genesis_block, protocol_config).await
},
None => TestStore::start().await,
};
let block_producer_dir = new_tempdir();
match genesis_block {
Some(genesis_block) => {
TestStore::bootstrap_from_mock_genesis(&block_producer_dir, genesis_block);
Some((genesis_block, protocol_config)) => {
TestStore::bootstrap_from_mock_genesis(
&block_producer_dir,
genesis_block,
protocol_config,
);
},
None => {
TestStore::bootstrap(&block_producer_dir);
Expand Down Expand Up @@ -1389,10 +1409,11 @@ async fn full_node_forwards_complete_transaction_batch_to_source_rpc() {
let (source_rpc, _source_store, _source_server) = start_source_rpc_with_genesis(
dummy_client::<NtxBuilderClient>(),
validator,
Some(&fixture.genesis_block),
Some((&fixture.genesis_block, &fixture.protocol_config)),
)
.await;
let local_store = TestStore::start_from_mock_genesis(&fixture.genesis_block).await;
let local_store =
TestStore::start_from_mock_genesis(&fixture.genesis_block, &fixture.protocol_config).await;
let full_node = RpcService::new(
Arc::clone(&local_store.state),
RpcBackend::full_node(source_rpc, None),
Expand Down
4 changes: 4 additions & 0 deletions crates/store/src/db/migrations/006_protocol_configs.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE TABLE protocol_configs (
commitment BLOB NOT NULL PRIMARY KEY CHECK (length(commitment) = 32),
protocol_config BLOB NOT NULL
) WITHOUT ROWID;
Comment on lines +1 to +4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we also want to store the block height at which a given config became "effective"? i.e., the genesis config would be effective as of block 0 etc.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed here: #2596 (comment)

3 changes: 2 additions & 1 deletion crates/store/src/db/migrations/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ use super::*;
use crate::db::models::queries::VALID_FOREVER;
use crate::db::schema;

const EXPECTED_SCHEMA_HASHES: [SchemaHash; 5] = [
const EXPECTED_SCHEMA_HASHES: [SchemaHash; 6] = [
SchemaHash::from_hex("cc92cb332410e6f63036b52cf953acb446c142d5c0fbbdbd6d3b4f466510b210"),
SchemaHash::from_hex("7c783947d0bb2c9745d28f4bdcf329f84ad970c36aa07ea85441e62718d8bbbb"),
SchemaHash::from_hex("e026a70464e897ae9a217f45c80d72341b1bfb757200e57e41145348473a9961"),
SchemaHash::from_hex("a581a13b00e4aa1d4539459e2b351c0585fad33c5a876f830c9b943adac92dea"),
SchemaHash::from_hex("34bd293251a2647715dd91fa245bcd98d635e8070871b4f8335b3a3db364fc1e"),
SchemaHash::from_hex("303f71c67f038e46bd5b3234b0f5c84c37bbc1ce9f5ba65b2368ebbb0f24319b"),
];

#[test]
Expand Down
Loading
Loading