-
Notifications
You must be signed in to change notification settings - Fork 104
feat: add miden-genesis tool for canonical genesis state #1797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e278e4a
38b7967
b01042f
335a170
e6baad1
e504d88
4c88772
39115b1
6992041
f8573b0
1c2b36a
d95d05f
ed18069
693ee9c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| [workspace] | ||
| members = [ | ||
| "bin/genesis", | ||
| "bin/network-monitor", | ||
| "bin/node", | ||
| "bin/remote-prover", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| [package] | ||
| authors.workspace = true | ||
| description = "A tool for generating canonical Miden genesis accounts and configuration" | ||
| edition.workspace = true | ||
| exclude.workspace = true | ||
| homepage.workspace = true | ||
| keywords = ["genesis", "miden"] | ||
| license.workspace = true | ||
| name = "miden-genesis" | ||
| publish = false | ||
| 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 } | ||
|
|
||
| [dev-dependencies] | ||
| miden-node-store = { workspace = true } | ||
| miden-node-utils = { workspace = true } | ||
| tempfile = { workspace = true } | ||
| tokio = { features = ["macros", "rt-multi-thread"], workspace = true } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| # Miden Genesis | ||
|
|
||
| A tool for generating canonical Miden genesis accounts and configuration. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is intended for internal use only, then I would add that disclaimer here, and repeat it where applicable. I'm concerned that local node users will attempt to use this. |
||
|
|
||
| ## 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 <HEX> \ | ||
| --ger-manager-public-key <HEX> | ||
| ``` | ||
|
|
||
| ## 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. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is intended for internal use only, why do we have two different "modes"? |
||
|
|
||
| The bridge account always uses NoAuth and has no secret keys. | ||
|
|
||
| ## Bootstrapping a node | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is going to get outdated pretty often and we'll forget to update this. It would be better to keep this in our operator docs. |
||
|
|
||
| ```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 <validator_key> | ||
|
|
||
| # 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:<HEX>`) | ||
|
|
||
| ## License | ||
|
|
||
| This project is [MIT licensed](../../LICENSE). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,254 @@ | ||
| use std::path::{Path, PathBuf}; | ||
| 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, | ||
| 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", requires = "ger_manager_public_key")] | ||
| bridge_admin_public_key: Option<String>, | ||
|
|
||
| /// 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<String>, | ||
| } | ||
|
|
||
| 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(), | ||
| ) | ||
| } | ||
|
|
||
| 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_pubkey(bridge_admin_public_key, "bridge admin")?; | ||
|
|
||
| // Generate or parse GER manager key. | ||
| let (ger_manager_pub, ger_manager_secret) = | ||
| 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( | ||
| 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_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_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); | ||
|
|
||
| // 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(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(output_dir.join("ger_manager.mac")) | ||
| .context("failed to write ger_manager.mac")?; | ||
|
|
||
| let bridge_id = bridge.id(); | ||
| AccountFile::new(bridge, vec![]) | ||
| .write(output_dir.join("bridge.mac")) | ||
| .context("failed to write bridge.mac")?; | ||
|
|
||
| // Write genesis.toml. | ||
| 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 | ||
| timestamp = {timestamp} | ||
|
|
||
| [fee_parameters] | ||
| verification_base_fee = 0 | ||
|
|
||
| [[account]] | ||
| path = "bridge.mac" | ||
| "#, | ||
| ); | ||
|
|
||
| fs_err::write(output_dir.join("genesis.toml"), genesis_toml) | ||
| .context("failed to write genesis.toml")?; | ||
|
|
||
| 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()); | ||
| println!(" genesis.toml"); | ||
|
|
||
| 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_pubkey( | ||
| hex_pubkey: Option<&str>, | ||
| label: &str, | ||
| ) -> anyhow::Result<(falcon512_rpo::PublicKey, Option<RpoSecretKey>)> { | ||
| 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 (public_key, secret_key) = generate_falcon_keypair(); | ||
| Ok((public_key, Some(secret_key))) | ||
| } | ||
| } | ||
|
|
||
| /// Bumps an account's nonce from 0 to 1 using an `AccountDelta`. | ||
| fn bump_nonce_to_one(mut account: Account) -> anyhow::Result<Account> { | ||
| let delta = AccountDelta::new( | ||
| account.id(), | ||
| AccountStorageDelta::default(), | ||
| AccountVaultDelta::default(), | ||
| ONE, | ||
| )?; | ||
| account.apply_delta(&delta)?; | ||
| Ok(account) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use miden_node_store::genesis::config::GenesisConfig; | ||
| use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; | ||
| use miden_protocol::utils::Serializable; | ||
|
|
||
| use super::*; | ||
|
|
||
| /// 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(); | ||
|
|
||
| let bridge = state.accounts.iter().find(|a| a.id() == bridge_id).unwrap(); | ||
| assert_eq!(bridge.nonce(), ONE); | ||
|
|
||
| state.into_block().await.expect("genesis block should build"); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn default_mode_includes_secret_keys() { | ||
| let dir = tempfile::tempdir().unwrap(); | ||
| run(dir.path(), None, None).unwrap(); | ||
|
|
||
| let admin = AccountFile::read(dir.path().join("bridge_admin.mac")).unwrap(); | ||
| assert_eq!(admin.auth_secret_keys.len(), 1); | ||
|
|
||
| let ger = AccountFile::read(dir.path().join("ger_manager.mac")).unwrap(); | ||
| assert_eq!(ger.auth_secret_keys.len(), 1); | ||
|
|
||
| assert_valid_genesis_block(dir.path()).await; | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn custom_public_keys_excludes_secret_keys() { | ||
| let dir = tempfile::tempdir().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()); | ||
|
|
||
| run(dir.path(), Some(&admin_hex), Some(&ger_hex)).unwrap(); | ||
|
|
||
| let admin = AccountFile::read(dir.path().join("bridge_admin.mac")).unwrap(); | ||
| 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()); | ||
|
|
||
| assert_valid_genesis_block(dir.path()).await; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you describe the different deployment flows and where this fits in?
Some bikeshedding
Should this be
miden-node make-genesis? Bear in mind that we probably want to access this binary somewhere where we can't (or don't want to) compile code. And therefore this should likely be installable and versionable.Some thought experiments
Maybe we should have a
miden-node genesissubcommand?