diff --git a/RELEASE.md b/RELEASE.md
index 403d940f..fd67370c 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -45,4 +45,4 @@ Github release assets.
These will then be automatically consumed by `boltup` consumers when selecting the
tag they want to use for their Bolt cli installation.
-e.g. `boltup --tag v0.3.0-alpha` will pick one of the tarballs in the `v0.3.0-alpha` release.
+e.g. `boltup --tag v0.1.0` will pick one of the tarballs in the `cli-v0.1.0` release.
diff --git a/bolt-cli/Cargo.lock b/bolt-cli/Cargo.lock
index 4a111268..132828e1 100644
--- a/bolt-cli/Cargo.lock
+++ b/bolt-cli/Cargo.lock
@@ -1257,7 +1257,7 @@ dependencies = [
[[package]]
name = "bolt"
-version = "0.3.0-alpha"
+version = "0.1.0"
dependencies = [
"alloy",
"alloy-node-bindings",
diff --git a/bolt-cli/Cargo.toml b/bolt-cli/Cargo.toml
index 6c46c4cf..117f9999 100644
--- a/bolt-cli/Cargo.toml
+++ b/bolt-cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "bolt"
-version = "0.3.0-alpha"
+version = "0.1.0"
edition = "2021"
[dependencies]
@@ -26,9 +26,9 @@ bls12_381 = "0.8.0"
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.7.0", features = [
- "full",
- "provider-anvil-api",
- "provider-anvil-node",
+ "full",
+ "provider-anvil-api",
+ "provider-anvil-node",
] }
# utils
diff --git a/bolt-cli/README.md b/bolt-cli/README.md
index e1bf3e0f..c65099cf 100644
--- a/bolt-cli/README.md
+++ b/bolt-cli/README.md
@@ -19,7 +19,7 @@ git clone git@github.com:chainbound/bolt.git
cd bolt-cli
# build and install the binary on your machine
-cargo install --path . --force
+cargo install --path . --force --locked
# test the installation
bolt --version
@@ -34,6 +34,7 @@ Available commands:
- [`send`](#send) - Send a preconfirmation request to a Bolt sidecar.
- [`validators`](#validators) - Subcommand for bolt validators.
- [`operators`](#operators) - Subcommand for bolt operators.
+- [`generate`](#generate) - Subcommand for generating bolt related data.
---
@@ -365,6 +366,31 @@ Options:
---
+### `generate`
+
+The `generate` subcommand contains functionality for generating bolt related data like BLS keypairs.
+
+
+Usage
+
+```text
+❯ bolt generate --help
+Useful data generation commands
+
+Usage: bolt generate
+
+Commands:
+ bls Generate a BLS keypair
+ help Print this message or the help of the given subcommand(s)
+
+Options:
+ -h, --help Print help
+```
+
+
+
+---
+
## Security
The Bolt CLI is designed to be used offline. It does not require any network connections
diff --git a/bolt-cli/src/commands/operators.rs b/bolt-cli/src/commands/operators.rs
index 5a11b492..76563ab4 100644
--- a/bolt-cli/src/commands/operators.rs
+++ b/bolt-cli/src/commands/operators.rs
@@ -4,6 +4,7 @@ use alloy::{
primitives::{utils::format_ether, Bytes},
providers::{Provider, ProviderBuilder, WalletProvider},
signers::{local::PrivateKeySigner, SignerSync},
+ sol_types::SolInterface,
};
use eyre::Context;
use tracing::{info, warn};
@@ -12,11 +13,11 @@ use crate::{
cli::{
Chain, EigenLayerSubcommand, OperatorsCommand, OperatorsSubcommand, SymbioticSubcommand,
},
- common::{bolt_manager::BoltManagerContract, request_confirmation},
+ common::{bolt_manager::BoltManagerContract, request_confirmation, try_parse_contract_error},
contracts::{
bolt::{
- BoltEigenLayerMiddleware,
- BoltSymbioticMiddleware::{self},
+ BoltEigenLayerMiddleware::{self, BoltEigenLayerMiddlewareErrors},
+ BoltSymbioticMiddleware::{self, BoltSymbioticMiddlewareErrors},
SignatureWithSaltAndExpiry,
},
deployments_for_chain,
@@ -149,23 +150,40 @@ impl OperatorsCommand {
Bytes::from(signer.sign_hash_sync(&signature_digest_hash)?.as_bytes());
let signature = SignatureWithSaltAndExpiry { signature, expiry, salt };
- let result = bolt_eigenlayer_middleware
+ match bolt_eigenlayer_middleware
.registerOperator(operator_rpc.to_string(), signature)
.send()
- .await?;
-
- info!(
- hash = ?result.tx_hash(),
- "registerOperator transaction sent, awaiting receipt..."
- );
-
- let receipt = result.get_receipt().await?;
- if !receipt.status() {
- eyre::bail!("Transaction failed: {:?}", receipt)
+ .await
+ {
+ Ok(pending) => {
+ 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 EigenLayer operator");
+ }
+ Err(e) => {
+ match try_parse_contract_error::(e)? {
+ BoltEigenLayerMiddlewareErrors::AlreadyRegistered(_) => {
+ eyre::bail!("Operator already registered in bolt")
+ }
+ BoltEigenLayerMiddlewareErrors::NotOperator(_) => {
+ eyre::bail!("Operator not registered in EigenLayer")
+ }
+ other => unreachable!(
+ "Unexpected error with selector {:?}",
+ other.selector()
+ ),
+ }
+ }
}
- info!("Succesfully registered EigenLayer operator");
-
Ok(())
}
EigenLayerSubcommand::Deregister { rpc_url, operator_private_key } => {
@@ -192,20 +210,33 @@ impl OperatorsCommand {
let bolt_eigenlayer_middleware =
BoltEigenLayerMiddleware::new(bolt_avs_address, provider);
- let result = bolt_eigenlayer_middleware.deregisterOperator().send().await?;
-
- info!(
- hash = ?result.tx_hash(),
- "deregisterOperator transaction sent, awaiting receipt..."
- );
-
- let receipt = result.get_receipt().await?;
- if !receipt.status() {
- eyre::bail!("Transaction failed: {:?}", receipt)
+ match bolt_eigenlayer_middleware.deregisterOperator().send().await {
+ Ok(pending) => {
+ info!(
+ hash = ?pending.tx_hash(),
+ "deregisterOperator transaction sent, awaiting receipt..."
+ );
+
+ let receipt = pending.get_receipt().await?;
+ if !receipt.status() {
+ eyre::bail!("Transaction failed: {:?}", receipt)
+ }
+
+ info!("Succesfully deregistered EigenLayer operator");
+ }
+ Err(e) => {
+ match try_parse_contract_error::(e)? {
+ BoltEigenLayerMiddlewareErrors::NotRegistered(_) => {
+ eyre::bail!("Operator not registered in bolt")
+ }
+ other => unreachable!(
+ "Unexpected error with selector {:?}",
+ other.selector()
+ ),
+ }
+ }
}
- info!("Succesfully deregistered EigenLayer operator");
-
Ok(())
}
EigenLayerSubcommand::Status { rpc_url: rpc, address } => {
@@ -268,21 +299,36 @@ impl OperatorsCommand {
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)
+ match middleware.registerOperator(operator_rpc.to_string()).send().await {
+ Ok(pending) => {
+ 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");
+ }
+ Err(e) => {
+ match try_parse_contract_error::(e)? {
+ BoltSymbioticMiddlewareErrors::AlreadyRegistered(_) => {
+ eyre::bail!("Operator already registered in bolt")
+ }
+ BoltSymbioticMiddlewareErrors::NotOperator(_) => {
+ eyre::bail!("Operator not registered in Symbiotic")
+ }
+ other => unreachable!(
+ "Unexpected error with selector {:?}",
+ other.selector()
+ ),
+ }
+ }
}
- info!("Succesfully registered Symbiotic operator");
-
Ok(())
}
SymbioticSubcommand::Deregister { rpc_url, operator_private_key } => {
@@ -310,20 +356,33 @@ impl OperatorsCommand {
provider,
);
- let pending = middleware.deregisterOperator().send().await?;
-
- info!(
- hash = ?pending.tx_hash(),
- "deregisterOperator transaction sent, awaiting receipt..."
- );
-
- let receipt = pending.get_receipt().await?;
- if !receipt.status() {
- eyre::bail!("Transaction failed: {:?}", receipt)
+ match middleware.deregisterOperator().send().await {
+ Ok(pending) => {
+ info!(
+ hash = ?pending.tx_hash(),
+ "deregisterOperator transaction sent, awaiting receipt..."
+ );
+
+ let receipt = pending.get_receipt().await?;
+ if !receipt.status() {
+ eyre::bail!("Transaction failed: {:?}", receipt)
+ }
+
+ info!("Succesfully deregistered Symbiotic operator");
+ }
+ Err(e) => {
+ match try_parse_contract_error::(e)? {
+ BoltSymbioticMiddlewareErrors::NotRegistered(_) => {
+ eyre::bail!("Operator not registered in bolt")
+ }
+ other => unreachable!(
+ "Unexpected error with selector {:?}",
+ other.selector()
+ ),
+ }
+ }
}
- info!("Succesfully deregistered Symbiotic operator");
-
Ok(())
}
SymbioticSubcommand::Status { rpc_url, address } => {
diff --git a/bolt-cli/src/commands/validators.rs b/bolt-cli/src/commands/validators.rs
index 7f786cfa..6ce825fb 100644
--- a/bolt-cli/src/commands/validators.rs
+++ b/bolt-cli/src/commands/validators.rs
@@ -2,6 +2,7 @@ use alloy::{
network::EthereumWallet,
providers::{Provider, ProviderBuilder},
signers::local::PrivateKeySigner,
+ sol_types::SolInterface,
};
use ethereum_consensus::crypto::PublicKey as BlsPublicKey;
use eyre::Context;
@@ -9,8 +10,11 @@ use tracing::{info, warn};
use crate::{
cli::{Chain, ValidatorsCommand, ValidatorsSubcommand},
- common::{hash::compress_bls_pubkey, request_confirmation},
- contracts::{bolt::BoltValidators, deployments_for_chain},
+ common::{hash::compress_bls_pubkey, request_confirmation, try_parse_contract_error},
+ contracts::{
+ bolt::BoltValidators::{self, BoltValidatorsErrors},
+ deployments_for_chain,
+ },
};
impl ValidatorsCommand {
@@ -54,26 +58,48 @@ impl ValidatorsCommand {
request_confirmation();
- let pending = bolt_validators
+ match bolt_validators
.batchRegisterValidatorsUnsafe(
pubkey_hashes,
max_committed_gas_limit,
authorized_operator,
)
.send()
- .await?;
+ .await
+ {
+ Ok(pending) => {
+ info!(
+ hash = ?pending.tx_hash(),
+ "batchRegisterValidatorsUnsafe transaction sent, awaiting receipt..."
+ );
+ let receipt = pending.get_receipt().await?;
+ if !receipt.status() {
+ eyre::bail!("Transaction failed: {:?}", receipt)
+ }
- info!(
- hash = ?pending.tx_hash(),
- "batchRegisterValidatorsUnsafe transaction sent, awaiting receipt..."
- );
- let receipt = pending.get_receipt().await?;
- if !receipt.status() {
- eyre::bail!("Transaction failed: {:?}", receipt)
+ info!("Successfully registered validators into bolt");
+ }
+ Err(e) => {
+ let decoded = try_parse_contract_error::(e)?;
+
+ match decoded {
+ BoltValidatorsErrors::ValidatorAlreadyExists(b) => {
+ eyre::bail!(format!(
+ "Validator already exists (pubkeyHash: {:?})",
+ b.pubkeyHash
+ ))
+ }
+ BoltValidatorsErrors::InvalidAuthorizedOperator(_) => {
+ eyre::bail!("Invalid authorized operator")
+ }
+ other => unreachable!(
+ "Unexpected error with selector {:?}",
+ other.selector()
+ ),
+ }
+ }
}
- info!("Successfully registered validators into bolt");
-
Ok(())
}
ValidatorsSubcommand::Status { rpc_url, pubkeys_path, pubkeys } => {
diff --git a/bolt-cli/src/common/bolt_manager.rs b/bolt-cli/src/common/bolt_manager.rs
index 35a763a6..d775af5e 100644
--- a/bolt-cli/src/common/bolt_manager.rs
+++ b/bolt-cli/src/common/bolt_manager.rs
@@ -1,20 +1,20 @@
#![allow(dead_code)] // TODO: rm this
-use std::str::FromStr;
-
use alloy::{
- contract::{Error as ContractError, Result as ContractResult},
- primitives::{Address, Bytes, B256},
+ contract::Result as ContractResult,
+ primitives::{Address, B256},
providers::{ProviderBuilder, RootProvider},
sol,
sol_types::{Error as SolError, SolInterface},
- transports::{http::Http, TransportError},
+ transports::http::Http,
};
use reqwest::{Client, Url};
use serde::Serialize;
use BoltManagerContract::{BoltManagerContractErrors, BoltManagerContractInstance, ProposerStatus};
+use super::try_parse_contract_error;
+
/// Bolt Manager contract bindings.
#[derive(Debug, Clone)]
pub struct BoltManager(BoltManagerContractInstance, RootProvider>>);
@@ -51,16 +51,7 @@ impl BoltManager {
// TODO: clean this after https://github.com/alloy-rs/alloy/issues/787 is merged
let error = match returndata.map(|data| data._0) {
Ok(proposer) => return Ok(Some(proposer)),
- Err(error) => match error {
- ContractError::TransportError(TransportError::ErrorResp(err)) => {
- let data = err.data.unwrap_or_default();
- let data = data.get().trim_matches('"');
- let data = Bytes::from_str(data).unwrap_or_default();
-
- BoltManagerContractErrors::abi_decode(&data, true)?
- }
- e => return Err(e),
- },
+ Err(error) => try_parse_contract_error::(error)?,
};
if matches!(error, BoltManagerContractErrors::ValidatorDoesNotExist(_)) {
diff --git a/bolt-cli/src/common/mod.rs b/bolt-cli/src/common/mod.rs
index a1f23b6d..66192a60 100644
--- a/bolt-cli/src/common/mod.rs
+++ b/bolt-cli/src/common/mod.rs
@@ -1,5 +1,9 @@
-use std::{fs, io::Write, path::PathBuf};
+use std::{fs, io::Write, path::PathBuf, str::FromStr};
+use alloy::{
+ contract::Error as ContractError, primitives::Bytes, sol_types::SolInterface,
+ transports::TransportError,
+};
use ethereum_consensus::crypto::PublicKey as BlsPublicKey;
use eyre::{Context, Result};
use serde::Serialize;
@@ -40,6 +44,41 @@ pub fn write_to_file(out: &str, data: &T) -> Result<()> {
Ok(())
}
+/// Try to decode a contract error into a specific Solidity error interface.
+/// If the error cannot be decoded or it is not a contract error, return the original error.
+///
+/// Example usage:
+///
+/// ```rust no_run
+/// sol! {
+/// library ErrorLib {
+/// error SomeError(uint256 code);
+/// }
+/// }
+///
+/// // call a contract that may return an error with the SomeError interface
+/// let returndata = match myContract.call().await {
+/// Ok(returndata) => returndata,
+/// Err(err) => {
+/// let decoded_error = try_decode_contract_error::(err)?;
+/// // handle the decoded error however you want; for example, return it
+/// return Err(decoded_error);
+/// },
+/// }
+/// ```
+pub fn try_parse_contract_error(error: ContractError) -> Result {
+ match error {
+ ContractError::TransportError(TransportError::ErrorResp(resp)) => {
+ let data = resp.data.unwrap_or_default();
+ let data = data.get().trim_matches('"');
+ let data = Bytes::from_str(data).unwrap_or_default();
+
+ T::abi_decode(&data, true).map_err(Into::into)
+ }
+ _ => Err(error),
+ }
+}
+
/// Asks whether the user wants to proceed further. If not, the process is exited.
#[allow(unreachable_code)]
pub fn request_confirmation() {
diff --git a/bolt-cli/src/contracts/bolt.rs b/bolt-cli/src/contracts/bolt.rs
index b74f8f7a..f4af64ba 100644
--- a/bolt-cli/src/contracts/bolt.rs
+++ b/bolt-cli/src/contracts/bolt.rs
@@ -25,10 +25,15 @@ sol! {
/// @return ValidatorInfo struct
function getValidatorByPubkeyHash(bytes20 pubkeyHash) public view returns (ValidatorInfo memory);
+ #[derive(Debug)]
error KeyNotFound();
+ #[derive(Debug)]
error InvalidQuery();
#[derive(Debug)]
error ValidatorDoesNotExist(bytes20 pubkeyHash);
+ #[derive(Debug)]
+ error ValidatorAlreadyExists(bytes20 pubkeyHash);
+ #[derive(Debug)]
error InvalidAuthorizedOperator();
}
}
@@ -55,6 +60,10 @@ sol! {
/// @dev This requires calling the EigenLayer AVS Directory contract to deregister the operator.
/// EigenLayer internally contains a mapping from `msg.sender` (our AVS contract) to the operator.
function deregisterOperator() public;
+
+ error AlreadyRegistered();
+ error NotOperator();
+ error NotRegistered();
}
#[allow(missing_docs)]
@@ -74,5 +83,9 @@ sol! {
/// @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);
+
+ error AlreadyRegistered();
+ error NotOperator();
+ error NotRegistered();
}
}
diff --git a/boltup/README.md b/boltup/README.md
index a2dea1a0..64f48c02 100644
--- a/boltup/README.md
+++ b/boltup/README.md
@@ -16,7 +16,7 @@ After the installation is complete, you can run `boltup` to install or update th
boltup --tag [version]
# Example
-boltup --tag v0.3.0-alpha.1
+boltup --tag v0.1.0
```
After the installation is complete, you can run `bolt` to use the bolt CLI tool.
diff --git a/boltup/boltup.sh b/boltup/boltup.sh
index 783eb7ed..ce7fb090 100755
--- a/boltup/boltup.sh
+++ b/boltup/boltup.sh
@@ -49,6 +49,9 @@ main() {
BOLTUP_TAG="v${BOLTUP_TAG}"
fi
+ # Add `cli-` prefix to the tag, that's were the release is located
+ BOLTUP_TAG="cli-${BOLTUP_TAG}"
+
say "installing bolt (tag ${BOLTUP_TAG})"
# Figure out the platform: one of "linux", "darwin" or "win32"
diff --git a/testnets/holesky/QUICK_START.md b/testnets/holesky/QUICK_START.md
index aace77b6..abb54d83 100644
--- a/testnets/holesky/QUICK_START.md
+++ b/testnets/holesky/QUICK_START.md
@@ -40,7 +40,7 @@ curl -L https://raw.githubusercontent.com/chainbound/bolt/unstable/boltup/instal
exec $SHELL
# install the bolt-cli binary for holesky
-boltup --tag v0.3.0-alpha
+boltup --tag v0.1.0
# check for successful installation
bolt --help
diff --git a/testnets/holesky/README.md b/testnets/holesky/README.md
index 833eb9b5..4056410f 100644
--- a/testnets/holesky/README.md
+++ b/testnets/holesky/README.md
@@ -889,7 +889,7 @@ curl -L https://raw.githubusercontent.com/chainbound/bolt/unstable/boltup/instal
exec $SHELL
# install the bolt-cli binary for holesky
-boltup --tag v0.3.0-alpha
+boltup --tag v0.1.0
# check for successful installation
bolt --help