Skip to content

Commit cf5cd27

Browse files
authored
feat(staking-cli): add mainnet support and metadata validation (#3922)
* feat(staking-cli): add mainnet support and metadata validation Mainnet support: - Add config.mainnet.toml with mainnet contract addresses - Add Network enum (Mainnet, Decaf, Local) for init command - Update init command to require --network parameter - Update README with mainnet contract addresses and Docker usage Metadata validation: - Add NodeMetadataContent schema types for metadata validation - Add validate_metadata_uri function with 5s timeout - Add --skip-metadata-validation flag to bypass validation - Add --consensus-public-key parameter for update-metadata-uri - Add parse_bls_pub_key helper function - Include --skip-metadata-validation hint in error messages Contract deployer: - Add configurable deploy cooldown via --post-deployment-cooldown - Refactor Contracts struct to use explicit fields instead of Deref Tests: - Add metadata validation tests for register-validator and update-metadata-uri - Remove unnecessary spawn_blocking from metadata validation tests - Add bls_public_key_str helper to TestSystemExt Documentation: - Major README update with improved formatting and examples - Document metadata validation and schema requirements - Add calldata export documentation * fix: contracts API for dev node * refactor(staking-cli): deduplicate generate_bls_pub_key test helper Move generate_bls_pub_key to module level with #[cfg(test)] so both test and validation_tests modules can share it. * test(staking-cli): add test for missing signer error Verify that running a command without providing any signer (--mnemonic, --private-key, or --ledger) fails with an appropriate error message. * fix(staking-cli): config command shows helpful message when no file exists Previously the config command would print "Config file at X" and show default values even when no config file existed. Now it correctly detects missing files and suggests running init. * docs(staking-cli): document multi-network config file usage Add section explaining how to use the -c flag to maintain separate config files for different networks (mainnet, decaf). Also clarify that config files are optional and what happens when none exists.
1 parent 1b62667 commit cf5cd27

File tree

15 files changed

+1235
-163
lines changed

15 files changed

+1235
-163
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ indexmap = { version = "2", features = ["serde"] }
147147
lazy_static = "1"
148148
libp2p-identity = { version = "0.2", features = ["ed25519", "serde"] }
149149
libp2p-swarm-derive = { version = "0.35" }
150-
moka = { version = "0.12.12", features = ["future"] }
151150
memoize = { version = "0.4", features = ["full"] }
151+
moka = { version = "0.12.12", features = ["future"] }
152152
multiaddr = { version = "0.18" }
153153
nohash-hasher = "0.2.0"
154154
num_cpus = "1"

contracts/rust/deployer/src/lib.rs

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use alloy::{
2121
};
2222
use anyhow::{anyhow, Context, Result};
2323
use clap::{builder::OsStr, Parser, ValueEnum};
24-
use derive_more::{derive::Deref, Display};
24+
use derive_more::Display;
2525
use espresso_types::{v0_1::L1Client, v0_3::Fetcher};
2626
use hotshot_contract_adapter::sol_types::*;
2727

@@ -274,8 +274,22 @@ impl From<Contract> for OsStr {
274274
}
275275

276276
/// Cache of contracts predeployed or deployed during this current run.
277-
#[derive(Deref, Debug, Clone, Default)]
278-
pub struct Contracts(HashMap<Contract, Address>);
277+
#[derive(Debug, Clone)]
278+
pub struct Contracts {
279+
addresses: HashMap<Contract, Address>,
280+
// TODO: having the cooldown field here is a bit hacky but we postpone a better solution to
281+
// avoid a large refactor.
282+
deploy_cooldown: Duration,
283+
}
284+
285+
impl Default for Contracts {
286+
fn default() -> Self {
287+
Self {
288+
addresses: HashMap::new(),
289+
deploy_cooldown: Duration::ZERO,
290+
}
291+
}
292+
}
279293

280294
impl From<DeployedContracts> for Contracts {
281295
fn from(deployed: DeployedContracts) -> Self {
@@ -337,17 +351,43 @@ impl From<DeployedContracts> for Contracts {
337351
if let Some(addr) = deployed.reward_claim_proxy {
338352
m.insert(Contract::RewardClaimProxy, addr);
339353
}
340-
Self(m)
354+
Self {
355+
addresses: m,
356+
deploy_cooldown: Duration::ZERO,
357+
}
341358
}
342359
}
343360

344361
impl Contracts {
345362
pub fn new() -> Self {
346-
Contracts(HashMap::new())
363+
Self::default()
364+
}
365+
366+
pub fn with_cooldown(cooldown: Duration) -> Self {
367+
Self {
368+
addresses: HashMap::new(),
369+
deploy_cooldown: cooldown,
370+
}
371+
}
372+
373+
pub fn set_cooldown(&mut self, cooldown: Duration) {
374+
self.deploy_cooldown = cooldown;
347375
}
348376

349377
pub fn address(&self, contract: Contract) -> Option<Address> {
350-
self.0.get(&contract).copied()
378+
self.addresses.get(&contract).copied()
379+
}
380+
381+
pub fn get(&self, contract: &Contract) -> Option<&Address> {
382+
self.addresses.get(contract)
383+
}
384+
385+
pub fn iter(&self) -> impl Iterator<Item = (&Contract, &Address)> {
386+
self.addresses.iter()
387+
}
388+
389+
pub fn remove(&mut self, contract: &Contract) -> Option<Address> {
390+
self.addresses.remove(contract)
351391
}
352392

353393
/// Deploy a contract (with logging and cached deployments)
@@ -358,7 +398,7 @@ impl Contracts {
358398
where
359399
P: Provider,
360400
{
361-
if let Some(addr) = self.0.get(&name) {
401+
if let Some(addr) = self.addresses.get(&name) {
362402
tracing::info!("skipping deployment of {name}, already deployed at {addr:#x}");
363403
return Ok(*addr);
364404
}
@@ -377,18 +417,18 @@ impl Contracts {
377417

378418
tracing::info!("deployed {name} at {addr:#x}");
379419

380-
// Cooldown after deployment to avoid rate limiting on public RPC nodes
381-
// This helps when deploying multiple contracts in sequence
382-
tokio::time::sleep(Duration::from_millis(1000)).await;
383-
tracing::info!("cooldown 1000ms after deployment");
420+
if !self.deploy_cooldown.is_zero() {
421+
tokio::time::sleep(self.deploy_cooldown).await;
422+
tracing::info!("cooldown {:?} after deployment", self.deploy_cooldown);
423+
}
384424

385-
self.0.insert(name, addr);
425+
self.addresses.insert(name, addr);
386426
Ok(addr)
387427
}
388428

389429
/// Write a .env file.
390430
pub fn write(&self, mut w: impl Write) -> Result<()> {
391-
for (contract, address) in &self.0 {
431+
for (contract, address) in &self.addresses {
392432
writeln!(w, "{contract}={address:#x}")?;
393433
}
394434
Ok(())
@@ -2316,7 +2356,7 @@ mod tests {
23162356

23172357
impl Contracts {
23182358
fn insert(&mut self, name: Contract, address: Address) -> Option<Address> {
2319-
self.0.insert(name, address)
2359+
self.addresses.insert(name, address)
23202360
}
23212361
}
23222362

@@ -4486,7 +4526,7 @@ mod tests {
44864526
let cached_impl_addr = contracts.address(Contract::FeeContract);
44874527

44884528
// For patch upgrades, we need to clear the cache to allow redeployment
4489-
contracts.0.remove(&Contract::FeeContract);
4529+
contracts.remove(&Contract::FeeContract);
44904530

44914531
// Test the upgrade function directly
44924532
let receipt = upgrade_fee_v1(&provider, &mut contracts).await?;
@@ -4566,7 +4606,7 @@ mod tests {
45664606

45674607
// Second upgrade to V2 (re-applying same version)
45684608
// For patch upgrades, we need to clear the cache to allow redeployment
4569-
contracts.0.remove(&Contract::LightClientV2);
4609+
contracts.remove(&Contract::LightClientV2);
45704610

45714611
// Second upgrade to V2 (re-applying same version)
45724612
upgrade_light_client_v2(

sequencer/src/bin/deploy.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,16 @@ struct Options {
405405
#[clap(flatten)]
406406
logging: logging::Config,
407407

408+
/// Cooldown after each contract deployment to avoid outdated state during post deployment
409+
/// verification steps.
410+
#[clap(
411+
long,
412+
env = "ESPRESSO_POST_DEPLOYMENT_COOLDOWN",
413+
default_value = "1s",
414+
value_parser = parse_duration,
415+
)]
416+
post_deployment_cooldown: Duration,
417+
408418
/// Command to run
409419
///
410420
/// For backwards compatibility, the default is to deploy contracts, if no
@@ -432,6 +442,8 @@ async fn main() -> anyhow::Result<()> {
432442
};
433443

434444
let mut contracts = Contracts::from(opt.contracts);
445+
contracts.set_cooldown(opt.post_deployment_cooldown);
446+
435447
let provider = if opt.ledger {
436448
let signer = connect_ledger(opt.account_index as usize).await?;
437449
tracing::info!("Using ledger for signing, watch ledger device for prompts.");

0 commit comments

Comments
 (0)