diff --git a/Cargo.toml b/Cargo.toml index a47c1ff..e732185 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ path = "bin/submit_transaction.rs" init4-bin-base = "0.3" signet-zenith = { git = "https://github.com/init4tech/signet-sdk", branch = "main" } +signet-constants = { git = "https://github.com/init4tech/signet-sdk", branch = "main" } signet-types = { git = "https://github.com/init4tech/signet-sdk", branch = "main" } signet-bundle = { git = "https://github.com/init4tech/signet-sdk", branch = "main" } signet-sim = { git = "https://github.com/init4tech/signet-sdk", branch = "main" } diff --git a/bin/builder.rs b/bin/builder.rs index 0611245..362755a 100644 --- a/bin/builder.rs +++ b/bin/builder.rs @@ -1,10 +1,7 @@ use builder::{ config::BuilderConfig, service::serve_builder, - tasks::{ - block::Simulator, bundler, metrics::MetricsTask, oauth::Authenticator, submit::SubmitTask, - tx_poller, - }, + tasks::{block::Simulator, bundler, metrics::MetricsTask, submit::SubmitTask, tx_poller}, }; use init4_bin_base::{deps::tracing, utils::from_env::FromEnv}; use signet_sim::SimCache; @@ -22,35 +19,26 @@ async fn main() -> eyre::Result<()> { let config = BuilderConfig::from_env()?.clone(); let constants = SignetSystemConstants::pecorino(); - let authenticator = Authenticator::new(&config)?; + let token = config.oauth_token(); - let (host_provider, sequencer_signer) = - tokio::try_join!(config.connect_host_provider(), config.connect_sequencer_signer(),)?; + let (host_provider, quincey) = + tokio::try_join!(config.connect_host_provider(), config.connect_quincey())?; let ru_provider = config.connect_ru_provider(); let zenith = config.connect_zenith(host_provider.clone()); - let metrics = MetricsTask { host_provider: host_provider.clone() }; + let metrics = MetricsTask { host_provider }; let (tx_channel, metrics_jh) = metrics.spawn(); - let submit = SubmitTask { - token: authenticator.token(), - host_provider, - zenith, - client: reqwest::Client::new(), - sequencer_signer, - config: config.clone(), - outbound_tx_channel: tx_channel, - }; + let submit = + SubmitTask { zenith, quincey, config: config.clone(), outbound_tx_channel: tx_channel }; let tx_poller = tx_poller::TxPoller::new(&config); let (tx_receiver, tx_poller_jh) = tx_poller.spawn(); - let bundle_poller = bundler::BundlePoller::new(&config, authenticator.token()); + let bundle_poller = bundler::BundlePoller::new(&config, token); let (bundle_receiver, bundle_poller_jh) = bundle_poller.spawn(); - let authenticator_jh = authenticator.spawn(); - let (submit_channel, submit_jh) = submit.spawn(); let sim_items = SimCache::new(); @@ -94,9 +82,6 @@ async fn main() -> eyre::Result<()> { _ = server => { tracing::info!("server finished"); } - _ = authenticator_jh => { - tracing::info!("authenticator finished"); - } } tracing::info!("shutting down"); diff --git a/src/config.rs b/src/config.rs index 30e7788..61d1808 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,8 @@ -use crate::signer::{LocalOrAws, SignerError}; +use crate::{ + quincey::Quincey, + signer::{LocalOrAws, SignerError}, + tasks::oauth::{Authenticator, SharedToken}, +}; use alloy::{ network::{Ethereum, EthereumWallet}, primitives::Address, @@ -209,4 +213,31 @@ impl BuilderConfig { pub const fn connect_zenith(&self, provider: HostProvider) -> ZenithInstance { Zenith::new(self.zenith_address, provider) } + + /// Get an oauth2 token for the builder, starting the authenticator if it + // is not already running. + pub fn oauth_token(&self) -> SharedToken { + static ONCE: std::sync::OnceLock = std::sync::OnceLock::new(); + + ONCE.get_or_init(|| { + let authenticator = Authenticator::new(self).unwrap(); + let token = authenticator.token(); + authenticator.spawn(); + token + }) + .clone() + } + + /// Connect to a Quincey, owned or shared. + pub async fn connect_quincey(&self) -> eyre::Result { + if let Some(signer) = self.connect_sequencer_signer().await? { + return Ok(Quincey::new_owned(signer)); + } + + let client = reqwest::Client::new(); + let url = url::Url::parse(&self.quincey_url)?; + let token = self.oauth_token(); + + Ok(Quincey::new_remote(client, url, token)) + } } diff --git a/src/constants.rs b/src/constants.rs deleted file mode 100644 index 9a55d30..0000000 --- a/src/constants.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Constants used in the builder. - -use alloy::primitives::{Address, address}; - -/// The default basefee to use for simulation if RPC fails. -pub const BASEFEE_DEFAULT: u64 = 7; -/// Pecorino Host Chain ID used for the Pecorino network. -pub const PECORINO_HOST_CHAIN_ID: u64 = 3151908; -/// Pecorino Chain ID used for the Pecorino network. -pub const PECORINO_CHAIN_ID: u64 = 14174; -/// Block number at which the Pecorino rollup contract is deployed. -pub const PECORINO_DEPLOY_HEIGHT: u64 = 149984; -/// Address of the orders contract on the host. -pub const HOST_ORDERS: Address = address!("0x4E8cC181805aFC307C83298242271142b8e2f249"); -/// Address of the passage contract on the host. -pub const HOST_PASSAGE: Address = address!("0xd553C4CA4792Af71F4B61231409eaB321c1Dd2Ce"); -/// Address of the transactor contract on the host. -pub const HOST_TRANSACTOR: Address = address!("0x1af3A16857C28917Ab2C4c78Be099fF251669200"); -/// Address of the USDC token contract on the host. -pub const HOST_USDC: Address = address!("0x885F8DB528dC8a38aA3DDad9D3F619746B4a6A81"); -/// Address of the USDT token contract on the host. -pub const HOST_USDT: Address = address!("0x7970D259D4a96764Fa9B23FF0715A35f06f52D1A"); -/// Address of the WBTC token contract on the host. -pub const HOST_WBTC: Address = address!("0x7970D259D4a96764Fa9B23FF0715A35f06f52D1A"); -/// Address of the orders contract on the rollup. -pub const ROLLUP_ORDERS: Address = address!("0x4E8cC181805aFC307C83298242271142b8e2f249"); -/// Address of the passage contract on the rollup. -pub const ROLLUP_PASSAGE: Address = address!("0xd553C4CA4792Af71F4B61231409eaB321c1Dd2Ce"); -/// Base fee recipient address. -pub const BASE_FEE_RECIPIENT: Address = address!("0xe0eDA3701D44511ce419344A4CeD30B52c9Ba231"); -/// Address of the USDC token contract on the rollup. -pub const ROLLUP_USDC: Address = address!("0x0B8BC5e60EE10957E0d1A0d95598fA63E65605e2"); -/// Address of the USDT token contract on the rollup. -pub const ROLLUP_USDT: Address = address!("0xF34326d3521F1b07d1aa63729cB14A372f8A737C"); -/// Address of the WBTC token contract on the rollup. -pub const ROLLUP_WBTC: Address = address!("0xE3d7066115f7d6b65F88Dff86288dB4756a7D733"); diff --git a/src/lib.rs b/src/lib.rs index b1f8c05..967685e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,12 +12,12 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -/// Constants for the Builder. -pub mod constants; - /// Configuration for the Builder binary. pub mod config; +/// Quincey client for signing requests. +pub mod quincey; + /// Implements the `/healthcheck` endpoint. pub mod service; diff --git a/src/quincey.rs b/src/quincey.rs new file mode 100644 index 0000000..2962e30 --- /dev/null +++ b/src/quincey.rs @@ -0,0 +1,80 @@ +use crate::{signer::LocalOrAws, tasks::oauth::SharedToken}; +use alloy::signers::Signer; +use eyre::bail; +use init4_bin_base::deps::tracing::{self, debug, info, instrument, trace}; +use oauth2::TokenResponse; +use reqwest::Client; +use signet_types::{SignRequest, SignResponse}; + +/// A quincey client for making requests to the Quincey API. +#[derive(Debug, Clone)] +pub enum Quincey { + /// A remote quincey, this is used for production environments. + /// The client will access the Quincey API over HTTP(S) via OAuth. + Remote { + /// The remote client. + client: Client, + /// The base URL for the remote API. + url: reqwest::Url, + /// OAuth shared token. + token: SharedToken, + }, + /// An owned quincey, either local or AWS. This is used primarily for + /// testing and development environments. The client will simulate the + /// Quincey API using a local or AWS KMS key. + Owned(LocalOrAws), +} + +impl Quincey { + /// Creates a new Quincey client from the provided URL and token. + pub const fn new_remote(client: Client, url: reqwest::Url, token: SharedToken) -> Self { + Self::Remote { client, url, token } + } + + /// Creates a new Quincey client for making requests to the Quincey API. + pub const fn new_owned(client: LocalOrAws) -> Self { + Self::Owned(client) + } + + async fn sup_owned(&self, sig_request: &SignRequest) -> eyre::Result { + let Self::Owned(signer) = &self else { eyre::bail!("not an owned client") }; + + info!("signing with owned quincey"); + signer + .sign_hash(&sig_request.signing_hash()) + .await + .map_err(Into::into) + .map(|sig| SignResponse { sig, req: *sig_request }) + } + + async fn sup_remote(&self, sig_request: &SignRequest) -> eyre::Result { + let Self::Remote { client, url, token } = &self else { bail!("not a remote client") }; + + let Some(token) = token.read() else { bail!("no token available") }; + + let resp: reqwest::Response = client + .post(url.clone()) + .json(sig_request) + .bearer_auth(token.access_token().secret()) + .send() + .await? + .error_for_status()?; + + let body = resp.bytes().await?; + + debug!(bytes = body.len(), "retrieved response body"); + trace!(body = %String::from_utf8_lossy(&body), "response body"); + + serde_json::from_slice(&body).map_err(Into::into) + } + + /// Get a signature for the provided request, by either using the owned + /// or remote client. + #[instrument(skip(self))] + pub async fn get_signature(&self, sig_request: &SignRequest) -> eyre::Result { + match self { + Self::Owned(_) => self.sup_owned(sig_request).await, + Self::Remote { .. } => self.sup_remote(sig_request).await, + } + } +} diff --git a/src/tasks/block.rs b/src/tasks/block.rs index c878968..b18095b 100644 --- a/src/tasks/block.rs +++ b/src/tasks/block.rs @@ -3,7 +3,6 @@ //! and turns them into valid Pecorino blocks for network submission. use crate::{ config::{BuilderConfig, RuProvider}, - constants::{BASEFEE_DEFAULT, PECORINO_CHAIN_ID}, tasks::bundler::Bundle, }; use alloy::{ @@ -347,7 +346,7 @@ impl Simulator { Some(basefee) => basefee, None => { warn!("get basefee failed - RPC error likely occurred"); - BASEFEE_DEFAULT + todo!() } }; debug!(basefee = basefee, "setting basefee"); @@ -433,7 +432,7 @@ impl trevm::Cfg for PecorinoCfg { fn fill_cfg_env(&self, cfg_env: &mut CfgEnv) { let CfgEnv { chain_id, spec, .. } = cfg_env; - *chain_id = PECORINO_CHAIN_ID; + *chain_id = signet_constants::pecorino::RU_CHAIN_ID; *spec = SpecId::default(); } } diff --git a/src/tasks/submit.rs b/src/tasks/submit.rs index 4b2d911..4280b93 100644 --- a/src/tasks/submit.rs +++ b/src/tasks/submit.rs @@ -1,7 +1,6 @@ use crate::{ config::{HostProvider, ZenithInstance}, - signer::LocalOrAws, - tasks::oauth::SharedToken, + quincey::Quincey, utils::extract_signature_components, }; use alloy::{ @@ -11,16 +10,14 @@ use alloy::{ primitives::{FixedBytes, TxHash, U256}, providers::{Provider as _, SendableTx, WalletProvider}, rpc::types::eth::TransactionRequest, - signers::Signer, sol_types::{SolCall, SolError}, transports::TransportError, }; -use eyre::{Context, bail, eyre}; +use eyre::{bail, eyre}; use init4_bin_base::deps::{ metrics::{counter, histogram}, - tracing::{self, Instrument, debug, debug_span, error, info, instrument, trace, warn}, + tracing::{self, Instrument, debug, debug_span, error, info, instrument, warn}, }; -use oauth2::TokenResponse; use signet_sim::BuiltBlock; use signet_types::{SignRequest, SignResponse}; use signet_zenith::{ @@ -58,48 +55,23 @@ pub enum ControlFlow { /// Submits sidecars in ethereum txns to mainnet ethereum #[derive(Debug)] pub struct SubmitTask { - /// Ethereum Provider - pub host_provider: HostProvider, /// Zenith pub zenith: ZenithInstance, - /// Reqwest - pub client: reqwest::Client, - /// Sequencer Signer - pub sequencer_signer: Option, + + /// Quincey + pub quincey: Quincey, + /// Config pub config: crate::config::BuilderConfig, - /// Authenticator - pub token: SharedToken, + /// Channel over which to send pending transactions pub outbound_tx_channel: mpsc::UnboundedSender, } impl SubmitTask { - #[instrument(skip(self))] - async fn sup_quincey(&self, sig_request: &SignRequest) -> eyre::Result { - info!( - host_block_number = %sig_request.host_block_number, - ru_chain_id = %sig_request.ru_chain_id, - "pinging quincey for signature" - ); - - let Some(token) = self.token.read() else { bail!("no token available") }; - - let resp: reqwest::Response = self - .client - .post(self.config.quincey_url.as_ref()) - .json(sig_request) - .bearer_auth(token.access_token().secret()) - .send() - .await? - .error_for_status()?; - - let body = resp.bytes().await?; - - debug!(bytes = body.len(), "retrieved response body"); - trace!(body = %String::from_utf8_lossy(&body), "response body"); - - serde_json::from_slice(&body).map_err(Into::into) + /// Get the provider from the zenith instance + const fn provider(&self) -> &HostProvider { + self.zenith.provider() } /// Constructs the signing request from the in-progress block passed to it and assigns the @@ -140,7 +112,7 @@ impl SubmitTask { /// Returns the next host block height async fn next_host_block_height(&self) -> eyre::Result { - let result = self.host_provider.get_block_number().await?; + let result = self.provider().get_block_number().await?; let next = result.checked_add(1).ok_or_else(|| eyre!("next host block height overflow"))?; Ok(next) } @@ -164,12 +136,12 @@ impl SubmitTask { let fills = vec![]; // NB: ignored until fills are implemented let tx = self .build_blob_tx(fills, header, v, r, s, in_progress)? - .with_from(self.host_provider.default_signer_address()) + .with_from(self.provider().default_signer_address()) .with_to(self.config.builder_helper_address) .with_gas_limit(1_000_000); if let Err(TransportError::ErrorResp(e)) = - self.host_provider.call(tx.clone()).block(BlockNumberOrTag::Pending.into()).await + self.provider().call(tx.clone()).block(BlockNumberOrTag::Pending.into()).await { error!( code = e.code, @@ -203,12 +175,12 @@ impl SubmitTask { "sending transaction to network" ); - let SendableTx::Envelope(tx) = self.host_provider.fill(tx).await? else { + let SendableTx::Envelope(tx) = self.provider().fill(tx).await? else { bail!("failed to fill transaction") }; // Send the tx via the primary host_provider - let fut = spawn_provider_send!(&self.host_provider, &tx); + let fut = spawn_provider_send!(self.provider(), &tx); // Spawn send_tx futures for all additional broadcast host_providers for host_provider in self.config.connect_additional_broadcast() { @@ -237,26 +209,6 @@ impl SubmitTask { Ok(ControlFlow::Done) } - /// Sign with a local signer if available, otherwise ask quincey - /// for a signature (politely). - #[instrument(skip_all, fields(is_local = self.sequencer_signer.is_some()))] - async fn get_signature(&self, req: SignRequest) -> eyre::Result { - let sig = if let Some(signer) = &self.sequencer_signer { - signer.sign_hash(&req.signing_hash()).await? - } else { - self.sup_quincey(&req) - .await - .wrap_err("failed to get signature from quincey") - .inspect(|_| { - counter!("builder.quincey_signature_acquired").increment(1); - })? - .sig - }; - - debug!(sig = hex::encode(sig.as_bytes()), "acquired signature"); - Ok(SignResponse { req, sig }) - } - #[instrument(skip_all)] async fn handle_inbound(&self, block: &BuiltBlock) -> eyre::Result { info!(txns = block.tx_count(), "handling inbound block"); @@ -272,7 +224,7 @@ impl SubmitTask { "constructed signature request for host block" ); - let signed = self.get_signature(sig_request).await?; + let signed = self.quincey.get_signature(&sig_request).await?; self.submit_transaction(&signed, block).await } diff --git a/src/test_utils.rs b/src/test_utils.rs index 457eb17..f532250 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1,9 +1,5 @@ //! Test utilities for testing builder tasks -use crate::{ - config::BuilderConfig, - constants::{PECORINO_CHAIN_ID, PECORINO_HOST_CHAIN_ID}, - tasks::block::PecorinoBlockEnv, -}; +use crate::{config::BuilderConfig, tasks::block::PecorinoBlockEnv}; use alloy::{ consensus::{SignableTransaction, TxEip1559, TxEnvelope}, primitives::{Address, FixedBytes, TxKind, U256}, @@ -25,8 +21,8 @@ use std::{ /// Sets up a block builder with test values pub fn setup_test_config() -> Result { let config = BuilderConfig { - host_chain_id: PECORINO_HOST_CHAIN_ID, - ru_chain_id: PECORINO_CHAIN_ID, + host_chain_id: signet_constants::pecorino::HOST_CHAIN_ID, + ru_chain_id: signet_constants::pecorino::RU_CHAIN_ID, host_rpc_url: "https://host-rpc.pecorino.signet.sh".into(), ru_rpc_url: "https://rpc.pecorino.signet.sh".into(), tx_broadcast_urls: vec!["http://localhost:9000".into()], @@ -64,7 +60,7 @@ pub fn new_signed_tx( mpfpg: u128, ) -> Result { let tx = TxEip1559 { - chain_id: PECORINO_CHAIN_ID, + chain_id: signet_constants::pecorino::RU_CHAIN_ID, nonce, max_fee_per_gas: 50_000, max_priority_fee_per_gas: mpfpg, diff --git a/tests/block_builder_test.rs b/tests/block_builder_test.rs index 8771873..12bec7d 100644 --- a/tests/block_builder_test.rs +++ b/tests/block_builder_test.rs @@ -9,7 +9,6 @@ mod tests { signers::local::PrivateKeySigner, }; use builder::{ - constants::PECORINO_CHAIN_ID, tasks::block::Simulator, test_utils::{new_signed_tx, setup_logging, setup_test_config, test_block_env}, }; @@ -37,7 +36,7 @@ mod tests { let constants = SignetSystemConstants::pecorino(); // Create an anvil instance for testing - let anvil_instance = Anvil::new().chain_id(PECORINO_CHAIN_ID).spawn(); + let anvil_instance = Anvil::new().chain_id(signet_constants::pecorino::RU_CHAIN_ID).spawn(); // Create a wallet let keys = anvil_instance.keys(); @@ -94,7 +93,7 @@ mod tests { let constants = SignetSystemConstants::pecorino(); // Create an anvil instance for testing - let anvil_instance = Anvil::new().chain_id(PECORINO_CHAIN_ID).spawn(); + let anvil_instance = Anvil::new().chain_id(signet_constants::pecorino::RU_CHAIN_ID).spawn(); // Create a wallet let keys = anvil_instance.keys();