|
| 1 | +use alloy_sol_macro::sol; |
| 2 | +use alloy_sol_types::SolCall; |
1 | 3 | use revm::{DatabaseRef, InMemoryDB, db::BundleState}; |
2 | | -use revm_primitives::{ExecutionResult, SpecId, TxEnv, hex}; |
| 4 | +use revm_primitives::{Address, ExecutionResult, SpecId, TxEnv, hex}; |
3 | 5 | use tracing::{error, info}; |
4 | 6 |
|
5 | 7 | use crate::{ |
6 | 8 | execute::{prepare_env, resolve_block_timestamp}, |
7 | 9 | genesis::{ |
8 | 10 | GenesisConfig, call_get_active_validators, print_active_validators_result, |
9 | 11 | }, |
10 | | - utils::execute_revm_sequential, |
| 12 | + utils::{GOVERNANCE_ADDR, execute_revm_sequential, new_system_call_txn}, |
11 | 13 | }; |
12 | 14 |
|
| 15 | +sol! { |
| 16 | + interface IGovernance { |
| 17 | + function owner() external view returns (address); |
| 18 | + function isInitialized() external view returns (bool); |
| 19 | + } |
| 20 | +} |
| 21 | + |
13 | 22 | /// Generic template for handling execution results |
14 | 23 | /// |
15 | 24 | /// This function provides a common structure for all print_* functions, |
@@ -97,16 +106,246 @@ fn verify_active_validators(db: impl DatabaseRef, bundle_state: BundleState, con |
97 | 106 | ) |
98 | 107 | } |
99 | 108 |
|
| 109 | +/// Verify that `Governance.owner()` and `Governance.isInitialized()` reflect |
| 110 | +/// the values the config asked for. Without this, a typo or zero address in |
| 111 | +/// `governanceOwner` is silently baked into a sealed genesis artifact and |
| 112 | +/// only surfaces after launch — when `renounceOwnership` is already disabled |
| 113 | +/// and the executor set is unmanageable for the lifetime of the chain. |
| 114 | +fn verify_governance_owner( |
| 115 | + db: impl DatabaseRef, |
| 116 | + bundle_state: BundleState, |
| 117 | + config: &GenesisConfig, |
| 118 | +) -> Result<(), String> { |
| 119 | + let expected_owner: Address = config |
| 120 | + .governance_owner |
| 121 | + .parse() |
| 122 | + .map_err(|e| format!("Invalid governanceOwner address in config {:?}: {}", config.governance_owner, e))?; |
| 123 | + verify_governance_owner_at( |
| 124 | + db, |
| 125 | + bundle_state, |
| 126 | + expected_owner, |
| 127 | + config.chain_id, |
| 128 | + resolve_block_timestamp(config), |
| 129 | + ) |
| 130 | +} |
| 131 | + |
| 132 | +/// Primitive-typed variant of `verify_governance_owner`. Exists so unit tests |
| 133 | +/// can drive the on-chain check without constructing a full `GenesisConfig`. |
| 134 | +fn verify_governance_owner_at( |
| 135 | + db: impl DatabaseRef, |
| 136 | + bundle_state: BundleState, |
| 137 | + expected_owner: Address, |
| 138 | + chain_id: u64, |
| 139 | + block_timestamp_secs: u64, |
| 140 | +) -> Result<(), String> { |
| 141 | + let owner_txn = new_system_call_txn( |
| 142 | + GOVERNANCE_ADDR, |
| 143 | + IGovernance::ownerCall {}.abi_encode().into(), |
| 144 | + ); |
| 145 | + |
| 146 | + execute_verification( |
| 147 | + db, |
| 148 | + bundle_state, |
| 149 | + owner_txn, |
| 150 | + "governance owner", |
| 151 | + chain_id, |
| 152 | + block_timestamp_secs, |
| 153 | + |result| { |
| 154 | + handle_execution_result(result, "Governance.owner()", |output_bytes| { |
| 155 | + let decoded = IGovernance::ownerCall::abi_decode_returns(output_bytes, false) |
| 156 | + .map_err(|e| format!("Failed to decode Governance.owner(): {:?}", e))?; |
| 157 | + let actual_owner = decoded._0; |
| 158 | + if actual_owner != expected_owner { |
| 159 | + let msg = format!( |
| 160 | + "Governance owner mismatch: config governanceOwner={:?}, on-chain owner()={:?}", |
| 161 | + expected_owner, actual_owner |
| 162 | + ); |
| 163 | + error!("❌ {}", msg); |
| 164 | + return Err(msg); |
| 165 | + } |
| 166 | + info!("✅ Governance.owner() = {:?} matches config", actual_owner); |
| 167 | + Ok(()) |
| 168 | + }) |
| 169 | + }, |
| 170 | + ) |
| 171 | +} |
| 172 | + |
| 173 | +/// Verify `Governance.isInitialized() == true` so a missing/skipped Genesis |
| 174 | +/// initialize call cannot ship as a "valid" artifact. |
| 175 | +fn verify_governance_initialized( |
| 176 | + db: impl DatabaseRef, |
| 177 | + bundle_state: BundleState, |
| 178 | + config: &GenesisConfig, |
| 179 | +) -> Result<(), String> { |
| 180 | + verify_governance_initialized_at( |
| 181 | + db, |
| 182 | + bundle_state, |
| 183 | + config.chain_id, |
| 184 | + resolve_block_timestamp(config), |
| 185 | + ) |
| 186 | +} |
| 187 | + |
| 188 | +fn verify_governance_initialized_at( |
| 189 | + db: impl DatabaseRef, |
| 190 | + bundle_state: BundleState, |
| 191 | + chain_id: u64, |
| 192 | + block_timestamp_secs: u64, |
| 193 | +) -> Result<(), String> { |
| 194 | + let txn = new_system_call_txn( |
| 195 | + GOVERNANCE_ADDR, |
| 196 | + IGovernance::isInitializedCall {}.abi_encode().into(), |
| 197 | + ); |
| 198 | + |
| 199 | + execute_verification( |
| 200 | + db, |
| 201 | + bundle_state, |
| 202 | + txn, |
| 203 | + "governance isInitialized", |
| 204 | + chain_id, |
| 205 | + block_timestamp_secs, |
| 206 | + |result| { |
| 207 | + handle_execution_result(result, "Governance.isInitialized()", |output_bytes| { |
| 208 | + let decoded = IGovernance::isInitializedCall::abi_decode_returns(output_bytes, false) |
| 209 | + .map_err(|e| format!("Failed to decode Governance.isInitialized(): {:?}", e))?; |
| 210 | + if !decoded._0 { |
| 211 | + let msg = "Governance.isInitialized() returned false — Genesis.initialize did not run".to_string(); |
| 212 | + error!("❌ {}", msg); |
| 213 | + return Err(msg); |
| 214 | + } |
| 215 | + info!("✅ Governance.isInitialized() = true"); |
| 216 | + Ok(()) |
| 217 | + }) |
| 218 | + }, |
| 219 | + ) |
| 220 | +} |
| 221 | + |
100 | 222 | pub fn verify_result( |
101 | 223 | db: InMemoryDB, |
102 | 224 | bundle_state: BundleState, |
103 | 225 | config: &GenesisConfig, |
104 | 226 | ) -> Result<(), String> { |
105 | 227 | verify_active_validators(db.clone(), bundle_state.clone(), config)?; |
| 228 | + verify_governance_initialized(db.clone(), bundle_state.clone(), config)?; |
| 229 | + verify_governance_owner(db.clone(), bundle_state.clone(), config)?; |
106 | 230 | // Add more verification steps as needed: |
107 | 231 | // - verify_jwks() |
108 | 232 | // - verify_epoch_config() |
109 | 233 | // - verify_randomness_config() |
110 | 234 | // etc. |
111 | 235 | Ok(()) |
112 | 236 | } |
| 237 | + |
| 238 | +#[cfg(test)] |
| 239 | +mod tests { |
| 240 | + use super::*; |
| 241 | + use revm_primitives::{AccountInfo, Bytecode, Bytes, U256}; |
| 242 | + |
| 243 | + // The verifier hits Governance via the SYSTEM_CALLER account. revm needs |
| 244 | + // that caller to exist (with enough nonce headroom) before it will run |
| 245 | + // the call; otherwise the txn aborts before our stub bytecode executes |
| 246 | + // and we'd be testing the wrong thing. |
| 247 | + fn plant_system_caller(db: &mut InMemoryDB) { |
| 248 | + let info = AccountInfo { |
| 249 | + balance: U256::from(1_000_000_000_000_000_000_u128), |
| 250 | + nonce: 1, |
| 251 | + code_hash: revm_primitives::KECCAK_EMPTY, |
| 252 | + code: None, |
| 253 | + }; |
| 254 | + db.insert_account_info(crate::utils::SYSTEM_CALLER, info); |
| 255 | + } |
| 256 | + |
| 257 | + /// Plant raw EVM bytecode at `addr`. The bytecode ignores calldata, so the |
| 258 | + /// same stub works for any function selector — sufficient for tests that |
| 259 | + /// only call one method per planted contract. |
| 260 | + fn plant_code(db: &mut InMemoryDB, addr: Address, code: Vec<u8>) { |
| 261 | + let bytecode = Bytecode::new_raw(Bytes::from(code)); |
| 262 | + let info = AccountInfo { |
| 263 | + balance: U256::ZERO, |
| 264 | + nonce: 1, |
| 265 | + code_hash: bytecode.hash_slow(), |
| 266 | + code: Some(bytecode), |
| 267 | + }; |
| 268 | + db.insert_account_info(addr, info); |
| 269 | + } |
| 270 | + |
| 271 | + /// Stub bytecode that always ABI-returns `addr` as a 32-byte word. |
| 272 | + /// PUSH20 addr; PUSH1 0; MSTORE; PUSH1 32; PUSH1 0; RETURN |
| 273 | + fn stub_returning_address(addr: Address) -> Vec<u8> { |
| 274 | + let mut code = Vec::with_capacity(29); |
| 275 | + code.push(0x73); |
| 276 | + code.extend_from_slice(addr.as_slice()); |
| 277 | + code.extend_from_slice(&[0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3]); |
| 278 | + code |
| 279 | + } |
| 280 | + |
| 281 | + /// Stub bytecode that always ABI-returns a single uint256 word `value`. |
| 282 | + fn stub_returning_uint(value: u8) -> Vec<u8> { |
| 283 | + vec![0x60, value, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3] |
| 284 | + } |
| 285 | + |
| 286 | + #[test] |
| 287 | + fn governance_owner_matches_passes() { |
| 288 | + let owner: Address = "0x000000000000000000000000000000000000dEaD" |
| 289 | + .parse() |
| 290 | + .unwrap(); |
| 291 | + let mut db = InMemoryDB::default(); |
| 292 | + plant_system_caller(&mut db); |
| 293 | + plant_code(&mut db, GOVERNANCE_ADDR, stub_returning_address(owner)); |
| 294 | + |
| 295 | + let result = verify_governance_owner_at(db, BundleState::default(), owner, 1337, 1_700_000_000); |
| 296 | + assert!(result.is_ok(), "expected Ok, got {:?}", result); |
| 297 | + } |
| 298 | + |
| 299 | + #[test] |
| 300 | + fn governance_owner_mismatch_fails() { |
| 301 | + let on_chain: Address = "0x000000000000000000000000000000000000dEaD" |
| 302 | + .parse() |
| 303 | + .unwrap(); |
| 304 | + let expected: Address = "0x000000000000000000000000000000000000bEEf" |
| 305 | + .parse() |
| 306 | + .unwrap(); |
| 307 | + let mut db = InMemoryDB::default(); |
| 308 | + plant_system_caller(&mut db); |
| 309 | + plant_code(&mut db, GOVERNANCE_ADDR, stub_returning_address(on_chain)); |
| 310 | + |
| 311 | + let result = |
| 312 | + verify_governance_owner_at(db, BundleState::default(), expected, 1337, 1_700_000_000); |
| 313 | + let err = result.expect_err("expected Err on owner mismatch"); |
| 314 | + assert!( |
| 315 | + err.to_lowercase().contains("mismatch"), |
| 316 | + "error should mention mismatch, got: {}", |
| 317 | + err |
| 318 | + ); |
| 319 | + // The error must surface BOTH addresses so a CI log makes the gap |
| 320 | + // diagnosable without re-running the tool. |
| 321 | + assert!(err.contains("dead"), "error should include on-chain owner, got: {}", err); |
| 322 | + assert!(err.contains("beef"), "error should include expected owner, got: {}", err); |
| 323 | + } |
| 324 | + |
| 325 | + #[test] |
| 326 | + fn governance_initialized_true_passes() { |
| 327 | + let mut db = InMemoryDB::default(); |
| 328 | + plant_system_caller(&mut db); |
| 329 | + plant_code(&mut db, GOVERNANCE_ADDR, stub_returning_uint(1)); |
| 330 | + |
| 331 | + let result = |
| 332 | + verify_governance_initialized_at(db, BundleState::default(), 1337, 1_700_000_000); |
| 333 | + assert!(result.is_ok(), "expected Ok, got {:?}", result); |
| 334 | + } |
| 335 | + |
| 336 | + #[test] |
| 337 | + fn governance_initialized_false_fails() { |
| 338 | + let mut db = InMemoryDB::default(); |
| 339 | + plant_system_caller(&mut db); |
| 340 | + plant_code(&mut db, GOVERNANCE_ADDR, stub_returning_uint(0)); |
| 341 | + |
| 342 | + let result = |
| 343 | + verify_governance_initialized_at(db, BundleState::default(), 1337, 1_700_000_000); |
| 344 | + let err = result.expect_err("expected Err when isInitialized=false"); |
| 345 | + assert!( |
| 346 | + err.contains("initialize did not run") || err.contains("returned false"), |
| 347 | + "error should explain the missing initialize, got: {}", |
| 348 | + err |
| 349 | + ); |
| 350 | + } |
| 351 | +} |
0 commit comments