Skip to content

Commit 6d0cc1d

Browse files
ByteYueclaude
andauthored
feat(genesis-tool): verify Governance owner + isInitialized post-genesis (#104)
* feat(genesis-tool): verify Governance owner + isInitialized post-genesis Generating a sealed genesis.json silently accepted a wrong `governanceOwner` in the config — the field was passed to `Genesis.initialize` but never read back from chain state. Because `Governance.renounceOwnership` is disabled, a typo or zero address would lock the executor set for the chain's lifetime with no recovery path. Add two on-chain probes that run as part of `verify_result`: - `Governance.owner()` must equal the config's `governanceOwner` - `Governance.isInitialized()` must be `true` Covered by unit tests that plant minimal stub bytecode in an InMemoryDB, so the mismatch path is exercised without needing forge output or a full GenesisConfig. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(genesis-tool): extend `verify` subcommand with Governance owner check The post-generation self-check added in the previous commit caught a wrong `governanceOwner` at generation time, but anyone running `genesis-tool verify` against a sealed `genesis.json` later (CI release gate, third-party operator, ceremony witness) had no way to detect the same problem on the saved artifact. Add the same on-chain probes to `verify_genesis_file`: - `Governance.isInitialized()` must be true - `Governance.owner()` must be non-zero - If `--config-file <path>` is provided, the owner must also equal the config's `governanceOwner`. The config is read loosely (as serde_json::Value) so verify stays decoupled from GenesisConfig's full schema and keeps working if new required fields are added later. Governance errors are accumulated into the existing `VerifyResult.errors` vector rather than short-circuiting; one verify invocation should surface every problem, not just the first. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8846e24 commit 6d0cc1d

3 files changed

Lines changed: 432 additions & 12 deletions

File tree

genesis-tool/src/main.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ enum Commands {
7272
/// Path to the genesis.json file to verify
7373
#[arg(short, long)]
7474
genesis_file: String,
75+
76+
/// Optional path to the genesis config used to produce the artifact.
77+
/// When provided, `Governance.owner()` must equal the config's
78+
/// `governanceOwner`. Without it we only check the on-chain owner is
79+
/// non-zero and `isInitialized()` is true.
80+
#[arg(short, long)]
81+
config_file: Option<String>,
7582
},
7683
}
7784

@@ -132,8 +139,8 @@ async fn main() -> Result<()> {
132139
Commands::Generate { byte_code_dir, config_file, output } => {
133140
run_generate(byte_code_dir, config_file, output).await
134141
}
135-
Commands::Verify { genesis_file } => {
136-
run_verify(genesis_file)
142+
Commands::Verify { genesis_file, config_file } => {
143+
run_verify(genesis_file, config_file.as_deref())
137144
}
138145
};
139146

@@ -184,10 +191,10 @@ async fn run_generate(byte_code_dir: &str, config_file: &str, output: &str) -> R
184191
Ok(())
185192
}
186193

187-
fn run_verify(genesis_file: &str) -> Result<()> {
194+
fn run_verify(genesis_file: &str, config_file: Option<&str>) -> Result<()> {
188195
info!("Starting Gravity Genesis Verify");
189-
190-
let result = verify::verify_genesis_file(genesis_file)?;
196+
197+
let result = verify::verify_genesis_file(genesis_file, config_file)?;
191198
verify::print_verify_summary(&result);
192199

193200
if result.success {

genesis-tool/src/post_genesis.rs

Lines changed: 241 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1+
use alloy_sol_macro::sol;
2+
use alloy_sol_types::SolCall;
13
use revm::{DatabaseRef, InMemoryDB, db::BundleState};
2-
use revm_primitives::{ExecutionResult, SpecId, TxEnv, hex};
4+
use revm_primitives::{Address, ExecutionResult, SpecId, TxEnv, hex};
35
use tracing::{error, info};
46

57
use crate::{
68
execute::{prepare_env, resolve_block_timestamp},
79
genesis::{
810
GenesisConfig, call_get_active_validators, print_active_validators_result,
911
},
10-
utils::execute_revm_sequential,
12+
utils::{GOVERNANCE_ADDR, execute_revm_sequential, new_system_call_txn},
1113
};
1214

15+
sol! {
16+
interface IGovernance {
17+
function owner() external view returns (address);
18+
function isInitialized() external view returns (bool);
19+
}
20+
}
21+
1322
/// Generic template for handling execution results
1423
///
1524
/// 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
97106
)
98107
}
99108

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+
100222
pub fn verify_result(
101223
db: InMemoryDB,
102224
bundle_state: BundleState,
103225
config: &GenesisConfig,
104226
) -> Result<(), String> {
105227
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)?;
106230
// Add more verification steps as needed:
107231
// - verify_jwks()
108232
// - verify_epoch_config()
109233
// - verify_randomness_config()
110234
// etc.
111235
Ok(())
112236
}
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

Comments
 (0)