diff --git a/.changelog/unreleased/improvements/3843-check-genesis.md b/.changelog/unreleased/improvements/3843-check-genesis.md new file mode 100644 index 0000000000..a2b8320b47 --- /dev/null +++ b/.changelog/unreleased/improvements/3843-check-genesis.md @@ -0,0 +1,3 @@ +- Validate a chain ID of genesis on ABCI InitChain request + prior to applying it to ensure it's not been tampered with. + ([\#3843](https://github.com/anoma/namada/pull/3843)) \ No newline at end of file diff --git a/.github/workflows/scripts/e2e.json b/.github/workflows/scripts/e2e.json index 107820fdd5..5a4f302dbf 100644 --- a/.github/workflows/scripts/e2e.json +++ b/.github/workflows/scripts/e2e.json @@ -30,5 +30,6 @@ "e2e::wallet_tests::wallet_encrypted_key_cmds_env_var": 1, "e2e::wallet_tests::wallet_unencrypted_key_cmds": 1, "e2e::ledger_tests::masp_txs_and_queries": 82, - "e2e::ledger_tests::test_genesis_chain_id_change": 35 + "e2e::ledger_tests::test_genesis_chain_id_change": 35, + "e2e::ledger_tests::test_genesis_manipulation": 103 } \ No newline at end of file diff --git a/crates/apps_lib/src/config/genesis/chain.rs b/crates/apps_lib/src/config/genesis/chain.rs index ac4ad408c4..e23448e780 100644 --- a/crates/apps_lib/src/config/genesis/chain.rs +++ b/crates/apps_lib/src/config/genesis/chain.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use borsh::{BorshDeserialize, BorshSerialize}; use borsh_ext::BorshSerializeExt; +use eyre::eyre; use namada_macros::BorshDeserializer; #[cfg(feature = "migrations")] use namada_migrations::*; @@ -106,6 +107,8 @@ impl Finalized { /// Try to read all genesis and the chain metadata TOML files from the given /// directory. + /// + /// The consistency of the files is checked with [`Finalized::is_valid`]. pub fn read_toml_files(input_dir: &Path) -> eyre::Result { let vps_file = input_dir.join(templates::VPS_FILE_NAME); let tokens_file = input_dir.join(templates::TOKENS_FILE_NAME); @@ -121,14 +124,20 @@ impl Finalized { let parameters = read_toml(¶meters_file, "Parameters")?; let transactions = read_toml(&transactions_file, "Transactions")?; let metadata = read_toml(&metadata_file, "Chain metadata")?; - Ok(Self { + let genesis = Self { vps, tokens, balances, parameters, transactions, metadata, - }) + }; + + if !genesis.is_valid() { + return Err(eyre!("Invalid genesis files")); + } + + Ok(genesis) } /// Find the address of the configured native token @@ -485,6 +494,54 @@ impl Finalized { pub fn get_token_address(&self, alias: &Alias) -> Option<&Address> { self.tokens.token.get(alias).map(|token| &token.address) } + + // Validate the chain ID against the genesis contents + pub fn is_valid(&self) -> bool { + let Self { + vps, + tokens, + balances, + parameters, + transactions, + metadata, + } = self.clone(); + let Metadata { + chain_id, + genesis_time, + consensus_timeout_commit, + address_gen, + } = metadata.clone(); + + let Some(chain_id_prefix) = chain_id.prefix() else { + tracing::warn!( + "Invalid Chain ID \"{chain_id}\" - unable to find a prefix" + ); + return false; + }; + let metadata = Metadata { + chain_id: chain_id_prefix.clone(), + genesis_time, + consensus_timeout_commit, + address_gen, + }; + let to_finalize = ToFinalize { + vps, + tokens, + balances, + parameters, + transactions, + metadata, + }; + let derived_chain_id = derive_chain_id(chain_id_prefix, &to_finalize); + let is_valid = derived_chain_id == chain_id; + if !is_valid { + tracing::warn!( + "Invalid chain ID. This indicates that something in the \ + genesis files might have been modified." + ); + } + is_valid + } } /// Create the [`Finalized`] chain configuration. Derives the chain ID from the @@ -541,8 +598,7 @@ pub fn finalize( parameters, transactions, }; - let to_finalize_bytes = to_finalize.serialize_to_vec(); - let chain_id = ChainId::from_genesis(chain_id_prefix, to_finalize_bytes); + let chain_id = derive_chain_id(chain_id_prefix, &to_finalize); // Construct the `Finalized` chain let ToFinalize { @@ -575,6 +631,15 @@ pub fn finalize( } } +/// Derive a chain ID from genesis contents +pub fn derive_chain_id( + chain_id_prefix: ChainIdPrefix, + to_finalize: &ToFinalize, +) -> ChainId { + let to_finalize_bytes = to_finalize.serialize_to_vec(); + ChainId::from_genesis(chain_id_prefix, to_finalize_bytes) +} + /// Chain genesis config to be finalized. This struct is used to derive the /// chain ID to construct a [`Finalized`] chain genesis config. #[derive( diff --git a/crates/core/src/chain.rs b/crates/core/src/chain.rs index 44f76556af..552908aa09 100644 --- a/crates/core/src/chain.rs +++ b/crates/core/src/chain.rs @@ -109,6 +109,13 @@ impl ChainId { } errors } + + /// Find the prefix of a valid ChainId. + pub fn prefix(&self) -> Option { + let ChainId(chain_id) = self; + let (prefix, _) = chain_id.rsplit_once(CHAIN_ID_PREFIX_SEP)?; + Some(ChainIdPrefix(prefix.to_string())) + } } /// Height of a block, i.e. the level. The `default` is the diff --git a/crates/node/src/shell/init_chain.rs b/crates/node/src/shell/init_chain.rs index 05624f1871..f427176911 100644 --- a/crates/node/src/shell/init_chain.rs +++ b/crates/node/src/shell/init_chain.rs @@ -133,14 +133,14 @@ where let genesis = { let chain_dir = self.base_dir.join(chain_id); genesis::chain::Finalized::read_toml_files(&chain_dir) - .expect("Missing genesis files") + .expect("Missing or invalid genesis files") }; #[cfg(any(test, fuzzing, feature = "benches"))] let genesis = { let chain_dir = self.base_dir.join(chain_id); if chain_dir.join(genesis::chain::METADATA_FILE_NAME).exists() { genesis::chain::Finalized::read_toml_files(&chain_dir) - .expect("Missing genesis files") + .expect("Missing or invalid genesis files") } else { genesis::make_dev_genesis(num_validators, &chain_dir) } diff --git a/crates/tests/src/e2e/ledger_tests.rs b/crates/tests/src/e2e/ledger_tests.rs index 4ba8f540cb..42ad7d1793 100644 --- a/crates/tests/src/e2e/ledger_tests.rs +++ b/crates/tests/src/e2e/ledger_tests.rs @@ -22,14 +22,16 @@ use color_eyre::eyre::Result; use color_eyre::owo_colors::OwoColorize; use namada_apps_lib::cli::context::ENV_VAR_CHAIN_ID; use namada_apps_lib::client::utils::PRE_GENESIS_DIR; +use namada_apps_lib::config::genesis::chain; +use namada_apps_lib::config::genesis::templates::TokenBalances; use namada_apps_lib::config::utils::convert_tm_addr_to_socket_addr; use namada_apps_lib::config::{self, ethereum_bridge}; use namada_apps_lib::tendermint_config::net::Address as TendermintAddress; -use namada_apps_lib::wallet; +use namada_apps_lib::wallet::{self, Alias}; use namada_core::chain::ChainId; use namada_core::token::NATIVE_MAX_DECIMAL_PLACES; use namada_sdk::address::Address; -use namada_sdk::chain::Epoch; +use namada_sdk::chain::{ChainIdPrefix, Epoch}; use namada_sdk::time::DateTimeUtc; use namada_sdk::token; use namada_test_utils::TestWasms; @@ -2738,3 +2740,71 @@ fn test_genesis_chain_id_change() -> Result<()> { Ok(()) } + +/// Test that any changes done to a genesis config after a chain is finalized +/// will make it fail validation. +#[test] +fn test_genesis_manipulation() -> Result<()> { + let test = setup::single_node_net().unwrap(); + + set_ethereum_bridge_mode( + &test, + &test.net.chain_id, + Who::Validator(0), + ethereum_bridge::ledger::Mode::Off, + None, + ); + + let chain_dir = test.get_chain_dir(Who::Validator(0)); + let genesis = chain::Finalized::read_toml_files(&chain_dir).unwrap(); + + let modified_genesis = [ + { + let mut genesis = genesis.clone(); + genesis + .balances + .token + .insert(Alias::from("test"), TokenBalances(Default::default())); + genesis + }, + { + let mut genesis = genesis.clone(); + genesis.balances.token.remove(&Alias::from("NAM")); + genesis + }, + { + let mut genesis = genesis.clone(); + genesis.metadata.address_gen = None; + genesis + }, + { + let mut genesis = genesis.clone(); + // Invalid chain ID + genesis.metadata.chain_id = ChainId("Invalid ID".to_string()); + genesis + }, + { + let mut genesis = genesis.clone(); + // Random valid chain ID + genesis.metadata.chain_id = ChainId::from_genesis( + ChainIdPrefix::from_str("TEST").unwrap(), + [1, 2, 3], + ); + genesis + }, + ]; + + for genesis in modified_genesis { + // Any modification should invalide the genesis + assert!(!genesis.is_valid()); + + genesis.write_toml_files(&chain_dir).unwrap(); + + // A node should fail to start-up + let result = + start_namada_ledger_node_wait_wasm(&test, Some(0), Some(40)); + assert!(result.is_err()) + } + + Ok(()) +}