Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add client signature test #168

Merged
merged 3 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions bolt-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ This is a simple CLI tool to interact with Bolt.

1. Prepare the environment variables (either in a `.env` file, or as CLI arguments):

- `RPC_URL` or `--rpc-url`: the URL of the Bolt RPC server (default: Chainbound's RPC)
- `PRIVATE_KEY` or `--private-key`: the private key of the account to send transactions from
- `NONCE_OFFSET` or `--nonce-offset`: the offset to add to the account's nonce (default: 0)
- `BLOB` or `--blob`: bool flag to send a blob-carrying transaction (default: false)
- `BOLT_RPC_URL` or `--rpc-url`: the URL of the Bolt RPC server (default: Chainbound's RPC)
- `BOLT_PRIVATE_KEY` or `--private-key`: the private key of the account to send transactions from
- `BOLT_NONCE_OFFSET` or `--nonce-offset`: the offset to add to the account's nonce (default: 0)
- `--blob`: bool flag to send a blob-carrying transaction (default: false)

**Optionally**, you can use the following flags to fetch the lookahead data from the beacon chain directly
instead of relying on the RPC server:

- `--use-registry`: bool flag to fetch data from a local node instead of the RPC_URL (default: false)
- `--registry-address`: the address of the bolt-registry contract
- `--beacon-client-url`: the URL of the CL node to use
- `BOLT_REGISTRY_ADDRESS` or `--registry-address`: the address of the bolt-registry contract
- `BOLT_BEACON_CLIENT_URL` or `--beacon-client-url`: the URL of the CL node to use

1. Run the CLI tool with the desired command and arguments, if any.

Expand Down
86 changes: 64 additions & 22 deletions bolt-client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ use alloy::{
eips::eip2718::Encodable2718,
hex,
network::{EthereumWallet, TransactionBuilder},
primitives::Address,
primitives::{Address, B256},
providers::{Provider, ProviderBuilder},
signers::local::PrivateKeySigner,
};
use beacon_api_client::mainnet::Client as BeaconApiClient;
use clap::Parser;
use eyre::{bail, Result};
use serde_json::{json, Value};
use tracing::info;
use url::Url;

Expand All @@ -24,7 +25,7 @@ struct Opts {
#[clap(
short = 'p',
long,
default_value = "http://135.181.191.125:8015/",
default_value = "http://135.181.191.125:8015/rpc",
env = "BOLT_RPC_URL"
)]
rpc_url: Url,
Expand All @@ -35,11 +36,14 @@ struct Opts {
#[clap(short, long, default_value_t = 0, env = "BOLT_NONCE_OFFSET")]
nonce_offset: u64,
/// Flag for generating a blob tx instead of a regular tx
#[clap(short = 'B', long, default_value_t = false)]
#[clap(long, default_value_t = false)]
blob: bool,
/// Number of transactions to send in a sequence
#[clap(short, long, default_value_t = 1)]
count: u64,
/// Flag for sending all "count" transactions in a single bundle
#[clap(long, default_value_t = false)]
bundle: bool,

/// Flag for using the registry to fetch the lookahead
#[clap(short, long, default_value_t = false, requires_ifs([("true", "registry_address"), ("true", "beacon_client_url")]))]
Expand All @@ -65,7 +69,7 @@ async fn main() -> Result<()> {
info!("starting bolt-client");

let _ = dotenvy::dotenv();
let opts = Opts::parse();
let mut opts = Opts::parse();

let wallet: PrivateKeySigner = opts.private_key.parse().expect("invalid private key");
let transaction_signer: EthereumWallet = wallet.clone().into();
Expand All @@ -80,38 +84,77 @@ async fn main() -> Result<()> {
let duties = get_proposer_duties(&beacon_api_client, curr_slot, curr_slot / 32).await?;
match registry.next_preconfer_from_registry(duties).await {
Ok(Some((endpoint, slot))) => (Url::parse(&endpoint)?, slot),
Ok(None) => bail!("no next preconfer slot found"),
Ok(None) => bail!("no next preconfer slot found, try again later"),
Err(e) => bail!("error fetching next preconfer slot from registry: {:?}", e),
}
} else {
// TODO: remove "cbOnly=true"
let url =
opts.rpc_url.join("proposers/lookahead?onlyActive=true&onlyFuture=true&cbOnly=true")?;
let lookahead_response = reqwest::get(url).await?.json::<serde_json::Value>().await?;
opts.rpc_url.join("proposers/lookahead?activeOnly=true&futureOnly=true&cbOnly=true")?;
let lookahead_response = reqwest::get(url).await?.json::<Value>().await?;
if lookahead_response.as_array().unwrap_or(&vec![]).is_empty() {
bail!("no bolt proposer found in lookahead, try again later");
}
let next_preconfer_slot = lookahead_response[0].get("slot").unwrap().as_u64().unwrap();
(opts.rpc_url.join("/rpc")?, next_preconfer_slot)
(opts.rpc_url, next_preconfer_slot)
};

let mut tx = if opts.blob { generate_random_blob_tx() } else { generate_random_tx() };
tx.set_from(sender);
tx.set_chain_id(provider.get_chain_id().await?);
tx.set_nonce(provider.get_transaction_count(sender).await? + opts.nonce_offset);
let mut txs_rlp = Vec::with_capacity(opts.count as usize);
let mut tx_hashes = Vec::with_capacity(opts.count as usize);
for _ in 0..opts.count {
let mut tx = if opts.blob { generate_random_blob_tx() } else { generate_random_tx() };
tx.set_from(sender);
tx.set_chain_id(provider.get_chain_id().await?);
tx.set_nonce(provider.get_transaction_count(sender).await? + opts.nonce_offset);

// Set the nonce offset for the next transaction
opts.nonce_offset += 1;

let tx_signed = tx.build(&transaction_signer).await?;
let tx_hash = tx_signed.tx_hash();
let tx_rlp = hex::encode(tx_signed.encoded_2718());

if opts.bundle {
// store transactions in a bundle to send them all at once
txs_rlp.push(tx_rlp);
tx_hashes.push(*tx_hash);
} else {
// Send rpc requests singularly for each transaction
send_rpc_request(
vec![tx_rlp.clone()],
vec![*tx_hash],
target_slot,
target_sidecar_url.clone(),
&wallet,
)
.await?;
}
}

let tx_signed = tx.build(&transaction_signer).await?;
let tx_hash = tx_signed.tx_hash();
let tx_rlp = hex::encode(tx_signed.encoded_2718());
if opts.bundle {
send_rpc_request(txs_rlp, tx_hashes, target_slot, target_sidecar_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",
vec![serde_json::json!({
json!({
"slot": target_slot,
"txs": vec![tx_rlp],
})],
"txs": txs_rlp,
}),
);

info!("Transaction hash: {}", tx_hash);

let signature = sign_request(vec![tx_hash], target_slot, &wallet).await?;
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)
Expand All @@ -126,6 +169,5 @@ async fn main() -> Result<()> {
// 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(())
}
49 changes: 44 additions & 5 deletions bolt-client/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ pub fn generate_random_blob_tx() -> TransactionRequest {
.with_blob_sidecar(sidecar)
}

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

Expand All @@ -68,7 +68,7 @@ pub async fn get_proposer_duties(
}

pub async fn sign_request(
tx_hashes: Vec<&B256>,
tx_hashes: Vec<B256>,
target_slot: u64,
wallet: &PrivateKeySigner,
) -> eyre::Result<String> {
Expand All @@ -89,7 +89,12 @@ pub async fn sign_request(
mod tests {
use std::str::FromStr;

use alloy::{primitives::B256, signers::local::PrivateKeySigner};
use alloy::{
primitives::{keccak256, Signature, B256},
signers::local::PrivateKeySigner,
};

use crate::sign_request;

#[tokio::test]
async fn test_sign_request() -> eyre::Result<()> {
Expand All @@ -98,12 +103,46 @@ mod tests {
B256::from_str("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")?;
let target_slot = 42;

let signature = super::sign_request(vec![&tx_hash], target_slot, &wallet).await?;
let signature = sign_request(vec![tx_hash], target_slot, &wallet).await?;
let parts: Vec<&str> = signature.split(':').collect();

assert_eq!(parts.len(), 2);
assert_eq!(parts[0], wallet.address().to_string());
assert_eq!(parts[1].len(), 130);
Ok(())
}

#[tokio::test]
async fn test_verify_signature() -> eyre::Result<()> {
// Randomly generated private key
let private_key = "0xfa4c3c87627a58684fb519f7b01a31ef31e56f414e8aa56a15f574381a5a7a9c";
let tx_hash = "0x6938dbd0649ce26af79b0cca677b493257bd87c17d25ff717feba33c8b3920b3";
let expected_signature = "0x10386a2aF29854954645C9710A038AcF4B2F1752:0x8db9bbcc1db5257c80138bd1df0185305918dbc8a607f63458ea885a6ccce5177a73417d693953b9f5c017a927e9c8acbf24c05b09a55f1f3fa83db57931ed9e1c";
let target_slot = 254464;

let wallet = PrivateKeySigner::from_str(private_key)?;
let tx_hash = B256::from_str(tx_hash)?;

let signature = sign_request(vec![tx_hash], target_slot, &wallet).await?;

assert_eq!(signature, expected_signature);

let expected_signer = expected_signature.split(':').next().unwrap();
let expected_sig = expected_signature.split(':').last().unwrap();
let sig = Signature::from_str(expected_sig)?;

// recompute the prehash again
let digest = {
let mut data = Vec::new();
data.extend_from_slice(tx_hash.as_slice());
data.extend_from_slice(target_slot.to_le_bytes().as_slice());
keccak256(data)
};

let recovered_address = sig.recover_address_from_prehash(&digest)?;
assert_eq!(recovered_address, wallet.address());
assert_eq!(recovered_address.to_string(), expected_signer);

Ok(())
}
}
Loading