From 4272f2f2620cc74826ccf4ec26211ad06727d439 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Tue, 19 Nov 2024 18:08:44 +0100 Subject: [PATCH 01/35] feat(bolt-cli): port CompressedHash --- bolt-cli/src/common/hash.rs | 27 +++++++++++++++++++++++++++ bolt-cli/src/common/mod.rs | 2 ++ 2 files changed, 29 insertions(+) create mode 100644 bolt-cli/src/common/hash.rs diff --git a/bolt-cli/src/common/hash.rs b/bolt-cli/src/common/hash.rs new file mode 100644 index 000000000..28277f730 --- /dev/null +++ b/bolt-cli/src/common/hash.rs @@ -0,0 +1,27 @@ +// NOTE: Primitive types should be flattened into a single module +// for cleaner imports and better organization. + +use alloy::primitives::{keccak256, FixedBytes, B512}; +use ethereum_consensus::crypto::PublicKey as BlsPublicKey; + +/// A 20-byte compressed hash of a BLS public key. +/// +/// Reference: https://github.com/chainbound/bolt/blob/bec46baae6d7c16dddd81e5e72710ca8e3064f82/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol#L65-L69 +pub type CompressedHash = FixedBytes<20>; + +/// Compress a BLS public key into a 20-byte hash. +pub fn compress_bls_pubkey(pubkey: &BlsPublicKey) -> CompressedHash { + let mut onchain_pubkey_repr = B512::ZERO; + + // copy the pubkey bytes into the rightmost 48 bytes of the 512-bit buffer. + // the result should look like this: + // + // 0x00000000000000000000000000000000b427fd179b35ef085409e4a98fb3ab84ee29c689df5c64020eab0b20a4f91170f610177db172dc091682df627c9f4021 + // |<---------- 16 bytes ---------->||<----------------------------------------- 48 bytes ----------------------------------------->| + onchain_pubkey_repr[16..].copy_from_slice(pubkey.as_ref()); + + // hash the pubkey + let hash = keccak256(onchain_pubkey_repr); + + CompressedHash::from_slice(hash.get(0..20).expect("hash is longer than 20 bytes")) +} diff --git a/bolt-cli/src/common/mod.rs b/bolt-cli/src/common/mod.rs index d730df81b..81eab6f9e 100644 --- a/bolt-cli/src/common/mod.rs +++ b/bolt-cli/src/common/mod.rs @@ -16,6 +16,8 @@ pub mod keystore; /// Utilities for signing and verifying messages. pub mod signing; +pub mod hash; + /// Parse a BLS public key from a string pub fn parse_bls_public_key(delegatee_pubkey: &str) -> Result { let hex_pk = delegatee_pubkey.strip_prefix("0x").unwrap_or(delegatee_pubkey); From a384f49fd684bfab6bd244c9977f308536a89750 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Tue, 19 Nov 2024 18:09:00 +0100 Subject: [PATCH 02/35] feat(bolt-cli): register command --- bolt-cli/src/cli.rs | 32 ++++++++++- bolt-cli/src/commands/mod.rs | 4 ++ bolt-cli/src/commands/register.rs | 92 +++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 bolt-cli/src/commands/register.rs diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index 399e613e1..39a6572f9 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -1,3 +1,6 @@ +use std::path::PathBuf; + +use alloy::primitives::Address; use clap::{ builder::styling::{AnsiColor, Color, Style}, Parser, Subcommand, ValueEnum, @@ -25,6 +28,9 @@ pub enum Cmd { /// Send a preconfirmation request to a Bolt proposer. Send(Box), + + /// Register validators in the Bolt network. + Register(RegisterCommand), } impl Cmd { @@ -34,6 +40,7 @@ impl Cmd { Cmd::Delegate(cmd) => cmd.run().await, Cmd::Pubkeys(cmd) => cmd.run().await, Cmd::Send(cmd) => cmd.run().await, + Cmd::Register(cmd) => cmd.run().await, } } } @@ -127,6 +134,29 @@ pub struct SendCommand { pub devnet_sidecar_url: Option, } +/// Command for registering validators in the Bolt network. +#[derive(Debug, Clone, Parser)] +pub struct RegisterCommand { + /// The chain for which the delegation message is intended. + #[clap(long, env = "CHAIN", default_value = "holesky")] + pub chain: Chain, + + #[clap(long, env = "MAX_COMMITTED_GAS_LIMIT", default_value_t = 10_000_000)] + pub max_committed_gas_limit: u32, + + #[clap(long, env = "AUTHORIZED_OPERATOR")] + pub authorized_operator: Address, + + #[clap(long, env = "PUBKEYS_PATH", default_value = "pubkeys.json")] + pub pubkeys_path: PathBuf, + + #[clap(long, env = "ADMIN_PRIVATE_KEY")] + pub admin_private_key: String, + + #[clap(long, env = "RPC_URL")] + pub rpc_url: Url, +} + /// The action to perform. #[derive(Debug, Clone, ValueEnum)] #[clap(rename_all = "kebab_case")] @@ -225,7 +255,7 @@ pub struct TlsCredentials { } /// Supported chains for the CLI -#[derive(Debug, Clone, Copy, ValueEnum)] +#[derive(Debug, Clone, Copy, ValueEnum, Hash, PartialEq, Eq)] #[clap(rename_all = "kebab_case")] pub enum Chain { Mainnet, diff --git a/bolt-cli/src/commands/mod.rs b/bolt-cli/src/commands/mod.rs index e2467b1c9..39439b5ff 100644 --- a/bolt-cli/src/commands/mod.rs +++ b/bolt-cli/src/commands/mod.rs @@ -9,3 +9,7 @@ pub mod pubkeys; /// Module for the bolt `send` command to create and /// broadcast preconfirmations in Bolt. pub mod send; + +/// Module for the bolt `register` command to register +/// validators in the Bolt network. +pub mod register; diff --git a/bolt-cli/src/commands/register.rs b/bolt-cli/src/commands/register.rs new file mode 100644 index 000000000..91bbf50e6 --- /dev/null +++ b/bolt-cli/src/commands/register.rs @@ -0,0 +1,92 @@ +use std::collections::HashMap; + +use alloy::{ + network::EthereumWallet, + primitives::{address, Address}, + providers::ProviderBuilder, + signers::local::PrivateKeySigner, + sol, +}; +use ethereum_consensus::crypto::PublicKey as BlsPublicKey; +use eyre::Context; + +use crate::{ + cli::{Chain, RegisterCommand}, + common::hash::compress_bls_pubkey, +}; + +#[derive(Debug, PartialEq, Eq, Hash)] +enum BoltContract { + Validators, +} + +impl RegisterCommand { + /// Run the `delegate` command. + pub async fn run(self) -> eyre::Result<()> { + let bolt_validators_address = bolt_validators_address(self.chain); + + let pubkeys_file = std::fs::File::open(&self.pubkeys_path)?; + let keys: Vec = serde_json::from_reader(pubkeys_file)?; + let pubkey_hashes: Vec<_> = keys.iter().map(compress_bls_pubkey).collect(); + + let wallet: PrivateKeySigner = + self.admin_private_key.parse().wrap_err("invalid private key")?; + let transaction_signer = EthereumWallet::from(wallet); + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(transaction_signer) + .on_http(self.rpc_url.clone()); + + let bolt_validators = BoltValidatorsContract::new(bolt_validators_address, provider); + + let call = bolt_validators.batchRegisterValidatorsUnsafe( + pubkey_hashes, + self.max_committed_gas_limit, + self.authorized_operator, + ); + + let result = call.send().await?; + + println!("Call result: {:?}", result); + + Ok(()) + } +} + +// PERF: this should be done at compile time +fn deployments() -> HashMap> { + let mut deployments = HashMap::new(); + let mut holesky_deployments = HashMap::new(); + holesky_deployments + .insert(BoltContract::Validators, address!("47D2DC1DE1eFEFA5e6944402f2eda3981D36a9c8")); + deployments.insert(Chain::Holesky, holesky_deployments); + + deployments +} + +fn bolt_validators_address(chain: Chain) -> Address { + *deployments() + .get(&chain) + .unwrap_or_else(|| panic!("{:?} chain supported", chain)) + .get(&BoltContract::Validators) + .expect("Validators contract address not found") +} + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + interface BoltValidatorsContract { + /// @notice Register a batch of Validators and authorize a Collateral Provider and Operator for them + /// @dev This function allows anyone to register a list of Validators. + /// @param pubkeyHashes List of BLS public key hashes for the Validators to be registered + /// @param maxCommittedGasLimit The maximum gas that the Validator can commit for preconfirmations + /// @param authorizedOperator The address of the authorized operator + function batchRegisterValidatorsUnsafe(bytes20[] calldata pubkeyHashes, uint32 maxCommittedGasLimit, address authorizedOperator); + + error KeyNotFound(); + error InvalidQuery(); + #[derive(Debug)] + error ValidatorDoesNotExist(bytes20 pubkeyHash); + error InvalidAuthorizedOperator(); + } +} From 81ef23742ed2db8d64772ffd94ab4e4d2844882f Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Tue, 19 Nov 2024 18:53:43 +0100 Subject: [PATCH 03/35] test(bolt-cli): register validators --- bolt-cli/.gitignore | 1 + bolt-cli/Cargo.lock | 221 ++++++++++++++++++------------ bolt-cli/Cargo.toml | 7 +- bolt-cli/src/commands/register.rs | 39 +++++- bolt-cli/test_data/pubkeys.json | 3 + 5 files changed, 177 insertions(+), 94 deletions(-) create mode 100644 bolt-cli/test_data/pubkeys.json diff --git a/bolt-cli/.gitignore b/bolt-cli/.gitignore index 12571ac34..c8b1db124 100644 --- a/bolt-cli/.gitignore +++ b/bolt-cli/.gitignore @@ -5,3 +5,4 @@ delegations.json pubkeys.json +!test_data/pubkeys.json diff --git a/bolt-cli/Cargo.lock b/bolt-cli/Cargo.lock index 57a5eaa8d..009d4ce57 100644 --- a/bolt-cli/Cargo.lock +++ b/bolt-cli/Cargo.lock @@ -60,9 +60,9 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "alloy" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8ebf106e84a1c37f86244df7da0c7587e697b71a0d565cce079449b85ac6f8" +checksum = "b5b524b8c28a7145d1fe4950f84360b5de3e307601679ff0558ddc20ea229399" dependencies = [ "alloy-consensus", "alloy-contract", @@ -70,6 +70,7 @@ dependencies = [ "alloy-eips", "alloy-genesis", "alloy-network", + "alloy-node-bindings", "alloy-provider", "alloy-pubsub", "alloy-rpc-client", @@ -89,38 +90,39 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4932d790c723181807738cf1ac68198ab581cd699545b155601332541ee47bd" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "num_enum", "strum", ] [[package]] name = "alloy-consensus" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed961a48297c732a5d97ee321aa8bb5009ecadbcb077d8bec90cb54e651629" +checksum = "ae09ffd7c29062431dd86061deefe4e3c6f07fa0d674930095f8dcedb0baf02c" dependencies = [ "alloy-eips", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "alloy-serde", "auto_impl", "c-kzg", "derive_more 1.0.0", + "k256 0.13.4", "serde", ] [[package]] name = "alloy-contract" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460ab80ce4bda1c80bcf96fe7460520476f2c7b734581c6567fac2708e2a60ef" +checksum = "66430a72d5bf5edead101c8c2f0a24bada5ec9f3cf9909b3e08b6d6899b4803e" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", "alloy-network", "alloy-network-primitives", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-provider", "alloy-pubsub", "alloy-rpc-types-eth", @@ -133,25 +135,25 @@ dependencies = [ [[package]] name = "alloy-core" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cce174ca699ddee3bfb2ec1fbd99ad7efd05eca20c5c888d8320db41f7e8f04" +checksum = "9c8316d83e590f4163b221b8180008f302bda5cf5451202855cdd323e588849c" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "alloy-sol-types", ] [[package]] name = "alloy-dyn-abi" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5647fce5a168f9630f935bf7821c4207b1755184edaeba783cb4e11d35058484" +checksum = "ef2364c782a245cf8725ea6dbfca5f530162702b5d685992ea03ce64529136cc" dependencies = [ "alloy-json-abi", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-sol-type-parser", "alloy-sol-types", "const-hex", @@ -167,18 +169,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0069cf0642457f87a01a014f6dc29d5d893cd4fd8fddf0c3cdfad1bb3ebafc41" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "serde", ] [[package]] name = "alloy-eip7702" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ffc577390ce50234e02d841214b3dc0bea6aaaae8e04bbf3cb82e9a45da9eb" +checksum = "5f6cee6a35793f3db8a5ffe60e86c695f321d081a567211245f503e8c498fce8" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "derive_more 1.0.0", "k256 0.13.4", @@ -187,13 +189,13 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69e06cf9c37be824b9d26d6d101114fdde6af0c87de2828b414c05c4b3daa71" +checksum = "5b6aa3961694b30ba53d41006131a2fca3bdab22e4c344e46db2c639e7c2dfdd" dependencies = [ "alloy-eip2930", "alloy-eip7702", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "alloy-serde", "c-kzg", @@ -205,22 +207,22 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde15e14944a88bd6a57d325e9a49b75558746fe16aaccc79713ae50a6a9574c" +checksum = "e53f7877ded3921d18a0a9556d55bedf84535567198c9edab2aa23106da91855" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-serde", "serde", ] [[package]] name = "alloy-json-abi" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b5671117c38b1c2306891f97ad3828d85487087f54ebe2c7591a055ea5bcea7" +checksum = "b84c506bf264110fa7e90d9924f742f40ef53c6572ea56a0b0bd714a567ed389" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-sol-type-parser", "serde", "serde_json", @@ -228,11 +230,11 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af5979e0d5a7bf9c7eb79749121e8256e59021af611322aee56e77e20776b4b3" +checksum = "3694b7e480728c0b3e228384f223937f14c10caef5a4c766021190fc8f283d35" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-sol-types", "serde", "serde_json", @@ -242,15 +244,15 @@ dependencies = [ [[package]] name = "alloy-network" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "204237129086ce5dc17a58025e93739b01b45313841f98fa339eb1d780511e57" +checksum = "ea94b8ceb5c75d7df0a93ba0acc53b55a22b47b532b600a800a87ef04eb5b0b4" dependencies = [ "alloy-consensus", "alloy-eips", "alloy-json-rpc", "alloy-network-primitives", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rpc-types-eth", "alloy-serde", "alloy-signer", @@ -258,22 +260,41 @@ dependencies = [ "async-trait", "auto_impl", "futures-utils-wasm", + "serde", + "serde_json", "thiserror", ] [[package]] name = "alloy-network-primitives" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514f70ee2a953db21631cd817b13a1571474ec77ddc03d47616d5e8203489fde" +checksum = "df9f3e281005943944d15ee8491534a1c7b3cbf7a7de26f8c433b842b93eb5f9" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-serde", "serde", ] +[[package]] +name = "alloy-node-bindings" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9805d126f24be459b958973c0569c73e1aadd27d4535eee82b2b6764aa03616" +dependencies = [ + "alloy-genesis", + "alloy-primitives 0.8.12", + "k256 0.13.4", + "rand", + "serde_json", + "tempfile", + "thiserror", + "tracing", + "url", +] + [[package]] name = "alloy-primitives" version = "0.7.7" @@ -298,9 +319,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71738eb20c42c5fb149571e76536a0f309d142f3957c28791662b96baf77a3d" +checksum = "9fce5dbd6a4f118eecc4719eaa9c7ffc31c315e6c5ccde3642db927802312425" dependencies = [ "alloy-rlp", "bytes", @@ -326,9 +347,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4814d141ede360bb6cd1b4b064f1aab9de391e7c4d0d4d50ac89ea4bc1e25fbd" +checksum = "40c1f9eede27bf4c13c099e8e64d54efd7ce80ef6ea47478aa75d5d74e2dba3b" dependencies = [ "alloy-chains", "alloy-consensus", @@ -336,10 +357,14 @@ dependencies = [ "alloy-json-rpc", "alloy-network", "alloy-network-primitives", - "alloy-primitives 0.8.9", + "alloy-node-bindings", + "alloy-primitives 0.8.12", "alloy-pubsub", "alloy-rpc-client", + "alloy-rpc-types-anvil", "alloy-rpc-types-eth", + "alloy-signer", + "alloy-signer-local", "alloy-transport", "alloy-transport-http", "alloy-transport-ipc", @@ -366,12 +391,12 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba46eb69ddf7a9925b81f15229cb74658e6eebe5dd30a5b74e2cd040380573" +checksum = "90f1f34232f77341076541c405482e4ae12f0ee7153d8f9969fc1691201b2247" dependencies = [ "alloy-json-rpc", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-transport", "bimap", "futures", @@ -385,9 +410,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26154390b1d205a4a7ac7352aa2eb4f81f391399d4e2f546fb81a2f8bb383f62" +checksum = "da0822426598f95e45dd1ea32a738dac057529a709ee645fcc516ffa4cbde08f" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -407,12 +432,12 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc2bd1e7403463a5f2c61e955bcc9d3072b63aa177442b0f9aa6a6d22a941e3" +checksum = "374dbe0dc3abdc2c964f36b3d3edf9cdb3db29d16bda34aa123f03d810bec1dd" dependencies = [ "alloy-json-rpc", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-pubsub", "alloy-transport", "alloy-transport-http", @@ -433,26 +458,39 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea9bf1abdd506f985a53533f5ac01296bcd6102c5e139bbc5d40bc468d2c916" +checksum = "c74832aa474b670309c20fffc2a869fa141edab7c79ff7963fad0a08de60bae1" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", + "alloy-rpc-types-anvil", "alloy-rpc-types-engine", "alloy-rpc-types-eth", "alloy-serde", "serde", ] +[[package]] +name = "alloy-rpc-types-anvil" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca97963132f78ddfc60e43a017348e6d52eea983925c23652f5b330e8e02291" +dependencies = [ + "alloy-primitives 0.8.12", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + [[package]] name = "alloy-rpc-types-engine" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "886d22d41992287a235af2f3af4299b5ced2bcafb81eb835572ad35747476946" +checksum = "3f56294dce86af23ad6ee8df46cf8b0d292eb5d1ff67dc88a0886051e32b1faf" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "alloy-serde", "derive_more 1.0.0", @@ -462,14 +500,14 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b034779a4850b4b03f5be5ea674a1cf7d746b2da762b34d1860ab45e48ca27" +checksum = "a8a477281940d82d29315846c7216db45b15e90bcd52309da9f54bcf7ad94a11" dependencies = [ "alloy-consensus", "alloy-eips", "alloy-network-primitives", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-rlp", "alloy-serde", "alloy-sol-types", @@ -481,22 +519,22 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028e72eaa9703e4882344983cfe7636ce06d8cce104a78ea62fd19b46659efc4" +checksum = "4dfa4a7ccf15b2492bb68088692481fd6b2604ccbee1d0d6c44c21427ae4df83" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "serde", "serde_json", ] [[package]] name = "alloy-signer" -version = "0.5.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eca011160d18a7dc6d8cdc1e8dc13e2e86c908f8e41b02aa76e429d6fe7085" +checksum = "2e10aec39d60dc27edcac447302c7803d2371946fb737245320a05b78eb2fafd" dependencies = [ - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "async-trait", "auto_impl", "elliptic-curve 0.13.8", @@ -506,13 +544,13 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6614f02fc1d5b079b2a4a5320018317b506fd0a6d67c1fd5542a71201724986c" +checksum = "d8396f6dff60700bc1d215ee03d86ff56de268af96e2bf833a14d0bafcab9882" dependencies = [ "alloy-consensus", "alloy-network", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-signer", "async-trait", "k256 0.13.4", @@ -522,9 +560,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0900b83f4ee1f45c640ceee596afbc118051921b9438fdb5a3175c1a7e05f8b" +checksum = "9343289b4a7461ed8bab8618504c995c049c082b70c7332efd7b32125633dc05" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -536,9 +574,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41b1e78dde06b5e12e6702fa8c1d30621bf07728ba75b801fb801c9c6a0ba10" +checksum = "4222d70bec485ceccc5d8fd4f2909edd65b5d5e43d4aca0b5dcee65d519ae98f" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", @@ -555,9 +593,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-input" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91dc311a561a306664393407b88d3e53ae58581624128afd8a15faa5de3627dc" +checksum = "2e17f2677369571b976e51ea1430eb41c3690d344fef567b840bfc0b01b6f83a" dependencies = [ "alloy-json-abi", "const-hex", @@ -572,9 +610,9 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d1fbee9e698f3ba176b6e7a145f4aefe6d2b746b611e8bb246fe11a0e9f6c4" +checksum = "aa64d80ae58ffaafdff9d5d84f58d03775f66c84433916dc9a64ed16af5755da" dependencies = [ "serde", "winnow", @@ -582,12 +620,12 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "086f41bc6ebcd8cb15f38ba20e47be38dd03692149681ce8061c35d960dbf850" +checksum = "6520d427d4a8eb7aa803d852d7a52ceb0c519e784c292f64bb339e636918cf27" dependencies = [ "alloy-json-abi", - "alloy-primitives 0.8.9", + "alloy-primitives 0.8.12", "alloy-sol-macro", "const-hex", "serde", @@ -595,9 +633,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be77579633ebbc1266ae6fd7694f75c408beb1aeb6865d0b18f22893c265a061" +checksum = "f99acddb34000d104961897dbb0240298e8b775a7efffb9fda2a1a3efedd65b3" dependencies = [ "alloy-json-rpc", "base64 0.22.1", @@ -615,9 +653,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fd1a5d0827939847983b46f2f79510361f901dc82f8e3c38ac7397af142c6e" +checksum = "5dc013132e34eeadaa0add7e74164c1503988bfba8bae885b32e0918ba85a8a6" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -630,9 +668,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8073d1186bfeeb8fbdd1292b6f1a0731f3aed8e21e1463905abfae0b96a887a6" +checksum = "063edc0660e81260653cc6a95777c29d54c2543a668aa5da2359fb450d25a1ba" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -649,9 +687,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "0.5.4" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f27837bb4a1d6c83a28231c94493e814882f0e9058648a97e908a5f3fc9fcf" +checksum = "abd170e600801116d5efe64f74a4fc073dbbb35c807013a7d0a388742aeebba0" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -1166,6 +1204,7 @@ name = "bolt" version = "0.3.0-alpha" dependencies = [ "alloy", + "alloy-node-bindings", "blst", "clap", "dotenvy", @@ -4037,9 +4076,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.8.9" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5e0c2ea8db64b2898b62ea2fbd60204ca95e0b2c6bdf53ff768bbe916fbe4d" +checksum = "f76fe0a3e1476bdaa0775b9aec5b869ed9520c2b2fedfe9c6df3618f8ea6290b" dependencies = [ "paste", "proc-macro2", @@ -4699,9 +4738,9 @@ checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasmtimer" -version = "0.2.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7ed9d8b15c7fb594d72bfb4b5a276f3d2029333cd93a932f376f5937f6f80ee" +checksum = "0048ad49a55b9deb3953841fa1fc5858f0efbcb7a18868c899a360269fac1b23" dependencies = [ "futures", "js-sys", diff --git a/bolt-cli/Cargo.toml b/bolt-cli/Cargo.toml index 1d9addcde..e3ed4bbad 100644 --- a/bolt-cli/Cargo.toml +++ b/bolt-cli/Cargo.toml @@ -23,7 +23,11 @@ blst = "0.3.12" # ethereum ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "cf3c404" } lighthouse_eth2_keystore = { package = "eth2_keystore", git = "https://github.com/sigp/lighthouse", rev = "a87f19d" } -alloy = { version = "0.5.2", features = ["full"] } +alloy = { version = "0.6.4", features = [ + "full", + "provider-anvil-api", + "provider-anvil-node", +] } # utils dotenvy = "0.15.7" @@ -37,6 +41,7 @@ rand = "0.8.5" [dev-dependencies] tempfile = "3.13.0" +alloy-node-bindings = "0.6.3" [build-dependencies] tonic-build = "0.12.3" diff --git a/bolt-cli/src/commands/register.rs b/bolt-cli/src/commands/register.rs index 91bbf50e6..f5be29645 100644 --- a/bolt-cli/src/commands/register.rs +++ b/bolt-cli/src/commands/register.rs @@ -45,9 +45,9 @@ impl RegisterCommand { self.authorized_operator, ); - let result = call.send().await?; + let result = call.send().await?.watch().await?; - println!("Call result: {:?}", result); + println!("transaction hash: {:?}", result); Ok(()) } @@ -90,3 +90,38 @@ sol! { error InvalidAuthorizedOperator(); } } + +#[cfg(test)] +mod tests { + use alloy::{ + primitives::{Address, U256}, + providers::{ext::AnvilApi, Provider, ProviderBuilder}, + signers::k256::ecdsa::SigningKey, + }; + + use crate::cli::RegisterCommand; + + #[tokio::test] + async fn test_register_validators() { + let rpc_url = "https://holesky.drpc.org"; + let provider = ProviderBuilder::new().on_anvil_with_config(|anvil| anvil.fork(rpc_url)); + let anvil_url = provider.client().transport().url(); + + let mut rnd = rand::thread_rng(); + let secret_key = SigningKey::random(&mut rnd); + let account = Address::from_private_key(&secret_key); + + provider.anvil_set_balance(account, U256::from(u64::MAX)).await.expect("set balance"); + + let command = RegisterCommand { + chain: crate::cli::Chain::Holesky, + max_committed_gas_limit: 30_000_000, + admin_private_key: format!("{:x}", secret_key.to_bytes()), + authorized_operator: account, + pubkeys_path: "./test_data/pubkeys.json".parse().unwrap(), + rpc_url: anvil_url.parse().unwrap(), + }; + + command.run().await.expect("run command"); + } +} diff --git a/bolt-cli/test_data/pubkeys.json b/bolt-cli/test_data/pubkeys.json new file mode 100644 index 000000000..091cb69e8 --- /dev/null +++ b/bolt-cli/test_data/pubkeys.json @@ -0,0 +1,3 @@ +[ + "0x0f6db8a87c31897f504bbd22e7a83a3613607499ae94c0fce4a6e0cab13692a5f69114cff399d96a38f78a77b045e9c7" +] From 294df455dd386bc50692e72fcd875f3831099854 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 10:00:37 +0100 Subject: [PATCH 04/35] refactor(bolt-cli): validators command --- bolt-cli/src/cli.rs | 48 ++++++---- bolt-cli/src/commands/mod.rs | 5 +- .../commands/{register.rs => validators.rs} | 95 +++++++++++-------- 3 files changed, 89 insertions(+), 59 deletions(-) rename bolt-cli/src/commands/{register.rs => validators.rs} (53%) diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index 39a6572f9..aa611a448 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -30,7 +30,7 @@ pub enum Cmd { Send(Box), /// Register validators in the Bolt network. - Register(RegisterCommand), + Validators(ValidatorsCommand), } impl Cmd { @@ -40,7 +40,7 @@ impl Cmd { Cmd::Delegate(cmd) => cmd.run().await, Cmd::Pubkeys(cmd) => cmd.run().await, Cmd::Send(cmd) => cmd.run().await, - Cmd::Register(cmd) => cmd.run().await, + Cmd::Validators(cmd) => cmd.run().await, } } } @@ -134,27 +134,31 @@ pub struct SendCommand { pub devnet_sidecar_url: Option, } -/// Command for registering validators in the Bolt network. #[derive(Debug, Clone, Parser)] -pub struct RegisterCommand { - /// The chain for which the delegation message is intended. - #[clap(long, env = "CHAIN", default_value = "holesky")] - pub chain: Chain, +pub struct ValidatorsCommand { + #[clap(subcommand)] + pub subcommand: ValidatorsSubcommand, +} - #[clap(long, env = "MAX_COMMITTED_GAS_LIMIT", default_value_t = 10_000_000)] - pub max_committed_gas_limit: u32, +#[derive(Debug, Clone, Parser)] +pub enum ValidatorsSubcommand { + /// The source of the private key. + Register { + #[clap(long, env = "RPC_URL")] + rpc_url: Url, - #[clap(long, env = "AUTHORIZED_OPERATOR")] - pub authorized_operator: Address, + #[clap(long, env = "MAX_COMMITTED_GAS_LIMIT")] + max_committed_gas_limit: u32, - #[clap(long, env = "PUBKEYS_PATH", default_value = "pubkeys.json")] - pub pubkeys_path: PathBuf, + #[clap(long, env = "AUTHORIZED_OPERATOR")] + authorized_operator: Address, - #[clap(long, env = "ADMIN_PRIVATE_KEY")] - pub admin_private_key: String, + #[clap(long, env = "PUBKEYS_PATH", default_value = "pubkeys.json")] + pubkeys_path: PathBuf, - #[clap(long, env = "RPC_URL")] - pub rpc_url: Url, + #[clap(long, env = "ADMIN_PRIVATE_KEY")] + admin_private_key: String, + }, } /// The action to perform. @@ -274,6 +278,16 @@ impl Chain { Chain::Kurtosis => [16, 0, 0, 56], } } + + pub fn from_id(id: u64) -> Option { + match id { + 1 => Some(Self::Mainnet), + 17000 => Some(Self::Holesky), + 3151908 => Some(Self::Kurtosis), + 7014190335 => Some(Self::Helder), + _ => None, + } + } } /// Styles for the CLI application. diff --git a/bolt-cli/src/commands/mod.rs b/bolt-cli/src/commands/mod.rs index 39439b5ff..f100347a8 100644 --- a/bolt-cli/src/commands/mod.rs +++ b/bolt-cli/src/commands/mod.rs @@ -10,6 +10,5 @@ pub mod pubkeys; /// broadcast preconfirmations in Bolt. pub mod send; -/// Module for the bolt `register` command to register -/// validators in the Bolt network. -pub mod register; +/// Module for the validators-related commands to interact with the bolt network. +pub mod validators; diff --git a/bolt-cli/src/commands/register.rs b/bolt-cli/src/commands/validators.rs similarity index 53% rename from bolt-cli/src/commands/register.rs rename to bolt-cli/src/commands/validators.rs index f5be29645..a82247154 100644 --- a/bolt-cli/src/commands/register.rs +++ b/bolt-cli/src/commands/validators.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use alloy::{ network::EthereumWallet, primitives::{address, Address}, - providers::ProviderBuilder, + providers::{Provider, ProviderBuilder}, signers::local::PrivateKeySigner, sol, }; @@ -11,7 +11,7 @@ use ethereum_consensus::crypto::PublicKey as BlsPublicKey; use eyre::Context; use crate::{ - cli::{Chain, RegisterCommand}, + cli::{Chain, ValidatorsCommand, ValidatorsSubcommand}, common::hash::compress_bls_pubkey, }; @@ -20,36 +20,52 @@ enum BoltContract { Validators, } -impl RegisterCommand { - /// Run the `delegate` command. +impl ValidatorsCommand { pub async fn run(self) -> eyre::Result<()> { - let bolt_validators_address = bolt_validators_address(self.chain); - - let pubkeys_file = std::fs::File::open(&self.pubkeys_path)?; - let keys: Vec = serde_json::from_reader(pubkeys_file)?; - let pubkey_hashes: Vec<_> = keys.iter().map(compress_bls_pubkey).collect(); - - let wallet: PrivateKeySigner = - self.admin_private_key.parse().wrap_err("invalid private key")?; - let transaction_signer = EthereumWallet::from(wallet); - let provider = ProviderBuilder::new() - .with_recommended_fillers() - .wallet(transaction_signer) - .on_http(self.rpc_url.clone()); - - let bolt_validators = BoltValidatorsContract::new(bolt_validators_address, provider); - - let call = bolt_validators.batchRegisterValidatorsUnsafe( - pubkey_hashes, - self.max_committed_gas_limit, - self.authorized_operator, - ); - - let result = call.send().await?.watch().await?; - - println!("transaction hash: {:?}", result); - - Ok(()) + match self.subcommand { + ValidatorsSubcommand::Register { + max_committed_gas_limit, + pubkeys_path, + admin_private_key, + authorized_operator, + rpc_url, + } => { + let wallet: PrivateKeySigner = + admin_private_key.parse().wrap_err("invalid private key")?; + let transaction_signer = EthereumWallet::from(wallet); + + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(transaction_signer) + .on_http(rpc_url.clone()); + + let chain_id = provider.get_chain_id().await?; + let chain = Chain::from_id(chain_id) + .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + + let bolt_validators_address = bolt_validators_address(chain); + + let pubkeys_file = std::fs::File::open(&pubkeys_path)?; + let keys: Vec = serde_json::from_reader(pubkeys_file)?; + let pubkey_hashes: Vec<_> = keys.iter().map(compress_bls_pubkey).collect(); + + let bolt_validators = + BoltValidatorsContract::new(bolt_validators_address, provider); + + let call = bolt_validators.batchRegisterValidatorsUnsafe( + pubkey_hashes, + max_committed_gas_limit, + authorized_operator, + ); + + let result = call.send().await?; + println!("Transaction submitted successfully, waiting for inclusion"); + let result = result.watch().await?; + println!("Transaction included. Transaction hash: {:?}", result); + + Ok(()) + } + } } } @@ -99,7 +115,7 @@ mod tests { signers::k256::ecdsa::SigningKey, }; - use crate::cli::RegisterCommand; + use crate::cli::{ValidatorsCommand, ValidatorsSubcommand}; #[tokio::test] async fn test_register_validators() { @@ -113,13 +129,14 @@ mod tests { provider.anvil_set_balance(account, U256::from(u64::MAX)).await.expect("set balance"); - let command = RegisterCommand { - chain: crate::cli::Chain::Holesky, - max_committed_gas_limit: 30_000_000, - admin_private_key: format!("{:x}", secret_key.to_bytes()), - authorized_operator: account, - pubkeys_path: "./test_data/pubkeys.json".parse().unwrap(), - rpc_url: anvil_url.parse().unwrap(), + let command = ValidatorsCommand { + subcommand: ValidatorsSubcommand::Register { + max_committed_gas_limit: 30_000_000, + admin_private_key: format!("{:x}", secret_key.to_bytes()), + authorized_operator: account, + pubkeys_path: "./test_data/pubkeys.json".parse().unwrap(), + rpc_url: anvil_url.parse().unwrap(), + }, }; command.run().await.expect("run command"); From fa1ece1d828ea605cd625b1268be0359eee4cf18 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 10:02:28 +0100 Subject: [PATCH 05/35] ci(bolt-cli): add Foundry, needed for Anvil --- .github/workflows/bolt_cli_ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/bolt_cli_ci.yml b/.github/workflows/bolt_cli_ci.yml index 6ea7c18c7..e0dc693ab 100644 --- a/.github/workflows/bolt_cli_ci.yml +++ b/.github/workflows/bolt_cli_ci.yml @@ -42,5 +42,8 @@ jobs: with: crate: cargo-nextest + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - name: Run bolt-cli tests run: cd bolt-cli && cargo nextest run --workspace --retries 3 From 42496fb4ff7b2e6452e1858e5151d48f0b1cedcc Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 10:57:39 +0100 Subject: [PATCH 06/35] feat(bolt-cli): EigenLayer and Symbiotic commands stub --- bolt-cli/src/cli.rs | 82 +++++++++++++++++++++++++++--- bolt-cli/src/commands/mod.rs | 3 ++ bolt-cli/src/commands/operators.rs | 7 +++ 3 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 bolt-cli/src/commands/operators.rs diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index aa611a448..ae0c7a1b6 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use alloy::primitives::Address; +use alloy::primitives::{Address, B256, U256}; use clap::{ builder::styling::{AnsiColor, Color, Style}, Parser, Subcommand, ValueEnum, @@ -9,7 +9,7 @@ use reqwest::Url; use crate::common::keystore::DEFAULT_KEYSTORE_PASSWORD; -/// `bolt` is a CLI tool to interact with Bolt Protocol ✨ +/// `bolt` is a CLI tool to interact with bolt Protocol ✨ #[derive(Parser, Debug, Clone)] #[command(author, version, styles = cli_styles(), about, arg_required_else_help(true))] pub struct Opts { @@ -26,11 +26,14 @@ pub enum Cmd { /// Output a list of pubkeys in JSON format. Pubkeys(PubkeysCommand), - /// Send a preconfirmation request to a Bolt proposer. + /// Send a preconfirmation request to a bolt proposer. Send(Box), - /// Register validators in the Bolt network. + /// Handle validators in the bolt network. Validators(ValidatorsCommand), + + /// Handle operators in the bolt network. + Operators(OperatorsCommand), } impl Cmd { @@ -41,6 +44,7 @@ impl Cmd { Cmd::Pubkeys(cmd) => cmd.run().await, Cmd::Send(cmd) => cmd.run().await, Cmd::Validators(cmd) => cmd.run().await, + Cmd::Operators(cmd) => cmd.run().await, } } } @@ -82,10 +86,10 @@ pub struct PubkeysCommand { pub source: KeySource, } -/// Command for sending a preconfirmation request to a Bolt proposer. +/// Command for sending a preconfirmation request to a bolt proposer. #[derive(Debug, Clone, Parser)] pub struct SendCommand { - /// Bolt RPC URL to send requests to and fetch lookahead info from. + /// bolt RPC URL to send requests to and fetch lookahead info from. #[clap(long, env = "BOLT_RPC_URL", default_value = "https://rpc-holesky.bolt.chainbound.io")] pub bolt_rpc_url: Url, @@ -93,7 +97,7 @@ pub struct SendCommand { #[clap(long, env = "PRIVATE_KEY", hide_env_values = true)] pub private_key: String, - /// The Bolt Sidecar URL to send requests to. If provided, this will override + /// The bolt Sidecar URL to send requests to. If provided, this will override /// the canonical bolt RPC URL and disregard any registration information. /// /// This is useful for testing and development purposes. @@ -161,6 +165,70 @@ pub enum ValidatorsSubcommand { }, } +#[derive(Debug, Clone, Parser)] +pub struct OperatorsCommand { + #[clap(subcommand)] + pub subcommand: OperatorsSubcommand, +} + +#[derive(Debug, Clone, Parser)] +pub enum OperatorsSubcommand { + /// Commands to interact with EigenLayer and bolt. + #[clap(name = "eigenlayer")] // and not eigen-layer + EigenLayer { + #[clap(subcommand)] + subcommand: EigenLayerSubcommand, + }, + /// Commands to interact with Symbiotic and bolt. + Symbiotic { + #[clap(subcommand)] + subcommand: SymbioticSubcommand, + }, +} + +#[derive(Debug, Clone, Parser)] +pub enum EigenLayerSubcommand { + /// Step 1: Deposit into a strategy. + DepositIntoStrategy { + /// The address of the strategy to deposit into. + #[clap(long, env = "EIGENLAYER_STRATEGY_ADDRESS")] + strategy: Address, + /// The amount to deposit into the strategy. + #[clap(long, env = "EIGENLAYER_STRATEGY_DEPOSIT_AMOUNT")] + amount: U256, + }, + + /// Step 2: Register into the bolt AVS. + RegisterIntoBoltAVS { + /// The URL of the operator RPC. + #[clap(long, env = "OPERATOR_RPC")] + operator_rpc: Url, + /// The salt for the operator signature. + #[clap(long, env = "OPERATOR_SIGNATURE_SALT")] + salt: B256, + /// The expiry timestamp for the operator signature. + #[clap(long, env = "OPERATOR_SIGNATURE_EXPIRY")] + expiry: u64, + }, + + /// Step 3: Check your operation registration in bolt + CheckOperatorRegistration { + /// The address of the operator to check. + #[clap(long, env = "OPERATOR_ADDRESS")] + address: Address, + }, +} + +#[derive(Debug, Clone, Parser)] +pub enum SymbioticSubcommand { + /// Register into the bolt manager contract as an opeartor. + RegisterIntoBolt { + /// The URL of the operator RPC. + #[clap(long, env = "OPERATOR_RPC")] + operator_rpc: Url, + }, +} + /// The action to perform. #[derive(Debug, Clone, ValueEnum)] #[clap(rename_all = "kebab_case")] diff --git a/bolt-cli/src/commands/mod.rs b/bolt-cli/src/commands/mod.rs index f100347a8..2c4669d8c 100644 --- a/bolt-cli/src/commands/mod.rs +++ b/bolt-cli/src/commands/mod.rs @@ -12,3 +12,6 @@ pub mod send; /// Module for the validators-related commands to interact with the bolt network. pub mod validators; + +/// Module for the operators-related commands to interact with the bolt network. +pub mod operators; diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs new file mode 100644 index 000000000..a0b99f520 --- /dev/null +++ b/bolt-cli/src/commands/operators.rs @@ -0,0 +1,7 @@ +use crate::cli::OperatorsCommand; + +impl OperatorsCommand { + pub async fn run(self) -> eyre::Result<()> { + Ok(()) + } +} From c66aa916039cb9cea4ee0c4c2107de323e485dce Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 11:54:04 +0100 Subject: [PATCH 07/35] feat(bolt-cli): contracts bindings module --- bolt-cli/src/contracts/eigenlayer.rs | 146 +++++++++++++++++++++++++++ bolt-cli/src/contracts/erc20.rs | 88 ++++++++++++++++ bolt-cli/src/contracts/mod.rs | 32 ++++++ bolt-cli/src/main.rs | 3 + 4 files changed, 269 insertions(+) create mode 100644 bolt-cli/src/contracts/eigenlayer.rs create mode 100644 bolt-cli/src/contracts/erc20.rs create mode 100644 bolt-cli/src/contracts/mod.rs diff --git a/bolt-cli/src/contracts/eigenlayer.rs b/bolt-cli/src/contracts/eigenlayer.rs new file mode 100644 index 000000000..61c0632b5 --- /dev/null +++ b/bolt-cli/src/contracts/eigenlayer.rs @@ -0,0 +1,146 @@ +use alloy::{providers::RootProvider, sol, transports::http::Http}; + +use reqwest::Client; +use IStrategy::IStrategyInstance; +use IStrategyManager::IStrategyManagerInstance; + +pub struct IStrategyContract(IStrategyInstance, RootProvider>>); +pub struct IStrategyManagerContract( + IStrategyManagerInstance, RootProvider>>, +); + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + + // Reference source code: https://github.com/Layr-Labs/eigenlayer-contracts/blob/testnet-holesky/src/contracts/interfaces/IStrategy.sol + // + // NOTE: IERC20 tokens are replaced with `address` because there's no support for it: https://docs.rs/alloy-sol-macro/latest/alloy_sol_macro/macro.sol.html#solidity + + /** + * @title Minimal interface for an `Strategy` contract. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice Custom `Strategy` implementations may expand extensively on this interface. + */ + interface IStrategy { + /** + * @notice Used to deposit tokens into this Strategy + * @param token is the ERC20 token being deposited + * @param amount is the amount of token being deposited + * @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's + * `depositIntoStrategy` function, and individual share balances are recorded in the strategyManager as well. + * @return newShares is the number of new shares issued at the current exchange ratio. + */ + function deposit(address token, uint256 amount) external returns (uint256); + + /** + * @notice Used to withdraw tokens from this Strategy, to the `recipient`'s address + * @param recipient is the address to receive the withdrawn funds + * @param token is the ERC20 token being transferred out + * @param amountShares is the amount of shares being withdrawn + * @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's + * other functions, and individual share balances are recorded in the strategyManager as well. + */ + function withdraw(address recipient, address token, uint256 amountShares) external; + + /** + * @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy. + * @notice In contrast to `sharesToUnderlyingView`, this function **may** make state modifications + * @param amountShares is the amount of shares to calculate its conversion into the underlying token + * @return The amount of underlying tokens corresponding to the input `amountShares` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function sharesToUnderlying(uint256 amountShares) external returns (uint256); + + /** + * @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy. + * @notice In contrast to `underlyingToSharesView`, this function **may** make state modifications + * @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares + * @return The amount of underlying tokens corresponding to the input `amountShares` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function underlyingToShares(uint256 amountUnderlying) external returns (uint256); + + /** + * @notice convenience function for fetching the current underlying value of all of the `user`'s shares in + * this strategy. In contrast to `userUnderlyingView`, this function **may** make state modifications + */ + function userUnderlying(address user) external returns (uint256); + + /** + * @notice convenience function for fetching the current total shares of `user` in this strategy, by + * querying the `strategyManager` contract + */ + function shares(address user) external view returns (uint256); + + /** + * @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy. + * @notice In contrast to `sharesToUnderlying`, this function guarantees no state modifications + * @param amountShares is the amount of shares to calculate its conversion into the underlying token + * @return The amount of shares corresponding to the input `amountUnderlying` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function sharesToUnderlyingView(uint256 amountShares) external view returns (uint256); + + /** + * @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy. + * @notice In contrast to `underlyingToShares`, this function guarantees no state modifications + * @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares + * @return The amount of shares corresponding to the input `amountUnderlying` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function underlyingToSharesView(uint256 amountUnderlying) external view returns (uint256); + + /** + * @notice convenience function for fetching the current underlying value of all of the `user`'s shares in + * this strategy. In contrast to `userUnderlying`, this function guarantees no state modifications + */ + function userUnderlyingView(address user) external view returns (uint256); + + /// @notice The underlying token for shares in this Strategy + function underlyingToken() external view returns (address); + + /// @notice The total number of extant shares in this Strategy + function totalShares() external view returns (uint256); + + /// @notice Returns either a brief string explaining the strategy's goal & purpose, or a link to metadata that explains in more detail. + function explanation() external view returns (string memory); + } +} + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + + /** + * @title Interface for the primary entrypoint for funds into EigenLayer. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice See the `StrategyManager` contract itself for implementation details. + */ + interface IStrategyManager { + /** + * @notice Emitted when a new deposit occurs on behalf of `staker`. + * @param staker Is the staker who is depositing funds into EigenLayer. + * @param strategy Is the strategy that `staker` has deposited into. + * @param token Is the token that `staker` deposited. + * @param shares Is the number of new shares `staker` has been granted in `strategy`. + */ + event Deposit(address staker, address token, address strategy, uint256 shares); + + /** + * @notice Deposits `amount` of `token` into the specified `strategy`, with the resultant shares credited to `msg.sender` + * @param strategy is the specified strategy where deposit is to be made, + * @param token is the denomination in which the deposit is to be made, + * @param amount is the amount of token to be deposited in the strategy by the staker + * @return shares The amount of new shares in the `strategy` created as part of the action. + * @dev The `msg.sender` must have previously approved this contract to transfer at least `amount` of `token` on their behalf. + * @dev Cannot be called by an address that is 'frozen' (this function will revert if the `msg.sender` is frozen). + * + * WARNING: Depositing tokens that allow reentrancy (eg. ERC-777) into a strategy is not recommended. This can lead to attack vectors + * where the token balance and corresponding strategy shares are not in sync upon reentrancy. + */ + function depositIntoStrategy(address strategy, address token, uint256 amount) external returns (uint256 shares); + } +} diff --git a/bolt-cli/src/contracts/erc20.rs b/bolt-cli/src/contracts/erc20.rs new file mode 100644 index 000000000..f9c051309 --- /dev/null +++ b/bolt-cli/src/contracts/erc20.rs @@ -0,0 +1,88 @@ +use alloy::{providers::RootProvider, sol, transports::http::Http}; + +use reqwest::Client; +use IERC20::IERC20Instance; + +pub struct IERC20Contract(IERC20Instance, RootProvider>>); + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + + // Reference source code: https://github.com/openzeppelin/openzeppelin-contracts/blob/HEAD/contracts/token/ERC20/IERC20.sol + + /** + * @dev Interface of the ERC-20 standard as defined in the ERC. + */ + interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); + } +} diff --git a/bolt-cli/src/contracts/mod.rs b/bolt-cli/src/contracts/mod.rs new file mode 100644 index 000000000..d5ebef4ca --- /dev/null +++ b/bolt-cli/src/contracts/mod.rs @@ -0,0 +1,32 @@ +use std::collections::HashMap; + +use alloy::primitives::{address, Address}; + +use crate::cli::Chain; + +pub mod eigenlayer; +pub mod erc20; + +#[derive(Debug, PartialEq, Eq, Hash)] +pub enum BoltContract { + Validators, +} + +// PERF: this should be done at compile time +pub fn deployments() -> HashMap> { + let mut deployments = HashMap::new(); + let mut holesky_deployments = HashMap::new(); + holesky_deployments + .insert(BoltContract::Validators, address!("47D2DC1DE1eFEFA5e6944402f2eda3981D36a9c8")); + deployments.insert(Chain::Holesky, holesky_deployments); + + deployments +} + +pub fn bolt_validators_address(chain: Chain) -> Address { + *deployments() + .get(&chain) + .unwrap_or_else(|| panic!("{:?} chain supported", chain)) + .get(&BoltContract::Validators) + .expect("Validators contract address not found") +} diff --git a/bolt-cli/src/main.rs b/bolt-cli/src/main.rs index dc59ec8ad..d74fe8016 100644 --- a/bolt-cli/src/main.rs +++ b/bolt-cli/src/main.rs @@ -12,6 +12,9 @@ mod common; /// Protocol Buffers definitions generated by `prost`. mod pb; +/// Contracts and interfaces bindings for interacting with the Bolt network. +mod contracts; + #[tokio::main] async fn main() -> eyre::Result<()> { let _ = dotenvy::dotenv(); From 95530abd66dff4720bcd7a2bd58b6fc13cec37c6 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 11:54:40 +0100 Subject: [PATCH 08/35] fix(bolt-cli): missing args after refactor --- bolt-cli/src/cli.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index ae0c7a1b6..ea83ffb39 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -190,6 +190,12 @@ pub enum OperatorsSubcommand { pub enum EigenLayerSubcommand { /// Step 1: Deposit into a strategy. DepositIntoStrategy { + /// The URL of the RPC to broadcast the transaction. + #[clap(long, env = "RPC_URL")] + rpc_url: Url, + /// The private key of the operator. + #[clap(long, env = "OPERATOR_PRIVATE_KEY")] + operator_private_key: String, /// The address of the strategy to deposit into. #[clap(long, env = "EIGENLAYER_STRATEGY_ADDRESS")] strategy: Address, @@ -200,6 +206,12 @@ pub enum EigenLayerSubcommand { /// Step 2: Register into the bolt AVS. RegisterIntoBoltAVS { + /// The URL of the RPC to broadcast the transaction. + #[clap(long, env = "RPC_URL")] + rpc_url: Url, + /// The private key of the operator. + #[clap(long, env = "OPERATOR_PRIVATE_KEY")] + operator_private_key: String, /// The URL of the operator RPC. #[clap(long, env = "OPERATOR_RPC")] operator_rpc: Url, @@ -213,6 +225,9 @@ pub enum EigenLayerSubcommand { /// Step 3: Check your operation registration in bolt CheckOperatorRegistration { + /// The URL of the RPC to broadcast the transaction. + #[clap(long, env = "RPC")] + rpc: Url, /// The address of the operator to check. #[clap(long, env = "OPERATOR_ADDRESS")] address: Address, From 74e9bb307a3cf9af874c64e16d98e2347c8e3c42 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 11:54:53 +0100 Subject: [PATCH 09/35] chore(bolt-cli): wallet from sk util --- bolt-cli/src/common/signing.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bolt-cli/src/common/signing.rs b/bolt-cli/src/common/signing.rs index 59c70a220..e7f4cbfd3 100644 --- a/bolt-cli/src/common/signing.rs +++ b/bolt-cli/src/common/signing.rs @@ -1,10 +1,10 @@ -use alloy::primitives::B256; +use alloy::{network::EthereumWallet, primitives::B256, signers::local::PrivateKeySigner}; use blst::{min_pk::Signature, BLST_ERROR}; use ethereum_consensus::{ crypto::PublicKey as BlsPublicKey, deneb::{compute_fork_data_root, compute_signing_root, Root}, }; -use eyre::{eyre, Result}; +use eyre::{eyre, Context, Result}; use crate::cli::Chain; @@ -65,3 +65,9 @@ pub fn verify_root( Err(eyre!("bls verification failed")) } } + +/// Returns an [EthereumWallet] from a private key hex string. +pub fn wallet_from_sk(sk: String) -> eyre::Result { + let wallet: PrivateKeySigner = sk.parse().wrap_err("invalid private key")?; + Ok(EthereumWallet::from(wallet)) +} From f40e26571ab7be242d38fddcee191b449be98800 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 12:58:16 +0100 Subject: [PATCH 10/35] feat(bolt-cli): operator eigenlayer deposit-into-strategy impl --- bolt-cli/src/cli.rs | 8 +- bolt-cli/src/commands/operators.rs | 83 +++++++++++++++++- bolt-cli/src/commands/validators.rs | 41 ++------- bolt-cli/src/contracts/eigenlayer.rs | 15 +--- bolt-cli/src/contracts/mod.rs | 123 ++++++++++++++++++++++++--- 5 files changed, 202 insertions(+), 68 deletions(-) diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index ea83ffb39..ce8fcc586 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -7,7 +7,7 @@ use clap::{ }; use reqwest::Url; -use crate::common::keystore::DEFAULT_KEYSTORE_PASSWORD; +use crate::{common::keystore::DEFAULT_KEYSTORE_PASSWORD, contracts::EigenLayerStrategy}; /// `bolt` is a CLI tool to interact with bolt Protocol ✨ #[derive(Parser, Debug, Clone)] @@ -196,9 +196,9 @@ pub enum EigenLayerSubcommand { /// The private key of the operator. #[clap(long, env = "OPERATOR_PRIVATE_KEY")] operator_private_key: String, - /// The address of the strategy to deposit into. - #[clap(long, env = "EIGENLAYER_STRATEGY_ADDRESS")] - strategy: Address, + /// The name of the strategy to deposit into. + #[clap(long, env = "EIGENLAYER_STRATEGY")] + strategy: EigenLayerStrategy, /// The amount to deposit into the strategy. #[clap(long, env = "EIGENLAYER_STRATEGY_DEPOSIT_AMOUNT")] amount: U256, diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index a0b99f520..96e7a5d72 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -1,7 +1,86 @@ -use crate::cli::OperatorsCommand; +use crate::{ + cli::{ + Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand, SymbioticSubcommand, + }, + common::signing::wallet_from_sk, + contracts::{ + deployments_for_chain, + eigenlayer::{IStrategy::IStrategyInstance, IStrategyManager::IStrategyManagerInstance}, + erc20::IERC20::IERC20Instance, + strategy_to_address, + }, +}; +use alloy::providers::{Provider, ProviderBuilder}; impl OperatorsCommand { pub async fn run(self) -> eyre::Result<()> { - Ok(()) + match self.subcommand { + OperatorsSubcommand::EigenLayer { subcommand } => match subcommand { + EigenLayerSubcommand::DepositIntoStrategy { + rpc_url, + strategy, + amount, + operator_private_key, + } => { + let wallet = wallet_from_sk(operator_private_key)?; + + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(wallet) + .on_http(rpc_url.clone()); + + let chain_id = provider.get_chain_id().await?; + let chain = Chain::from_id(chain_id) + .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + + let deployments = deployments_for_chain(chain); + + let strategy_address = + strategy_to_address(strategy, deployments.eigen_layer.supported_strategies); + let strategy = IStrategyInstance::new(strategy_address, provider.clone()); + let strategy_manager_address = deployments.eigen_layer.strategy_manager; + let strategy_manager = + IStrategyManagerInstance::new(strategy_manager_address, provider.clone()); + + let token = strategy.underlyingToken().call().await?.token; + + let token_erc20 = IERC20Instance::new(token, provider); + let success = + token_erc20.approve(strategy_manager_address, amount).call().await?._0; + if !success { + panic!("Failed to approve token transfer"); + } + + let shares = strategy_manager + .depositIntoStrategy(strategy_address, token, amount) + .call() + .await? + .shares; + + println!( + "Deposited {} tokens into strategy, received {} shares", + amount, shares + ); + } + EigenLayerSubcommand::RegisterIntoBoltAVS { + rpc_url, + operator_rpc, + salt, + expiry, + operator_private_key, + } => { + todo!() + } + EigenLayerSubcommand::CheckOperatorRegistration { rpc, address } => { + todo!() + } + }, + OperatorsSubcommand::Symbiotic { subcommand } => match subcommand { + SymbioticSubcommand::RegisterIntoBolt { operator_rpc } => { + todo!() + } + }, + } + todo!(); } } diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs index a82247154..b72dbdede 100644 --- a/bolt-cli/src/commands/validators.rs +++ b/bolt-cli/src/commands/validators.rs @@ -1,25 +1,15 @@ -use std::collections::HashMap; - use alloy::{ - network::EthereumWallet, - primitives::{address, Address}, providers::{Provider, ProviderBuilder}, - signers::local::PrivateKeySigner, sol, }; use ethereum_consensus::crypto::PublicKey as BlsPublicKey; -use eyre::Context; use crate::{ cli::{Chain, ValidatorsCommand, ValidatorsSubcommand}, - common::hash::compress_bls_pubkey, + common::{hash::compress_bls_pubkey, signing::wallet_from_sk}, + contracts::deployments_for_chain, }; -#[derive(Debug, PartialEq, Eq, Hash)] -enum BoltContract { - Validators, -} - impl ValidatorsCommand { pub async fn run(self) -> eyre::Result<()> { match self.subcommand { @@ -30,20 +20,18 @@ impl ValidatorsCommand { authorized_operator, rpc_url, } => { - let wallet: PrivateKeySigner = - admin_private_key.parse().wrap_err("invalid private key")?; - let transaction_signer = EthereumWallet::from(wallet); + let wallet = wallet_from_sk(admin_private_key)?; let provider = ProviderBuilder::new() .with_recommended_fillers() - .wallet(transaction_signer) + .wallet(wallet) .on_http(rpc_url.clone()); let chain_id = provider.get_chain_id().await?; let chain = Chain::from_id(chain_id) .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); - let bolt_validators_address = bolt_validators_address(chain); + let bolt_validators_address = deployments_for_chain(chain).bolt.validators; let pubkeys_file = std::fs::File::open(&pubkeys_path)?; let keys: Vec = serde_json::from_reader(pubkeys_file)?; @@ -69,25 +57,6 @@ impl ValidatorsCommand { } } -// PERF: this should be done at compile time -fn deployments() -> HashMap> { - let mut deployments = HashMap::new(); - let mut holesky_deployments = HashMap::new(); - holesky_deployments - .insert(BoltContract::Validators, address!("47D2DC1DE1eFEFA5e6944402f2eda3981D36a9c8")); - deployments.insert(Chain::Holesky, holesky_deployments); - - deployments -} - -fn bolt_validators_address(chain: Chain) -> Address { - *deployments() - .get(&chain) - .unwrap_or_else(|| panic!("{:?} chain supported", chain)) - .get(&BoltContract::Validators) - .expect("Validators contract address not found") -} - sol! { #[allow(missing_docs)] #[sol(rpc)] diff --git a/bolt-cli/src/contracts/eigenlayer.rs b/bolt-cli/src/contracts/eigenlayer.rs index 61c0632b5..3f0f7002e 100644 --- a/bolt-cli/src/contracts/eigenlayer.rs +++ b/bolt-cli/src/contracts/eigenlayer.rs @@ -1,13 +1,4 @@ -use alloy::{providers::RootProvider, sol, transports::http::Http}; - -use reqwest::Client; -use IStrategy::IStrategyInstance; -use IStrategyManager::IStrategyManagerInstance; - -pub struct IStrategyContract(IStrategyInstance, RootProvider>>); -pub struct IStrategyManagerContract( - IStrategyManagerInstance, RootProvider>>, -); +use alloy::sol; sol! { #[allow(missing_docs)] @@ -32,7 +23,7 @@ sol! { * `depositIntoStrategy` function, and individual share balances are recorded in the strategyManager as well. * @return newShares is the number of new shares issued at the current exchange ratio. */ - function deposit(address token, uint256 amount) external returns (uint256); + function deposit(address token, uint256 amount) external returns (uint256 shares); /** * @notice Used to withdraw tokens from this Strategy, to the `recipient`'s address @@ -99,7 +90,7 @@ sol! { function userUnderlyingView(address user) external view returns (uint256); /// @notice The underlying token for shares in this Strategy - function underlyingToken() external view returns (address); + function underlyingToken() external view returns (address token); /// @notice The total number of extant shares in this Strategy function totalShares() external view returns (uint256); diff --git a/bolt-cli/src/contracts/mod.rs b/bolt-cli/src/contracts/mod.rs index d5ebef4ca..0c21ad9d8 100644 --- a/bolt-cli/src/contracts/mod.rs +++ b/bolt-cli/src/contracts/mod.rs @@ -1,32 +1,127 @@ use std::collections::HashMap; use alloy::primitives::{address, Address}; +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; use crate::cli::Chain; pub mod eigenlayer; pub mod erc20; -#[derive(Debug, PartialEq, Eq, Hash)] -pub enum BoltContract { - Validators, +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Contracts { + pub bolt: Bolt, + pub symbiotic: Symbiotic, + pub eigen_layer: EigenLayer, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Bolt { + pub validators: Address, + pub parameters: Address, + pub manager: Address, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct Symbiotic { + pub network: Address, + pub operator_registry: Address, + pub network_opt_in_service: Address, + pub vault_factory: Address, + pub vault_configurator: Address, + pub network_registry: Address, + pub network_middleware_service: Address, + pub middleware: Address, +} + +// - [`stETH`](https://holesky.etherscan.io/address/0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034) +// - [`rETH`](https://holesky.etherscan.io/address/0x7322c24752f79c05FFD1E2a6FCB97020C1C264F1) +// - [`wETH`](https://holesky.etherscan.io/address/0x94373a4919B3240D86eA41593D5eBa789FEF3848) +// - [`cbETH`](https://holesky.etherscan.io/address/0x8720095Fa5739Ab051799211B146a2EEE4Dd8B37) +// - [`mETH`](https://holesky.etherscan.io/address/0xe3C063B1BEe9de02eb28352b55D49D85514C67FF) + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct EigenLayer { + pub avs_directory: Address, + pub delegation_manager: Address, + pub strategy_manager: Address, + pub middleware: Address, + pub supported_strategies: EigenLayerStrategies, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct EigenLayerStrategies { + st_eth: Address, + r_eth: Address, + w_eth: Address, + cb_eth: Address, + m_eth: Address, +} + +#[derive(Clone, Serialize, Deserialize, Debug, ValueEnum)] +#[allow(clippy::enum_variant_names)] +#[serde(rename_all = "kebab-case")] +pub enum EigenLayerStrategy { + StEth, + REth, + WEth, + CbEth, + MEth, +} + +pub fn strategy_to_address( + strategy: EigenLayerStrategy, + addresses: EigenLayerStrategies, +) -> Address { + match strategy { + EigenLayerStrategy::StEth => addresses.st_eth, + EigenLayerStrategy::REth => addresses.r_eth, + EigenLayerStrategy::WEth => addresses.w_eth, + EigenLayerStrategy::CbEth => addresses.cb_eth, + EigenLayerStrategy::MEth => addresses.m_eth, + } } // PERF: this should be done at compile time -pub fn deployments() -> HashMap> { +pub fn deployments() -> HashMap { let mut deployments = HashMap::new(); - let mut holesky_deployments = HashMap::new(); - holesky_deployments - .insert(BoltContract::Validators, address!("47D2DC1DE1eFEFA5e6944402f2eda3981D36a9c8")); - deployments.insert(Chain::Holesky, holesky_deployments); + deployments.insert(Chain::Holesky, HOLESKY_DEPLOYMENTS); deployments } -pub fn bolt_validators_address(chain: Chain) -> Address { - *deployments() - .get(&chain) - .unwrap_or_else(|| panic!("{:?} chain supported", chain)) - .get(&BoltContract::Validators) - .expect("Validators contract address not found") +pub fn deployments_for_chain(chain: Chain) -> Contracts { + deployments().get(&chain).cloned().expect("no deployments for chain") } + +const HOLESKY_DEPLOYMENTS: Contracts = Contracts { + bolt: Bolt { + validators: address!("47D2DC1DE1eFEFA5e6944402f2eda3981D36a9c8"), + parameters: address!("20d1cf3A5BD5928dB3118b2CfEF54FDF9fda5c12"), + manager: address!("440202829b493F9FF43E730EB5e8379EEa3678CF"), + }, + symbiotic: Symbiotic { + network: address!("b017002D8024d8c8870A5CECeFCc63887650D2a4"), + operator_registry: address!("6F75a4ffF97326A00e52662d82EA4FdE86a2C548"), + network_opt_in_service: address!("58973d16FFA900D11fC22e5e2B6840d9f7e13401"), + vault_factory: address!("407A039D94948484D356eFB765b3c74382A050B4"), + vault_configurator: address!("D2191FE92987171691d552C219b8caEf186eb9cA"), + network_registry: address!("7d03b7343BF8d5cEC7C0C27ecE084a20113D15C9"), + network_middleware_service: address!("62a1ddfD86b4c1636759d9286D3A0EC722D086e3"), + middleware: address!("04f40d9CaE475E5BaA462acE53E5c58A0DD8D8e8"), + }, + eigen_layer: EigenLayer { + avs_directory: address!("055733000064333CaDDbC92763c58BF0192fFeBf"), + delegation_manager: address!("A44151489861Fe9e3055d95adC98FbD462B948e7"), + strategy_manager: address!("dfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6"), + middleware: address!("a632a3e652110Bb2901D5cE390685E6a9838Ca04"), + supported_strategies: EigenLayerStrategies { + st_eth: address!("3F1c547b21f65e10480dE3ad8E19fAAC46C95034"), + r_eth: address!("7322c24752f79c05FFD1E2a6FCB97020C1C264F1"), + w_eth: address!("94373a4919B3240D86eA41593D5eBa789FEF3848"), + cb_eth: address!("8720095Fa5739Ab051799211B146a2EEE4Dd8B37"), + m_eth: address!("e3C063B1BEe9de02eb28352b55D49D85514C67FF"), + }, + }, +}; From 9edd5fb3340a96be36e9d70cd4ce6a8c72558d8b Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 13:05:20 +0100 Subject: [PATCH 11/35] chore(bolt-cli): docs --- bolt-cli/src/cli.rs | 5 +++++ bolt-cli/src/common/hash.rs | 3 --- bolt-cli/src/common/mod.rs | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index ce8fcc586..c0ca81bf2 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -148,18 +148,23 @@ pub struct ValidatorsCommand { pub enum ValidatorsSubcommand { /// The source of the private key. Register { + /// The URL of the RPC to broadcast the transaction. #[clap(long, env = "RPC_URL")] rpc_url: Url, + /// The max gas limit the validator is willing to reserve to commitments. #[clap(long, env = "MAX_COMMITTED_GAS_LIMIT")] max_committed_gas_limit: u32, + /// The authorized operator for the validator. #[clap(long, env = "AUTHORIZED_OPERATOR")] authorized_operator: Address, + /// The path to the JSON pubkeys file, containing an array of BLS public keys. #[clap(long, env = "PUBKEYS_PATH", default_value = "pubkeys.json")] pubkeys_path: PathBuf, + /// The private key to sign the transactions with. #[clap(long, env = "ADMIN_PRIVATE_KEY")] admin_private_key: String, }, diff --git a/bolt-cli/src/common/hash.rs b/bolt-cli/src/common/hash.rs index 28277f730..0816134c2 100644 --- a/bolt-cli/src/common/hash.rs +++ b/bolt-cli/src/common/hash.rs @@ -1,6 +1,3 @@ -// NOTE: Primitive types should be flattened into a single module -// for cleaner imports and better organization. - use alloy::primitives::{keccak256, FixedBytes, B512}; use ethereum_consensus::crypto::PublicKey as BlsPublicKey; diff --git a/bolt-cli/src/common/mod.rs b/bolt-cli/src/common/mod.rs index 81eab6f9e..f1726e56f 100644 --- a/bolt-cli/src/common/mod.rs +++ b/bolt-cli/src/common/mod.rs @@ -16,6 +16,7 @@ pub mod keystore; /// Utilities for signing and verifying messages. pub mod signing; +/// Utilities for hashing messages and custom types. pub mod hash; /// Parse a BLS public key from a string From 8673c89dbd26366fb4d81b40b2829555a5f0fae8 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 13:05:27 +0100 Subject: [PATCH 12/35] chore(bolt-cli): imports --- bolt-cli/src/contracts/erc20.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bolt-cli/src/contracts/erc20.rs b/bolt-cli/src/contracts/erc20.rs index f9c051309..469ce7e0a 100644 --- a/bolt-cli/src/contracts/erc20.rs +++ b/bolt-cli/src/contracts/erc20.rs @@ -1,9 +1,4 @@ -use alloy::{providers::RootProvider, sol, transports::http::Http}; - -use reqwest::Client; -use IERC20::IERC20Instance; - -pub struct IERC20Contract(IERC20Instance, RootProvider>>); +use alloy::sol; sol! { #[allow(missing_docs)] From 2c8e9326d314ff5073dd1fb7ea23632f85e16847 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 13:11:38 +0100 Subject: [PATCH 13/35] chore(bolt-cli): bolt contract bindings --- bolt-cli/src/commands/validators.rs | 29 +++-------------------------- bolt-cli/src/contracts/bolt.rs | 20 ++++++++++++++++++++ bolt-cli/src/contracts/mod.rs | 1 + 3 files changed, 24 insertions(+), 26 deletions(-) create mode 100644 bolt-cli/src/contracts/bolt.rs diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs index b72dbdede..7e2d9ccd9 100644 --- a/bolt-cli/src/commands/validators.rs +++ b/bolt-cli/src/commands/validators.rs @@ -1,13 +1,10 @@ -use alloy::{ - providers::{Provider, ProviderBuilder}, - sol, -}; +use alloy::providers::{Provider, ProviderBuilder}; use ethereum_consensus::crypto::PublicKey as BlsPublicKey; use crate::{ cli::{Chain, ValidatorsCommand, ValidatorsSubcommand}, common::{hash::compress_bls_pubkey, signing::wallet_from_sk}, - contracts::deployments_for_chain, + contracts::{bolt::BoltValidators, deployments_for_chain}, }; impl ValidatorsCommand { @@ -37,8 +34,7 @@ impl ValidatorsCommand { let keys: Vec = serde_json::from_reader(pubkeys_file)?; let pubkey_hashes: Vec<_> = keys.iter().map(compress_bls_pubkey).collect(); - let bolt_validators = - BoltValidatorsContract::new(bolt_validators_address, provider); + let bolt_validators = BoltValidators::new(bolt_validators_address, provider); let call = bolt_validators.batchRegisterValidatorsUnsafe( pubkey_hashes, @@ -57,25 +53,6 @@ impl ValidatorsCommand { } } -sol! { - #[allow(missing_docs)] - #[sol(rpc)] - interface BoltValidatorsContract { - /// @notice Register a batch of Validators and authorize a Collateral Provider and Operator for them - /// @dev This function allows anyone to register a list of Validators. - /// @param pubkeyHashes List of BLS public key hashes for the Validators to be registered - /// @param maxCommittedGasLimit The maximum gas that the Validator can commit for preconfirmations - /// @param authorizedOperator The address of the authorized operator - function batchRegisterValidatorsUnsafe(bytes20[] calldata pubkeyHashes, uint32 maxCommittedGasLimit, address authorizedOperator); - - error KeyNotFound(); - error InvalidQuery(); - #[derive(Debug)] - error ValidatorDoesNotExist(bytes20 pubkeyHash); - error InvalidAuthorizedOperator(); - } -} - #[cfg(test)] mod tests { use alloy::{ diff --git a/bolt-cli/src/contracts/bolt.rs b/bolt-cli/src/contracts/bolt.rs new file mode 100644 index 000000000..43068e24e --- /dev/null +++ b/bolt-cli/src/contracts/bolt.rs @@ -0,0 +1,20 @@ +use alloy::sol; + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + interface BoltValidators { + /// @notice Register a batch of Validators and authorize a Collateral Provider and Operator for them + /// @dev This function allows anyone to register a list of Validators. + /// @param pubkeyHashes List of BLS public key hashes for the Validators to be registered + /// @param maxCommittedGasLimit The maximum gas that the Validator can commit for preconfirmations + /// @param authorizedOperator The address of the authorized operator + function batchRegisterValidatorsUnsafe(bytes20[] calldata pubkeyHashes, uint32 maxCommittedGasLimit, address authorizedOperator); + + error KeyNotFound(); + error InvalidQuery(); + #[derive(Debug)] + error ValidatorDoesNotExist(bytes20 pubkeyHash); + error InvalidAuthorizedOperator(); + } +} diff --git a/bolt-cli/src/contracts/mod.rs b/bolt-cli/src/contracts/mod.rs index 0c21ad9d8..b4c2bf38a 100644 --- a/bolt-cli/src/contracts/mod.rs +++ b/bolt-cli/src/contracts/mod.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::cli::Chain; +pub mod bolt; pub mod eigenlayer; pub mod erc20; From 83335f72d4ec95eee829226451e3ef7df24dda34 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 15:19:03 +0100 Subject: [PATCH 14/35] fix/test(bolt-cli): operator eigenlayer deposit-into-strategy --- bolt-cli/src/cli.rs | 6 +- bolt-cli/src/commands/operators.rs | 113 ++++++++++++++++++++++++---- bolt-cli/src/commands/validators.rs | 4 +- bolt-cli/src/common/signing.rs | 4 +- bolt-cli/src/contracts/mod.rs | 13 ++-- 5 files changed, 111 insertions(+), 29 deletions(-) diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index c0ca81bf2..c5c0ecd06 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -166,7 +166,7 @@ pub enum ValidatorsSubcommand { /// The private key to sign the transactions with. #[clap(long, env = "ADMIN_PRIVATE_KEY")] - admin_private_key: String, + admin_private_key: B256, }, } @@ -200,7 +200,7 @@ pub enum EigenLayerSubcommand { rpc_url: Url, /// The private key of the operator. #[clap(long, env = "OPERATOR_PRIVATE_KEY")] - operator_private_key: String, + operator_private_key: B256, /// The name of the strategy to deposit into. #[clap(long, env = "EIGENLAYER_STRATEGY")] strategy: EigenLayerStrategy, @@ -216,7 +216,7 @@ pub enum EigenLayerSubcommand { rpc_url: Url, /// The private key of the operator. #[clap(long, env = "OPERATOR_PRIVATE_KEY")] - operator_private_key: String, + operator_private_key: B256, /// The URL of the operator RPC. #[clap(long, env = "OPERATOR_RPC")] operator_rpc: Url, diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index 96e7a5d72..8741c93f0 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -10,7 +10,7 @@ use crate::{ strategy_to_address, }, }; -use alloy::providers::{Provider, ProviderBuilder}; +use alloy::providers::{Provider, ProviderBuilder, WalletProvider}; impl OperatorsCommand { pub async fn run(self) -> eyre::Result<()> { @@ -37,30 +37,53 @@ impl OperatorsCommand { let strategy_address = strategy_to_address(strategy, deployments.eigen_layer.supported_strategies); - let strategy = IStrategyInstance::new(strategy_address, provider.clone()); + let strategy_contract = + IStrategyInstance::new(strategy_address, provider.clone()); let strategy_manager_address = deployments.eigen_layer.strategy_manager; let strategy_manager = IStrategyManagerInstance::new(strategy_manager_address, provider.clone()); - let token = strategy.underlyingToken().call().await?.token; + let token = strategy_contract.underlyingToken().call().await?.token; + println!("Token address: {:?}", token); - let token_erc20 = IERC20Instance::new(token, provider); - let success = - token_erc20.approve(strategy_manager_address, amount).call().await?._0; - if !success { - panic!("Failed to approve token transfer"); - } + let token_erc20 = IERC20Instance::new(token, provider.clone()); - let shares = strategy_manager - .depositIntoStrategy(strategy_address, token, amount) + let balance = token_erc20 + .balanceOf(provider.clone().default_signer_address()) .call() .await? - .shares; + ._0; + println!("Balance: {:?}", balance); + let result = + token_erc20.approve(strategy_manager_address, amount).send().await?; println!( - "Deposited {} tokens into strategy, received {} shares", - amount, shares + "Approving transfer of {} {:?}, waiting for inclusion", + amount, strategy ); + let result = result.watch().await?; + println!("Approval transaction included. Transaction hash: {:?}", result); + + let allowance = token_erc20 + .allowance( + provider.clone().default_signer_address(), + strategy_manager_address, + ) + .call() + .await? + ._0; + println!("Allowance: {:?}", allowance); + + let result = strategy_manager + .depositIntoStrategy(strategy_address, token, amount) + .send() + .await?; + + println!("Submitted transaction to deposit into strategy successfully, waiting for inclusion"); + let result = result.watch().await; + println!("Deposit transaction included. Transaction hash: {:?}", result); + + Ok(()) } EigenLayerSubcommand::RegisterIntoBoltAVS { rpc_url, @@ -81,6 +104,66 @@ impl OperatorsCommand { } }, } - todo!(); + } +} + +#[cfg(test)] +mod tests { + use crate::{ + cli::{Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand}, + contracts::{ + deployments_for_chain, eigenlayer::IStrategy, strategy_to_address, EigenLayerStrategy, + }, + }; + use alloy::{ + primitives::{keccak256, utils::parse_units, Address, B256, U256}, + providers::{ext::AnvilApi, Provider, ProviderBuilder}, + signers::k256::ecdsa::SigningKey, + sol_types::SolValue, + }; + + #[tokio::test] + async fn test_deposit_into_strategy() { + let rpc_url = "https://holesky.drpc.org"; + let provider = ProviderBuilder::new().on_anvil_with_config(|anvil| anvil.fork(rpc_url)); + let anvil_url = provider.client().transport().url(); + + let mut rnd = rand::thread_rng(); + let secret_key = SigningKey::random(&mut rnd); + let account = Address::from_private_key(&secret_key); + + // Add balance to the operator + provider.anvil_set_balance(account, U256::from(u64::MAX)).await.expect("set balance"); + + let deployments = deployments_for_chain(Chain::Holesky); + + let weth_strategy_address = strategy_to_address( + EigenLayerStrategy::WEth, + deployments.eigen_layer.supported_strategies, + ); + let strategy = IStrategy::new(weth_strategy_address, provider.clone()); + let weth_address = strategy.underlyingToken().call().await.expect("underlying token").token; + + // Mock WETH balance using the Anvil API. + let hashed_slot = keccak256((account, U256::from(3)).abi_encode()); + let mocked_balance: U256 = parse_units("100.0", "ether").expect("parse ether").into(); + provider + .anvil_set_storage_at(weth_address, hashed_slot.into(), mocked_balance.into()) + .await + .expect("to set storage"); + + let command = OperatorsCommand { + subcommand: OperatorsSubcommand::EigenLayer { + subcommand: EigenLayerSubcommand::DepositIntoStrategy { + rpc_url: anvil_url.parse().expect("valid url"), + operator_private_key: B256::try_from(secret_key.to_bytes().as_slice()) + .expect("valid secret key"), + strategy: EigenLayerStrategy::WEth, + amount: parse_units("1.0", "ether").expect("parse ether").into(), + }, + }, + }; + + command.run().await.expect("run command"); } } diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs index 7e2d9ccd9..66a3649d0 100644 --- a/bolt-cli/src/commands/validators.rs +++ b/bolt-cli/src/commands/validators.rs @@ -56,7 +56,7 @@ impl ValidatorsCommand { #[cfg(test)] mod tests { use alloy::{ - primitives::{Address, U256}, + primitives::{Address, B256, U256}, providers::{ext::AnvilApi, Provider, ProviderBuilder}, signers::k256::ecdsa::SigningKey, }; @@ -78,7 +78,7 @@ mod tests { let command = ValidatorsCommand { subcommand: ValidatorsSubcommand::Register { max_committed_gas_limit: 30_000_000, - admin_private_key: format!("{:x}", secret_key.to_bytes()), + admin_private_key: B256::try_from(secret_key.to_bytes().as_slice()).unwrap(), authorized_operator: account, pubkeys_path: "./test_data/pubkeys.json".parse().unwrap(), rpc_url: anvil_url.parse().unwrap(), diff --git a/bolt-cli/src/common/signing.rs b/bolt-cli/src/common/signing.rs index e7f4cbfd3..f2a1f7cea 100644 --- a/bolt-cli/src/common/signing.rs +++ b/bolt-cli/src/common/signing.rs @@ -67,7 +67,7 @@ pub fn verify_root( } /// Returns an [EthereumWallet] from a private key hex string. -pub fn wallet_from_sk(sk: String) -> eyre::Result { - let wallet: PrivateKeySigner = sk.parse().wrap_err("invalid private key")?; +pub fn wallet_from_sk(sk: B256) -> eyre::Result { + let wallet = PrivateKeySigner::from_bytes(&sk).wrap_err("valid private key")?; Ok(EthereumWallet::from(wallet)) } diff --git a/bolt-cli/src/contracts/mod.rs b/bolt-cli/src/contracts/mod.rs index b4c2bf38a..96bc3f9a2 100644 --- a/bolt-cli/src/contracts/mod.rs +++ b/bolt-cli/src/contracts/mod.rs @@ -60,7 +60,7 @@ pub struct EigenLayerStrategies { m_eth: Address, } -#[derive(Clone, Serialize, Deserialize, Debug, ValueEnum)] +#[derive(Copy, Clone, Serialize, Deserialize, Debug, ValueEnum)] #[allow(clippy::enum_variant_names)] #[serde(rename_all = "kebab-case")] pub enum EigenLayerStrategy { @@ -84,7 +84,6 @@ pub fn strategy_to_address( } } -// PERF: this should be done at compile time pub fn deployments() -> HashMap { let mut deployments = HashMap::new(); deployments.insert(Chain::Holesky, HOLESKY_DEPLOYMENTS); @@ -118,11 +117,11 @@ const HOLESKY_DEPLOYMENTS: Contracts = Contracts { strategy_manager: address!("dfB5f6CE42aAA7830E94ECFCcAd411beF4d4D5b6"), middleware: address!("a632a3e652110Bb2901D5cE390685E6a9838Ca04"), supported_strategies: EigenLayerStrategies { - st_eth: address!("3F1c547b21f65e10480dE3ad8E19fAAC46C95034"), - r_eth: address!("7322c24752f79c05FFD1E2a6FCB97020C1C264F1"), - w_eth: address!("94373a4919B3240D86eA41593D5eBa789FEF3848"), - cb_eth: address!("8720095Fa5739Ab051799211B146a2EEE4Dd8B37"), - m_eth: address!("e3C063B1BEe9de02eb28352b55D49D85514C67FF"), + st_eth: address!("7D704507b76571a51d9caE8AdDAbBFd0ba0e63d3"), + r_eth: address!("3A8fBdf9e77DFc25d09741f51d3E181b25d0c4E0"), + w_eth: address!("80528D6e9A2BAbFc766965E0E26d5aB08D9CFaF9"), + cb_eth: address!("70EB4D3c164a6B4A5f908D4FBb5a9cAfFb66bAB6"), + m_eth: address!("accc5A86732BE85b5012e8614AF237801636F8e5"), }, }, }; From 1c31cf5d974131c4b2e136b1cc38842be604e606 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 16:18:16 +0100 Subject: [PATCH 15/35] feat(bolt-cli): operator eigenlayer register-into-bolt wip --- bolt-cli/src/cli.rs | 2 +- bolt-cli/src/commands/operators.rs | 119 +++++++++++++++++++++++++-- bolt-cli/src/contracts/bolt.rs | 20 +++++ bolt-cli/src/contracts/eigenlayer.rs | 77 +++++++++++++++++ bolt-cli/src/contracts/mod.rs | 4 + 5 files changed, 214 insertions(+), 8 deletions(-) diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index c5c0ecd06..ff3079013 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -225,7 +225,7 @@ pub enum EigenLayerSubcommand { salt: B256, /// The expiry timestamp for the operator signature. #[clap(long, env = "OPERATOR_SIGNATURE_EXPIRY")] - expiry: u64, + expiry: U256, }, /// Step 3: Check your operation registration in bolt diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index 8741c93f0..f4847a4bd 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -4,13 +4,22 @@ use crate::{ }, common::signing::wallet_from_sk, contracts::{ + bolt::{BoltEigenLayerMiddleware, SignatureWithSaltAndExpiry}, deployments_for_chain, - eigenlayer::{IStrategy::IStrategyInstance, IStrategyManager::IStrategyManagerInstance}, + eigenlayer::{ + AVSDirectory, IStrategy::IStrategyInstance, IStrategyManager::IStrategyManagerInstance, + }, erc20::IERC20::IERC20Instance, strategy_to_address, }, }; -use alloy::providers::{Provider, ProviderBuilder, WalletProvider}; +use alloy::{ + network::EthereumWallet, + primitives::{Bytes, U256}, + providers::{Provider, ProviderBuilder, WalletProvider}, + signers::{local::PrivateKeySigner, SignerSync}, +}; +use eyre::Context; impl OperatorsCommand { pub async fn run(self) -> eyre::Result<()> { @@ -92,7 +101,50 @@ impl OperatorsCommand { expiry, operator_private_key, } => { - todo!() + let signer = PrivateKeySigner::from_bytes(&operator_private_key) + .wrap_err("valid private key")?; + + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(EthereumWallet::from(signer.clone())) + .on_http(rpc_url.clone()); + + let chain_id = provider.get_chain_id().await?; + let chain = Chain::from_id(chain_id) + .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + + let deployments = deployments_for_chain(chain); + + let bolt_avs_address = deployments.bolt.eigenlayer_middleware; + let bolt_eigenlayer_middleware = + BoltEigenLayerMiddleware::new(bolt_avs_address, provider.clone()); + + let avs_directory = + AVSDirectory::new(deployments.eigen_layer.avs_directory, provider.clone()); + let signature_digest_hash = avs_directory + .calculateOperatorAVSRegistrationDigestHash( + provider.clone().default_signer_address(), + bolt_avs_address, + salt, + expiry, + ) + .call() + .await? + ._0; + + let signature = + Bytes::from(signer.sign_hash_sync(&signature_digest_hash)?.as_bytes()); + let signature = SignatureWithSaltAndExpiry { signature, expiry, salt }; + + let result = bolt_eigenlayer_middleware + .registerOperator(operator_rpc.to_string(), signature) + .send() + .await?; + println!("Submitted transaction to registered operator into Bolt successfully, waiting for inclusion..."); + let result = result.watch().await?; + println!("Registration transaction included. Transaction hash: {:?}", result); + + Ok(()) } EigenLayerSubcommand::CheckOperatorRegistration { rpc, address } => { todo!() @@ -112,7 +164,9 @@ mod tests { use crate::{ cli::{Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand}, contracts::{ - deployments_for_chain, eigenlayer::IStrategy, strategy_to_address, EigenLayerStrategy, + deployments_for_chain, + eigenlayer::{DelegationManager, IStrategy, OperatorDetails}, + strategy_to_address, EigenLayerStrategy, }, }; use alloy::{ @@ -123,7 +177,7 @@ mod tests { }; #[tokio::test] - async fn test_deposit_into_strategy() { + async fn test_eigenlayer_flow() { let rpc_url = "https://holesky.drpc.org"; let provider = ProviderBuilder::new().on_anvil_with_config(|anvil| anvil.fork(rpc_url)); let anvil_url = provider.client().transport().url(); @@ -152,7 +206,41 @@ mod tests { .await .expect("to set storage"); - let command = OperatorsCommand { + // 1. Register the operator into EigenLayer. This should be done by the operator using the + // EigenLayer CLI, but we do it here for testing purposes. + + let delegation_manager = + DelegationManager::new(deployments.eigen_layer.delegation_manager, provider.clone()); + delegation_manager + .registerAsOperator( + OperatorDetails { + earningsReceiver: account, + delegationApprover: Address::ZERO, + stakerOptOutWindowBlocks: 32, + }, + "https://bolt.chainbound.io/rpc".to_string(), + ) + .send() + .await + .expect("to send register as operator") + .watch() + .await + .expect("to watch register as operator"); + if !delegation_manager + .isOperator(account) + .call() + .await + .expect("to check if operator is registered") + ._0 + { + panic!("Operator not registered"); + } + + println!("Registered operator with address {}", account); + + // 2. Deposit into the strategy + + let deposit_into_strategy = OperatorsCommand { subcommand: OperatorsSubcommand::EigenLayer { subcommand: EigenLayerSubcommand::DepositIntoStrategy { rpc_url: anvil_url.parse().expect("valid url"), @@ -164,6 +252,23 @@ mod tests { }, }; - command.run().await.expect("run command"); + deposit_into_strategy.run().await.expect("to deposit into strategy"); + + // 3. Register the operator into Bolt AVS + + let register_operator = OperatorsCommand { + subcommand: OperatorsSubcommand::EigenLayer { + subcommand: EigenLayerSubcommand::RegisterIntoBoltAVS { + rpc_url: anvil_url.parse().expect("valid url"), + operator_private_key: B256::try_from(secret_key.to_bytes().as_slice()) + .expect("valid secret key"), + operator_rpc: "https://bolt.chainbound.io/rpc".parse().expect("valid url"), + salt: B256::ZERO, + expiry: U256::MAX, + }, + }, + }; + + register_operator.run().await.expect("to register operator"); } } diff --git a/bolt-cli/src/contracts/bolt.rs b/bolt-cli/src/contracts/bolt.rs index 43068e24e..6fe148df1 100644 --- a/bolt-cli/src/contracts/bolt.rs +++ b/bolt-cli/src/contracts/bolt.rs @@ -18,3 +18,23 @@ sol! { error InvalidAuthorizedOperator(); } } + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + struct SignatureWithSaltAndExpiry { + bytes signature; + bytes32 salt; + uint256 expiry; + } + + #[allow(missing_docs)] + #[sol(rpc)] + interface BoltEigenLayerMiddleware { + /// @notice Allow an operator to signal opt-in to Bolt Protocol. + /// @dev This requires calling the EigenLayer AVS Directory contract to register the operator. + /// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator. + /// The msg.sender of this call will be the operator address. + function registerOperator(string calldata rpc, SignatureWithSaltAndExpiry calldata operatorSignature) public; + } +} diff --git a/bolt-cli/src/contracts/eigenlayer.rs b/bolt-cli/src/contracts/eigenlayer.rs index 3f0f7002e..28625f1c4 100644 --- a/bolt-cli/src/contracts/eigenlayer.rs +++ b/bolt-cli/src/contracts/eigenlayer.rs @@ -135,3 +135,80 @@ sol! { function depositIntoStrategy(address strategy, address token, uint256 amount) external returns (uint256 shares); } } + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + struct SignatureWithSaltAndExpiry { + bytes signature; + bytes32 salt; + uint256 expiry; + } + + #[allow(missing_docs)] + #[sol(rpc)] + /** + * @title Interface for the primary entrypoint for funds into EigenLayer. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice See the `StrategyManager` contract itself for implementation details. + */ + interface AVSDirectory { + + /** + * @notice Called by an avs to register an operator with the avs. + * @param operator The address of the operator to register. + * @param operatorSignature The signature, salt, and expiry of the operator's signature. + */ + function registerOperatorToAVS(address operator, SignatureWithSaltAndExpiry memory operatorSignature) external; + + /** + * @notice Calculates the digest hash to be signed by an operator to register with an AVS + * @param operator The account registering as an operator + * @param avs The AVS the operator is registering to + * @param salt A unique and single use value associated with the approver signature. + * @param expiry Time after which the approver's signature becomes invalid + */ + function calculateOperatorAVSRegistrationDigestHash( + address operator, + address avs, + bytes32 salt, + uint256 expiry + ) external view returns (bytes32); + } +} + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + // @notice Struct used for storing information about a single operator who has registered with EigenLayer + struct OperatorDetails { + // @notice address to receive the rewards that the operator earns via serving applications built on EigenLayer. + address earningsReceiver; + /** + * @notice Address to verify signatures when a staker wishes to delegate to the operator, as well as controlling "forced undelegations". + * @dev Signature verification follows these rules: + * 1) If this address is left as address(0), then any staker will be free to delegate to the operator, i.e. no signature verification will be performed. + * 2) If this address is an EOA (i.e. it has no code), then we follow standard ECDSA signature verification for delegations to the operator. + * 3) If this address is a contract (i.e. it has code) then we forward a call to the contract and verify that it returns the correct EIP-1271 "magic value". + */ + address delegationApprover; + /** + * @notice A minimum delay -- measured in blocks -- enforced between: + * 1) the operator signalling their intent to register for a service, via calling `Slasher.optIntoSlashing` + * and + * 2) the operator completing registration for the service, via the service ultimately calling `Slasher.recordFirstStakeUpdate` + * @dev note that for a specific operator, this value *cannot decrease*, i.e. if the operator wishes to modify their OperatorDetails, + * then they are only allowed to either increase this value or keep it the same. + */ + uint32 stakerOptOutWindowBlocks; + } + + #[allow(missing_docs)] + #[sol(rpc)] + interface DelegationManager { + function registerAsOperator(OperatorDetails calldata registeringOperatorDetails, string calldata metadataURI) external; + + function isOperator(address operator) public view returns (bool); + } +} diff --git a/bolt-cli/src/contracts/mod.rs b/bolt-cli/src/contracts/mod.rs index 96bc3f9a2..28a1e8523 100644 --- a/bolt-cli/src/contracts/mod.rs +++ b/bolt-cli/src/contracts/mod.rs @@ -22,6 +22,8 @@ pub struct Bolt { pub validators: Address, pub parameters: Address, pub manager: Address, + pub eigenlayer_middleware: Address, + pub symbiotic_middleware: Address, } #[derive(Clone, Serialize, Deserialize, Debug)] @@ -100,6 +102,8 @@ const HOLESKY_DEPLOYMENTS: Contracts = Contracts { validators: address!("47D2DC1DE1eFEFA5e6944402f2eda3981D36a9c8"), parameters: address!("20d1cf3A5BD5928dB3118b2CfEF54FDF9fda5c12"), manager: address!("440202829b493F9FF43E730EB5e8379EEa3678CF"), + eigenlayer_middleware: address!("a632a3e652110Bb2901D5cE390685E6a9838Ca04"), + symbiotic_middleware: address!("04f40d9CaE475E5BaA462acE53E5c58A0DD8D8e8"), }, symbiotic: Symbiotic { network: address!("b017002D8024d8c8870A5CECeFCc63887650D2a4"), From a3e2fe0c57b1262b289d0e3d351f806033d169c3 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 17:31:41 +0100 Subject: [PATCH 16/35] fix(bolt-cli): register as operator test --- bolt-cli/src/commands/operators.rs | 48 ++++++++++++++++------------- bolt-cli/src/commands/validators.rs | 12 +++++--- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index f4847a4bd..28914ad09 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -163,6 +163,7 @@ impl OperatorsCommand { mod tests { use crate::{ cli::{Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand}, + common::signing::wallet_from_sk, contracts::{ deployments_for_chain, eigenlayer::{DelegationManager, IStrategy, OperatorDetails}, @@ -171,20 +172,25 @@ mod tests { }; use alloy::{ primitives::{keccak256, utils::parse_units, Address, B256, U256}, - providers::{ext::AnvilApi, Provider, ProviderBuilder}, - signers::k256::ecdsa::SigningKey, + providers::{ext::AnvilApi, Provider, ProviderBuilder, WalletProvider}, sol_types::SolValue, }; + use rand::Rng; #[tokio::test] async fn test_eigenlayer_flow() { + let mut rnd = rand::thread_rng(); + let secret_key = B256::from(rnd.gen::<[u8; 32]>()); + let wallet = wallet_from_sk(secret_key).expect("to create wallet"); + let rpc_url = "https://holesky.drpc.org"; - let provider = ProviderBuilder::new().on_anvil_with_config(|anvil| anvil.fork(rpc_url)); + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(wallet) + .on_anvil_with_config(|anvil| anvil.fork(rpc_url)); let anvil_url = provider.client().transport().url(); - let mut rnd = rand::thread_rng(); - let secret_key = SigningKey::random(&mut rnd); - let account = Address::from_private_key(&secret_key); + let account = provider.default_signer_address(); // Add balance to the operator provider.anvil_set_balance(account, U256::from(u64::MAX)).await.expect("set balance"); @@ -206,15 +212,17 @@ mod tests { .await .expect("to set storage"); + let random_address = Address::from(rnd.gen::<[u8; 20]>()); + // 1. Register the operator into EigenLayer. This should be done by the operator using the // EigenLayer CLI, but we do it here for testing purposes. let delegation_manager = DelegationManager::new(deployments.eigen_layer.delegation_manager, provider.clone()); - delegation_manager + let receipt = delegation_manager .registerAsOperator( OperatorDetails { - earningsReceiver: account, + earningsReceiver: random_address, delegationApprover: Address::ZERO, stakerOptOutWindowBlocks: 32, }, @@ -223,20 +231,20 @@ mod tests { .send() .await .expect("to send register as operator") - .watch() + .get_receipt() .await - .expect("to watch register as operator"); - if !delegation_manager + .expect("to get receipt for register as operator"); + + assert!(receipt.status(), "operator should be registered"); + println!("Registered operator with address {}", account); + + let is_operator = delegation_manager .isOperator(account) .call() .await .expect("to check if operator is registered") - ._0 - { - panic!("Operator not registered"); - } - - println!("Registered operator with address {}", account); + ._0; + println!("is operator {}", is_operator); // 2. Deposit into the strategy @@ -244,8 +252,7 @@ mod tests { subcommand: OperatorsSubcommand::EigenLayer { subcommand: EigenLayerSubcommand::DepositIntoStrategy { rpc_url: anvil_url.parse().expect("valid url"), - operator_private_key: B256::try_from(secret_key.to_bytes().as_slice()) - .expect("valid secret key"), + operator_private_key: secret_key, strategy: EigenLayerStrategy::WEth, amount: parse_units("1.0", "ether").expect("parse ether").into(), }, @@ -260,8 +267,7 @@ mod tests { subcommand: OperatorsSubcommand::EigenLayer { subcommand: EigenLayerSubcommand::RegisterIntoBoltAVS { rpc_url: anvil_url.parse().expect("valid url"), - operator_private_key: B256::try_from(secret_key.to_bytes().as_slice()) - .expect("valid secret key"), + operator_private_key: secret_key, operator_rpc: "https://bolt.chainbound.io/rpc".parse().expect("valid url"), salt: B256::ZERO, expiry: U256::MAX, diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs index 66a3649d0..d3d555899 100644 --- a/bolt-cli/src/commands/validators.rs +++ b/bolt-cli/src/commands/validators.rs @@ -34,7 +34,8 @@ impl ValidatorsCommand { let keys: Vec = serde_json::from_reader(pubkeys_file)?; let pubkey_hashes: Vec<_> = keys.iter().map(compress_bls_pubkey).collect(); - let bolt_validators = BoltValidators::new(bolt_validators_address, provider); + let bolt_validators = + BoltValidators::new(bolt_validators_address, provider.clone()); let call = bolt_validators.batchRegisterValidatorsUnsafe( pubkey_hashes, @@ -42,10 +43,13 @@ impl ValidatorsCommand { authorized_operator, ); - let result = call.send().await?; + let pending_tx = call.send().await?; println!("Transaction submitted successfully, waiting for inclusion"); - let result = result.watch().await?; - println!("Transaction included. Transaction hash: {:?}", result); + let hash = pending_tx.watch().await?; + println!("Transaction included. Transaction hash: {:?}", hash); + let receipt = + provider.get_transaction_receipt(hash).await?.expect("to find receipt"); + assert!(receipt.status(), "transaction failed"); Ok(()) } From 3e427ed55541810bdc18d6492d75c19cef5e3a16 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 17:35:47 +0100 Subject: [PATCH 17/35] chore(bolt-cli): wait for receipt before ending command --- bolt-cli/src/commands/operators.rs | 12 +++++++----- bolt-cli/src/commands/validators.rs | 6 ++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index 28914ad09..549d4a1c5 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -15,7 +15,7 @@ use crate::{ }; use alloy::{ network::EthereumWallet, - primitives::{Bytes, U256}, + primitives::Bytes, providers::{Provider, ProviderBuilder, WalletProvider}, signers::{local::PrivateKeySigner, SignerSync}, }; @@ -89,8 +89,9 @@ impl OperatorsCommand { .await?; println!("Submitted transaction to deposit into strategy successfully, waiting for inclusion"); - let result = result.watch().await; - println!("Deposit transaction included. Transaction hash: {:?}", result); + let receipt = result.get_receipt().await?; + println!("Deposit transaction included. Receipt: {:#?}", receipt); + assert!(receipt.status(), "transaction failed"); Ok(()) } @@ -141,8 +142,9 @@ impl OperatorsCommand { .send() .await?; println!("Submitted transaction to registered operator into Bolt successfully, waiting for inclusion..."); - let result = result.watch().await?; - println!("Registration transaction included. Transaction hash: {:?}", result); + let receipt = result.get_receipt().await?; + println!("Registration transaction included. Receipt: {:#?}", receipt); + assert!(receipt.status(), "transaction failed"); Ok(()) } diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs index d3d555899..8af357168 100644 --- a/bolt-cli/src/commands/validators.rs +++ b/bolt-cli/src/commands/validators.rs @@ -45,10 +45,8 @@ impl ValidatorsCommand { let pending_tx = call.send().await?; println!("Transaction submitted successfully, waiting for inclusion"); - let hash = pending_tx.watch().await?; - println!("Transaction included. Transaction hash: {:?}", hash); - let receipt = - provider.get_transaction_receipt(hash).await?.expect("to find receipt"); + let receipt = pending_tx.get_receipt().await?; + println!("Transaction included. Receipt: {:?}", receipt); assert!(receipt.status(), "transaction failed"); Ok(()) From 0bd8dc8279cdc731a06225ee9e9f4f729dda8d3f Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 17:41:57 +0100 Subject: [PATCH 18/35] feat(bolt-cli): operators eigenlayer check-operator-registration --- bolt-cli/src/cli.rs | 4 ++-- bolt-cli/src/commands/operators.rs | 30 ++++++++++++++++++++++++++--- bolt-cli/src/common/bolt_manager.rs | 2 ++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index ff3079013..d1460ec16 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -231,8 +231,8 @@ pub enum EigenLayerSubcommand { /// Step 3: Check your operation registration in bolt CheckOperatorRegistration { /// The URL of the RPC to broadcast the transaction. - #[clap(long, env = "RPC")] - rpc: Url, + #[clap(long, env = "RPC_URL")] + rpc_url: Url, /// The address of the operator to check. #[clap(long, env = "OPERATOR_ADDRESS")] address: Address, diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index 549d4a1c5..c0a3e8bd0 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -2,7 +2,7 @@ use crate::{ cli::{ Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand, SymbioticSubcommand, }, - common::signing::wallet_from_sk, + common::{bolt_manager::BoltManagerContract, signing::wallet_from_sk}, contracts::{ bolt::{BoltEigenLayerMiddleware, SignatureWithSaltAndExpiry}, deployments_for_chain, @@ -148,8 +148,20 @@ impl OperatorsCommand { Ok(()) } - EigenLayerSubcommand::CheckOperatorRegistration { rpc, address } => { - todo!() + EigenLayerSubcommand::CheckOperatorRegistration { rpc_url: rpc, address } => { + let provider = ProviderBuilder::new().on_http(rpc.clone()); + let chain_id = provider.get_chain_id().await?; + let chain = Chain::from_id(chain_id) + .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + + let deployments = deployments_for_chain(chain); + let bolt_manager = + BoltManagerContract::new(deployments.bolt.manager, provider.clone()); + let result = bolt_manager.isOperator(address).call().await?._0; + println!("Operator is registered: {}", result); + assert!(result, "operator is not registered"); + + Ok(()) } }, OperatorsSubcommand::Symbiotic { subcommand } => match subcommand { @@ -278,5 +290,17 @@ mod tests { }; register_operator.run().await.expect("to register operator"); + + // 4. Check operator registration + let check_operator_registration = OperatorsCommand { + subcommand: OperatorsSubcommand::EigenLayer { + subcommand: EigenLayerSubcommand::CheckOperatorRegistration { + rpc_url: anvil_url.parse().expect("valid url"), + address: account, + }, + }, + }; + + check_operator_registration.run().await.expect("to check operator registration"); } } diff --git a/bolt-cli/src/common/bolt_manager.rs b/bolt-cli/src/common/bolt_manager.rs index 261347ab3..35a763a68 100644 --- a/bolt-cli/src/common/bolt_manager.rs +++ b/bolt-cli/src/common/bolt_manager.rs @@ -90,6 +90,8 @@ sol! { function getProposerStatus(bytes32 pubkeyHash) external view returns (ProposerStatus memory); + function isOperator(address operator) public view returns (bool); + error InvalidQuery(); error ValidatorDoesNotExist(); } From 485b3d87cde648627e88b7a2d22fbba44bf1fd40 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 17:47:54 +0100 Subject: [PATCH 19/35] refactor(bolt-cli): eigenlayer commands --- bolt-cli/src/commands/operators.rs | 40 +++++++++-------------------- bolt-cli/src/commands/validators.rs | 14 +++++++--- bolt-cli/src/common/signing.rs | 10 ++------ 3 files changed, 24 insertions(+), 40 deletions(-) diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index c0a3e8bd0..839f44ce2 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -2,7 +2,7 @@ use crate::{ cli::{ Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand, SymbioticSubcommand, }, - common::{bolt_manager::BoltManagerContract, signing::wallet_from_sk}, + common::bolt_manager::BoltManagerContract, contracts::{ bolt::{BoltEigenLayerMiddleware, SignatureWithSaltAndExpiry}, deployments_for_chain, @@ -31,11 +31,12 @@ impl OperatorsCommand { amount, operator_private_key, } => { - let wallet = wallet_from_sk(operator_private_key)?; + let signer = PrivateKeySigner::from_bytes(&operator_private_key) + .wrap_err("valid private key")?; let provider = ProviderBuilder::new() .with_recommended_fillers() - .wallet(wallet) + .wallet(EthereumWallet::from(signer)) .on_http(rpc_url.clone()); let chain_id = provider.get_chain_id().await?; @@ -53,42 +54,23 @@ impl OperatorsCommand { IStrategyManagerInstance::new(strategy_manager_address, provider.clone()); let token = strategy_contract.underlyingToken().call().await?.token; - println!("Token address: {:?}", token); - let token_erc20 = IERC20Instance::new(token, provider.clone()); - let balance = token_erc20 - .balanceOf(provider.clone().default_signer_address()) - .call() - .await? - ._0; - println!("Balance: {:?}", balance); - let result = token_erc20.approve(strategy_manager_address, amount).send().await?; println!( "Approving transfer of {} {:?}, waiting for inclusion", amount, strategy ); - let result = result.watch().await?; - println!("Approval transaction included. Transaction hash: {:?}", result); - - let allowance = token_erc20 - .allowance( - provider.clone().default_signer_address(), - strategy_manager_address, - ) - .call() - .await? - ._0; - println!("Allowance: {:?}", allowance); + let receipt = result.get_receipt().await?; + println!("Approval transaction included. Receipt: {:#?}", receipt); let result = strategy_manager .depositIntoStrategy(strategy_address, token, amount) .send() .await?; - println!("Submitted transaction to deposit into strategy successfully, waiting for inclusion"); + let receipt = result.get_receipt().await?; println!("Deposit transaction included. Receipt: {:#?}", receipt); assert!(receipt.status(), "transaction failed"); @@ -142,6 +124,7 @@ impl OperatorsCommand { .send() .await?; println!("Submitted transaction to registered operator into Bolt successfully, waiting for inclusion..."); + let receipt = result.get_receipt().await?; println!("Registration transaction included. Receipt: {:#?}", receipt); assert!(receipt.status(), "transaction failed"); @@ -177,7 +160,6 @@ impl OperatorsCommand { mod tests { use crate::{ cli::{Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand}, - common::signing::wallet_from_sk, contracts::{ deployments_for_chain, eigenlayer::{DelegationManager, IStrategy, OperatorDetails}, @@ -185,8 +167,10 @@ mod tests { }, }; use alloy::{ + network::EthereumWallet, primitives::{keccak256, utils::parse_units, Address, B256, U256}, providers::{ext::AnvilApi, Provider, ProviderBuilder, WalletProvider}, + signers::local::PrivateKeySigner, sol_types::SolValue, }; use rand::Rng; @@ -195,12 +179,12 @@ mod tests { async fn test_eigenlayer_flow() { let mut rnd = rand::thread_rng(); let secret_key = B256::from(rnd.gen::<[u8; 32]>()); - let wallet = wallet_from_sk(secret_key).expect("to create wallet"); + let wallet = PrivateKeySigner::from_bytes(&secret_key).expect("valid private key"); let rpc_url = "https://holesky.drpc.org"; let provider = ProviderBuilder::new() .with_recommended_fillers() - .wallet(wallet) + .wallet(EthereumWallet::from(wallet)) .on_anvil_with_config(|anvil| anvil.fork(rpc_url)); let anvil_url = provider.client().transport().url(); diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs index 8af357168..5e4f4f0ab 100644 --- a/bolt-cli/src/commands/validators.rs +++ b/bolt-cli/src/commands/validators.rs @@ -1,9 +1,14 @@ -use alloy::providers::{Provider, ProviderBuilder}; +use alloy::{ + network::EthereumWallet, + providers::{Provider, ProviderBuilder}, + signers::local::PrivateKeySigner, +}; use ethereum_consensus::crypto::PublicKey as BlsPublicKey; +use eyre::Context; use crate::{ cli::{Chain, ValidatorsCommand, ValidatorsSubcommand}, - common::{hash::compress_bls_pubkey, signing::wallet_from_sk}, + common::hash::compress_bls_pubkey, contracts::{bolt::BoltValidators, deployments_for_chain}, }; @@ -17,11 +22,12 @@ impl ValidatorsCommand { authorized_operator, rpc_url, } => { - let wallet = wallet_from_sk(admin_private_key)?; + let signer = PrivateKeySigner::from_bytes(&admin_private_key) + .wrap_err("valid private key")?; let provider = ProviderBuilder::new() .with_recommended_fillers() - .wallet(wallet) + .wallet(EthereumWallet::from(signer)) .on_http(rpc_url.clone()); let chain_id = provider.get_chain_id().await?; diff --git a/bolt-cli/src/common/signing.rs b/bolt-cli/src/common/signing.rs index f2a1f7cea..59c70a220 100644 --- a/bolt-cli/src/common/signing.rs +++ b/bolt-cli/src/common/signing.rs @@ -1,10 +1,10 @@ -use alloy::{network::EthereumWallet, primitives::B256, signers::local::PrivateKeySigner}; +use alloy::primitives::B256; use blst::{min_pk::Signature, BLST_ERROR}; use ethereum_consensus::{ crypto::PublicKey as BlsPublicKey, deneb::{compute_fork_data_root, compute_signing_root, Root}, }; -use eyre::{eyre, Context, Result}; +use eyre::{eyre, Result}; use crate::cli::Chain; @@ -65,9 +65,3 @@ pub fn verify_root( Err(eyre!("bls verification failed")) } } - -/// Returns an [EthereumWallet] from a private key hex string. -pub fn wallet_from_sk(sk: B256) -> eyre::Result { - let wallet = PrivateKeySigner::from_bytes(&sk).wrap_err("valid private key")?; - Ok(EthereumWallet::from(wallet)) -} From 2d6c2ebf52a561e4eeaff3184b86f7db0d88e66e Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Wed, 20 Nov 2024 18:41:09 +0100 Subject: [PATCH 20/35] ci(bolt-cli): fix attempt --- .github/workflows/bolt_cli_ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/bolt_cli_ci.yml b/.github/workflows/bolt_cli_ci.yml index e0dc693ab..530d81b30 100644 --- a/.github/workflows/bolt_cli_ci.yml +++ b/.github/workflows/bolt_cli_ci.yml @@ -44,6 +44,8 @@ jobs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly - name: Run bolt-cli tests run: cd bolt-cli && cargo nextest run --workspace --retries 3 From 89c361949457a394e71be55f214a9121d0fd7bd3 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Wed, 20 Nov 2024 18:35:16 +0100 Subject: [PATCH 21/35] feat(cli): add additional bindings + symbiotic --- bolt-cli/src/contracts/bolt.rs | 21 +++++++++++++++++++ bolt-cli/src/contracts/mod.rs | 1 + bolt-cli/src/contracts/symbiotic.rs | 31 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 bolt-cli/src/contracts/symbiotic.rs diff --git a/bolt-cli/src/contracts/bolt.rs b/bolt-cli/src/contracts/bolt.rs index 6fe148df1..4fe53ed9f 100644 --- a/bolt-cli/src/contracts/bolt.rs +++ b/bolt-cli/src/contracts/bolt.rs @@ -37,4 +37,25 @@ sol! { /// The msg.sender of this call will be the operator address. function registerOperator(string calldata rpc, SignatureWithSaltAndExpiry calldata operatorSignature) public; } + + #[allow(missing_docs)] + #[sol(rpc)] + interface BoltSymbioticMiddleware { + /// @notice Allow an operator to signal opt-in to Bolt Protocol. + /// msg.sender must be an operator in the Symbiotic network. + function registerOperator(string calldata rpc) public; + + /// @notice Get the collaterals and amounts staked by an operator across the supported strategies. + /// + /// @param operator The operator address to get the collaterals and amounts staked for. + /// @return collaterals The collaterals staked by the operator. + /// @dev Assumes that the operator is registered and enabled. + function getOperatorCollaterals(address operator) public view returns (address[] memory, uint256[] memory); + } + + #[allow(missing_docs)] + #[sol(rpc)] + interface IBoltManager { + function isOperator(address operator) public view returns (bool); + } } diff --git a/bolt-cli/src/contracts/mod.rs b/bolt-cli/src/contracts/mod.rs index 28a1e8523..729e210a5 100644 --- a/bolt-cli/src/contracts/mod.rs +++ b/bolt-cli/src/contracts/mod.rs @@ -9,6 +9,7 @@ use crate::cli::Chain; pub mod bolt; pub mod eigenlayer; pub mod erc20; +pub mod symbiotic; #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Contracts { diff --git a/bolt-cli/src/contracts/symbiotic.rs b/bolt-cli/src/contracts/symbiotic.rs new file mode 100644 index 000000000..0ebe13e01 --- /dev/null +++ b/bolt-cli/src/contracts/symbiotic.rs @@ -0,0 +1,31 @@ +use alloy::sol; + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + // Reference source code: https://github.com/symbioticfi/core/blob/main/src/interfaces/service/IOptInService.sol + interface IOptInService { + /** + * @notice Get if a given "who" is opted-in to a particular "where" entity at a given timestamp using a hint. + * @param who address of the "who" + * @param where address of the "where" entity + * @param timestamp time point to get if the "who" is opted-in at + * @param hint hint for the checkpoint index + * @return if the "who" is opted-in at the given timestamp + */ + function isOptedInAt( + address who, + address where, + uint48 timestamp, + bytes calldata hint + ) external view returns (bool); + + /** + * @notice Check if a given "who" is opted-in to a particular "where" entity. + * @param who address of the "who" + * @param where address of the "where" entity + * @return if the "who" is opted-in + */ + function isOptedIn(address who, address where) external view returns (bool); + } +} From 9906f78d22a10aeac24154d4a1e9a6ea1578bf62 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Wed, 20 Nov 2024 19:03:07 +0100 Subject: [PATCH 22/35] feat(cli): symbiotic register operator --- bolt-cli/src/cli.rs | 14 +++-- bolt-cli/src/commands/operators.rs | 92 ++++++++++++++++++++++++------ bolt-cli/src/contracts/bolt.rs | 6 -- 3 files changed, 86 insertions(+), 26 deletions(-) diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index d1460ec16..fde20b446 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -194,7 +194,7 @@ pub enum OperatorsSubcommand { #[derive(Debug, Clone, Parser)] pub enum EigenLayerSubcommand { /// Step 1: Deposit into a strategy. - DepositIntoStrategy { + Deposit { /// The URL of the RPC to broadcast the transaction. #[clap(long, env = "RPC_URL")] rpc_url: Url, @@ -210,7 +210,7 @@ pub enum EigenLayerSubcommand { }, /// Step 2: Register into the bolt AVS. - RegisterIntoBoltAVS { + Register { /// The URL of the RPC to broadcast the transaction. #[clap(long, env = "RPC_URL")] rpc_url: Url, @@ -229,7 +229,7 @@ pub enum EigenLayerSubcommand { }, /// Step 3: Check your operation registration in bolt - CheckOperatorRegistration { + Status { /// The URL of the RPC to broadcast the transaction. #[clap(long, env = "RPC_URL")] rpc_url: Url, @@ -242,7 +242,13 @@ pub enum EigenLayerSubcommand { #[derive(Debug, Clone, Parser)] pub enum SymbioticSubcommand { /// Register into the bolt manager contract as an opeartor. - RegisterIntoBolt { + Register { + /// The URL of the RPC to broadcast the transaction. + #[clap(long, env = "RPC_URL")] + rpc_url: Url, + /// The private key of the operator. + #[clap(long, env = "OPERATOR_PRIVATE_KEY")] + operator_private_key: B256, /// The URL of the operator RPC. #[clap(long, env = "OPERATOR_RPC")] operator_rpc: Url, diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index 839f44ce2..e98f216a1 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -1,31 +1,38 @@ +use alloy::{ + network::EthereumWallet, + primitives::Bytes, + providers::{Provider, ProviderBuilder, WalletProvider}, + signers::{local::PrivateKeySigner, SignerSync}, +}; +use eyre::Context; +use tracing::info; + use crate::{ cli::{ Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand, SymbioticSubcommand, }, common::bolt_manager::BoltManagerContract, contracts::{ - bolt::{BoltEigenLayerMiddleware, SignatureWithSaltAndExpiry}, + bolt::{ + BoltEigenLayerMiddleware, + BoltSymbioticMiddleware::{self}, + SignatureWithSaltAndExpiry, + }, deployments_for_chain, eigenlayer::{ AVSDirectory, IStrategy::IStrategyInstance, IStrategyManager::IStrategyManagerInstance, }, erc20::IERC20::IERC20Instance, strategy_to_address, + symbiotic::IOptInService, }, }; -use alloy::{ - network::EthereumWallet, - primitives::Bytes, - providers::{Provider, ProviderBuilder, WalletProvider}, - signers::{local::PrivateKeySigner, SignerSync}, -}; -use eyre::Context; impl OperatorsCommand { pub async fn run(self) -> eyre::Result<()> { match self.subcommand { OperatorsSubcommand::EigenLayer { subcommand } => match subcommand { - EigenLayerSubcommand::DepositIntoStrategy { + EigenLayerSubcommand::Deposit { rpc_url, strategy, amount, @@ -77,7 +84,7 @@ impl OperatorsCommand { Ok(()) } - EigenLayerSubcommand::RegisterIntoBoltAVS { + EigenLayerSubcommand::Register { rpc_url, operator_rpc, salt, @@ -131,7 +138,7 @@ impl OperatorsCommand { Ok(()) } - EigenLayerSubcommand::CheckOperatorRegistration { rpc_url: rpc, address } => { + EigenLayerSubcommand::Status { rpc_url: rpc, address } => { let provider = ProviderBuilder::new().on_http(rpc.clone()); let chain_id = provider.get_chain_id().await?; let chain = Chain::from_id(chain_id) @@ -148,8 +155,61 @@ impl OperatorsCommand { } }, OperatorsSubcommand::Symbiotic { subcommand } => match subcommand { - SymbioticSubcommand::RegisterIntoBolt { operator_rpc } => { - todo!() + SymbioticSubcommand::Register { operator_rpc, operator_private_key, rpc_url } => { + let signer = PrivateKeySigner::from_bytes(&operator_private_key) + .wrap_err("valid private key")?; + + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(EthereumWallet::from(signer.clone())) + .on_http(rpc_url); + + let chain_id = provider.get_chain_id().await?; + let chain = Chain::from_id(chain_id) + .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + + let deployments = deployments_for_chain(chain); + + info!(operator = %signer.address(), rpc = %operator_rpc, ?chain, "Registering Symbiotic operator"); + + // Check if operator is opted in to the bolt network + if !IOptInService::new( + deployments.symbiotic.network_opt_in_service, + provider.clone(), + ) + .isOptedIn(signer.address(), deployments.symbiotic.network) + .call() + .await? + ._0 + { + eyre::bail!( + "Operator with address {} not opted in to the bolt network ({})", + signer.address(), + deployments.symbiotic.network + ); + } + + let middleware = BoltSymbioticMiddleware::new( + deployments.bolt.symbiotic_middleware, + provider.clone(), + ); + + let pending = + middleware.registerOperator(operator_rpc.to_string()).send().await?; + + info!( + hash = ?pending.tx_hash(), + "registerOperator transaction sent, awaiting receipt..." + ); + + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully registered Symbiotic operator"); + + Ok(()) } }, } @@ -248,7 +308,7 @@ mod tests { let deposit_into_strategy = OperatorsCommand { subcommand: OperatorsSubcommand::EigenLayer { - subcommand: EigenLayerSubcommand::DepositIntoStrategy { + subcommand: EigenLayerSubcommand::Deposit { rpc_url: anvil_url.parse().expect("valid url"), operator_private_key: secret_key, strategy: EigenLayerStrategy::WEth, @@ -263,7 +323,7 @@ mod tests { let register_operator = OperatorsCommand { subcommand: OperatorsSubcommand::EigenLayer { - subcommand: EigenLayerSubcommand::RegisterIntoBoltAVS { + subcommand: EigenLayerSubcommand::Register { rpc_url: anvil_url.parse().expect("valid url"), operator_private_key: secret_key, operator_rpc: "https://bolt.chainbound.io/rpc".parse().expect("valid url"), @@ -278,7 +338,7 @@ mod tests { // 4. Check operator registration let check_operator_registration = OperatorsCommand { subcommand: OperatorsSubcommand::EigenLayer { - subcommand: EigenLayerSubcommand::CheckOperatorRegistration { + subcommand: EigenLayerSubcommand::Status { rpc_url: anvil_url.parse().expect("valid url"), address: account, }, diff --git a/bolt-cli/src/contracts/bolt.rs b/bolt-cli/src/contracts/bolt.rs index 4fe53ed9f..a07e226df 100644 --- a/bolt-cli/src/contracts/bolt.rs +++ b/bolt-cli/src/contracts/bolt.rs @@ -52,10 +52,4 @@ sol! { /// @dev Assumes that the operator is registered and enabled. function getOperatorCollaterals(address operator) public view returns (address[] memory, uint256[] memory); } - - #[allow(missing_docs)] - #[sol(rpc)] - interface IBoltManager { - function isOperator(address operator) public view returns (bool); - } } From 7ded97d9703668f13a7d074bc4b08eda1d85bd3f Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Wed, 20 Nov 2024 19:07:33 +0100 Subject: [PATCH 23/35] feat(cli): symbiotic check operator status --- bolt-cli/src/cli.rs | 11 ++++++++++- bolt-cli/src/commands/operators.rs | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index fde20b446..2e31a7849 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -241,7 +241,7 @@ pub enum EigenLayerSubcommand { #[derive(Debug, Clone, Parser)] pub enum SymbioticSubcommand { - /// Register into the bolt manager contract as an opeartor. + /// Register into the bolt manager contract as a Symbiotic operator. Register { /// The URL of the RPC to broadcast the transaction. #[clap(long, env = "RPC_URL")] @@ -253,6 +253,15 @@ pub enum SymbioticSubcommand { #[clap(long, env = "OPERATOR_RPC")] operator_rpc: Url, }, + /// Check the status of a Symbiotic operator. + Status { + /// The URL of the RPC to broadcast the transaction. + #[clap(long, env = "RPC_URL")] + rpc_url: Url, + /// The address of the operator to check. + #[clap(long, env = "OPERATOR_ADDRESS")] + address: Address, + }, } /// The action to perform. diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index e98f216a1..013fc4b0b 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -5,7 +5,7 @@ use alloy::{ signers::{local::PrivateKeySigner, SignerSync}, }; use eyre::Context; -use tracing::info; +use tracing::{info, warn}; use crate::{ cli::{ @@ -209,6 +209,23 @@ impl OperatorsCommand { info!("Succesfully registered Symbiotic operator"); + Ok(()) + } + SymbioticSubcommand::Status { rpc_url, address } => { + let provider = ProviderBuilder::new().on_http(rpc_url.clone()); + let chain_id = provider.get_chain_id().await?; + let chain = Chain::from_id(chain_id) + .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + + let deployments = deployments_for_chain(chain); + let bolt_manager = + BoltManagerContract::new(deployments.bolt.manager, provider.clone()); + if bolt_manager.isOperator(address).call().await?._0 { + info!(?address, "Symbiotic operator is registered"); + } else { + warn!(?address, "Operator not registered"); + } + Ok(()) } }, From a12af3a1d66ea2d76c180277279070efa2f19ce0 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Wed, 20 Nov 2024 19:21:35 +0100 Subject: [PATCH 24/35] feat(cli): add user confirmation prompt --- bolt-cli/src/commands/operators.rs | 58 ++++++++++++++++++++++++++++-- bolt-cli/src/main.rs | 5 +-- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index 013fc4b0b..6d0d74be6 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -1,3 +1,5 @@ +use std::io::Write; + use alloy::{ network::EthereumWallet, primitives::Bytes, @@ -61,6 +63,13 @@ impl OperatorsCommand { IStrategyManagerInstance::new(strategy_manager_address, provider.clone()); let token = strategy_contract.underlyingToken().call().await?.token; + println!("Token address: {:?}", token); + + if !request_confirmation() { + info!("Aborting"); + return Ok(()); + } + let token_erc20 = IERC20Instance::new(token, provider.clone()); let result = @@ -103,6 +112,13 @@ impl OperatorsCommand { let chain = Chain::from_id(chain_id) .unwrap_or_else(|| panic!("chain id {} not supported", chain_id)); + info!(operator = %signer.address(), rpc = %operator_rpc, ?chain, "Registering EigenLayer operator"); + + if !request_confirmation() { + info!("Aborting"); + return Ok(()); + } + let deployments = deployments_for_chain(chain); let bolt_avs_address = deployments.bolt.eigenlayer_middleware; @@ -139,6 +155,10 @@ impl OperatorsCommand { Ok(()) } EigenLayerSubcommand::Status { rpc_url: rpc, address } => { + if !request_confirmation() { + return Ok(()); + } + let provider = ProviderBuilder::new().on_http(rpc.clone()); let chain_id = provider.get_chain_id().await?; let chain = Chain::from_id(chain_id) @@ -147,9 +167,11 @@ impl OperatorsCommand { let deployments = deployments_for_chain(chain); let bolt_manager = BoltManagerContract::new(deployments.bolt.manager, provider.clone()); - let result = bolt_manager.isOperator(address).call().await?._0; - println!("Operator is registered: {}", result); - assert!(result, "operator is not registered"); + if bolt_manager.isOperator(address).call().await?._0 { + info!(?address, "EigenLayer operator is registered"); + } else { + warn!(?address, "Operator not registered"); + } Ok(()) } @@ -172,6 +194,11 @@ impl OperatorsCommand { info!(operator = %signer.address(), rpc = %operator_rpc, ?chain, "Registering Symbiotic operator"); + if !request_confirmation() { + info!("Aborting"); + return Ok(()); + } + // Check if operator is opted in to the bolt network if !IOptInService::new( deployments.symbiotic.network_opt_in_service, @@ -233,6 +260,31 @@ impl OperatorsCommand { } } +fn request_confirmation() -> bool { + loop { + info!("Do you want to continue? (yes/no): "); + + print!("Answer: "); + std::io::stdout().flush().expect("Failed to flush"); + + let mut input = String::new(); + std::io::stdin().read_line(&mut input).expect("Failed to read input"); + + let input = input.trim().to_lowercase(); + + match input.as_str() { + "yes" | "y" => { + return true; + } + "no" | "n" => { + return false; + } + _ => { + println!("Invalid input. Please type 'yes' or 'no'."); + } + } + } +} #[cfg(test)] mod tests { use crate::{ diff --git a/bolt-cli/src/main.rs b/bolt-cli/src/main.rs index d74fe8016..cdf5a003b 100644 --- a/bolt-cli/src/main.rs +++ b/bolt-cli/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use tracing::error; /// CLI command definitions and options. mod cli; @@ -18,10 +19,10 @@ mod contracts; #[tokio::main] async fn main() -> eyre::Result<()> { let _ = dotenvy::dotenv(); - let _ = tracing_subscriber::fmt::try_init(); + let _ = tracing_subscriber::fmt().with_target(false).try_init(); if let Err(err) = rustls::crypto::ring::default_provider().install_default() { - eprintln!("Failed to install default TLS provider: {:?}", err); + error!("Failed to install default TLS provider: {:?}", err); } cli::Opts::parse().command.run().await From a0a643cb3ffd01bc3c922386339fa921fdb5e5e1 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Wed, 20 Nov 2024 19:37:12 +0100 Subject: [PATCH 25/35] feat(cli): some cleanup --- bolt-cli/src/commands/operators.rs | 47 +++++++++++++++++++++--------- bolt-cli/src/contracts/mod.rs | 13 +++++++++ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index 6d0d74be6..13b9375a1 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -2,7 +2,7 @@ use std::io::Write; use alloy::{ network::EthereumWallet, - primitives::Bytes, + primitives::{utils::format_ether, Bytes}, providers::{Provider, ProviderBuilder, WalletProvider}, signers::{local::PrivateKeySigner, SignerSync}, }; @@ -42,6 +42,7 @@ impl OperatorsCommand { } => { let signer = PrivateKeySigner::from_bytes(&operator_private_key) .wrap_err("valid private key")?; + let operator = signer.address(); let provider = ProviderBuilder::new() .with_recommended_fillers() @@ -63,7 +64,8 @@ impl OperatorsCommand { IStrategyManagerInstance::new(strategy_manager_address, provider.clone()); let token = strategy_contract.underlyingToken().call().await?.token; - println!("Token address: {:?}", token); + + info!(%strategy, %token, amount = format_ether(amount), ?operator, "Depositing funds into EigenLayer strategy"); if !request_confirmation() { info!("Aborting"); @@ -72,24 +74,34 @@ impl OperatorsCommand { let token_erc20 = IERC20Instance::new(token, provider.clone()); + let balance = token_erc20 + .balanceOf(provider.clone().default_signer_address()) + .call() + .await? + ._0; + + info!("Operator token balance: {}", format_ether(balance)); + let result = token_erc20.approve(strategy_manager_address, amount).send().await?; - println!( - "Approving transfer of {} {:?}, waiting for inclusion", - amount, strategy - ); - let receipt = result.get_receipt().await?; - println!("Approval transaction included. Receipt: {:#?}", receipt); + + info!(hash = ?result.tx_hash(), "Approving transfer of {} {:?}, awaiting receipt...", amount, strategy); + let result = result.watch().await?; + info!("Approval transaction included. Transaction hash: {:?}", result); let result = strategy_manager .depositIntoStrategy(strategy_address, token, amount) .send() .await?; - println!("Submitted transaction to deposit into strategy successfully, waiting for inclusion"); + info!(hash = ?result.tx_hash(), "Submitted deposit transaction, awaiting receipt..."); let receipt = result.get_receipt().await?; - println!("Deposit transaction included. Receipt: {:#?}", receipt); - assert!(receipt.status(), "transaction failed"); + + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully deposited collateral into strategy"); Ok(()) } @@ -146,11 +158,18 @@ impl OperatorsCommand { .registerOperator(operator_rpc.to_string(), signature) .send() .await?; - println!("Submitted transaction to registered operator into Bolt successfully, waiting for inclusion..."); + + info!( + hash = ?result.tx_hash(), + "registerOperator transaction sent, awaiting receipt..." + ); let receipt = result.get_receipt().await?; - println!("Registration transaction included. Receipt: {:#?}", receipt); - assert!(receipt.status(), "transaction failed"); + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } + + info!("Succesfully registered Symbiotic operator"); Ok(()) } diff --git a/bolt-cli/src/contracts/mod.rs b/bolt-cli/src/contracts/mod.rs index 729e210a5..888f17ba8 100644 --- a/bolt-cli/src/contracts/mod.rs +++ b/bolt-cli/src/contracts/mod.rs @@ -74,6 +74,19 @@ pub enum EigenLayerStrategy { MEth, } +impl std::fmt::Display for EigenLayerStrategy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let output = match self { + EigenLayerStrategy::StEth => "stETH", + EigenLayerStrategy::REth => "rETH", + EigenLayerStrategy::WEth => "wETH", + EigenLayerStrategy::CbEth => "cbETH", + EigenLayerStrategy::MEth => "mETH", + }; + write!(f, "{}", output) + } +} + pub fn strategy_to_address( strategy: EigenLayerStrategy, addresses: EigenLayerStrategies, From d6a0c7ef9a358d365e720d4d041185af2a595f66 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Wed, 20 Nov 2024 19:42:13 +0100 Subject: [PATCH 26/35] ci(cli): don't double run ci --- .github/workflows/bolt_cli_ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/bolt_cli_ci.yml b/.github/workflows/bolt_cli_ci.yml index 530d81b30..30e798898 100644 --- a/.github/workflows/bolt_cli_ci.yml +++ b/.github/workflows/bolt_cli_ci.yml @@ -2,6 +2,9 @@ name: Bolt CLI CI on: push: + branches: + - unstable + - main paths: - "bolt-cli/**" pull_request: From eef2cd18b390001010306c044e0ae7da4e82a454 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Thu, 21 Nov 2024 11:01:48 +0100 Subject: [PATCH 27/35] fix(bolt-cli): request_confirmation logic --- bolt-cli/src/commands/operators.rs | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index 13b9375a1..0ecddf4ce 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -67,10 +67,7 @@ impl OperatorsCommand { info!(%strategy, %token, amount = format_ether(amount), ?operator, "Depositing funds into EigenLayer strategy"); - if !request_confirmation() { - info!("Aborting"); - return Ok(()); - } + request_confirmation(); let token_erc20 = IERC20Instance::new(token, provider.clone()); @@ -126,10 +123,7 @@ impl OperatorsCommand { info!(operator = %signer.address(), rpc = %operator_rpc, ?chain, "Registering EigenLayer operator"); - if !request_confirmation() { - info!("Aborting"); - return Ok(()); - } + request_confirmation(); let deployments = deployments_for_chain(chain); @@ -174,10 +168,6 @@ impl OperatorsCommand { Ok(()) } EigenLayerSubcommand::Status { rpc_url: rpc, address } => { - if !request_confirmation() { - return Ok(()); - } - let provider = ProviderBuilder::new().on_http(rpc.clone()); let chain_id = provider.get_chain_id().await?; let chain = Chain::from_id(chain_id) @@ -213,10 +203,7 @@ impl OperatorsCommand { info!(operator = %signer.address(), rpc = %operator_rpc, ?chain, "Registering Symbiotic operator"); - if !request_confirmation() { - info!("Aborting"); - return Ok(()); - } + request_confirmation(); // Check if operator is opted in to the bolt network if !IOptInService::new( @@ -279,7 +266,11 @@ impl OperatorsCommand { } } -fn request_confirmation() -> bool { +/// Asks whether the user wants to proceed further. If not, the process is exited. +fn request_confirmation() { + #[cfg(test)] + return; + loop { info!("Do you want to continue? (yes/no): "); @@ -293,10 +284,11 @@ fn request_confirmation() -> bool { match input.as_str() { "yes" | "y" => { - return true; + return; } "no" | "n" => { - return false; + info!("Aborting"); + std::process::exit(0); } _ => { println!("Invalid input. Please type 'yes' or 'no'."); From 1c17b30ce007223f9ddd77f0a685db32328cee41 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Thu, 21 Nov 2024 11:10:20 +0100 Subject: [PATCH 28/35] chore(bolt-cli): request confirmation also on register validators command --- bolt-cli/src/commands/operators.rs | 34 +---------------------------- bolt-cli/src/commands/validators.rs | 12 +++++++++- bolt-cli/src/common/mod.rs | 34 ++++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 35 deletions(-) diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index 0ecddf4ce..ce4ef640d 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -1,5 +1,3 @@ -use std::io::Write; - use alloy::{ network::EthereumWallet, primitives::{utils::format_ether, Bytes}, @@ -13,7 +11,7 @@ use crate::{ cli::{ Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand, SymbioticSubcommand, }, - common::bolt_manager::BoltManagerContract, + common::{bolt_manager::BoltManagerContract, request_confirmation}, contracts::{ bolt::{ BoltEigenLayerMiddleware, @@ -266,36 +264,6 @@ impl OperatorsCommand { } } -/// Asks whether the user wants to proceed further. If not, the process is exited. -fn request_confirmation() { - #[cfg(test)] - return; - - loop { - info!("Do you want to continue? (yes/no): "); - - print!("Answer: "); - std::io::stdout().flush().expect("Failed to flush"); - - let mut input = String::new(); - std::io::stdin().read_line(&mut input).expect("Failed to read input"); - - let input = input.trim().to_lowercase(); - - match input.as_str() { - "yes" | "y" => { - return; - } - "no" | "n" => { - info!("Aborting"); - std::process::exit(0); - } - _ => { - println!("Invalid input. Please type 'yes' or 'no'."); - } - } - } -} #[cfg(test)] mod tests { use crate::{ diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs index 5e4f4f0ab..5a912f179 100644 --- a/bolt-cli/src/commands/validators.rs +++ b/bolt-cli/src/commands/validators.rs @@ -5,10 +5,11 @@ use alloy::{ }; use ethereum_consensus::crypto::PublicKey as BlsPublicKey; use eyre::Context; +use tracing::info; use crate::{ cli::{Chain, ValidatorsCommand, ValidatorsSubcommand}, - common::hash::compress_bls_pubkey, + common::{hash::compress_bls_pubkey, request_confirmation}, contracts::{bolt::BoltValidators, deployments_for_chain}, }; @@ -40,6 +41,15 @@ impl ValidatorsCommand { let keys: Vec = serde_json::from_reader(pubkeys_file)?; let pubkey_hashes: Vec<_> = keys.iter().map(compress_bls_pubkey).collect(); + info!( + validators = ?keys.len(), + ?max_committed_gas_limit, + ?authorized_operator, + "Registering validators into bolt", + ); + + request_confirmation(); + let bolt_validators = BoltValidators::new(bolt_validators_address, provider.clone()); diff --git a/bolt-cli/src/common/mod.rs b/bolt-cli/src/common/mod.rs index f1726e56f..6f51fb86c 100644 --- a/bolt-cli/src/common/mod.rs +++ b/bolt-cli/src/common/mod.rs @@ -1,8 +1,9 @@ -use std::{fs, path::PathBuf}; +use std::{fs, io::Write, path::PathBuf}; use ethereum_consensus::crypto::PublicKey as BlsPublicKey; use eyre::{Context, Result}; use serde::Serialize; +use tracing::info; /// BoltManager contract bindings. pub mod bolt_manager; @@ -35,3 +36,34 @@ pub fn write_to_file(out: &str, data: &T) -> Result<()> { serde_json::to_writer_pretty(out_file, data)?; Ok(()) } + +/// Asks whether the user wants to proceed further. If not, the process is exited. +pub fn request_confirmation() { + #[cfg(test)] + return; + + loop { + info!("Do you want to continue? (yes/no): "); + + print!("Answer: "); + std::io::stdout().flush().expect("Failed to flush"); + + let mut input = String::new(); + std::io::stdin().read_line(&mut input).expect("Failed to read input"); + + let input = input.trim().to_lowercase(); + + match input.as_str() { + "yes" | "y" => { + return; + } + "no" | "n" => { + info!("Aborting"); + std::process::exit(0); + } + _ => { + println!("Invalid input. Please type 'yes' or 'no'."); + } + } + } +} From d3163094b78f1d84625c439a1cb6f7935e1836a1 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Thu, 21 Nov 2024 11:20:03 +0100 Subject: [PATCH 29/35] chore(bolt-cli): logs refactor on register validators --- bolt-cli/src/commands/validators.rs | 31 +++++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs index 5a912f179..4580e586d 100644 --- a/bolt-cli/src/commands/validators.rs +++ b/bolt-cli/src/commands/validators.rs @@ -45,25 +45,34 @@ impl ValidatorsCommand { validators = ?keys.len(), ?max_committed_gas_limit, ?authorized_operator, + ?chain, "Registering validators into bolt", ); - request_confirmation(); - let bolt_validators = BoltValidators::new(bolt_validators_address, provider.clone()); - let call = bolt_validators.batchRegisterValidatorsUnsafe( - pubkey_hashes, - max_committed_gas_limit, - authorized_operator, + request_confirmation(); + + let pending = bolt_validators + .batchRegisterValidatorsUnsafe( + pubkey_hashes, + max_committed_gas_limit, + authorized_operator, + ) + .send() + .await?; + + info!( + hash = ?pending.tx_hash(), + "batchRegisterValidatorsUnsafe transaction sent, awaiting receipt..." ); + let receipt = pending.get_receipt().await?; + if !receipt.status() { + eyre::bail!("Transaction failed: {:?}", receipt) + } - let pending_tx = call.send().await?; - println!("Transaction submitted successfully, waiting for inclusion"); - let receipt = pending_tx.get_receipt().await?; - println!("Transaction included. Receipt: {:?}", receipt); - assert!(receipt.status(), "transaction failed"); + info!("Successfully registered validators into bolt"); Ok(()) } From 01992bf07564cb590640263982fa941e693a0757 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Thu, 21 Nov 2024 13:37:36 +0100 Subject: [PATCH 30/35] test(bolt-cli): symbiotic flow --- bolt-cli/.gitignore | 2 + bolt-cli/src/commands/operators.rs | 134 ++++++++++++++++++++++++++++- bolt-cli/src/contracts/mod.rs | 15 ++-- 3 files changed, 143 insertions(+), 8 deletions(-) diff --git a/bolt-cli/.gitignore b/bolt-cli/.gitignore index c8b1db124..fe4b04c0a 100644 --- a/bolt-cli/.gitignore +++ b/bolt-cli/.gitignore @@ -6,3 +6,5 @@ delegations.json pubkeys.json !test_data/pubkeys.json + +symbiotic-cli/ diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index ce4ef640d..e816d25c3 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -266,8 +266,12 @@ impl OperatorsCommand { #[cfg(test)] mod tests { + use std::process::{Command, Output}; + use crate::{ - cli::{Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand}, + cli::{ + Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand, SymbioticSubcommand, + }, contracts::{ deployments_for_chain, eigenlayer::{DelegationManager, IStrategy, OperatorDetails}, @@ -276,7 +280,7 @@ mod tests { }; use alloy::{ network::EthereumWallet, - primitives::{keccak256, utils::parse_units, Address, B256, U256}, + primitives::{address, keccak256, utils::parse_units, Address, B256, U256}, providers::{ext::AnvilApi, Provider, ProviderBuilder, WalletProvider}, signers::local::PrivateKeySigner, sol_types::SolValue, @@ -395,4 +399,130 @@ mod tests { check_operator_registration.run().await.expect("to check operator registration"); } + + /// Ignored since it requires Symbiotic CLI: https://docs.symbiotic.fi/guides/cli/#installation + /// To run this test, install the CLI, and then move the binary in the `symbiotic-cli` directory + /// which is git-ignored for this purpose. + #[tokio::test] + #[ignore = "requires Symbiotic CLI installed"] + async fn test_symbiotic_flow() { + let mut rnd = rand::thread_rng(); + let secret_key = B256::from(rnd.gen::<[u8; 32]>()); + let wallet = PrivateKeySigner::from_bytes(&secret_key).expect("valid private key"); + + let rpc_url = "https://rpc-holesky.bolt.chainbound.io/rpc"; + let provider = ProviderBuilder::new() + .with_recommended_fillers() + .wallet(EthereumWallet::from(wallet)) + .on_anvil_with_config(|anvil| anvil.fork(rpc_url)); + let anvil_url = provider.client().transport().url(); + + let account = provider.default_signer_address(); + + // Add balance to the operator + provider.anvil_set_balance(account, U256::from(u64::MAX)).await.expect("set balance"); + + let deployments = deployments_for_chain(Chain::Holesky); + + let weth_address = address!("94373a4919B3240D86eA41593D5eBa789FEF3848"); + + // Mock WETH balance using the Anvil API. + let hashed_slot = keccak256((account, U256::from(3)).abi_encode()); + let mocked_balance: U256 = parse_units("100.0", "ether").expect("parse ether").into(); + provider + .anvil_set_storage_at(weth_address, hashed_slot.into(), mocked_balance.into()) + .await + .expect("to set storage"); + + let print_output = |output: Output| { + println!("{}", String::from_utf8_lossy(&output.stdout)); + }; + + // We now follow the steps described in the Holesky guide + + let register_operator = Command::new("python3") + .arg("symbiotic-cli/symb.py") + .arg("--chain") + .arg("holesky") + .arg("--provider") + .arg(anvil_url) + .arg("register-operator") + .arg("--private-key") + .arg(secret_key.to_string()) + .output() + .expect("to register operator"); + + print_output(register_operator); + + let opt_in_network = Command::new("python3") + .arg("symbiotic-cli/symb.py") + .arg("--chain") + .arg("holesky") + .arg("--provider") + .arg(anvil_url) + .arg("opt-in-network") + .arg("--private-key") + .arg(secret_key.to_string()) + .arg(deployments.symbiotic.network.to_string()) + .output() + .expect("to opt-in-network"); + + print_output(opt_in_network); + + let vault = deployments.symbiotic.supported_vaults[3]; // WETH vault + + let opt_in_vault = Command::new("python3") + .arg("symbiotic-cli/symb.py") + .arg("--chain") + .arg("holesky") + .arg("--provider") + .arg(anvil_url) + .arg("opt-in-vault") + .arg("--private-key") + .arg(secret_key.to_string()) + .arg(vault.to_string()) + .output() + .expect("to opt-in-vault"); + + print_output(opt_in_vault); + + let deposit = Command::new("python3") + .arg("symbiotic-cli/symb.py") + .arg("--chain") + .arg("holesky") + .arg("--provider") + .arg(anvil_url) + .arg("deposit") + .arg("--private-key") + .arg(secret_key.to_string()) + .arg(vault.to_string()) + .arg("1") // 1 ether + .output() + .expect("to opt-in-vault"); + + print_output(deposit); + + let register_into_bolt = OperatorsCommand { + subcommand: OperatorsSubcommand::Symbiotic { + subcommand: SymbioticSubcommand::Register { + rpc_url: anvil_url.parse().expect("valid url"), + operator_private_key: secret_key, + operator_rpc: "https://bolt.chainbound.io".parse().expect("valid url"), + }, + }, + }; + + register_into_bolt.run().await.expect("to register into bolt"); + + let check_status = OperatorsCommand { + subcommand: OperatorsSubcommand::Symbiotic { + subcommand: SymbioticSubcommand::Status { + rpc_url: anvil_url.parse().expect("valid url"), + address: account, + }, + }, + }; + + check_status.run().await.expect("to check operator status"); + } } diff --git a/bolt-cli/src/contracts/mod.rs b/bolt-cli/src/contracts/mod.rs index 888f17ba8..52603efff 100644 --- a/bolt-cli/src/contracts/mod.rs +++ b/bolt-cli/src/contracts/mod.rs @@ -37,14 +37,9 @@ pub struct Symbiotic { pub network_registry: Address, pub network_middleware_service: Address, pub middleware: Address, + pub supported_vaults: [Address; 6], } -// - [`stETH`](https://holesky.etherscan.io/address/0x3F1c547b21f65e10480dE3ad8E19fAAC46C95034) -// - [`rETH`](https://holesky.etherscan.io/address/0x7322c24752f79c05FFD1E2a6FCB97020C1C264F1) -// - [`wETH`](https://holesky.etherscan.io/address/0x94373a4919B3240D86eA41593D5eBa789FEF3848) -// - [`cbETH`](https://holesky.etherscan.io/address/0x8720095Fa5739Ab051799211B146a2EEE4Dd8B37) -// - [`mETH`](https://holesky.etherscan.io/address/0xe3C063B1BEe9de02eb28352b55D49D85514C67FF) - #[derive(Clone, Serialize, Deserialize, Debug)] pub struct EigenLayer { pub avs_directory: Address, @@ -128,6 +123,14 @@ const HOLESKY_DEPLOYMENTS: Contracts = Contracts { network_registry: address!("7d03b7343BF8d5cEC7C0C27ecE084a20113D15C9"), network_middleware_service: address!("62a1ddfD86b4c1636759d9286D3A0EC722D086e3"), middleware: address!("04f40d9CaE475E5BaA462acE53E5c58A0DD8D8e8"), + supported_vaults: [ + address!("c79c533a77691641d52ebD5e87E51dCbCaeb0D78"), + address!("e5708788c90e971f73D928b7c5A8FD09137010e0"), + address!("11c5b9A9cd8269580aDDbeE38857eE451c1CFacd"), + address!("C56Ba584929c6f381744fA2d7a028fA927817f2b"), + address!("cDdeFfcD2bA579B8801af1d603812fF64c301462"), + address!("91e84e12Bb65576C0a6614c5E6EbbB2eA595E10f"), + ], }, eigen_layer: EigenLayer { avs_directory: address!("055733000064333CaDDbC92763c58BF0192fFeBf"), From 1b1dd576523b780f9cc7fbc8432682839d4f20e8 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Thu, 21 Nov 2024 13:45:20 +0100 Subject: [PATCH 31/35] chore(contracts): remove forge scripts for operators and validators --- .../RegisterEigenLayerOperator.s.sol | 154 ------------------ .../operators/RegisterSymbioticOperator.s.sol | 82 ---------- .../validators/RegisterValidators.s.sol | 126 -------------- 3 files changed, 362 deletions(-) delete mode 100644 bolt-contracts/script/holesky/operators/RegisterEigenLayerOperator.s.sol delete mode 100644 bolt-contracts/script/holesky/operators/RegisterSymbioticOperator.s.sol delete mode 100644 bolt-contracts/script/holesky/validators/RegisterValidators.s.sol diff --git a/bolt-contracts/script/holesky/operators/RegisterEigenLayerOperator.s.sol b/bolt-contracts/script/holesky/operators/RegisterEigenLayerOperator.s.sol deleted file mode 100644 index 3fa63dbe8..000000000 --- a/bolt-contracts/script/holesky/operators/RegisterEigenLayerOperator.s.sol +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {Script, console} from "forge-std/Script.sol"; - -import {IAVSDirectory} from "@eigenlayer/src/contracts/interfaces/IAVSDirectory.sol"; -import {IDelegationManager} from "@eigenlayer/src/contracts/interfaces/IDelegationManager.sol"; -import {IStrategyManager} from "@eigenlayer/src/contracts/interfaces/IStrategyManager.sol"; -import {IStrategy, IERC20} from "@eigenlayer/src/contracts/interfaces/IStrategy.sol"; -import {ISignatureUtils} from "@eigenlayer/src/contracts/interfaces/ISignatureUtils.sol"; - -import {BoltEigenLayerMiddlewareV2} from "../../../src/contracts/BoltEigenLayerMiddlewareV2.sol"; -import {IBoltMiddlewareV1} from "../../../src/interfaces/IBoltMiddlewareV1.sol"; -import {IBoltManagerV2} from "../../../src/interfaces/IBoltManagerV2.sol"; - -contract RegisterEigenLayerOperator is Script { - struct OperatorConfig { - string rpc; - bytes32 salt; - uint256 expiry; - } - - function S01_depositIntoStrategy() public { - uint256 operatorSk = vm.envUint("OPERATOR_SK"); - - IStrategyManager strategyManager = _readStrategyManager(); - - string memory json = vm.readFile("config/holesky/operators/eigenlayer/depositIntoStrategy.json"); - - IStrategy strategy = IStrategy(vm.parseJsonAddress(json, ".strategy")); - IERC20 token = IERC20(vm.parseJsonAddress(json, ".token")); - uint256 amount = vm.parseJsonUint(json, ".amount") * 1 ether; - - vm.startBroadcast(operatorSk); - // Allowance must be set before depositing - token.approve(address(strategyManager), amount); - strategyManager.depositIntoStrategy(strategy, token, amount); - console.log("Successfully run StrategyManager.depositIntoStrategy"); - vm.stopBroadcast(); - } - - function S02_registerIntoBoltAVS() public { - uint256 operatorSk = vm.envUint("OPERATOR_SK"); - address operator = vm.addr(operatorSk); - - BoltEigenLayerMiddlewareV2 middleware = _readMiddleware(); - IAVSDirectory avsDirectory = _readAvsDirectory(); - OperatorConfig memory config = _readConfig("config/holesky/operators/eigenlayer/registerIntoBoltAVS.json"); - - console.log("Registering EigenLayer operator"); - console.log("Operator address:", operator); - console.log("Operator RPC:", config.rpc); - - bytes32 digest = avsDirectory.calculateOperatorAVSRegistrationDigestHash({ - operator: operator, - avs: address(middleware), - salt: config.salt, - expiry: config.expiry - }); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(operatorSk, digest); - bytes memory rawSignature = abi.encodePacked(r, s, v); - - ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature = - ISignatureUtils.SignatureWithSaltAndExpiry(rawSignature, config.salt, config.expiry); - - vm.startBroadcast(operatorSk); - - middleware.registerOperator(config.rpc, operatorSignature); - console.log("Successfully registered EigenLayer operator"); - - vm.stopBroadcast(); - } - - function S03_checkOperatorRegistration() public view { - address operatorAddress = vm.envAddress("OPERATOR_ADDRESS"); - console.log("Checking operator registration for address", operatorAddress); - - IBoltManagerV2 boltManager = _readBoltManager(); - bool isRegistered = boltManager.isOperator(operatorAddress); - console.log("Operator is registered:", isRegistered); - require(isRegistered, "Operator is not registered"); - - BoltEigenLayerMiddlewareV2 middleware = _readMiddleware(); - (address[] memory tokens, uint256[] memory amounts) = middleware.getOperatorCollaterals(operatorAddress); - - for (uint256 i; i < tokens.length; ++i) { - if (amounts[i] > 0) { - console.log("Collateral found:", tokens[i], "- amount:", amounts[i]); - } - } - } - - function _readMiddleware() public view returns (BoltEigenLayerMiddlewareV2) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - - return BoltEigenLayerMiddlewareV2(vm.parseJsonAddress(json, ".eigenLayer.middleware")); - } - - function _readAvsDirectory() public view returns (IAVSDirectory) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - - return IAVSDirectory(vm.parseJsonAddress(json, ".eigenLayer.avsDirectory")); - } - - function _readDelegationManager() public view returns (IDelegationManager) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - - return IDelegationManager(vm.parseJsonAddress(json, ".eigenLayer.delegationManager")); - } - - function _readStrategyManager() public view returns (IStrategyManager) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - return IStrategyManager(vm.parseJsonAddress(json, ".eigenLayer.strategyManager")); - } - - function _readBoltManager() public view returns (IBoltManagerV2) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - return IBoltManagerV2(vm.parseJsonAddress(json, ".bolt.manager")); - } - - function _readConfig( - string memory path - ) public view returns (OperatorConfig memory) { - string memory json = vm.readFile(path); - - bytes32 salt = bytes32(0); - uint256 expiry = UINT256_MAX; - - try vm.parseJsonBytes32(json, ".salt") returns (bytes32 val) { - salt = val; - } catch { - console.log("No salt found in config, using 0"); - } - - try vm.parseJsonUint(json, ".expiry") returns (uint256 val) { - expiry = val; - } catch { - console.log("No expiry found in config, using UINT256_MAX"); - } - - return OperatorConfig({rpc: vm.parseJsonString(json, ".rpc"), salt: salt, expiry: expiry}); - } -} diff --git a/bolt-contracts/script/holesky/operators/RegisterSymbioticOperator.s.sol b/bolt-contracts/script/holesky/operators/RegisterSymbioticOperator.s.sol deleted file mode 100644 index 0bcea2005..000000000 --- a/bolt-contracts/script/holesky/operators/RegisterSymbioticOperator.s.sol +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {Script, console} from "forge-std/Script.sol"; - -import {BoltSymbioticMiddlewareV2} from "../../../src/contracts/BoltSymbioticMiddlewareV2.sol"; -import {IBoltManagerV2} from "../../../src/interfaces/IBoltManagerV2.sol"; - -import {IOptInService} from "@symbiotic/interfaces/service/IOptInService.sol"; -import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; - -contract RegisterSymbioticOperator is Script { - struct Config { - BoltSymbioticMiddlewareV2 symbioticMiddleware; - IOptInService symbioticNetworkOptInService; - address symbioticNetwork; - } - - function S01_registerIntoBolt() public { - uint256 operatorSk = vm.envUint("OPERATOR_SK"); - string memory rpc = vm.envString("OPERATOR_RPC"); - - address operator = vm.addr(operatorSk); - - Config memory config = _readConfig(); - - console.log("Registering Symbiotic operator into Bolt"); - console.log("Operator address:", operator); - - // First, make sure the operator is opted into the network - require( - config.symbioticNetworkOptInService.isOptedIn(operator, config.symbioticNetwork), - "Operator must be opted in into Bolt Network" - ); - - console.log("Operator RPC:", rpc); - - vm.startBroadcast(operatorSk); - config.symbioticMiddleware.registerOperator(rpc); - console.log("Successfully registered Symbiotic operator"); - - vm.stopBroadcast(); - - (address[] memory tokens, uint256[] memory amounts) = - config.symbioticMiddleware.getOperatorCollaterals(operator); - - console.log("Operator collateral:"); - for (uint256 i; i < tokens.length; ++i) { - console.log("Collateral:", tokens[i], "Amount:", amounts[i]); - } - } - - function S02_checkOperatorRegistration() public view { - address operatorAddress = vm.envAddress("OPERATOR_ADDRESS"); - console.log("Checking operator registration for address", operatorAddress); - - IBoltManagerV2 boltManager = _readBoltManager(); - bool isRegistered = boltManager.isOperator(operatorAddress); - - console.log("Operator is registered:", isRegistered); - require(isRegistered, "Operator is not registered"); - } - - function _readConfig() public view returns (Config memory) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - - return Config({ - symbioticNetwork: vm.parseJsonAddress(json, ".symbiotic.network"), - symbioticMiddleware: BoltSymbioticMiddlewareV2(vm.parseJsonAddress(json, ".symbiotic.middleware")), - symbioticNetworkOptInService: IOptInService(vm.parseJsonAddress(json, ".symbiotic.networkOptInService")) - }); - } - - function _readBoltManager() public view returns (IBoltManagerV2) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - return IBoltManagerV2(vm.parseJsonAddress(json, ".bolt.manager")); - } -} diff --git a/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol b/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol deleted file mode 100644 index cdd69ff2d..000000000 --- a/bolt-contracts/script/holesky/validators/RegisterValidators.s.sol +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.25; - -import {IBoltValidatorsV2} from "../../../src/interfaces/IBoltValidatorsV2.sol"; -import {BLS12381} from "../../../src/lib/bls/BLS12381.sol"; - -import {Script, console} from "forge-std/Script.sol"; - -/// @notice Script to register Ethereum validators to Bolt -/// @dev this script reads from the config file in /config/holesky/register_validators.json -contract RegisterValidators is Script { - using BLS12381 for BLS12381.G1Point; - - struct RegisterValidatorsConfig { - uint32 maxCommittedGasLimit; - address authorizedOperator; - // Note: for Unsafe registration (aka without BLS verification precompile) - // we use compressed pubkey hashes on-chain instead of decompressed points. - // BLS12381.G1Point[] pubkeys; - bytes20[] pubkeys; - } - - function run() public { - address controller = msg.sender; - - console.log("Registering validators to Bolt"); - console.log("Controller address: ", controller); - - IBoltValidatorsV2 validators = _readValidators(); - RegisterValidatorsConfig memory config = _parseConfig(); - - vm.startBroadcast(controller); - validators.batchRegisterValidatorsUnsafe(config.pubkeys, config.maxCommittedGasLimit, config.authorizedOperator); - vm.stopBroadcast(); - - console.log("Validators registered successfully"); - } - - function _readValidators() public view returns (IBoltValidatorsV2) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/deployments.json"); - string memory json = vm.readFile(path); - - return IBoltValidatorsV2(vm.parseJsonAddress(json, ".bolt.validators")); - } - - function _parseConfig() public view returns (RegisterValidatorsConfig memory config) { - string memory root = vm.projectRoot(); - string memory path = string.concat(root, "/config/holesky/validators.json"); - string memory json = vm.readFile(path); - - config.authorizedOperator = vm.parseJsonAddress(json, ".authorizedOperator"); - config.maxCommittedGasLimit = uint32(vm.parseJsonUint(json, ".maxCommittedGasLimit")); - console.log("Max committed gas limit:", config.maxCommittedGasLimit); - - string[] memory pubkeysRaw = vm.parseJsonStringArray(json, ".pubkeys"); - - // NOTE: for Unsafe registration (aka without BLS verification precompile) - // we use compressed pubkey hashes on-chain instead of decompressed points. - bytes20[] memory pubkeys = new bytes20[](pubkeysRaw.length); - for (uint256 i = 0; i < pubkeysRaw.length; i++) { - bytes memory pubkeyBytes = vm.parseBytes(pubkeysRaw[i]); - require(pubkeyBytes.length == 48, "Invalid pubkey length"); - - // compute the pubkey hash: - // 1. create a 64 byte buffer - // 2. copy the pubkey bytes to the rightmost 48 bytes of the buffer - // 3. hash the buffer - // 4. take the 20 leftmost bytes of the hash as the pubkey hash - bytes memory buffer = new bytes(64); - for (uint256 j = 0; j < 48; j++) { - buffer[j + 16] = pubkeyBytes[j]; - } - bytes20 pubkeyHash = bytes20(keccak256(buffer)); - - pubkeys[i] = pubkeyHash; - console.log("Registering pubkey hash:", vm.toString(abi.encodePacked(pubkeyHash))); - } - - // BLS12381.G1Point[] memory pubkeys = new BLS12381.G1Point[](pubkeysRaw.length); - // for (uint256 i = 0; i < pubkeysRaw.length; i++) { - // string memory pubkey = pubkeysRaw[i]; - - // string[] memory convertCmd = new string[](2); - // convertCmd[0] = "./script/pubkey_to_g1_wrapper.sh"; - // convertCmd[1] = pubkey; - - // bytes memory output = vm.ffi(convertCmd); - // string memory outputStr = string(output); - // string[] memory array = vm.split(outputStr, ","); - - // uint256[2] memory x = _bytesToParts(vm.parseBytes(array[0])); - // uint256[2] memory y = _bytesToParts(vm.parseBytes(array[1])); - - // pubkeys[i] = BLS12381.G1Point(x, y); - - // console.log("Registering pubkey:", vm.toString(abi.encodePacked(pubkeys[i].compress()))); - // } - - config.pubkeys = pubkeys; - } - - function _bytesToParts( - bytes memory data - ) public pure returns (uint256[2] memory out) { - require(data.length == 48, "Invalid data length"); - - uint256 value1; - uint256 value2; - - // Load the first 32 bytes into value1 - assembly { - value1 := mload(add(data, 32)) - } - value1 = value1 >> 128; // Clear unwanted upper bits - - // Load the next 16 bytes into value2 - assembly { - value2 := mload(add(data, 48)) - } - // value2 = value2 >> 128; - - out[0] = value1; - out[1] = value2; - } -} From ed198f9962e34980176860b9585aa8de05b82bcb Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Thu, 21 Nov 2024 13:56:48 +0100 Subject: [PATCH 32/35] fix(bolt-cli): register command docs --- bolt-cli/src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index 2e31a7849..2d3b513ef 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -146,7 +146,7 @@ pub struct ValidatorsCommand { #[derive(Debug, Clone, Parser)] pub enum ValidatorsSubcommand { - /// The source of the private key. + /// Register a batch of validators. Register { /// The URL of the RPC to broadcast the transaction. #[clap(long, env = "RPC_URL")] From 1e1b3f886f00d1ffa550155508d1a1b97d9b31d5 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Thu, 21 Nov 2024 14:28:21 +0100 Subject: [PATCH 33/35] fix(bolt-cli): amount in ETH --- bolt-cli/src/cli.rs | 2 +- bolt-cli/src/commands/operators.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index 2d3b513ef..63ae7a84c 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -204,7 +204,7 @@ pub enum EigenLayerSubcommand { /// The name of the strategy to deposit into. #[clap(long, env = "EIGENLAYER_STRATEGY")] strategy: EigenLayerStrategy, - /// The amount to deposit into the strategy. + /// The amount to deposit into the strategy, in ETH #[clap(long, env = "EIGENLAYER_STRATEGY_DEPOSIT_AMOUNT")] amount: U256, }, diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs index e816d25c3..b72ee7dd5 100644 --- a/bolt-cli/src/commands/operators.rs +++ b/bolt-cli/src/commands/operators.rs @@ -1,5 +1,6 @@ use alloy::{ network::EthereumWallet, + node_bindings::WEI_IN_ETHER, primitives::{utils::format_ether, Bytes}, providers::{Provider, ProviderBuilder, WalletProvider}, signers::{local::PrivateKeySigner, SignerSync}, @@ -63,6 +64,8 @@ impl OperatorsCommand { let token = strategy_contract.underlyingToken().call().await?.token; + let amount = amount * WEI_IN_ETHER; + info!(%strategy, %token, amount = format_ether(amount), ?operator, "Depositing funds into EigenLayer strategy"); request_confirmation(); @@ -364,7 +367,7 @@ mod tests { rpc_url: anvil_url.parse().expect("valid url"), operator_private_key: secret_key, strategy: EigenLayerStrategy::WEth, - amount: parse_units("1.0", "ether").expect("parse ether").into(), + amount: U256::from(1), }, }, }; From 0745d8975af4b870857893046ba0f9f0e18df73e Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Thu, 21 Nov 2024 14:31:48 +0100 Subject: [PATCH 34/35] docs(holesky): use bolt cli instead of forge scripts --- testnets/holesky/README.md | 320 ++++++++++++++++++++----------------- 1 file changed, 177 insertions(+), 143 deletions(-) diff --git a/testnets/holesky/README.md b/testnets/holesky/README.md index 1d273a116..40115a9c0 100644 --- a/testnets/holesky/README.md +++ b/testnets/holesky/README.md @@ -6,30 +6,33 @@ This document provides instructions for running Bolt on the Holesky testnet. -- [Prerequisites](#prerequisites) -- [On-Chain Registration](#on-chain-registration) - - [Validator Registration](#validator-registration) - - [Registration Steps](#registration-steps) - - [Bolt Network Entrypoint](#bolt-network-entrypoint) - - [Operator Registration](#operator-registration) - - [Symbiotic Registration Steps](#symbiotic-registration-steps) - - [EigenLayer Registration Steps](#eigenlayer-registration-steps) -- [Off-Chain Setup](#off-chain-setup) - - [Docker Mode (recommended)](#docker-mode-recommended) - - [Commit-Boost Mode](#commit-boost-mode) - - [Native Mode (advanced)](#native-mode-advanced) - - [Building and running the MEV-Boost fork binary](#building-and-running-the-mev-boost-fork-binary) - - [Building and running the Bolt sidecar binary](#building-and-running-the-bolt-sidecar-binary) - - [Configuration file](#configuration-file) - - [Observability](#observability) -- [Reference](#reference) - - [Command-line options](#command-line-options) - - [Delegations and signing options for Native and Docker Compose Mode](#delegations-and-signing-options-for-native-and-docker-compose-mode) - - [`bolt` CLI](#bolt-cli) - - [Installation and usage](#installation-and-usage) - - [Using a private key directly](#using-a-private-key-directly) - - [Using a ERC-2335 Keystore](#using-a-erc-2335-keystore) - - [Avoid restarting the beacon node](#avoid-restarting-the-beacon-node) +* [Prerequisites](#prerequisites) +* [On-Chain Registration](#on-chain-registration) + * [Validator Registration](#validator-registration) + * [Registration Steps](#registration-steps) + * [Bolt Network Entrypoint](#bolt-network-entrypoint) + * [Operator Registration](#operator-registration) + * [Symbiotic Registration Steps](#symbiotic-registration-steps) + * [EigenLayer Registration Steps](#eigenlayer-registration-steps) +* [Off-Chain Setup](#off-chain-setup) + * [Docker Mode (recommended)](#docker-mode-recommended) + * [Commit-Boost Mode](#commit-boost-mode) + * [Native Mode (advanced)](#native-mode-advanced) + * [Building and running the MEV-Boost fork binary](#building-and-running-the-mev-boost-fork-binary) + * [Building and running the Bolt sidecar binary](#building-and-running-the-bolt-sidecar-binary) + * [Configuration file](#configuration-file) + * [Observability](#observability) + * [Firewall Configuration](#firewall-configuration) +* [Reference](#reference) + * [Supported RPC nodes](#supported-rpc-nodes) + * [Supported Relays](#supported-relays) + * [Command-line options](#command-line-options) + * [Delegations and signing options for Native and Docker Compose Mode](#delegations-and-signing-options-for-native-and-docker-compose-mode) + * [`bolt` CLI](#bolt-cli) + * [Installation and usage](#installation-and-usage) + * [Using a private key directly](#using-a-private-key-directly) + * [Using a ERC-2335 Keystore](#using-a-erc-2335-keystore) + * [Avoid restarting the beacon node](#avoid-restarting-the-beacon-node) @@ -135,22 +138,48 @@ protocols. **Prerequisites** -- Install the Foundry toolkit: +- Make sure you have Rust installed on your machine. Follow the instructions + reported in the [official website](https://www.rust-lang.org/tools/install). -```bash -curl -L https://foundry.paradigm.xyz | bash -source $HOME/.bashrc -foundryup -``` +- Clone the Bolt repo and install the `bolt` CLI -- Clone the Bolt repo and navigate to the [contracts](https://github.com/chainbound/bolt/tree/unstable/bolt-contracts) directory: + ```bash + git clone https://github.com/chainbound/bolt + cd bolt-cli + cargo install --force --path . + ``` -```bash -git clone https://github.com/chainbound/bolt -cd bolt-contracts -forge install +The command above will install the `bolt` CLI in your system, which is a useful tool to +manage your validators and operators in the bolt Protocol. You can check if +it's installed by running `bolt --help`. + +```text +`bolt` is a CLI tool to interact with bolt Protocol ✨ + +Usage: bolt + +Commands: + delegate Generate BLS delegation or revocation messages + pubkeys Output a list of pubkeys in JSON format + send Send a preconfirmation request to a bolt proposer + validators Handle validators in the bolt network + operators Handle operators in the bolt network + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + -V, --version Print version ``` +> [!NOTE] +> All the `bolt` commands can be simulated on a Holesky fork using Anvil with +> the following command: +> +> `anvil --fork-url https://holesky.drpc.org --port 8545` +> +> In order to use this local fork, replace the `--rpc-url` flag (`$RPC_URL` env) +> with `http://localhost:8545` in all `bolt` commands below. + ## Validator Registration The [`BoltValidators`](../../bolt-contracts/src/contracts/BoltValidatorsV1.sol) contract is the only @@ -178,40 +207,29 @@ validator or change any preferences. ### Registration Steps -> [!NOTE] -> All of these scripts can be simulated on a Holesky fork using Anvil with the -> following command: -> -> `anvil --fork-url https://holesky.drpc.org --port 8545` -> -> In order to use this local fork, replace `$HOLESKY_RPC` with `localhost:8545` in -> all of the `forge` commands below. +To register your validators, you can use the `bolt` CLI. First, look at the +options available for the `validators register` command: -To register your validators, we provide the following Foundry script: -[`RegisterValidators.s.sol`](../../bolt-contracts/script/holesky/validators/RegisterValidators.s.sol). -Note that in order to run these scripts, you must be in the `bolt-contracts` -directory. - -- First, configure - [`bolt-contracts/config/holesky/validators.json`](../../bolt-contracts/config/holesky/validators.json) - to your requirements. Note that both `maxCommittedGasLimit` and - `authorizedOperator` must reflect the values you'll specify in later steps, during - the configuration of the sidecar. `pubkeys` should be configured with all of the - validator public keys that you wish to register. - -- Next up, decide on a controller account and save the key in an environment - variable: `export CONTROLLER_KEY=0x...`. This controller key will be used to run - the script and will mark the corresponding account as the [controller - account](https://github.com/chainbound/bolt/blob/06bdd8e75d759d91f6178ad73f962b1f4ad43fd8/bolt-contracts/src/interfaces/IBoltValidatorsV1.sol#L18-L19) - for these validators. - -- Finally, run the script: +```text +Usage: bolt validators register [OPTIONS] --rpc-url --max-committed-gas-limit --authorized-operator --admin-private-key -```bash -forge script script/holesky/validators/RegisterValidators.s.sol -vvvv --rpc-url $HOLESKY_RPC --private-key $CONTROLLER_KEY --broadcast +Options: + --rpc-url + The URL of the RPC to broadcast the transaction [env: RPC_URL=] + --max-committed-gas-limit + The max gas limit the validator is willing to reserve to commitments [env: MAX_COMMITTED_GAS_LIMIT=] + --authorized-operator + The authorized operator for the validator [env: AUTHORIZED_OPERATOR=] + --pubkeys-path + The path to the JSON pubkeys file, containing an array of BLS public keys [env: PUBKEYS_PATH=] [default: pubkeys.json] + --admin-private-key + The private key to sign the transactions with [env: ADMIN_PRIVATE_KEY=] + -h, --help + Print help ``` -If the script executed succesfully, your validators were registered. +Fill the required options and run the script. If the script executed +succesfully, your validators were registered. ## Bolt Network Entrypoint @@ -240,12 +258,6 @@ private key will be used to sign commitments on the corresponding validators' sidecars. However, we need a way to logically connect validators to an on-chain address associated with some stake, which is what the operator abstraction takes care of. -**In the next sections we assume you have saved the private key corresponding to -the operator address in `$OPERATOR_SK`.** This private key will be read by the -Forge scripts for registering operators and needs to be set correctly. You also -have to invoke the scripts from the [`bolt-contracts`](../../bolt-contracts) -directory. - ### Symbiotic Registration Steps As an operator, you will need to opt-in to the Bolt Network and any Vault that @@ -277,30 +289,41 @@ The opt-in process requires the following steps: **Internal Steps** After having deposited collateral into a vault you need to register into -Bolt as a Symbiotic operator. We've provided a script to facilitate the -procedure. If you want to use it, please follow these steps: +Bolt as a Symbiotic operator. You can do that using the `bolt` CLI. -1. set the operator private key to the `OPERATOR_SK` environment variable; -2. set the operator RPC URL which supports the Commitments API to the - `OPERATOR_RPC` environment variable; -3. run the following Forge script from the `bolt-contracts` directory: +First, read the requirements for the `bolt operator symbiotic register` command: - ```bash - forge script script/holesky/operators/RegisterSymbioticOperator.s.sol \ - --sig "S01_registerIntoBolt" \ - --rpc-url $HOLESKY_RPC \ - -vvvv \ - --broadcast - ``` +```text +Register into the bolt manager contract as a Symbiotic operator -To check if your operator is correctly registered, set the operator address -in the `OPERATOR_ADDRESS` environment variable and run the following script: +Usage: bolt operators symbiotic register --rpc-url --operator-private-key --operator-rpc -```bash -forge script script/holesky/operators/RegisterSymbioticOperator.s.sol \ - --sig "S02_checkOperatorRegistration" \ - --rpc-url $HOLESKY_RPC \ - -vvvv +Options: + --rpc-url + The URL of the RPC to broadcast the transaction [env: RPC_URL=] + --operator-private-key + The private key of the operator [env: OPERATOR_PRIVATE_KEY=] + --operator-rpc + The URL of the operator RPC [env: OPERATOR_RPC=] + -h, --help + Print help +``` + +Fill the required options and run the script. If the script executed +successfully, your validators were registered. + +To check your operator status, you can use the `bolt operator +symbiotic status` command: + +```text +Check the status of a Symbiotic operator + +Usage: bolt operators symbiotic status --rpc-url --address
+ +Options: + --rpc-url The URL of the RPC to broadcast the transaction [env: RPC_URL=] + --address
The address of the operator to check [env: OPERATOR_ADDRESS=] + -h, --help Print help ``` ### EigenLayer Registration Steps @@ -326,79 +349,90 @@ This will add the deposit into the collateral of the operator so that Bolt can read it. Note that you need to deposit a minimum of `1 ether` of the strategies underlying token in order to opt in. -We've provided a script to facilitate the procedure. If you want to use it, -please set the operator private key to an `OPERATOR_SK` environment variable. +You can deposit into a strategy by using the following `bolt operators +eigenlayer deposit` command: -First, you need to first configure the deposit details in this JSON -file: +```text +Step 1: Deposit into a strategy -```bash -$EDITOR ./config/holesky/operators/eigenlayer/depositIntoStrategy.json -``` +Usage: bolt operators eigenlayer deposit --rpc-url --operator-private-key --strategy --amount -Note that the amount is in ether (so for 1 ether, specify `1` instead of 1e18). +Options: + --rpc-url + The URL of the RPC to broadcast the transaction [env: RPC_URL=] + --operator-private-key + The private key of the operator [env: OPERATOR_PRIVATE_KEY=] + --strategy + The name of the strategy to deposit into [env: EIGENLAYER_STRATEGY=] [possible values: st-eth, r-eth, w-eth, cb-eth, m-eth] + --amount + The amount to deposit into the strategy, in ETH [env: EIGENLAYER_STRATEGY_DEPOSIT_AMOUNT=] + -h, --help + Print help +``` -Then you can run the following Forge script: +Note that the amount is in ETH, so if you want to deposit `1 ether` you need to +provide `--amount 1`. -```bash -forge script script/holesky/operators/RegisterEigenLayerOperator.s.sol \ - --sig "S01_depositIntoStrategy()" \ - --rpc-url $HOLESKY_RPC \ - -vvvv \ - --broadcast -``` +Fill the required options and run the script. If the script executed +successfully, you have deposited into the strategy. **Internal Steps** After having deposited collateral into a strategy you need to register into the -Bolt AVS. We've provided a script to facilitate the procedure. If you want to -use it, please set follow these steps: +Bolt AVS. You can use the `bolt operators eigenlayer register` command for it: -1. configure the operator details in this JSON file +```text +Step 2: Register into the bolt AVS - ```bash - $EDITOR ./config/holesky/operators/eigenlayer/registerIntoBoltAVS.json - ``` +Usage: bolt operators eigenlayer register --rpc-url --operator-private-key --operator-rpc --salt --expiry - In there you'll need to set the the following fields: +Options: + --rpc-url + The URL of the RPC to broadcast the transaction [env: RPC_URL=] + --operator-private-key + The private key of the operator [env: OPERATOR_PRIVATE_KEY=] + --operator-rpc + The URL of the operator RPC [env: OPERATOR_RPC=] + --salt + The salt for the operator signature [env: OPERATOR_SIGNATURE_SALT=] + --expiry + The expiry timestamp for the operator signature [env: OPERATOR_SIGNATURE_EXPIRY=] + -h, --help + Print help +``` - - `rpc` -- the RPC URL of your operator which supports the Commitments API - - `salt` -- an unique 32 bytes value to avoid replay attacks. To generate it on - both Linux and MacOS you can run: +A note on the `--salt` and `--expiry` parameters: - ```bash - echo -n "0x"; head -c 32 /dev/urandom | hexdump -e '32/1 "%02x" "\n"' - ``` +- `salt` -- an unique 32 bytes value to avoid replay attacks. To generate it on + both Linux and MacOS you can run: - - `expiry` -- the timestamp of the signature expiry in seconds. To generate it - on both Linux and MacOS run the following command, replacing - `` with the desired timestamp: + ```bash + echo -n "0x"; head -c 32 /dev/urandom | hexdump -e '32/1 "%02x" "\n"' + ``` - ```bash - echo -n "0x"; printf "%064x\n" - ``` +- `expiry` -- the timestamp of the signature expiry in seconds. To generate it + on both Linux and MacOS run the following command, replacing + `` with the desired timestamp: -2. set the operator private key to an `OPERATOR_SK` environment - variable; -3. run the following Forge script from the `bolt-contracts` - directory: + ```bash + echo -n "0x"; printf "%064x\n" + ``` -```bash -forge script script/holesky/operators/RegisterEigenLayerOperator.s.sol \ - --sig "S02_registerIntoBoltAVS" \ - --rpc-url $HOLESKY_RPC \ - -vvvv \ - --broadcast -``` +Once you have the required values, fill the options and run the script. If the +command executed successfully, your operator were registered into bolt. -To check if your operator is correctly registered, set the operator address -in the `OPERATOR_ADDRESS` environment variable and run the following script: +To check if the status of your operator, you can use the `bolt operators +eigenlayer status` command: ```bash -forge script script/holesky/operators/RegisterEigenLayerOperator.s.sol \ - --sig "S03_checkOperatorRegistration" \ - --rpc-url $HOLESKY_RPC \ - -vvvv +Step 3: Check your operation registration in bolt + +Usage: bolt operators eigenlayer status --rpc-url --address
+ +Options: + --rpc-url The URL of the RPC to broadcast the transaction [env: RPC_URL=] + --address
The address of the operator to check [env: OPERATOR_ADDRESS=] + -h, --help Print help ``` # Off-Chain Setup From 5521a897d233357c26305a4123c55121c65167f6 Mon Sep 17 00:00:00 2001 From: thedevbirb Date: Thu, 21 Nov 2024 15:28:02 +0100 Subject: [PATCH 35/35] fix(bolt-cli): don't require secret keys for pubkeys command unless wanted --- bolt-cli/src/cli.rs | 37 ++++++++++++++++++++++++++++--- bolt-cli/src/commands/delegate.rs | 8 +++---- bolt-cli/src/commands/pubkeys.rs | 36 +++++++++++++++++------------- testnets/holesky/README.md | 3 +++ 4 files changed, 61 insertions(+), 23 deletions(-) diff --git a/bolt-cli/src/cli.rs b/bolt-cli/src/cli.rs index 63ae7a84c..6dfc5ad0d 100644 --- a/bolt-cli/src/cli.rs +++ b/bolt-cli/src/cli.rs @@ -71,7 +71,7 @@ pub struct DelegateCommand { /// The source of the private key. #[clap(subcommand)] - pub source: KeySource, + pub source: SecretsSource, } /// Command for outputting a list of pubkeys in JSON format. @@ -83,7 +83,7 @@ pub struct PubkeysCommand { /// The source of the private keys from which to extract the pubkeys. #[clap(subcommand)] - pub source: KeySource, + pub source: KeysSource, } /// Command for sending a preconfirmation request to a bolt proposer. @@ -275,7 +275,38 @@ pub enum Action { } #[derive(Debug, Clone, Parser)] -pub enum KeySource { +pub enum KeysSource { + /// Use directly local public keys as source. + PublicKeys { + /// The public keys in hex format. Multiple public keys must be seperated by commas. + #[clap(long, env = "PUBLIC_KEYS", value_delimiter = ',', hide_env_values = true)] + public_keys: Vec, + }, + + /// Use local secret keys to generate the associated public keys. + SecretKeys { + /// The private key in hex format. Multiple secret keys must be seperated by commas. + #[clap(long, env = "SECRET_KEYS", value_delimiter = ',', hide_env_values = true)] + secret_keys: Vec, + }, + + /// Use an EIP-2335 filesystem keystore directory as source for public keys. + LocalKeystore { + /// The path to the keystore file. + #[clap(long, env = "KEYSTORE_PATH")] + path: String, + }, + + /// Use a remote DIRK keystore as source for public keys. + Dirk { + /// The options for connecting to the DIRK keystore. + #[clap(flatten)] + opts: DirkOpts, + }, +} + +#[derive(Debug, Clone, Parser)] +pub enum SecretsSource { /// Use local secret keys to generate the signed messages. SecretKeys { /// The private key in hex format. diff --git a/bolt-cli/src/commands/delegate.rs b/bolt-cli/src/commands/delegate.rs index 24f1e9a15..8dbf096c8 100644 --- a/bolt-cli/src/commands/delegate.rs +++ b/bolt-cli/src/commands/delegate.rs @@ -11,7 +11,7 @@ use serde::Serialize; use tracing::{debug, warn}; use crate::{ - cli::{Action, Chain, DelegateCommand, KeySource}, + cli::{Action, Chain, DelegateCommand, SecretsSource}, common::{ dirk::Dirk, keystore::{keystore_paths, KeystoreError, KeystoreSecret}, @@ -27,7 +27,7 @@ impl DelegateCommand { /// Run the `delegate` command. pub async fn run(self) -> Result<()> { match self.source { - KeySource::SecretKeys { secret_keys } => { + SecretsSource::SecretKeys { secret_keys } => { let delegatee_pubkey = parse_bls_public_key(&self.delegatee_pubkey)?; let signed_messages = generate_from_local_keys( &secret_keys, @@ -45,7 +45,7 @@ impl DelegateCommand { write_to_file(&self.out, &signed_messages)?; println!("Signed delegation messages generated and saved to {}", self.out); } - KeySource::LocalKeystore { opts } => { + SecretsSource::LocalKeystore { opts } => { let keystore_secret = KeystoreSecret::from_keystore_options(&opts)?; let delegatee_pubkey = parse_bls_public_key(&self.delegatee_pubkey)?; let signed_messages = generate_from_keystore( @@ -65,7 +65,7 @@ impl DelegateCommand { write_to_file(&self.out, &signed_messages)?; println!("Signed delegation messages generated and saved to {}", self.out); } - KeySource::Dirk { opts } => { + SecretsSource::Dirk { opts } => { let mut dirk = Dirk::connect(opts.url, opts.tls_credentials).await?; let delegatee_pubkey = parse_bls_public_key(&self.delegatee_pubkey)?; diff --git a/bolt-cli/src/commands/pubkeys.rs b/bolt-cli/src/commands/pubkeys.rs index ba10ff38a..0609fbe78 100644 --- a/bolt-cli/src/commands/pubkeys.rs +++ b/bolt-cli/src/commands/pubkeys.rs @@ -3,10 +3,10 @@ use eyre::Result; use lighthouse_eth2_keystore::Keystore; use crate::{ - cli::{KeySource, PubkeysCommand}, + cli::{KeysSource, PubkeysCommand}, common::{ dirk::Dirk, - keystore::{keystore_paths, KeystoreError, KeystoreSecret}, + keystore::{keystore_paths, KeystoreError}, write_to_file, }, pb::eth2_signer_api::Account, @@ -15,20 +15,23 @@ use crate::{ impl PubkeysCommand { pub async fn run(self) -> Result<()> { match self.source { - KeySource::SecretKeys { secret_keys } => { + KeysSource::PublicKeys { public_keys } => { + write_to_file(&self.out, &public_keys)?; + println!("Pubkeys saved to {}", self.out); + } + KeysSource::SecretKeys { secret_keys } => { let pubkeys = list_from_local_keys(&secret_keys)?; write_to_file(&self.out, &pubkeys)?; println!("Pubkeys generated and saved to {}", self.out); } - KeySource::LocalKeystore { opts } => { - let keystore_secret = KeystoreSecret::from_keystore_options(&opts)?; - let pubkeys = list_from_keystore(&opts.path, keystore_secret)?; + KeysSource::LocalKeystore { path } => { + let pubkeys = list_from_keystore(&path)?; write_to_file(&self.out, &pubkeys)?; - println!("Pubkeys generated and saved to {}", self.out); + println!("Pubkeys generated from local keystore and saved to {}", self.out); } - KeySource::Dirk { opts } => { + KeysSource::Dirk { opts } => { // Note: we don't need to unlock wallets to list pubkeys let mut dirk = Dirk::connect(opts.url, opts.tls_credentials).await?; @@ -36,7 +39,7 @@ impl PubkeysCommand { let pubkeys = list_from_dirk_accounts(&accounts)?; write_to_file(&self.out, &pubkeys)?; - println!("Pubkeys generated and saved to {}", self.out); + println!("Pubkeys generated from Dirk and saved to {}", self.out); } } @@ -57,18 +60,19 @@ pub fn list_from_local_keys(secret_keys: &[String]) -> Result> } /// Derive public keys from the keystore files in the provided directory. -pub fn list_from_keystore( - keys_path: &str, - keystore_secret: KeystoreSecret, -) -> Result> { +pub fn list_from_keystore(keys_path: &str) -> Result> { let keystores_paths = keystore_paths(keys_path)?; let mut pubkeys = Vec::with_capacity(keystores_paths.len()); for path in keystores_paths { let ks = Keystore::from_json_file(path).map_err(KeystoreError::Eth2Keystore)?; - let password = keystore_secret.get(ks.pubkey()).ok_or(KeystoreError::MissingPassword)?; - let kp = ks.decrypt_keypair(password.as_bytes()).map_err(KeystoreError::Eth2Keystore)?; - let pubkey = BlsPublicKey::try_from(kp.pk.serialize().to_vec().as_ref())?; + let pubkey = BlsPublicKey::try_from( + ks.public_key() + .expect("to parse public key from keystore") + .serialize() + .to_vec() + .as_ref(), + )?; pubkeys.push(pubkey); } diff --git a/testnets/holesky/README.md b/testnets/holesky/README.md index 40115a9c0..37d1db1bb 100644 --- a/testnets/holesky/README.md +++ b/testnets/holesky/README.md @@ -228,6 +228,9 @@ Options: Print help ``` +To generate the JSON file containing the pubkeys, you can use the `bolt pubkeys` +command. See `bolt pubkeys --help` for more info. + Fill the required options and run the script. If the script executed succesfully, your validators were registered.