Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f409971
chore: add change file for treasury initialization
m2ux Jan 28, 2026
dbad133
chore: add PR link to change file
m2ux Jan 28, 2026
2b5cba7
feat(toolkit): add treasury initialization from ICS observations
m2ux Jan 29, 2026
92bc732
chore(toolkit): suppress clippy too_many_arguments warning
m2ux Jan 29, 2026
48822f6
test(toolkit): add integration test for treasury initialization
m2ux Jan 29, 2026
a5d519a
Merge main into mike/feat/treasury-initialization
m2ux Jan 29, 2026
c36bf4b
fix: format and update comments
m2ux Jan 29, 2026
13d4fc6
chore: update change file with correct config filename
m2ux Jan 29, 2026
5f590ae
refactor(toolkit): remove low-value treasury config tests
m2ux Jan 29, 2026
5c1fe04
fix(toolkit): add clippy allow and remove low-value tests
m2ux Jan 30, 2026
dfdc415
chore: populate change file with required content
m2ux Jan 30, 2026
5c99f26
chore: fix config filename in change file
m2ux Jan 30, 2026
1aceab4
chore: use real ICS address for dev config
m2ux Jan 30, 2026
8a3c145
chore: add treasury config for devnet and govnet
m2ux Jan 30, 2026
1e40fa9
refactor: remove ics_contract_address from treasury config
m2ux Jan 30, 2026
b0cd032
refactor: restore illiquid_circulation_supply_validator_address to tr…
m2ux Jan 30, 2026
eb8c970
feat: add db-sync verification fields to treasury config
m2ux Jan 30, 2026
3d16363
feat: add treasury verifier for db-sync UTxO verification
m2ux Jan 30, 2026
5ce2fb6
feat: add db-sync verification CLI flags to generate-genesis
m2ux Jan 30, 2026
017c0d2
feat: add mock-based integration tests for treasury verifier
m2ux Jan 30, 2026
23a6fbc
refactor: move reference_block_hash from config to CLI argument
m2ux Jan 31, 2026
c3e5471
refactor: remove verification flags from generate-genesis
m2ux Feb 1, 2026
5ae4137
feat: add genesis-manifest.json generation with config hashes
m2ux Feb 1, 2026
fe863e4
refactor: remove GenerateVerifiedGenesis umbrella command
m2ux Feb 1, 2026
1f8821d
feat: rename verify-treasury-config to verify-genesis with manifest s…
m2ux Feb 1, 2026
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
3 changes: 3 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ midnight-node-ledger = { path = "ledger", default-features = false }
midnight-node-metadata = { path = "metadata", default-features = false }
midnight-node-ledger-helpers = { path = "ledger/helpers", default-features = false, features = ["test-utils"] }
midnight-node-res = { path = "res", default-features = false }
midnight-node-toolkit = { path = "util/toolkit", default-features = true }
pallet-midnight = { path = "pallets/midnight", default-features = false }
pallet-midnight-system = { path = "pallets/midnight-system", default-features = false }
pallet-midnight-rpc = { path = "pallets/midnight/rpc", default-features = false }
Expand Down
7 changes: 7 additions & 0 deletions changes/added/treasury-initialization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#toolkit
# Add treasury initialization to genesis generation

Implements treasury initialization for the Midnight mainnet genesis block. The treasury is initialized via a one-off transfer of Night tokens to the ICS contract, configured through a new `cnight-treasury-config.json` file. The genesis creation tool validates UTxO configurations and computes the correct Night amount for treasury initialization.

PR: https://github.com/midnightntwrk/midnight-node/pull/563
JIRA: https://shielded.atlassian.net/browse/PM-20981
1 change: 1 addition & 0 deletions node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ frame-benchmarking-cli = {workspace = true, optional = true, default-features =

# Local Dependencies
midnight-node-runtime = { workspace = true, default-features = true }
midnight-node-toolkit = { workspace = true, default-features = true }
midnight-node-ledger = { workspace = true, default-features = true }
midnight-node-ledger-helpers = {workspace = true, default-features = true }
midnight-node-res = { workspace = true, features = ["chain-spec"], default-features = true }
Expand Down
29 changes: 29 additions & 0 deletions node/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,28 @@ pub struct CNightGenesisCmd {
pub output: std::path::PathBuf,
}

#[derive(Debug, Parser)]
pub struct VerifyGenesisCmd {
/// Path to the genesis manifest file (JSON).
/// If provided, the treasury config hash will be verified against the manifest
/// to ensure the same config was used during generation.
#[arg(long)]
pub manifest: Option<std::path::PathBuf>,

/// Path to the cNight treasury configuration file (JSON).
#[arg(long)]
pub treasury_config: std::path::PathBuf,

/// Reference Cardano block hash at which to verify UTxOs (hex-encoded, 64 chars).
#[arg(long)]
pub reference_block_hash: String,

/// PostgreSQL connection URL for Cardano db-sync database.
/// Example: postgres://user:pass@localhost:5432/cexplorer
#[arg(long, env = "DB_SYNC_URL")]
pub db_sync_url: String,
}

#[allow(clippy::large_enum_variant)]
#[derive(Debug, clap::Subcommand)]
pub enum Subcommand {
Expand All @@ -69,6 +91,13 @@ pub enum Subcommand {
/// Generate cNIGHT generates DUST genesis file. This file is an input to chain spec generation, and can be used to validate the correctness of any given chain spec
GenerateCNightGenesis(CNightGenesisCmd),

/// Verify genesis treasury configuration against Cardano db-sync.
/// Validates that all UTxOs in the treasury config exist at the reference block
/// with the expected amounts at the ICS contract address.
/// If a manifest is provided, also verifies config hash matches what was used
/// during generation.
VerifyGenesis(VerifyGenesisCmd),

/// Export blocks.
ExportBlocks(sc_cli::ExportBlocksCmd),

Expand Down
93 changes: 93 additions & 0 deletions node/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ use crate::{
use clap::Parser;
use midnight_node_res::networks::MidnightNetwork as _;
use midnight_node_runtime::Block;
use midnight_node_toolkit::genesis_manifest::{GenesisManifest, hash_file};
use midnight_node_toolkit::treasury_config::CnightTreasuryConfig;
use midnight_node_toolkit::treasury_verifier::TreasuryVerifier;
use midnight_primitives_cnight_observation::CNightAddresses;
use sc_cli::{CliConfiguration, LoggerBuilder, RunCmd, SubstrateCli};
use sc_keystore::LocalKeystore;
use sc_service::{BasePath, PartialComponents, config::KeystoreConfig};
use sidechain_domain::mainchain_epoch::MainchainEpochConfig;
use sp_core::{ByteArray, Pair, offchain::KeyTypeId};
use sp_keystore::KeystorePtr;
use sqlx::postgres::PgPoolOptions;

#[cfg(feature = "runtime-benchmarks")]
use {
Expand Down Expand Up @@ -497,6 +501,95 @@ fn run_subcommand(subcommand: Subcommand, cfg: Cfg) -> sc_cli::Result<()> {
sc_cli::Error::Input(format!("cNGD genesis generation failed: {e}"))
})?;

Ok(())
})
},
Subcommand::VerifyGenesis(ref cmd) => {
// Init logging
LoggerBuilder::new(std::env::var("RUST_LOG").unwrap_or("info".to_string())).init()?;
// Init tokio runtime
let tokio_handle = sc_cli::build_runtime()?;
tokio_handle.block_on(async {
// If manifest provided, verify config hash first
if let Some(ref manifest_path) = cmd.manifest {
println!("Loading manifest from {:?}...", manifest_path);
let manifest = GenesisManifest::read_from_file(manifest_path).map_err(|e| {
sc_cli::Error::Input(format!("Failed to read manifest: {}", e))
})?;

// Hash the provided treasury config
let config_hash = hash_file(&cmd.treasury_config).map_err(|e| {
sc_cli::Error::Input(format!("Failed to hash treasury config: {}", e))
})?;

// Compare with manifest
if let Some(ref expected_hash) = manifest.treasury_config_hash {
if &config_hash != expected_hash {
return Err(sc_cli::Error::Input(format!(
"Treasury config hash mismatch!\n Expected (from manifest): {}\n Actual (provided file): {}\n\nThe provided treasury config does not match what was used during genesis generation.",
expected_hash, config_hash
)));
}
println!("Config hash verified: matches manifest");
} else {
println!(
"WARNING: Manifest does not contain treasury_config_hash, skipping hash verification"
);
}
}

// Load and validate treasury config
let config_str = std::fs::read_to_string(&cmd.treasury_config).map_err(|e| {
sc_cli::Error::Input(format!(
"Failed to read treasury config {:?}: {}",
cmd.treasury_config, e
))
})?;
let config: CnightTreasuryConfig =
serde_json::from_str(&config_str).map_err(|e| {
sc_cli::Error::Input(format!("Failed to parse treasury config: {}", e))
})?;
config.validate().map_err(|e| {
sc_cli::Error::Input(format!("Treasury config validation failed: {}", e))
})?;

println!(
"Treasury config loaded: {} Night from {} UTxOs",
config.total_night_amount,
config.utxos.len()
);

if config.utxos.is_empty() {
println!("No UTxOs to verify against Cardano.");
return Ok(());
}

// Connect to db-sync
println!("Connecting to db-sync...");
let pool = PgPoolOptions::new()
.max_connections(1)
.connect(&cmd.db_sync_url)
.await
.map_err(|e| {
sc_cli::Error::Input(format!("Failed to connect to db-sync: {}", e))
})?;

// Verify against Cardano
println!("Verifying treasury UTxOs at block {}...", &cmd.reference_block_hash);
let verifier = TreasuryVerifier::new(pool);
let results =
verifier.verify(&config, &cmd.reference_block_hash).await.map_err(|e| {
sc_cli::Error::Input(format!("Treasury verification failed: {}", e))
})?;

println!("Verification PASSED: {} UTxOs verified", results.len());
for result in &results {
println!(
" - {}#{}: {} cNight",
result.tx_hash, result.output_index, result.amount
);
}

Ok(())
})
},
Expand Down
6 changes: 6 additions & 0 deletions res/dev/cnight-treasury-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"illiquid_circulation_supply_validator_address": "addr_test1placeholderaddr",
"cnight_policy_id": "00000000000000000000000000000000000000000000000000000000",
"utxos": [],
"total_night_amount": 0
}
6 changes: 6 additions & 0 deletions res/devnet/cnight-treasury-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"illiquid_circulation_supply_validator_address": "addr_test1wqgdspp2cnethukgvrve6wnue8adjjzz5ty9x3z4t5s8c8cnck7xz",
"cnight_policy_id": "d2dbff622e509dda256fedbd31ef6e9fd98ed49ad91d5c0e07f68af1",
"utxos": [],
"total_night_amount": 0
}
6 changes: 6 additions & 0 deletions res/govnet/cnight-treasury-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"illiquid_circulation_supply_validator_address": "addr_test1wzds0wlu5fzt9shwq0zd698je4lmdjqv885k8x0hy9k27mgny4qlq",
"cnight_policy_id": "d2dbff622e509dda256fedbd31ef6e9fd98ed49ad91d5c0e07f68af1",
"utxos": [],
"total_night_amount": 0
}
6 changes: 6 additions & 0 deletions res/preview/cnight-treasury-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"illiquid_circulation_supply_validator_address": "addr_test1placeholderaddr",
"cnight_policy_id": "d2dbff622e509dda256fedbd31ef6e9fd98ed49ad91d5c0e07f68af1",
"utxos": [],
"total_night_amount": 0
}
4 changes: 4 additions & 0 deletions util/toolkit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ midnight-node-metadata.workspace = true
subxt.workspace = true
tokio.workspace = true
hex = { workspace = true, features = ["std", "serde"] }
sha2.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
rand.workspace = true
Expand All @@ -45,6 +46,9 @@ num_cpus = "1.17.0"
redb = "3.1.0"
bson = { version = "3.1.0", features = ["serde"] }
sqlx = { workspace = true, features = ["runtime-tokio", "postgres"] }
# Cardano/db-sync integration for treasury verification
partner-chains-db-sync-data-sources.workspace = true
sidechain-domain.workspace = true

[dev-dependencies]
test-case = "3.3.1"
Expand Down
82 changes: 76 additions & 6 deletions util/toolkit/src/commands/generate_genesis.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::cli_parsers::{self as cli};
use crate::genesis_manifest::GenesisManifest;
use crate::treasury_config::CnightTreasuryConfig;
use clap::Args;
use serde::Deserialize;
use std::path::{Path, PathBuf};
Expand All @@ -15,7 +17,7 @@ pub struct CNightGeneratesDustConfig {
system_tx: Vec<u8>,
}

#[derive(Args)]
#[derive(Debug, Clone, Args)]
pub struct GenerateGenesisArgs {
/// Seed for genesis block generation
#[arg(
Expand All @@ -38,6 +40,10 @@ pub struct GenerateGenesisArgs {
/// applied to the LedgerState
#[arg(long)]
cnight_generates_dust_config: Option<PathBuf>,
/// File containing cNight treasury configuration for initializing the treasury from ICS
/// contract observations
#[arg(long)]
cnight_treasury_config: Option<PathBuf>,
/// File containing ledger parameters config (JSON). If provided, these parameters will be used
/// instead of the default INITIAL_PARAMETERS.
#[arg(long)]
Expand Down Expand Up @@ -72,16 +78,38 @@ pub async fn execute(
})
.collect();

// Parse the cnight generates dust config file
// Parse the cNight generates dust config file (keep path for manifest)
let cnight_dust_config_path = args.cnight_generates_dust_config.clone();
let cnight_system_tx: Option<SystemTransaction> =
if let Some(filepath) = args.cnight_generates_dust_config {
if let Some(ref filepath) = cnight_dust_config_path {
let json_str = std::fs::read_to_string(filepath)?;
let config: CNightGeneratesDustConfig = serde_json::from_str(&json_str)?;
Some(tagged_deserialize(&mut &config.system_tx[..])?)
} else {
None
};

// Parse and validate the treasury config (keep path for manifest)
let treasury_config_path = args.cnight_treasury_config.clone();
let treasury_config: Option<CnightTreasuryConfig> =
if let Some(ref filepath) = treasury_config_path {
let json_str = std::fs::read_to_string(filepath)
.map_err(|e| format!("Failed to read treasury config {:?}: {}", filepath, e))?;
let config: CnightTreasuryConfig = serde_json::from_str(&json_str)
.map_err(|e| format!("Failed to parse treasury config: {}", e))?;
config
.validate()
.map_err(|e| format!("Treasury config validation failed: {}", e))?;
println!(
"Treasury config loaded: {} Night from {} UTxOs",
config.total_night_amount,
config.utxos.len()
);
Some(config)
} else {
None
};

// Parse the ledger parameters config file
let ledger_parameters: Option<LedgerParameters> =
if let Some(filepath) = args.ledger_parameters_config {
Expand All @@ -100,18 +128,60 @@ pub async fn execute(
args.funding,
&seeds?,
cnight_system_tx,
treasury_config,
ledger_parameters,
)
.await?;

let genesis_state_path = dir.join(format!("genesis_state_{}.mn", &args.network));
serialize_and_write(&genesis.state, &genesis_state_path)?;
let state_filename = format!("genesis_state_{}.mn", &args.network);
let block_filename = format!("genesis_block_{}.mn", &args.network);
let genesis_state_path = dir.join(&state_filename);
let genesis_tx_path = dir.join(&block_filename);

let genesis_tx_path = dir.join(format!("genesis_block_{}.mn", &args.network));
serialize_and_write(&genesis.state, &genesis_state_path)?;
serialize_and_write(&genesis.txs, &genesis_tx_path)?;

println!("Number of genesis txs: {}", genesis.txs.len());

// Create and write the genesis manifest
let mut manifest_builder = GenesisManifest::builder(&args.network)
.with_chain_spec_state(&state_filename, &genesis_state_path)
.map_err(|e| format!("Failed to hash genesis state: {}", e))?
.with_chain_spec_block(&block_filename, &genesis_tx_path)
.map_err(|e| format!("Failed to hash genesis block: {}", e))?;

// Add treasury config to manifest if used
if let Some(ref path) = treasury_config_path {
let filename = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "treasury-config.json".to_string());
manifest_builder = manifest_builder
.with_treasury_config(&filename, path)
.map_err(|e| format!("Failed to hash treasury config: {}", e))?;
}

// Add cNight generates dust config to manifest if used
if let Some(ref path) = cnight_dust_config_path {
let filename = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "cnight-generates-dust-config.json".to_string());
manifest_builder = manifest_builder
.with_cnight_generates_dust_config(&filename, path)
.map_err(|e| format!("Failed to hash cNight dust config: {}", e))?;
}

let manifest = manifest_builder
.build()
.map_err(|e| format!("Failed to build manifest: {}", e))?;

let manifest_path = dir.join("genesis-manifest.json");
manifest
.write_to_file(&manifest_path)
.map_err(|e| format!("Failed to write manifest: {}", e))?;
println!("Written manifest to {}", manifest_path.display());

Ok(genesis)
}

Expand Down
Loading