Skip to content

Commit ce366f3

Browse files
mmagicianclaude
andauthored
feat: add miden-genesis tool for canonical genesis state (#1797)
Co-authored-by: Claude (Opus) <noreply@anthropic.com>
1 parent 5a3b1bc commit ce366f3

File tree

6 files changed

+370
-0
lines changed

6 files changed

+370
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Enhancements
66

7+
- Added `miden-genesis` tool for generating canonical AggLayer genesis accounts and configuration ([#1797](https://github.com/0xMiden/node/pull/1797)).
8+
- Expose per-tree RocksDB tuning options ([#1782](https://github.com/0xMiden/node/pull/1782)).
79
- 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)).
810
- [BREAKING] Move block proving from Blocker Producer to the Store ([#1579](https://github.com/0xMiden/node/pull/1579)).
911
- [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)).

Cargo.lock

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[workspace]
22
members = [
3+
"bin/genesis",
34
"bin/network-monitor",
45
"bin/node",
56
"bin/remote-prover",

bin/genesis/Cargo.toml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[package]
2+
authors.workspace = true
3+
description = "A tool for generating canonical Miden genesis accounts and configuration"
4+
edition.workspace = true
5+
exclude.workspace = true
6+
homepage.workspace = true
7+
keywords = ["genesis", "miden"]
8+
license.workspace = true
9+
name = "miden-genesis"
10+
publish = false
11+
readme.workspace = true
12+
repository.workspace = true
13+
rust-version.workspace = true
14+
version.workspace = true
15+
16+
[lints]
17+
workspace = true
18+
19+
[dependencies]
20+
anyhow = { workspace = true }
21+
clap = { workspace = true }
22+
fs-err = { workspace = true }
23+
hex = { workspace = true }
24+
miden-agglayer = { version = "=0.14.0-alpha.1" }
25+
miden-protocol = { features = ["std"], workspace = true }
26+
miden-standards = { workspace = true }
27+
rand = { workspace = true }
28+
rand_chacha = { workspace = true }
29+
30+
[dev-dependencies]
31+
miden-node-store = { workspace = true }
32+
miden-node-utils = { workspace = true }
33+
tempfile = { workspace = true }
34+
tokio = { features = ["macros", "rt-multi-thread"], workspace = true }

bin/genesis/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Miden Genesis
2+
3+
A tool for generating canonical Miden genesis accounts and configuration.
4+
5+
## Usage
6+
7+
Generate all genesis accounts with fresh keypairs:
8+
9+
```bash
10+
miden-genesis --output-dir ./genesis
11+
```
12+
13+
Provide existing Falcon512 public keys (both must be specified together):
14+
15+
```bash
16+
miden-genesis --output-dir ./genesis \
17+
--bridge-admin-public-key <HEX> \
18+
--ger-manager-public-key <HEX>
19+
```
20+
21+
## Output
22+
23+
The tool generates the following files in the output directory:
24+
25+
- `bridge_admin.mac` - Bridge admin wallet (nonce=0, deployed later via transaction)
26+
- `ger_manager.mac` - GER manager wallet (nonce=0, deployed later via transaction)
27+
- `bridge.mac` - AggLayer bridge account (nonce=1, included in genesis block)
28+
- `genesis.toml` - Genesis configuration referencing only `bridge.mac`
29+
30+
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.
31+
32+
The bridge account always uses NoAuth and has no secret keys.
33+
34+
## Bootstrapping a node
35+
36+
```bash
37+
# 1. Generate genesis accounts
38+
miden-genesis --output-dir ./genesis
39+
40+
# 2. Bootstrap the genesis block
41+
miden-node validator bootstrap \
42+
--genesis-block-directory ./data \
43+
--accounts-directory ./accounts \
44+
--genesis-config-file ./genesis/genesis.toml \
45+
--validator.key.hex <validator_key>
46+
47+
# 3. Bootstrap the store
48+
miden-node store bootstrap --data-directory ./data
49+
50+
# 4. Start the node
51+
miden-node bundled start --data-directory ./data ...
52+
```
53+
54+
## TODO
55+
56+
- Support ECDSA (secp256k1) public keys in addition to Falcon512 (e.g. `--bridge-admin-public-key ecdsa:<HEX>`)
57+
58+
## License
59+
60+
This project is [MIT licensed](../../LICENSE).

bin/genesis/src/main.rs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
use std::path::{Path, PathBuf};
2+
use std::time::{SystemTime, UNIX_EPOCH};
3+
4+
use anyhow::Context;
5+
use clap::Parser;
6+
use miden_agglayer::create_bridge_account;
7+
use miden_protocol::account::auth::{AuthScheme, AuthSecretKey};
8+
use miden_protocol::account::delta::{AccountStorageDelta, AccountVaultDelta};
9+
use miden_protocol::account::{
10+
Account,
11+
AccountDelta,
12+
AccountFile,
13+
AccountStorageMode,
14+
AccountType,
15+
};
16+
use miden_protocol::crypto::dsa::falcon512_rpo::{self, SecretKey as RpoSecretKey};
17+
use miden_protocol::crypto::rand::RpoRandomCoin;
18+
use miden_protocol::utils::Deserializable;
19+
use miden_protocol::{Felt, ONE, Word};
20+
use miden_standards::AuthMethod;
21+
use miden_standards::account::wallets::create_basic_wallet;
22+
use rand::Rng;
23+
use rand_chacha::ChaCha20Rng;
24+
use rand_chacha::rand_core::SeedableRng;
25+
26+
/// Generate canonical Miden genesis accounts (bridge, bridge admin, GER manager)
27+
/// and a genesis.toml configuration file.
28+
#[derive(Parser)]
29+
#[command(name = "miden-genesis")]
30+
struct Cli {
31+
/// Output directory for generated files.
32+
#[arg(long, default_value = "./genesis")]
33+
output_dir: PathBuf,
34+
35+
/// Hex-encoded Falcon512 public key for the bridge admin account.
36+
/// If omitted, a new keypair is generated and the secret key is included in the .mac file.
37+
#[arg(long, value_name = "HEX", requires = "ger_manager_public_key")]
38+
bridge_admin_public_key: Option<String>,
39+
40+
/// Hex-encoded Falcon512 public key for the GER manager account.
41+
/// If omitted, a new keypair is generated and the secret key is included in the .mac file.
42+
#[arg(long, value_name = "HEX", requires = "bridge_admin_public_key")]
43+
ger_manager_public_key: Option<String>,
44+
}
45+
46+
fn main() -> anyhow::Result<()> {
47+
let cli = Cli::parse();
48+
run(
49+
&cli.output_dir,
50+
cli.bridge_admin_public_key.as_deref(),
51+
cli.ger_manager_public_key.as_deref(),
52+
)
53+
}
54+
55+
fn run(
56+
output_dir: &Path,
57+
bridge_admin_public_key: Option<&str>,
58+
ger_manager_public_key: Option<&str>,
59+
) -> anyhow::Result<()> {
60+
fs_err::create_dir_all(output_dir).context("failed to create output directory")?;
61+
62+
// Generate or parse bridge admin key.
63+
let (bridge_admin_pub, bridge_admin_secret) =
64+
resolve_pubkey(bridge_admin_public_key, "bridge admin")?;
65+
66+
// Generate or parse GER manager key.
67+
let (ger_manager_pub, ger_manager_secret) =
68+
resolve_pubkey(ger_manager_public_key, "GER manager")?;
69+
70+
// Create bridge admin wallet (nonce=0, local account to be deployed later).
71+
let bridge_admin = create_basic_wallet(
72+
rand::random(),
73+
AuthMethod::SingleSig {
74+
approver: (bridge_admin_pub.into(), AuthScheme::Falcon512Rpo),
75+
},
76+
AccountType::RegularAccountImmutableCode,
77+
AccountStorageMode::Public,
78+
)
79+
.context("failed to create bridge admin account")?;
80+
let bridge_admin_id = bridge_admin.id();
81+
82+
// Create GER manager wallet (nonce=0, local account to be deployed later).
83+
let ger_manager = create_basic_wallet(
84+
rand::random(),
85+
AuthMethod::SingleSig {
86+
approver: (ger_manager_pub.into(), AuthScheme::Falcon512Rpo),
87+
},
88+
AccountType::RegularAccountImmutableCode,
89+
AccountStorageMode::Public,
90+
)
91+
.context("failed to create GER manager account")?;
92+
let ger_manager_id = ger_manager.id();
93+
94+
// Create bridge account (NoAuth, nonce=0), then bump nonce to 1 for genesis.
95+
let mut rng = ChaCha20Rng::from_seed(rand::random());
96+
let bridge_seed: [u64; 4] = rng.random();
97+
let bridge_seed = Word::from(bridge_seed.map(Felt::new));
98+
let bridge = create_bridge_account(bridge_seed, bridge_admin_id, ger_manager_id);
99+
100+
// Bump bridge nonce to 1 (required for genesis accounts).
101+
// File-loaded accounts via [[account]] in genesis.toml are included as-is,
102+
// so we must set nonce=1 before writing the .mac file.
103+
let bridge = bump_nonce_to_one(bridge).context("failed to bump bridge account nonce")?;
104+
105+
// Write .mac files.
106+
let bridge_admin_secrets = bridge_admin_secret
107+
.map(|sk| vec![AuthSecretKey::Falcon512Rpo(sk)])
108+
.unwrap_or_default();
109+
AccountFile::new(bridge_admin, bridge_admin_secrets)
110+
.write(output_dir.join("bridge_admin.mac"))
111+
.context("failed to write bridge_admin.mac")?;
112+
113+
let ger_manager_secrets = ger_manager_secret
114+
.map(|sk| vec![AuthSecretKey::Falcon512Rpo(sk)])
115+
.unwrap_or_default();
116+
AccountFile::new(ger_manager, ger_manager_secrets)
117+
.write(output_dir.join("ger_manager.mac"))
118+
.context("failed to write ger_manager.mac")?;
119+
120+
let bridge_id = bridge.id();
121+
AccountFile::new(bridge, vec![])
122+
.write(output_dir.join("bridge.mac"))
123+
.context("failed to write bridge.mac")?;
124+
125+
// Write genesis.toml.
126+
let timestamp = u32::try_from(
127+
SystemTime::now()
128+
.duration_since(UNIX_EPOCH)
129+
.expect("system time before UNIX epoch")
130+
.as_secs(),
131+
)
132+
.expect("timestamp should fit in a u32 before the year 2106");
133+
134+
let genesis_toml = format!(
135+
r#"version = 1
136+
timestamp = {timestamp}
137+
138+
[fee_parameters]
139+
verification_base_fee = 0
140+
141+
[[account]]
142+
path = "bridge.mac"
143+
"#,
144+
);
145+
146+
fs_err::write(output_dir.join("genesis.toml"), genesis_toml)
147+
.context("failed to write genesis.toml")?;
148+
149+
println!("Genesis files written to {}", output_dir.display());
150+
println!(" bridge_admin.mac (id: {})", bridge_admin_id.to_hex());
151+
println!(" ger_manager.mac (id: {})", ger_manager_id.to_hex());
152+
println!(" bridge.mac (id: {})", bridge_id.to_hex());
153+
println!(" genesis.toml");
154+
155+
Ok(())
156+
}
157+
158+
/// Generates a new Falcon512 keypair using a random seed.
159+
fn generate_falcon_keypair() -> (falcon512_rpo::PublicKey, RpoSecretKey) {
160+
let mut rng = ChaCha20Rng::from_seed(rand::random());
161+
let auth_seed: [u64; 4] = rng.random();
162+
let mut coin = RpoRandomCoin::new(Word::from(auth_seed.map(Felt::new)));
163+
let secret_key = RpoSecretKey::with_rng(&mut coin);
164+
let public_key = secret_key.public_key();
165+
(public_key, secret_key)
166+
}
167+
168+
/// Resolves a Falcon512 key pair: either parses the provided hex public key or generates a new
169+
/// keypair.
170+
fn resolve_pubkey(
171+
hex_pubkey: Option<&str>,
172+
label: &str,
173+
) -> anyhow::Result<(falcon512_rpo::PublicKey, Option<RpoSecretKey>)> {
174+
if let Some(hex_str) = hex_pubkey {
175+
let bytes =
176+
hex::decode(hex_str).with_context(|| format!("invalid hex for {label} public key"))?;
177+
let pubkey = falcon512_rpo::PublicKey::read_from_bytes(&bytes)
178+
.with_context(|| format!("failed to deserialize {label} public key"))?;
179+
Ok((pubkey, None))
180+
} else {
181+
let (public_key, secret_key) = generate_falcon_keypair();
182+
Ok((public_key, Some(secret_key)))
183+
}
184+
}
185+
186+
/// Bumps an account's nonce from 0 to 1 using an `AccountDelta`.
187+
fn bump_nonce_to_one(mut account: Account) -> anyhow::Result<Account> {
188+
let delta = AccountDelta::new(
189+
account.id(),
190+
AccountStorageDelta::default(),
191+
AccountVaultDelta::default(),
192+
ONE,
193+
)?;
194+
account.apply_delta(&delta)?;
195+
Ok(account)
196+
}
197+
198+
#[cfg(test)]
199+
mod tests {
200+
use miden_node_store::genesis::config::GenesisConfig;
201+
use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey;
202+
use miden_protocol::utils::Serializable;
203+
204+
use super::*;
205+
206+
/// Parses the generated genesis.toml, builds a genesis block, and asserts the bridge account
207+
/// is included with nonce=1.
208+
async fn assert_valid_genesis_block(dir: &Path) {
209+
let bridge_id = AccountFile::read(dir.join("bridge.mac")).unwrap().account.id();
210+
211+
let config = GenesisConfig::read_toml_file(&dir.join("genesis.toml")).unwrap();
212+
let signer = SecretKey::read_from_bytes(&[0x01; 32]).unwrap();
213+
let (state, _) = config.into_state(signer).unwrap();
214+
215+
let bridge = state.accounts.iter().find(|a| a.id() == bridge_id).unwrap();
216+
assert_eq!(bridge.nonce(), ONE);
217+
218+
state.into_block().await.expect("genesis block should build");
219+
}
220+
221+
#[tokio::test]
222+
async fn default_mode_includes_secret_keys() {
223+
let dir = tempfile::tempdir().unwrap();
224+
run(dir.path(), None, None).unwrap();
225+
226+
let admin = AccountFile::read(dir.path().join("bridge_admin.mac")).unwrap();
227+
assert_eq!(admin.auth_secret_keys.len(), 1);
228+
229+
let ger = AccountFile::read(dir.path().join("ger_manager.mac")).unwrap();
230+
assert_eq!(ger.auth_secret_keys.len(), 1);
231+
232+
assert_valid_genesis_block(dir.path()).await;
233+
}
234+
235+
#[tokio::test]
236+
async fn custom_public_keys_excludes_secret_keys() {
237+
let dir = tempfile::tempdir().unwrap();
238+
239+
let (admin_pub, _) = generate_falcon_keypair();
240+
let (ger_pub, _) = generate_falcon_keypair();
241+
let admin_hex = hex::encode((&admin_pub).to_bytes());
242+
let ger_hex = hex::encode((&ger_pub).to_bytes());
243+
244+
run(dir.path(), Some(&admin_hex), Some(&ger_hex)).unwrap();
245+
246+
let admin = AccountFile::read(dir.path().join("bridge_admin.mac")).unwrap();
247+
assert!(admin.auth_secret_keys.is_empty());
248+
249+
let ger = AccountFile::read(dir.path().join("ger_manager.mac")).unwrap();
250+
assert!(ger.auth_secret_keys.is_empty());
251+
252+
assert_valid_genesis_block(dir.path()).await;
253+
}
254+
}

0 commit comments

Comments
 (0)