From 9d0bf9ccac7b6891313a30c3286c6c1be7dc30f5 Mon Sep 17 00:00:00 2001 From: Mathis Date: Fri, 20 Dec 2024 13:26:08 +0100 Subject: [PATCH] Add rust binary to update permissioned stake table (#2410) * Add rust binary to update permissioned stake table Compiles, but there's no test yet. * initialize logging --------- Co-authored-by: Abdul Basit <45506001+imabdulbasit@users.noreply.github.com> Co-authored-by: imabdulbasit --- sequencer/src/bin/deploy.rs | 1 + .../bin/update-permissioned-stake-table.rs | 104 ++++++++++++++++++ utils/src/stake_table.rs | 87 ++++++++++++++- 3 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 sequencer/src/bin/update-permissioned-stake-table.rs diff --git a/sequencer/src/bin/deploy.rs b/sequencer/src/bin/deploy.rs index afd6e96f7..d81b9550b 100644 --- a/sequencer/src/bin/deploy.rs +++ b/sequencer/src/bin/deploy.rs @@ -131,6 +131,7 @@ struct Options { /// stake_table_key = "BLS_VER_KEY~...", /// state_ver_key = "SCHNORR_VER_KEY~...", /// da = true, + /// stake = 1, # this value is ignored, but needs to be set /// }, /// ] #[clap(long, env = "ESPRESSO_SEQUENCER_INITIAL_PERMISSIONED_STAKE_TABLE_PATH")] diff --git a/sequencer/src/bin/update-permissioned-stake-table.rs b/sequencer/src/bin/update-permissioned-stake-table.rs new file mode 100644 index 000000000..9849516da --- /dev/null +++ b/sequencer/src/bin/update-permissioned-stake-table.rs @@ -0,0 +1,104 @@ +use anyhow::Result; +use clap::Parser; +use espresso_types::parse_duration; +use ethers::types::Address; +use sequencer_utils::{logging, stake_table::{update_stake_table, PermissionedStakeTableUpdate}}; +use std::{path::PathBuf, time::Duration}; +use url::Url; + +#[derive(Debug, Clone, Parser)] +struct Options { + /// RPC URL for the L1 provider. + #[clap( + short, + long, + env = "ESPRESSO_SEQUENCER_L1_PROVIDER", + default_value = "http://localhost:8545" + )] + rpc_url: Url, + + /// Request rate when polling L1. + #[clap( + long, + env = "ESPRESSO_SEQUENCER_L1_POLLING_INTERVAL", + default_value = "7s", + value_parser = parse_duration, + )] + pub l1_polling_interval: Duration, + + /// Mnemonic for an L1 wallet. + /// + /// This wallet is used to deploy the contracts, so the account indicated by ACCOUNT_INDEX must + /// be funded with with ETH. + #[clap( + long, + name = "MNEMONIC", + env = "ESPRESSO_SEQUENCER_ETH_MNEMONIC", + default_value = "test test test test test test test test test test test junk" + )] + mnemonic: String, + + /// Account index in the L1 wallet generated by MNEMONIC to use when deploying the contracts. + #[clap( + long, + name = "ACCOUNT_INDEX", + env = "ESPRESSO_DEPLOYER_ACCOUNT_INDEX", + default_value = "0" + )] + account_index: u32, + + /// Permissioned stake table contract address. + #[clap(long, env = "ESPRESSO_SEQUENCER_PERMISSIONED_STAKE_TABLE_ADDRESS")] + contract_address: Address, + + /// Path to the toml file containing the update information. + /// + /// Schema of toml file: + /// ```toml + /// stakers_to_remove = [ + /// { + /// stake_table_key = "BLS_VER_KEY~...", + /// state_ver_key = "SCHNORR_VER_KEY~...", + /// da = false, + /// stake = 1, # this value is ignored, but needs to be set + /// }, + /// ] + /// + /// new_stakers = [ + /// { + /// stake_table_key = "BLS_VER_KEY~...", + /// state_ver_key = "SCHNORR_VER_KEY~...", + /// da = true, + /// stake = 1, # this value is ignored, but needs to be set + /// }, + /// ] + /// ``` + #[clap( + long, + env = "ESPRESSO_SEQUENCER_PERMISSIONED_STAKE_TABLE_UPDATE_TOML_PATH", + verbatim_doc_comment + )] + update_toml_path: PathBuf, + #[clap(flatten)] + logging: logging::Config, +} + +#[tokio::main] +async fn main() -> Result<()> { + let opts = Options::parse(); + opts.logging.init(); + let update = PermissionedStakeTableUpdate::from_toml_file(&opts.update_toml_path)?; + + + update_stake_table( + opts.rpc_url, + opts.l1_polling_interval, + opts.mnemonic, + opts.account_index, + opts.contract_address, + update, + ) + .await?; + + Ok(()) +} diff --git a/utils/src/stake_table.rs b/utils/src/stake_table.rs index 0b3236083..1abf1e38c 100644 --- a/utils/src/stake_table.rs +++ b/utils/src/stake_table.rs @@ -2,12 +2,19 @@ /// /// The initial stake table is passed to the permissioned stake table contract /// on deployment. -use contract_bindings::permissioned_stake_table::NodeInfo; +use contract_bindings::permissioned_stake_table::{NodeInfo, PermissionedStakeTable}; +use ethers::{ + middleware::SignerMiddleware, + providers::{Http, Middleware as _, Provider}, + signers::{coins_bip39::English, MnemonicBuilder, Signer as _}, + types::Address, +}; use hotshot::types::BLSPubKey; use hotshot_contract_adapter::stake_table::NodeInfoJf; use hotshot_types::network::PeerConfigKeys; +use url::Url; -use std::{fs, path::Path}; +use std::{fs, path::Path, sync::Arc, time::Duration}; /// A stake table config stored in a file #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] @@ -48,6 +55,82 @@ impl From for Vec { } } +/// Information to add and remove stakers in the permissioned stake table contract. +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(bound(deserialize = ""))] +pub struct PermissionedStakeTableUpdate { + #[serde(default)] + stakers_to_remove: Vec>, + #[serde(default)] + new_stakers: Vec>, +} + +impl PermissionedStakeTableUpdate { + pub fn from_toml_file(path: &Path) -> anyhow::Result { + let config_file_as_string: String = fs::read_to_string(path) + .unwrap_or_else(|_| panic!("Could not read config file located at {}", path.display())); + + Ok( + toml::from_str::(&config_file_as_string).unwrap_or_else(|err| { + panic!( + "Unable to convert config file {} to TOML: {err}", + path.display() + ) + }), + ) + } + + fn stakers_to_remove(&self) -> Vec { + self.stakers_to_remove + .iter() + .map(|peer_config| { + let node_info: NodeInfoJf = peer_config.clone().into(); + node_info.into() + }) + .collect() + } + + fn new_stakers(&self) -> Vec { + self.new_stakers + .iter() + .map(|peer_config| { + let node_info: NodeInfoJf = peer_config.clone().into(); + node_info.into() + }) + .collect() + } +} + +pub async fn update_stake_table( + l1url: Url, + l1_interval: Duration, + mnemonic: String, + account_index: u32, + contract_address: Address, + update: PermissionedStakeTableUpdate, +) -> anyhow::Result<()> { + let provider = Provider::::try_from(l1url.to_string())?.interval(l1_interval); + let chain_id = provider.get_chainid().await?.as_u64(); + let wallet = MnemonicBuilder::::default() + .phrase(mnemonic.as_str()) + .index(account_index)? + .build()? + .with_chain_id(chain_id); + let l1 = Arc::new(SignerMiddleware::new(provider.clone(), wallet)); + + let contract = PermissionedStakeTable::new(contract_address, l1); + + tracing::info!("sending stake table update transaction"); + + let tx_receipt = contract + .update(update.stakers_to_remove(), update.new_stakers()) + .send() + .await? + .await?; + tracing::info!("Transaction receipt: {:?}", tx_receipt); + Ok(()) +} + #[cfg(test)] mod test { use crate::stake_table::PermissionedStakeTableConfig;