Skip to content

Commit

Permalink
feat: send preconfirmation requests to the rpc
Browse files Browse the repository at this point in the history
  • Loading branch information
merklefruit committed Oct 25, 2024
1 parent b2ad8fe commit cc8db38
Show file tree
Hide file tree
Showing 9 changed files with 1,219 additions and 24 deletions.
907 changes: 905 additions & 2 deletions bolt-cli/Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions bolt-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ 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-primitives = "0.8.9"
alloy-signer = "0.5.2"
alloy = { version = "0.5.2", features = ["full"] }

# utils
dotenvy = "0.15.7"
Expand All @@ -36,6 +35,7 @@ hex = "0.4.3"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
reqwest = "0.12.8"
rand = "0.8.5"

[dev-dependencies]
tempfile = "3.13.0"
Expand Down
30 changes: 15 additions & 15 deletions bolt-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ use clap::{
builder::styling::{AnsiColor, Color, Style},
Parser, Subcommand, ValueEnum,
};
use serde::Deserialize;
use reqwest::Url;

use crate::common::keystore::DEFAULT_KEYSTORE_PASSWORD;

/// `bolt` is a CLI tool to interact with Bolt Protocol ✨
#[derive(Parser, Debug, Clone, Deserialize)]
#[derive(Parser, Debug, Clone)]
#[command(author, version, styles = cli_styles(), about, arg_required_else_help(true))]
pub struct Opts {
/// The subcommand to run.
#[clap(subcommand)]
pub command: Commands,
}

#[derive(Subcommand, Debug, Clone, Deserialize)]
#[derive(Subcommand, Debug, Clone)]
pub enum Commands {
/// Generate BLS delegation or revocation messages.
Delegate(DelegateCommand),
Expand All @@ -28,7 +28,7 @@ pub enum Commands {
}

/// Command for generating BLS delegation or revocation messages.
#[derive(Debug, Clone, Deserialize, Parser)]
#[derive(Debug, Clone, Parser)]
pub struct DelegateCommand {
/// The BLS public key to which the delegation message should be signed.
#[clap(long, env = "DELEGATEE_PUBKEY")]
Expand All @@ -53,7 +53,7 @@ pub struct DelegateCommand {
}

/// Command for outputting a list of pubkeys in JSON format.
#[derive(Debug, Clone, Deserialize, Parser)]
#[derive(Debug, Clone, Parser)]
pub struct PubkeysCommand {
/// The output file for the pubkeys.
#[clap(long, env = "OUTPUT_FILE_PATH", default_value = "pubkeys.json")]
Expand All @@ -65,19 +65,19 @@ pub struct PubkeysCommand {
}

/// Command for sending a preconfirmation request to a Bolt proposer.
#[derive(Debug, Clone, Deserialize, Parser)]
#[derive(Debug, Clone, Parser)]
pub struct SendCommand {
/// Bolt Sidecar RPC URL to send requests to.
#[clap(long, env = "SIDECAR_RPC_URL")]
pub sidecar_rpc_url: String,
/// Bolt RPC URL to send requests to and fetch lookahead info from.
#[clap(long, env = "BOLT_RPC_URL", default_value = "http://135.181.191.125:58017")]
pub bolt_rpc_url: Url,

/// The private key to sign the transaction with.
#[clap(long, env = "PRIVATE_KEY", hide_env_values = true)]
pub private_key: String,
}

/// The action to perform.
#[derive(Debug, Clone, ValueEnum, Deserialize)]
#[derive(Debug, Clone, ValueEnum)]
#[clap(rename_all = "kebab_case")]
pub enum Action {
/// Create a delegation message.
Expand All @@ -86,7 +86,7 @@ pub enum Action {
Revoke,
}

#[derive(Debug, Clone, Parser, Deserialize)]
#[derive(Debug, Clone, Parser)]
pub enum KeySource {
/// Use local secret keys to generate the signed messages.
SecretKeys {
Expand All @@ -112,7 +112,7 @@ pub enum KeySource {
}

/// Options for reading a keystore folder.
#[derive(Debug, Clone, Deserialize, Parser)]
#[derive(Debug, Clone, Parser)]
pub struct LocalKeystoreOpts {
/// The path to the keystore file.
#[clap(long, env = "KEYSTORE_PATH", default_value = "validators")]
Expand All @@ -139,7 +139,7 @@ pub struct LocalKeystoreOpts {
}

/// Options for connecting to a DIRK keystore.
#[derive(Debug, Clone, Deserialize, Parser)]
#[derive(Debug, Clone, Parser)]
pub struct DirkOpts {
/// The URL of the DIRK keystore.
#[clap(long, env = "DIRK_URL")]
Expand All @@ -160,7 +160,7 @@ pub struct DirkOpts {
}

/// TLS credentials for connecting to a remote server.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Parser)]
#[derive(Debug, Clone, PartialEq, Eq, Parser)]
pub struct TlsCredentials {
/// Path to the client certificate file. (.crt)
#[clap(long, env = "CLIENT_CERT_PATH")]
Expand All @@ -174,7 +174,7 @@ pub struct TlsCredentials {
}

/// Supported chains for the CLI
#[derive(Debug, Clone, Copy, ValueEnum, Deserialize)]
#[derive(Debug, Clone, Copy, ValueEnum)]
#[clap(rename_all = "kebab_case")]
pub enum Chain {
Mainnet,
Expand Down
6 changes: 4 additions & 2 deletions bolt-cli/src/commands/delegate.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use alloy_primitives::B256;
use alloy_signer::k256::sha2::{Digest, Sha256};
use alloy::{
primitives::B256,
signers::k256::sha2::{Digest, Sha256},
};
use ethereum_consensus::crypto::{
PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, Signature as BlsSignature,
};
Expand Down
134 changes: 133 additions & 1 deletion bolt-cli/src/commands/send.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,142 @@
use eyre::Result;
use alloy::{
eips::eip2718::Encodable2718,
network::{EthereumWallet, TransactionBuilder},
primitives::{keccak256, B256, U256},
providers::{ProviderBuilder, SendableTx},
rpc::types::TransactionRequest,
signers::{local::PrivateKeySigner, Signer},
};
use eyre::{bail, Context, Result};
use rand::Rng;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tracing::info;

use crate::cli::SendCommand;

/// Path to the lookahead endpoint on the Bolt RPC server.
const BOLT_LOOKAHEAD_PATH: &str = "proposers/lookahead?activeOnly=true&futureOnly=true";

impl SendCommand {
/// Run the `send` command.
pub async fn run(self) -> Result<()> {
let wallet: PrivateKeySigner = self.private_key.parse().wrap_err("invalid private key")?;
let transaction_signer = EthereumWallet::from(wallet.clone());

let provider = ProviderBuilder::new()
.with_recommended_fillers()
.wallet(transaction_signer)
.on_http(self.bolt_rpc_url.clone());

// Fetch the lookahead info from the Bolt RPC server
let lookahead_url = self.bolt_rpc_url.join(BOLT_LOOKAHEAD_PATH)?;
let lookahead_res = reqwest::get(lookahead_url).await?.json::<Vec<LookaheadSlot>>().await?;
if lookahead_res.is_empty() {
println!("no bolt proposer found in the lookahead, try again later 🥲");
return Ok(());
}

// Extract the next preconfirmer slot from the lookahead info
let next_preconfirmer_slot = lookahead_res[0].slot;
info!("Next preconfirmer slot: {}", next_preconfirmer_slot);

// generate a simple self-transfer of ETH
let random_data = rand::thread_rng().gen::<[u8; 32]>();
let req = TransactionRequest::default()
.with_to(wallet.address())
.with_value(U256::from(100_000))
.with_input(random_data);

let raw_tx = match provider.fill(req).await? {
SendableTx::Builder(_) => bail!("expected a raw transaction"),
SendableTx::Envelope(raw) => raw.encoded_2718(),
};
let tx_hash = B256::from(keccak256(&raw_tx));

send_rpc_request(
vec![hex::encode(&raw_tx)],
vec![tx_hash],
next_preconfirmer_slot,
self.bolt_rpc_url,
&wallet,
)
.await?;

Ok(())
}
}

async fn send_rpc_request(
txs_rlp: Vec<String>,
tx_hashes: Vec<B256>,
target_slot: u64,
target_sidecar_url: Url,
wallet: &PrivateKeySigner,
) -> Result<()> {
let request = prepare_rpc_request(
"bolt_requestInclusion",
serde_json::json!({
"slot": target_slot,
"txs": txs_rlp,
}),
);

info!(?tx_hashes, target_slot, %target_sidecar_url);
let signature = sign_request(tx_hashes, target_slot, wallet).await?;

let response = reqwest::Client::new()
.post(target_sidecar_url)
.header("content-type", "application/json")
.header("x-bolt-signature", signature)
.body(serde_json::to_string(&request)?)
.send()
.await?;

let response = response.text().await?;

// strip out long series of zeros in the response (to avoid spamming blob contents)
let response = response.replace(&"0".repeat(32), ".").replace(&".".repeat(4), "");
info!("Response: {:?}", response);
Ok(())
}

async fn sign_request(
tx_hashes: Vec<B256>,
target_slot: u64,
wallet: &PrivateKeySigner,
) -> eyre::Result<String> {
let digest = {
let mut data = Vec::new();
let hashes = tx_hashes.iter().map(|hash| hash.as_slice()).collect::<Vec<_>>().concat();
data.extend_from_slice(&hashes);
data.extend_from_slice(target_slot.to_le_bytes().as_slice());
keccak256(data)
};

let signature = hex::encode(wallet.sign_hash(&digest).await?.as_bytes());

Ok(format!("{}:0x{}", wallet.address(), signature))
}

fn prepare_rpc_request(method: &str, params: Value) -> Value {
serde_json::json!({
"id": "1",
"jsonrpc": "2.0",
"method": method,
"params": vec![params],
})
}

/// Info about a specific slot in the beacon chain lookahead.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LookaheadSlot {
/// Slot number in the beacon chain
pub slot: u64,
/// Validator index that will propose in this slot
pub validator_index: u64,
/// Validator pubkey that will propose in this slot
pub validator_pubkey: String,
/// Optional URL of the Bolt sidecar associated with the proposer
pub sidecar_url: Option<String>,
}
Loading

0 comments on commit cc8db38

Please sign in to comment.