From e278e4aec212c0cdcc6b2fb158df0ca173982c05 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Mon, 16 Mar 2026 12:50:15 +0000 Subject: [PATCH 01/14] feat: add miden-genesis tool for canonical genesis state (#1788) New binary crate that generates canonical AggLayer genesis accounts (bridge, bridge admin, GER manager) and a genesis.toml config file. Only the bridge account is included in the genesis block; bridge admin and GER manager are generated as local accounts to be deployed later. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 15 +++ Cargo.toml | 1 + bin/genesis/Cargo.toml | 26 ++++++ bin/genesis/src/main.rs | 199 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 bin/genesis/Cargo.toml create mode 100644 bin/genesis/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 667ee1e5fe..66ab78dd12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2818,6 +2818,21 @@ dependencies = [ "unicode-width 0.1.14", ] +[[package]] +name = "miden-genesis" +version = "0.14.0-alpha.5" +dependencies = [ + "anyhow", + "clap", + "fs-err", + "hex", + "miden-agglayer", + "miden-protocol", + "miden-standards", + "rand", + "rand_chacha", +] + [[package]] name = "miden-large-smt-backend-rocksdb" version = "0.14.0-alpha.5" diff --git a/Cargo.toml b/Cargo.toml index 9c0323c2d9..afcc9daa7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "bin/genesis", "bin/network-monitor", "bin/node", "bin/remote-prover", diff --git a/bin/genesis/Cargo.toml b/bin/genesis/Cargo.toml new file mode 100644 index 0000000000..85de079eb6 --- /dev/null +++ b/bin/genesis/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "miden-genesis" +publish = false + +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true } +fs-err = { workspace = true } +hex = { workspace = true } +miden-agglayer = { version = "=0.14.0-alpha.1" } +miden-protocol = { features = ["std"], workspace = true } +miden-standards = { workspace = true } +rand = { workspace = true } +rand_chacha = { workspace = true } diff --git a/bin/genesis/src/main.rs b/bin/genesis/src/main.rs new file mode 100644 index 0000000000..872ac04c20 --- /dev/null +++ b/bin/genesis/src/main.rs @@ -0,0 +1,199 @@ +use std::path::PathBuf; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::Context; +use clap::Parser; +use miden_agglayer::create_bridge_account; +use miden_protocol::account::auth::{AuthScheme, AuthSecretKey}; +use miden_protocol::account::delta::{AccountStorageDelta, AccountVaultDelta}; +use miden_protocol::account::{ + Account, + AccountCode, + AccountDelta, + AccountFile, + AccountStorageMode, + AccountType, +}; +use miden_protocol::crypto::dsa::falcon512_rpo::{self, SecretKey as RpoSecretKey}; +use miden_protocol::crypto::rand::RpoRandomCoin; +use miden_protocol::utils::Deserializable; +use miden_protocol::{Felt, ONE, Word}; +use miden_standards::AuthMethod; +use miden_standards::account::wallets::create_basic_wallet; +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. +#[derive(Parser)] +#[command(name = "miden-genesis")] +struct Cli { + /// Output directory for generated files. + #[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. + #[arg(long, value_name = "HEX")] + 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. + #[arg(long, value_name = "HEX")] + ger_manager_public_key: Option, +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + fs_err::create_dir_all(&cli.output_dir).context("failed to create output directory")?; + + // Generate or parse bridge admin key. + let (bridge_admin_pub, bridge_admin_secret) = + resolve_falcon_key(cli.bridge_admin_public_key.as_deref(), "bridge admin")?; + + // Generate or parse GER manager key. + let (ger_manager_pub, ger_manager_secret) = + resolve_falcon_key(cli.ger_manager_public_key.as_deref(), "GER manager")?; + + // Create bridge admin wallet (nonce=0, local account to be deployed later). + let bridge_admin = create_basic_wallet( + rand::random(), + AuthMethod::SingleSig { + approver: (bridge_admin_pub.into(), AuthScheme::Falcon512Rpo), + }, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + ) + .context("failed to create bridge admin account")?; + let bridge_admin = strip_code_decorators(bridge_admin); + let bridge_admin_id = bridge_admin.id(); + + // Create GER manager wallet (nonce=0, local account to be deployed later). + let ger_manager = create_basic_wallet( + rand::random(), + AuthMethod::SingleSig { + approver: (ger_manager_pub.into(), AuthScheme::Falcon512Rpo), + }, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + ) + .context("failed to create GER manager account")?; + let ger_manager = strip_code_decorators(ger_manager); + let ger_manager_id = ger_manager.id(); + + // Create bridge account (NoAuth, nonce=0), then bump nonce to 1 for genesis. + let mut rng = ChaCha20Rng::from_seed(rand::random()); + let bridge_seed: [u64; 4] = rng.random(); + let bridge_seed = Word::from(bridge_seed.map(Felt::new)); + let bridge = create_bridge_account(bridge_seed, bridge_admin_id, ger_manager_id); + let bridge = strip_code_decorators(bridge); + + // 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. + let bridge_admin_secrets = bridge_admin_secret + .map(|sk| vec![AuthSecretKey::Falcon512Rpo(sk)]) + .unwrap_or_default(); + AccountFile::new(bridge_admin, bridge_admin_secrets) + .write(cli.output_dir.join("bridge_admin.mac")) + .context("failed to write bridge_admin.mac")?; + + let ger_manager_secrets = ger_manager_secret + .map(|sk| vec![AuthSecretKey::Falcon512Rpo(sk)]) + .unwrap_or_default(); + AccountFile::new(ger_manager, ger_manager_secrets) + .write(cli.output_dir.join("ger_manager.mac")) + .context("failed to write ger_manager.mac")?; + + let bridge_id = bridge.id(); + AccountFile::new(bridge, vec![]) + .write(cli.output_dir.join("bridge.mac")) + .context("failed to write bridge.mac")?; + + // Write genesis.toml. + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before UNIX epoch") + .as_secs(); + + let genesis_toml = format!( + r#"version = 1 +timestamp = {timestamp} + +[fee_parameters] +verification_base_fee = 0 + +[[account]] +path = "bridge.mac" +"#, + ); + + fs_err::write(cli.output_dir.join("genesis.toml"), genesis_toml) + .context("failed to write genesis.toml")?; + + println!("Genesis files written to {}", cli.output_dir.display()); + println!(" bridge_admin.mac (id: {})", bridge_admin_id.to_hex()); + println!(" ger_manager.mac (id: {})", ger_manager_id.to_hex()); + println!(" bridge.mac (id: {})", bridge_id.to_hex()); + println!(" genesis.toml"); + + Ok(()) +} + +/// Resolves a Falcon512 key pair: either parses the provided hex public key or generates a new +/// keypair. +fn resolve_falcon_key( + hex_pubkey: Option<&str>, + label: &str, +) -> anyhow::Result<(falcon512_rpo::PublicKey, Option)> { + if let Some(hex_str) = hex_pubkey { + let bytes = + hex::decode(hex_str).with_context(|| format!("invalid hex for {label} public key"))?; + let pubkey = falcon512_rpo::PublicKey::read_from_bytes(&bytes) + .with_context(|| format!("failed to deserialize {label} public key"))?; + Ok((pubkey, None)) + } else { + let mut rng = ChaCha20Rng::from_seed(rand::random()); + let auth_seed: [u64; 4] = rng.random(); + let mut coin = RpoRandomCoin::new(Word::from(auth_seed.map(Felt::new))); + let secret_key = RpoSecretKey::with_rng(&mut coin); + let public_key = secret_key.public_key(); + Ok((public_key, Some(secret_key))) + } +} + +/// Bumps an account's nonce from 0 to 1 using an `AccountDelta`. +/// +/// Genesis accounts loaded via `[[account]]` in genesis.toml are included as-is (no automatic +/// nonce bump). By convention, nonce=0 means "not yet deployed" and genesis accounts must have +/// nonce>=1. +fn bump_nonce_to_one(mut account: Account) -> anyhow::Result { + let delta = AccountDelta::new( + account.id(), + AccountStorageDelta::default(), + AccountVaultDelta::default(), + ONE, + )?; + account.apply_delta(&delta)?; + debug_assert_eq!(account.nonce(), ONE); + Ok(account) +} + +/// Strips source location decorators from an account's code MAST forest. +/// +/// This ensures serialized .mac files are deterministic regardless of build path. +fn strip_code_decorators(account: Account) -> Account { + let (id, vault, storage, code, nonce, seed) = account.into_parts(); + + let mut mast = code.mast(); + Arc::make_mut(&mut mast).strip_decorators(); + let code = AccountCode::from_parts(mast, code.procedures().to_vec()); + + Account::new_unchecked(id, vault, storage, code, nonce, seed) +} From 38b796752c670eb789bbb64f14c3b1f0fb14976c Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Mon, 16 Mar 2026 13:08:04 +0000 Subject: [PATCH 02/14] chore(genesis): add missing Cargo.toml metadata fields Add description, keywords, and exclude to match other bin/ crate conventions. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/genesis/Cargo.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bin/genesis/Cargo.toml b/bin/genesis/Cargo.toml index 85de079eb6..6821fb7000 100644 --- a/bin/genesis/Cargo.toml +++ b/bin/genesis/Cargo.toml @@ -1,11 +1,13 @@ [package] -name = "miden-genesis" -publish = false - authors.workspace = true +description = "A tool for generating canonical Miden genesis accounts and configuration" edition.workspace = true +exclude.workspace = true homepage.workspace = true +keywords = ["miden", "genesis"] license.workspace = true +name = "miden-genesis" +publish = false readme.workspace = true repository.workspace = true rust-version.workspace = true From b01042f316191e51e721d8680450287f1d3a43e5 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Mon, 16 Mar 2026 13:14:25 +0000 Subject: [PATCH 03/14] chore(genesis): fix toml formatting Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/genesis/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/genesis/Cargo.toml b/bin/genesis/Cargo.toml index 6821fb7000..a9ef0a3877 100644 --- a/bin/genesis/Cargo.toml +++ b/bin/genesis/Cargo.toml @@ -4,7 +4,7 @@ description = "A tool for generating canonical Miden genesis accounts edition.workspace = true exclude.workspace = true homepage.workspace = true -keywords = ["miden", "genesis"] +keywords = ["genesis", "miden"] license.workspace = true name = "miden-genesis" publish = false From 335a170f3032cde1e341b03f32c3bc5bd3219580 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Mon, 16 Mar 2026 13:14:37 +0000 Subject: [PATCH 04/14] chore: add changelog entry for miden-genesis tool Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de39ef4b76..df4a30b2a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Enhancements +- Added `miden-genesis` tool for generating canonical AggLayer genesis accounts and configuration ([#1797](https://github.com/0xMiden/node/pull/1797)). +- Expose per-tree RocksDB tuning options ([#1782](https://github.com/0xMiden/node/pull/1782)). - Added verbose `info!`-level logging to the network transaction builder for transaction execution, note filtering failures, and transaction outcomes ([#1770](https://github.com/0xMiden/node/pull/1770)). - [BREAKING] Move block proving from Blocker Producer to the Store ([#1579](https://github.com/0xMiden/node/pull/1579)). - [BREAKING] Updated miden-base dependencies to use `next` branch; renamed `NoteInputs` to `NoteStorage`, `.inputs()` to `.storage()`, and database `inputs` column to `storage` ([#1595](https://github.com/0xMiden/node/pull/1595)). From e6baad1487fbed7683288e0879c68c2be4a9ed01 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 17 Mar 2026 13:10:44 +0000 Subject: [PATCH 05/14] fix(genesis): cast timestamp to u32 to match protocol types GenesisConfig and BlockHeader use u32 timestamps. Cast to u32 using the same pattern as proposed_block.rs. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/genesis/src/main.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/bin/genesis/src/main.rs b/bin/genesis/src/main.rs index 872ac04c20..b01a742407 100644 --- a/bin/genesis/src/main.rs +++ b/bin/genesis/src/main.rs @@ -117,10 +117,13 @@ fn main() -> anyhow::Result<()> { .context("failed to write bridge.mac")?; // Write genesis.toml. - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time before UNIX epoch") - .as_secs(); + let timestamp = u32::try_from( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before UNIX epoch") + .as_secs(), + ) + .expect("timestamp should fit in a u32 before the year 2106"); let genesis_toml = format!( r#"version = 1 From e504d884be9074fa32cc084d463bffe8c1e1921e Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 17 Mar 2026 13:42:49 +0000 Subject: [PATCH 06/14] refactor(genesis): remove strip_code_decorators Decorator stripping was needed in build.rs for deterministic .mac files checked into the repo. The genesis tool generates files at runtime so stripping is unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/genesis/src/main.rs | 80 +++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/bin/genesis/src/main.rs b/bin/genesis/src/main.rs index b01a742407..7adae3a63e 100644 --- a/bin/genesis/src/main.rs +++ b/bin/genesis/src/main.rs @@ -1,5 +1,4 @@ -use std::path::PathBuf; -use std::sync::Arc; +use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::Context; @@ -9,7 +8,6 @@ use miden_protocol::account::auth::{AuthScheme, AuthSecretKey}; use miden_protocol::account::delta::{AccountStorageDelta, AccountVaultDelta}; use miden_protocol::account::{ Account, - AccountCode, AccountDelta, AccountFile, AccountStorageMode, @@ -47,16 +45,27 @@ struct Cli { fn main() -> anyhow::Result<()> { let cli = Cli::parse(); + run( + &cli.output_dir, + cli.bridge_admin_public_key.as_deref(), + cli.ger_manager_public_key.as_deref(), + ) +} - fs_err::create_dir_all(&cli.output_dir).context("failed to create output directory")?; +fn run( + output_dir: &Path, + bridge_admin_public_key: Option<&str>, + ger_manager_public_key: Option<&str>, +) -> anyhow::Result<()> { + fs_err::create_dir_all(output_dir).context("failed to create output directory")?; // Generate or parse bridge admin key. let (bridge_admin_pub, bridge_admin_secret) = - resolve_falcon_key(cli.bridge_admin_public_key.as_deref(), "bridge admin")?; + resolve_falcon_key(bridge_admin_public_key, "bridge admin")?; // Generate or parse GER manager key. let (ger_manager_pub, ger_manager_secret) = - resolve_falcon_key(cli.ger_manager_public_key.as_deref(), "GER manager")?; + resolve_falcon_key(ger_manager_public_key, "GER manager")?; // Create bridge admin wallet (nonce=0, local account to be deployed later). let bridge_admin = create_basic_wallet( @@ -68,7 +77,6 @@ fn main() -> anyhow::Result<()> { AccountStorageMode::Public, ) .context("failed to create bridge admin account")?; - let bridge_admin = strip_code_decorators(bridge_admin); let bridge_admin_id = bridge_admin.id(); // Create GER manager wallet (nonce=0, local account to be deployed later). @@ -81,7 +89,6 @@ fn main() -> anyhow::Result<()> { AccountStorageMode::Public, ) .context("failed to create GER manager account")?; - let ger_manager = strip_code_decorators(ger_manager); let ger_manager_id = ger_manager.id(); // Create bridge account (NoAuth, nonce=0), then bump nonce to 1 for genesis. @@ -89,7 +96,6 @@ fn main() -> anyhow::Result<()> { let bridge_seed: [u64; 4] = rng.random(); let bridge_seed = Word::from(bridge_seed.map(Felt::new)); let bridge = create_bridge_account(bridge_seed, bridge_admin_id, ger_manager_id); - let bridge = strip_code_decorators(bridge); // Bump bridge nonce to 1 (required for genesis accounts). // File-loaded accounts via [[account]] in genesis.toml are included as-is, @@ -101,19 +107,19 @@ fn main() -> anyhow::Result<()> { .map(|sk| vec![AuthSecretKey::Falcon512Rpo(sk)]) .unwrap_or_default(); AccountFile::new(bridge_admin, bridge_admin_secrets) - .write(cli.output_dir.join("bridge_admin.mac")) + .write(output_dir.join("bridge_admin.mac")) .context("failed to write bridge_admin.mac")?; let ger_manager_secrets = ger_manager_secret .map(|sk| vec![AuthSecretKey::Falcon512Rpo(sk)]) .unwrap_or_default(); AccountFile::new(ger_manager, ger_manager_secrets) - .write(cli.output_dir.join("ger_manager.mac")) + .write(output_dir.join("ger_manager.mac")) .context("failed to write ger_manager.mac")?; let bridge_id = bridge.id(); AccountFile::new(bridge, vec![]) - .write(cli.output_dir.join("bridge.mac")) + .write(output_dir.join("bridge.mac")) .context("failed to write bridge.mac")?; // Write genesis.toml. @@ -137,10 +143,10 @@ path = "bridge.mac" "#, ); - fs_err::write(cli.output_dir.join("genesis.toml"), genesis_toml) + fs_err::write(output_dir.join("genesis.toml"), genesis_toml) .context("failed to write genesis.toml")?; - println!("Genesis files written to {}", cli.output_dir.display()); + println!("Genesis files written to {}", output_dir.display()); println!(" bridge_admin.mac (id: {})", bridge_admin_id.to_hex()); println!(" ger_manager.mac (id: {})", ger_manager_id.to_hex()); println!(" bridge.mac (id: {})", bridge_id.to_hex()); @@ -188,15 +194,43 @@ fn bump_nonce_to_one(mut account: Account) -> anyhow::Result { Ok(account) } -/// Strips source location decorators from an account's code MAST forest. -/// -/// This ensures serialized .mac files are deterministic regardless of build path. -fn strip_code_decorators(account: Account) -> Account { - let (id, vault, storage, code, nonce, seed) = account.into_parts(); +#[cfg(test)] +mod tests { + use miden_protocol::ZERO; + + use super::*; + + #[test] + fn generates_expected_genesis_files() { + let dir = tempfile::tempdir().unwrap(); - let mut mast = code.mast(); - Arc::make_mut(&mut mast).strip_decorators(); - let code = AccountCode::from_parts(mast, code.procedures().to_vec()); + run(dir.path(), None, None).expect("run should succeed"); - Account::new_unchecked(id, vault, storage, code, nonce, seed) + // All 4 files should exist. + assert!(dir.path().join("bridge_admin.mac").exists()); + assert!(dir.path().join("ger_manager.mac").exists()); + assert!(dir.path().join("bridge.mac").exists()); + assert!(dir.path().join("genesis.toml").exists()); + + // Bridge account should have nonce=1 (genesis account). + let bridge = AccountFile::read(dir.path().join("bridge.mac")).unwrap(); + assert_eq!(bridge.account.nonce(), ONE); + assert!(bridge.auth_secret_keys.is_empty(), "bridge should have no secret keys"); + + // Bridge admin should have nonce=0 and include a secret key. + let admin = AccountFile::read(dir.path().join("bridge_admin.mac")).unwrap(); + assert_eq!(admin.account.nonce(), ZERO); + assert_eq!(admin.auth_secret_keys.len(), 1, "bridge admin should have a secret key"); + + // GER manager should have nonce=0 and include a secret key. + let ger = AccountFile::read(dir.path().join("ger_manager.mac")).unwrap(); + assert_eq!(ger.account.nonce(), ZERO); + assert_eq!(ger.auth_secret_keys.len(), 1, "GER manager should have a secret key"); + + // genesis.toml should reference only bridge.mac. + let toml_content = fs_err::read_to_string(dir.path().join("genesis.toml")).unwrap(); + assert!(toml_content.contains("path = \"bridge.mac\"")); + assert!(!toml_content.contains("bridge_admin.mac")); + assert!(!toml_content.contains("ger_manager.mac")); + } } From 4c88772ae70a88ed674846654683907aa9763208 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 17 Mar 2026 13:42:54 +0000 Subject: [PATCH 07/14] test(genesis): add integration test Verifies that the tool generates all expected files with correct properties: bridge has nonce=1, admin/GER manager have nonce=0 with secret keys, and genesis.toml only references bridge.mac. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/genesis/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/genesis/Cargo.toml b/bin/genesis/Cargo.toml index a9ef0a3877..417ca68a7c 100644 --- a/bin/genesis/Cargo.toml +++ b/bin/genesis/Cargo.toml @@ -26,3 +26,6 @@ miden-protocol = { features = ["std"], workspace = true } miden-standards = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } From 39115b18af259e6da29c14119273d2263c885d58 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 17 Mar 2026 13:48:25 +0000 Subject: [PATCH 08/14] feat(genesis): require both or neither public key, add tests Validate that either both --bridge-admin-public-key and --ger-manager-public-key are provided, or neither. Add integration tests for both default mode (generated keypairs with secrets) and custom mode (provided public keys without secrets). Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 1 + bin/genesis/src/main.rs | 48 +++++++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66ab78dd12..b976d8048b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2831,6 +2831,7 @@ dependencies = [ "miden-standards", "rand", "rand_chacha", + "tempfile", ] [[package]] diff --git a/bin/genesis/src/main.rs b/bin/genesis/src/main.rs index 7adae3a63e..b507f2c2a6 100644 --- a/bin/genesis/src/main.rs +++ b/bin/genesis/src/main.rs @@ -45,11 +45,16 @@ struct Cli { fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - run( - &cli.output_dir, - cli.bridge_admin_public_key.as_deref(), - cli.ger_manager_public_key.as_deref(), - ) + + match (cli.bridge_admin_public_key.as_deref(), cli.ger_manager_public_key.as_deref()) { + (Some(_), None) | (None, Some(_)) => { + anyhow::bail!( + "either both --bridge-admin-public-key and --ger-manager-public-key must be \ + provided, or neither" + ); + }, + (admin_pk, ger_pk) => run(&cli.output_dir, admin_pk, ger_pk), + } } fn run( @@ -197,11 +202,12 @@ fn bump_nonce_to_one(mut account: Account) -> anyhow::Result { #[cfg(test)] mod tests { use miden_protocol::ZERO; + use miden_protocol::utils::Serializable; use super::*; #[test] - fn generates_expected_genesis_files() { + fn default_mode_generates_accounts_with_secret_keys() { let dir = tempfile::tempdir().unwrap(); run(dir.path(), None, None).expect("run should succeed"); @@ -233,4 +239,34 @@ mod tests { assert!(!toml_content.contains("bridge_admin.mac")); assert!(!toml_content.contains("ger_manager.mac")); } + + #[test] + fn custom_public_keys_generates_accounts_without_secret_keys() { + let dir = tempfile::tempdir().unwrap(); + + // Generate two keypairs and pass their public keys as hex. + let (admin_pub, _) = resolve_falcon_key(None, "admin").unwrap(); + let (ger_pub, _) = resolve_falcon_key(None, "ger").unwrap(); + let admin_hex = hex::encode((&admin_pub).to_bytes()); + let ger_hex = hex::encode((&ger_pub).to_bytes()); + + run(dir.path(), Some(&admin_hex), Some(&ger_hex)).expect("run should succeed"); + + // Bridge admin and GER manager should have no secret keys when public keys are provided. + let admin = AccountFile::read(dir.path().join("bridge_admin.mac")).unwrap(); + assert!( + admin.auth_secret_keys.is_empty(), + "bridge admin should have no secret keys in custom mode" + ); + + let ger = AccountFile::read(dir.path().join("ger_manager.mac")).unwrap(); + assert!( + ger.auth_secret_keys.is_empty(), + "GER manager should have no secret keys in custom mode" + ); + + // Bridge should still have nonce=1. + let bridge = AccountFile::read(dir.path().join("bridge.mac")).unwrap(); + assert_eq!(bridge.account.nonce(), ONE); + } } From 699204151c5404f6a9a257fa416fb2d6d0aa3f3c Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 17 Mar 2026 15:11:54 +0000 Subject: [PATCH 09/14] refactor(genesis): use clap requires for public key validation Replace manual match-based validation with clap's requires attribute, so clap handles the error message when only one public key is provided. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/genesis/src/main.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/bin/genesis/src/main.rs b/bin/genesis/src/main.rs index b507f2c2a6..aa80df89a4 100644 --- a/bin/genesis/src/main.rs +++ b/bin/genesis/src/main.rs @@ -34,27 +34,22 @@ struct Cli { /// 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")] + #[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. - #[arg(long, value_name = "HEX")] + #[arg(long, value_name = "HEX", requires = "bridge_admin_public_key")] ger_manager_public_key: Option, } fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - - match (cli.bridge_admin_public_key.as_deref(), cli.ger_manager_public_key.as_deref()) { - (Some(_), None) | (None, Some(_)) => { - anyhow::bail!( - "either both --bridge-admin-public-key and --ger-manager-public-key must be \ - provided, or neither" - ); - }, - (admin_pk, ger_pk) => run(&cli.output_dir, admin_pk, ger_pk), - } + run( + &cli.output_dir, + cli.bridge_admin_public_key.as_deref(), + cli.ger_manager_public_key.as_deref(), + ) } fn run( From f8573b0b509048163afc69a3cf26c5e356702ab0 Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 17 Mar 2026 15:14:20 +0000 Subject: [PATCH 10/14] chore: generate_falcon_keypair helper --- bin/genesis/src/main.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/bin/genesis/src/main.rs b/bin/genesis/src/main.rs index aa80df89a4..0a5a080d47 100644 --- a/bin/genesis/src/main.rs +++ b/bin/genesis/src/main.rs @@ -61,11 +61,11 @@ fn run( // Generate or parse bridge admin key. let (bridge_admin_pub, bridge_admin_secret) = - resolve_falcon_key(bridge_admin_public_key, "bridge admin")?; + resolve_pubkey(bridge_admin_public_key, "bridge admin")?; // Generate or parse GER manager key. let (ger_manager_pub, ger_manager_secret) = - resolve_falcon_key(ger_manager_public_key, "GER manager")?; + resolve_pubkey(ger_manager_public_key, "GER manager")?; // Create bridge admin wallet (nonce=0, local account to be deployed later). let bridge_admin = create_basic_wallet( @@ -155,9 +155,19 @@ path = "bridge.mac" Ok(()) } +/// Generates a new Falcon512 keypair using a random seed. +fn generate_falcon_keypair() -> (falcon512_rpo::PublicKey, RpoSecretKey) { + let mut rng = ChaCha20Rng::from_seed(rand::random()); + let auth_seed: [u64; 4] = rng.random(); + let mut coin = RpoRandomCoin::new(Word::from(auth_seed.map(Felt::new))); + let secret_key = RpoSecretKey::with_rng(&mut coin); + let public_key = secret_key.public_key(); + (public_key, secret_key) +} + /// Resolves a Falcon512 key pair: either parses the provided hex public key or generates a new /// keypair. -fn resolve_falcon_key( +fn resolve_pubkey( hex_pubkey: Option<&str>, label: &str, ) -> anyhow::Result<(falcon512_rpo::PublicKey, Option)> { @@ -168,11 +178,7 @@ fn resolve_falcon_key( .with_context(|| format!("failed to deserialize {label} public key"))?; Ok((pubkey, None)) } else { - let mut rng = ChaCha20Rng::from_seed(rand::random()); - let auth_seed: [u64; 4] = rng.random(); - let mut coin = RpoRandomCoin::new(Word::from(auth_seed.map(Felt::new))); - let secret_key = RpoSecretKey::with_rng(&mut coin); - let public_key = secret_key.public_key(); + let (public_key, secret_key) = generate_falcon_keypair(); Ok((public_key, Some(secret_key))) } } @@ -240,8 +246,8 @@ mod tests { let dir = tempfile::tempdir().unwrap(); // Generate two keypairs and pass their public keys as hex. - let (admin_pub, _) = resolve_falcon_key(None, "admin").unwrap(); - let (ger_pub, _) = resolve_falcon_key(None, "ger").unwrap(); + let (admin_pub, _) = generate_falcon_keypair(); + let (ger_pub, _) = generate_falcon_keypair(); let admin_hex = hex::encode((&admin_pub).to_bytes()); let ger_hex = hex::encode((&ger_pub).to_bytes()); From 1c2b36a34e5160a301e101ad72a376dcffe4b629 Mon Sep 17 00:00:00 2001 From: Marti Date: Tue, 17 Mar 2026 15:14:41 +0000 Subject: [PATCH 11/14] chore: remove unnecessary code --- bin/genesis/src/main.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/bin/genesis/src/main.rs b/bin/genesis/src/main.rs index 0a5a080d47..5e1ec98e64 100644 --- a/bin/genesis/src/main.rs +++ b/bin/genesis/src/main.rs @@ -184,10 +184,6 @@ fn resolve_pubkey( } /// Bumps an account's nonce from 0 to 1 using an `AccountDelta`. -/// -/// Genesis accounts loaded via `[[account]]` in genesis.toml are included as-is (no automatic -/// nonce bump). By convention, nonce=0 means "not yet deployed" and genesis accounts must have -/// nonce>=1. fn bump_nonce_to_one(mut account: Account) -> anyhow::Result { let delta = AccountDelta::new( account.id(), @@ -196,7 +192,6 @@ fn bump_nonce_to_one(mut account: Account) -> anyhow::Result { ONE, )?; account.apply_delta(&delta)?; - debug_assert_eq!(account.nonce(), ONE); Ok(account) } @@ -233,12 +228,6 @@ mod tests { let ger = AccountFile::read(dir.path().join("ger_manager.mac")).unwrap(); assert_eq!(ger.account.nonce(), ZERO); assert_eq!(ger.auth_secret_keys.len(), 1, "GER manager should have a secret key"); - - // genesis.toml should reference only bridge.mac. - let toml_content = fs_err::read_to_string(dir.path().join("genesis.toml")).unwrap(); - assert!(toml_content.contains("path = \"bridge.mac\"")); - assert!(!toml_content.contains("bridge_admin.mac")); - assert!(!toml_content.contains("ger_manager.mac")); } #[test] From d95d05fcd7bc1ec4702cad6f9fd5b4cb703f9055 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 17 Mar 2026 16:27:22 +0000 Subject: [PATCH 12/14] docs(genesis): add README with usage and TODO for ECDSA support Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/genesis/README.md | 60 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 bin/genesis/README.md diff --git a/bin/genesis/README.md b/bin/genesis/README.md new file mode 100644 index 0000000000..ddd9f0a5fc --- /dev/null +++ b/bin/genesis/README.md @@ -0,0 +1,60 @@ +# Miden Genesis + +A tool for generating canonical Miden genesis accounts and configuration. + +## Usage + +Generate all genesis accounts with fresh keypairs: + +```bash +miden-genesis --output-dir ./genesis +``` + +Provide existing Falcon512 public keys (both must be specified together): + +```bash +miden-genesis --output-dir ./genesis \ + --bridge-admin-public-key \ + --ger-manager-public-key +``` + +## Output + +The tool generates the following files in the output directory: + +- `bridge_admin.mac` - Bridge admin wallet (nonce=0, deployed later via transaction) +- `ger_manager.mac` - GER manager wallet (nonce=0, deployed later via transaction) +- `bridge.mac` - AggLayer bridge account (nonce=1, included in genesis block) +- `genesis.toml` - Genesis configuration referencing only `bridge.mac` + +When public keys are omitted, the `.mac` files for bridge admin and GER manager include generated secret keys. When public keys are provided, no secret keys are included. + +The bridge account always uses NoAuth and has no secret keys. + +## Bootstrapping a node + +```bash +# 1. Generate genesis accounts +miden-genesis --output-dir ./genesis + +# 2. Bootstrap the genesis block +miden-node validator bootstrap \ + --genesis-block-directory ./data \ + --accounts-directory ./accounts \ + --genesis-config-file ./genesis/genesis.toml \ + --validator.key.hex + +# 3. Bootstrap the store +miden-node store bootstrap --data-directory ./data + +# 4. Start the node +miden-node bundled start --data-directory ./data ... +``` + +## TODO + +- Support ECDSA (secp256k1) public keys in addition to Falcon512 (e.g. `--bridge-admin-public-key ecdsa:`) + +## License + +This project is [MIT licensed](../../LICENSE). From ed1806906b9fea48c9d244074daccc4b96f19169 Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 17 Mar 2026 17:03:53 +0000 Subject: [PATCH 13/14] test(genesis): add e2e test that builds a valid genesis block Verifies the full round-trip: generate files, parse genesis.toml with GenesisConfig, build genesis state, confirm bridge account is present with nonce=1, and build the actual genesis block. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 3 +++ bin/genesis/Cargo.toml | 5 ++++- bin/genesis/src/main.rs | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b976d8048b..c4579bbbbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2827,11 +2827,14 @@ dependencies = [ "fs-err", "hex", "miden-agglayer", + "miden-node-store", + "miden-node-utils", "miden-protocol", "miden-standards", "rand", "rand_chacha", "tempfile", + "tokio", ] [[package]] diff --git a/bin/genesis/Cargo.toml b/bin/genesis/Cargo.toml index 417ca68a7c..51c1460524 100644 --- a/bin/genesis/Cargo.toml +++ b/bin/genesis/Cargo.toml @@ -28,4 +28,7 @@ rand = { workspace = true } rand_chacha = { workspace = true } [dev-dependencies] -tempfile = { workspace = true } +miden-node-store = { workspace = true } +miden-node-utils = { workspace = true } +tempfile = { workspace = true } +tokio = { features = ["macros", "rt-multi-thread"], workspace = true } diff --git a/bin/genesis/src/main.rs b/bin/genesis/src/main.rs index 5e1ec98e64..784ef7a66f 100644 --- a/bin/genesis/src/main.rs +++ b/bin/genesis/src/main.rs @@ -259,4 +259,37 @@ mod tests { let bridge = AccountFile::read(dir.path().join("bridge.mac")).unwrap(); assert_eq!(bridge.account.nonce(), ONE); } + + #[tokio::test] + async fn genesis_config_produces_valid_genesis_block() { + use miden_node_store::genesis::config::GenesisConfig; + use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; + + let dir = tempfile::tempdir().unwrap(); + run(dir.path(), None, None).expect("run should succeed"); + + // Read the bridge account to know its expected ID. + let bridge_file = AccountFile::read(dir.path().join("bridge.mac")).unwrap(); + let expected_bridge_id = bridge_file.account.id(); + + // Parse the generated genesis.toml. + let config = GenesisConfig::read_toml_file(&dir.path().join("genesis.toml")) + .expect("genesis.toml should be parseable"); + + // Build the genesis state and block. + let signer = SecretKey::read_from_bytes(&[0x01; 32]).unwrap(); + let (state, _secrets) = config.into_state(signer).expect("genesis state should build"); + + // The genesis state should contain the bridge account (+ the default MIDEN faucet). + let bridge_in_genesis = state + .accounts + .iter() + .find(|a| a.id() == expected_bridge_id) + .expect("bridge account should be in genesis state"); + assert_eq!(bridge_in_genesis.nonce(), ONE); + + // Build the actual genesis block to verify it's valid. + let block = state.into_block().await.expect("genesis block should build successfully"); + assert_eq!(block.inner().header().block_num(), miden_protocol::block::BlockNumber::GENESIS); + } } From 693ee9ce414dab45448fda92de71869c64f6b49c Mon Sep 17 00:00:00 2001 From: "Claude (Opus)" Date: Tue, 17 Mar 2026 17:06:46 +0000 Subject: [PATCH 14/14] refactor(genesis): consolidate tests with shared genesis block assertion Both tests now verify the full round-trip through GenesisConfig and genesis block creation via a shared helper, removing redundant checks. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/genesis/src/main.rs | 97 ++++++++++++----------------------------- 1 file changed, 28 insertions(+), 69 deletions(-) diff --git a/bin/genesis/src/main.rs b/bin/genesis/src/main.rs index 784ef7a66f..425dc159dc 100644 --- a/bin/genesis/src/main.rs +++ b/bin/genesis/src/main.rs @@ -197,99 +197,58 @@ fn bump_nonce_to_one(mut account: Account) -> anyhow::Result { #[cfg(test)] mod tests { - use miden_protocol::ZERO; + use miden_node_store::genesis::config::GenesisConfig; + use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; use miden_protocol::utils::Serializable; use super::*; - #[test] - fn default_mode_generates_accounts_with_secret_keys() { - let dir = tempfile::tempdir().unwrap(); + /// Parses the generated genesis.toml, builds a genesis block, and asserts the bridge account + /// is included with nonce=1. + async fn assert_valid_genesis_block(dir: &Path) { + let bridge_id = AccountFile::read(dir.join("bridge.mac")).unwrap().account.id(); + + let config = GenesisConfig::read_toml_file(&dir.join("genesis.toml")).unwrap(); + let signer = SecretKey::read_from_bytes(&[0x01; 32]).unwrap(); + let (state, _) = config.into_state(signer).unwrap(); - run(dir.path(), None, None).expect("run should succeed"); + let bridge = state.accounts.iter().find(|a| a.id() == bridge_id).unwrap(); + assert_eq!(bridge.nonce(), ONE); - // All 4 files should exist. - assert!(dir.path().join("bridge_admin.mac").exists()); - assert!(dir.path().join("ger_manager.mac").exists()); - assert!(dir.path().join("bridge.mac").exists()); - assert!(dir.path().join("genesis.toml").exists()); + state.into_block().await.expect("genesis block should build"); + } - // Bridge account should have nonce=1 (genesis account). - let bridge = AccountFile::read(dir.path().join("bridge.mac")).unwrap(); - assert_eq!(bridge.account.nonce(), ONE); - assert!(bridge.auth_secret_keys.is_empty(), "bridge should have no secret keys"); + #[tokio::test] + async fn default_mode_includes_secret_keys() { + let dir = tempfile::tempdir().unwrap(); + run(dir.path(), None, None).unwrap(); - // Bridge admin should have nonce=0 and include a secret key. let admin = AccountFile::read(dir.path().join("bridge_admin.mac")).unwrap(); - assert_eq!(admin.account.nonce(), ZERO); - assert_eq!(admin.auth_secret_keys.len(), 1, "bridge admin should have a secret key"); + assert_eq!(admin.auth_secret_keys.len(), 1); - // GER manager should have nonce=0 and include a secret key. let ger = AccountFile::read(dir.path().join("ger_manager.mac")).unwrap(); - assert_eq!(ger.account.nonce(), ZERO); - assert_eq!(ger.auth_secret_keys.len(), 1, "GER manager should have a secret key"); + assert_eq!(ger.auth_secret_keys.len(), 1); + + assert_valid_genesis_block(dir.path()).await; } - #[test] - fn custom_public_keys_generates_accounts_without_secret_keys() { + #[tokio::test] + async fn custom_public_keys_excludes_secret_keys() { let dir = tempfile::tempdir().unwrap(); - // Generate two keypairs and pass their public keys as hex. let (admin_pub, _) = generate_falcon_keypair(); let (ger_pub, _) = generate_falcon_keypair(); let admin_hex = hex::encode((&admin_pub).to_bytes()); let ger_hex = hex::encode((&ger_pub).to_bytes()); - run(dir.path(), Some(&admin_hex), Some(&ger_hex)).expect("run should succeed"); + run(dir.path(), Some(&admin_hex), Some(&ger_hex)).unwrap(); - // Bridge admin and GER manager should have no secret keys when public keys are provided. let admin = AccountFile::read(dir.path().join("bridge_admin.mac")).unwrap(); - assert!( - admin.auth_secret_keys.is_empty(), - "bridge admin should have no secret keys in custom mode" - ); + assert!(admin.auth_secret_keys.is_empty()); let ger = AccountFile::read(dir.path().join("ger_manager.mac")).unwrap(); - assert!( - ger.auth_secret_keys.is_empty(), - "GER manager should have no secret keys in custom mode" - ); - - // Bridge should still have nonce=1. - let bridge = AccountFile::read(dir.path().join("bridge.mac")).unwrap(); - assert_eq!(bridge.account.nonce(), ONE); - } - - #[tokio::test] - async fn genesis_config_produces_valid_genesis_block() { - use miden_node_store::genesis::config::GenesisConfig; - use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; - - let dir = tempfile::tempdir().unwrap(); - run(dir.path(), None, None).expect("run should succeed"); - - // Read the bridge account to know its expected ID. - let bridge_file = AccountFile::read(dir.path().join("bridge.mac")).unwrap(); - let expected_bridge_id = bridge_file.account.id(); + assert!(ger.auth_secret_keys.is_empty()); - // Parse the generated genesis.toml. - let config = GenesisConfig::read_toml_file(&dir.path().join("genesis.toml")) - .expect("genesis.toml should be parseable"); - - // Build the genesis state and block. - let signer = SecretKey::read_from_bytes(&[0x01; 32]).unwrap(); - let (state, _secrets) = config.into_state(signer).expect("genesis state should build"); - - // The genesis state should contain the bridge account (+ the default MIDEN faucet). - let bridge_in_genesis = state - .accounts - .iter() - .find(|a| a.id() == expected_bridge_id) - .expect("bridge account should be in genesis state"); - assert_eq!(bridge_in_genesis.nonce(), ONE); - - // Build the actual genesis block to verify it's valid. - let block = state.into_block().await.expect("genesis block should build successfully"); - assert_eq!(block.inner().header().block_num(), miden_protocol::block::BlockNumber::GENESIS); + assert_valid_genesis_block(dir.path()).await; } }