diff --git a/Cargo.toml b/Cargo.toml index 572f034..19b2c45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ path = "bin/submit_transaction.rs" integration = [] [dependencies] -init4-bin-base = "0.3" +init4-bin-base = { version = "0.3.4", features = ["perms"] } signet-bundle = { git = "https://github.com/init4tech/signet-sdk", rev = "b8251ff0fec7cb14ca87e6f95c14f56bc2593049" } signet-constants = { git = "https://github.com/init4tech/signet-sdk", rev = "b8251ff0fec7cb14ca87e6f95c14f56bc2593049" } @@ -46,13 +46,6 @@ alloy = { version = "0.12.6", features = [ "serde", ] } -aws-config = "1.1.7" -aws-sdk-kms = "1.15.0" - -hex = { package = "const-hex", version = "1", default-features = false, features = [ - "alloc", -] } - serde = { version = "1.0.197", features = ["derive"] } axum = "0.7.5" @@ -60,10 +53,9 @@ eyre = "0.6.12" openssl = { version = "0.10", features = ["vendored"] } reqwest = { version = "0.11.24", features = ["blocking", "json"] } serde_json = "1.0" -thiserror = "1.0.68" tokio = { version = "1.36.0", features = ["full", "macros", "rt-multi-thread"] } -async-trait = "0.1.80" -oauth2 = "4.4.2" +oauth2 = "5" chrono = "0.4.41" tokio-stream = "0.1.17" +url = "2.5.4" diff --git a/bin/submit_transaction.rs b/bin/submit_transaction.rs index 1c46427..81377f1 100644 --- a/bin/submit_transaction.rs +++ b/bin/submit_transaction.rs @@ -3,7 +3,6 @@ use alloy::{ primitives::{Address, U256}, providers::{Provider as _, ProviderBuilder, WalletProvider}, rpc::types::eth::TransactionRequest, - signers::aws::AwsSigner, }; use builder::config::HostProvider; use init4_bin_base::{ @@ -12,7 +11,7 @@ use init4_bin_base::{ tracing, }, init4, - utils::from_env::FromEnv, + utils::{from_env::FromEnv, signer::LocalOrAwsConfig}, }; use std::time::{Duration, Instant}; use tokio::time::timeout; @@ -21,10 +20,7 @@ use tokio::time::timeout; struct Config { #[from_env(var = "RPC_URL", desc = "Ethereum RPC URL")] rpc_url: String, - #[from_env(var = "CHAIN_ID", desc = "Ethereum chain ID")] - chain_id: u64, - #[from_env(var = "AWS_KMS_KEY_ID", desc = "AWS KMS key ID")] - kms_key_id: String, + kms_key_id: LocalOrAwsConfig, #[from_env(var = "RECIPIENT_ADDRESS", desc = "Recipient address")] recipient_address: Address, #[from_env(var = "SLEEP_TIME", desc = "Time to sleep between transactions")] @@ -33,10 +29,7 @@ struct Config { impl Config { async fn provider(&self) -> HostProvider { - let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; - let client = aws_sdk_kms::Client::new(&config); - let signer = - AwsSigner::new(client, self.kms_key_id.clone(), Some(self.chain_id)).await.unwrap(); + let signer = self.kms_key_id.connect_remote().await.unwrap(); ProviderBuilder::new() .wallet(EthereumWallet::from(signer)) diff --git a/src/config.rs b/src/config.rs index c795fc1..88e441c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,4 @@ -use crate::{ - quincey::Quincey, - signer::{LocalOrAws, SignerError}, - tasks::oauth::{Authenticator, SharedToken}, -}; +use crate::quincey::Quincey; use alloy::{ network::{Ethereum, EthereumWallet}, primitives::Address, @@ -15,8 +11,14 @@ use alloy::{ }, }; use eyre::Result; -use init4_bin_base::utils::{calc::SlotCalculator, from_env::FromEnv}; -use oauth2::url; +use init4_bin_base::{ + perms::{Authenticator, OAuthConfig, SharedToken}, + utils::{ + calc::SlotCalculator, + from_env::FromEnv, + signer::{LocalOrAws, SignerError}, + }, +}; use signet_zenith::Zenith; use std::borrow::Cow; @@ -41,7 +43,7 @@ pub type HostProvider = FillProvider< /// Configuration for a builder running a specific rollup on a specific host /// chain. -#[derive(serde::Deserialize, Debug, Clone, FromEnv)] +#[derive(Debug, Clone, FromEnv)] pub struct BuilderConfig { /// The chain ID of the host chain #[from_env(var = "HOST_CHAIN_ID", desc = "The chain ID of the host chain")] @@ -129,30 +131,10 @@ pub struct BuilderConfig { desc = "Duration in seconds transactions can live in the tx-pool cache" )] pub tx_pool_cache_duration: u64, - /// OAuth client ID for the builder. - #[from_env(var = "OAUTH_CLIENT_ID", desc = "OAuth client ID for the builder")] - pub oauth_client_id: String, - /// OAuth client secret for the builder. - #[from_env(var = "OAUTH_CLIENT_SECRET", desc = "OAuth client secret for the builder")] - pub oauth_client_secret: String, - /// OAuth authenticate URL for the builder for performing OAuth logins. - #[from_env( - var = "OAUTH_AUTHENTICATE_URL", - desc = "OAuth authenticate URL for the builder for performing OAuth logins" - )] - pub oauth_authenticate_url: String, - /// OAuth token URL for the builder to get an OAuth2 access token - #[from_env( - var = "OAUTH_TOKEN_URL", - desc = "OAuth token URL for the builder to get an OAuth2 access token" - )] - pub oauth_token_url: String, - /// The oauth token refresh interval in seconds. - #[from_env( - var = "AUTH_TOKEN_REFRESH_INTERVAL", - desc = "The oauth token refresh interval in seconds" - )] - pub oauth_token_refresh_interval: u64, + + /// Oauth2 configuration for the builder to connect to ini4 services. + pub oauth: OAuthConfig, + /// The max number of simultaneous block simulations to run. #[from_env( var = "CONCURRENCY_LIMIT", @@ -220,7 +202,7 @@ impl BuilderConfig { static ONCE: std::sync::OnceLock = std::sync::OnceLock::new(); ONCE.get_or_init(|| { - let authenticator = Authenticator::new(self).unwrap(); + let authenticator = Authenticator::new(&self.oauth); let token = authenticator.token(); authenticator.spawn(); token diff --git a/src/lib.rs b/src/lib.rs index 967685e..8eab8d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,9 +21,6 @@ pub mod quincey; /// Implements the `/healthcheck` endpoint. pub mod service; -/// Builder transaction signer. -pub mod signer; - /// Actor-based tasks used to construct a builder. pub mod tasks; diff --git a/src/quincey.rs b/src/quincey.rs index 2962e30..c4ad2c2 100644 --- a/src/quincey.rs +++ b/src/quincey.rs @@ -1,7 +1,10 @@ -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 init4_bin_base::{ + deps::tracing::{self, debug, info, instrument, trace}, + perms::SharedToken, + utils::signer::LocalOrAws, +}; use oauth2::TokenResponse; use reqwest::Client; use signet_types::{SignRequest, SignResponse}; diff --git a/src/signer.rs b/src/signer.rs deleted file mode 100644 index ae3ccf6..0000000 --- a/src/signer.rs +++ /dev/null @@ -1,115 +0,0 @@ -use alloy::consensus::SignableTransaction; -use alloy::primitives::{Address, B256, ChainId}; -use alloy::signers::Signature; -use alloy::signers::aws::{AwsSigner, AwsSignerError}; -use alloy::signers::local::{LocalSignerError, PrivateKeySigner}; -use aws_config::BehaviorVersion; - -/// Abstraction over local signer or -#[derive(Debug, Clone)] -pub enum LocalOrAws { - /// Local signer - Local(PrivateKeySigner), - /// AWS signer - Aws(AwsSigner), -} - -/// Error during signing -#[allow(clippy::large_enum_variant)] // type about to be deleted -#[derive(Debug, thiserror::Error)] -pub enum SignerError { - /// Error during [`AwsSigner`] instantiation - #[error("failed to connect AWS signer: {0}")] - AwsSigner(#[from] AwsSignerError), - /// Error loading the private key - #[error("failed to load private key: {0}")] - Wallet(#[from] LocalSignerError), - /// Error parsing hex - #[error("failed to parse hex: {0}")] - Hex(#[from] hex::FromHexError), -} - -impl LocalOrAws { - /// Load a privkey or AWS signer from environment variables. - pub async fn load(key: &str, chain_id: Option) -> Result { - if let Ok(wallet) = LocalOrAws::wallet(key) { - Ok(LocalOrAws::Local(wallet)) - } else { - let signer = LocalOrAws::aws_signer(key, chain_id).await?; - Ok(LocalOrAws::Aws(signer)) - } - } - - /// Load the wallet from environment variables. - /// - /// # Panics - /// - /// Panics if the env var contents is not a valid secp256k1 private key. - #[allow(clippy::result_large_err)] // type about to be deleted - fn wallet(private_key: &str) -> Result { - let bytes = hex::decode(private_key.strip_prefix("0x").unwrap_or(private_key))?; - Ok(PrivateKeySigner::from_slice(&bytes).unwrap()) - } - - /// Load the AWS signer from environment variables./s - async fn aws_signer(key_id: &str, chain_id: Option) -> Result { - let config = aws_config::load_defaults(BehaviorVersion::latest()).await; - let client = aws_sdk_kms::Client::new(&config); - AwsSigner::new(client, key_id.to_string(), chain_id).await.map_err(Into::into) - } -} - -#[async_trait::async_trait] -impl alloy::network::TxSigner for LocalOrAws { - fn address(&self) -> Address { - match self { - LocalOrAws::Local(signer) => signer.address(), - LocalOrAws::Aws(signer) => signer.address(), - } - } - - async fn sign_transaction( - &self, - tx: &mut dyn SignableTransaction, - ) -> alloy::signers::Result { - match self { - LocalOrAws::Local(signer) => signer.sign_transaction(tx).await, - LocalOrAws::Aws(signer) => signer.sign_transaction(tx).await, - } - } -} - -#[async_trait::async_trait] -impl alloy::signers::Signer for LocalOrAws { - /// Signs the given hash. - async fn sign_hash(&self, hash: &B256) -> alloy::signers::Result { - match self { - LocalOrAws::Local(signer) => signer.sign_hash(hash).await, - LocalOrAws::Aws(signer) => signer.sign_hash(hash).await, - } - } - - /// Returns the signer's Ethereum Address. - fn address(&self) -> Address { - match self { - LocalOrAws::Local(signer) => signer.address(), - LocalOrAws::Aws(signer) => signer.address(), - } - } - - /// Returns the signer's chain ID. - fn chain_id(&self) -> Option { - match self { - LocalOrAws::Local(signer) => signer.chain_id(), - LocalOrAws::Aws(signer) => signer.chain_id(), - } - } - - /// Sets the signer's chain ID. - fn set_chain_id(&mut self, chain_id: Option) { - match self { - LocalOrAws::Local(signer) => signer.set_chain_id(chain_id), - LocalOrAws::Aws(signer) => signer.set_chain_id(chain_id), - } - } -} diff --git a/src/tasks/cache/bundle.rs b/src/tasks/cache/bundle.rs index 4d4d8bb..b8232b9 100644 --- a/src/tasks/cache/bundle.rs +++ b/src/tasks/cache/bundle.rs @@ -1,6 +1,9 @@ //! Bundler service responsible for fetching bundles and sending them to the simulator. -use crate::{config::BuilderConfig, tasks::oauth::SharedToken}; -use init4_bin_base::deps::tracing::{Instrument, debug, debug_span, error, trace, warn}; +use crate::config::BuilderConfig; +use init4_bin_base::{ + deps::tracing::{Instrument, debug, debug_span, error, trace, warn}, + perms::SharedToken, +}; use oauth2::TokenResponse; use reqwest::{Client, Url}; use serde::{Deserialize, Serialize}; diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index dd94ab0..eee57d5 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -7,9 +7,6 @@ pub mod cache; /// Tx submission metric task pub mod metrics; -/// OAuth token refresh task -pub mod oauth; - /// Tx submission task pub mod submit; diff --git a/src/tasks/oauth.rs b/src/tasks/oauth.rs deleted file mode 100644 index ffe03b8..0000000 --- a/src/tasks/oauth.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Service responsible for authenticating with the cache with Oauth tokens. -//! This authenticator periodically fetches a new token every set amount of seconds. -use crate::config::BuilderConfig; -use init4_bin_base::deps::tracing::{error, info}; -use oauth2::{ - AuthUrl, ClientId, ClientSecret, EmptyExtraTokenFields, StandardTokenResponse, TokenUrl, - basic::{BasicClient, BasicTokenType}, - reqwest::async_http_client, -}; -use std::sync::{Arc, Mutex}; -use tokio::task::JoinHandle; - -type Token = StandardTokenResponse; - -/// A shared token that can be read and written to by multiple threads. -#[derive(Debug, Clone, Default)] -pub struct SharedToken(Arc>>); - -impl SharedToken { - /// Read the token from the shared token. - pub fn read(&self) -> Option { - self.0.lock().unwrap().clone() - } - - /// Write a new token to the shared token. - pub fn write(&self, token: Token) { - let mut lock = self.0.lock().unwrap(); - *lock = Some(token); - } - - /// Check if the token is authenticated. - pub fn is_authenticated(&self) -> bool { - self.0.lock().unwrap().is_some() - } -} - -/// A self-refreshing, periodically fetching authenticator for the block -/// builder. This task periodically fetches a new token, and stores it in a -/// [`SharedToken`]. -#[derive(Debug)] -pub struct Authenticator { - /// Configuration - pub config: BuilderConfig, - client: BasicClient, - token: SharedToken, -} - -impl Authenticator { - /// Creates a new Authenticator from the provided builder config. - pub fn new(config: &BuilderConfig) -> eyre::Result { - let client = BasicClient::new( - ClientId::new(config.oauth_client_id.clone()), - Some(ClientSecret::new(config.oauth_client_secret.clone())), - AuthUrl::new(config.oauth_authenticate_url.clone())?, - Some(TokenUrl::new(config.oauth_token_url.clone())?), - ); - - Ok(Self { config: config.clone(), client, token: Default::default() }) - } - - /// Requests a new authentication token and, if successful, sets it to as the token - pub async fn authenticate(&self) -> eyre::Result<()> { - let token = self.fetch_oauth_token().await?; - self.set_token(token); - Ok(()) - } - - /// Returns true if there is Some token set - pub fn is_authenticated(&self) -> bool { - self.token.is_authenticated() - } - - /// Sets the Authenticator's token to the provided value - fn set_token(&self, token: StandardTokenResponse) { - self.token.write(token); - } - - /// Returns the currently set token - pub fn token(&self) -> SharedToken { - self.token.clone() - } - - /// Fetches an oauth token - pub async fn fetch_oauth_token(&self) -> eyre::Result { - let token_result = - self.client.exchange_client_credentials().request_async(async_http_client).await?; - - Ok(token_result) - } - - /// Spawns a task that periodically fetches a new token every 300 seconds. - pub fn spawn(self) -> JoinHandle<()> { - let interval = self.config.oauth_token_refresh_interval; - - let handle: JoinHandle<()> = tokio::spawn(async move { - loop { - info!("Refreshing oauth token"); - match self.authenticate().await { - Ok(_) => { - info!("Successfully refreshed oauth token"); - } - Err(e) => { - error!(%e, "Failed to refresh oauth token"); - } - }; - let _sleep = tokio::time::sleep(tokio::time::Duration::from_secs(interval)).await; - } - }); - - handle - } -} diff --git a/src/test_utils.rs b/src/test_utils.rs index 89a2e41..63e2ee6 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -11,6 +11,7 @@ use init4_bin_base::{ deps::tracing_subscriber::{ EnvFilter, Layer, fmt, layer::SubscriberExt, registry, util::SubscriberInitExt, }, + perms::OAuthConfig, utils::calc::SlotCalculator, }; use std::{ @@ -36,11 +37,13 @@ pub fn setup_test_config() -> Result { rollup_block_gas_limit: 3_000_000_000, tx_pool_url: "http://localhost:9000/".into(), tx_pool_cache_duration: 5, - oauth_client_id: "some_client_id".into(), - oauth_client_secret: "some_client_secret".into(), - oauth_authenticate_url: "http://localhost:8080".into(), - oauth_token_url: "http://localhost:8080".into(), - oauth_token_refresh_interval: 300, // 5 minutes + oauth: OAuthConfig { + oauth_client_id: "some_client_id".into(), + oauth_client_secret: "some_client_secret".into(), + oauth_authenticate_url: "http://localhost:8080".parse().unwrap(), + oauth_token_url: "http://localhost:8080".parse().unwrap(), + oauth_token_refresh_interval: 300, // 5 minutes + }, builder_helper_address: Address::default(), concurrency_limit: 1000, slot_calculator: SlotCalculator::new( diff --git a/tests/authenticator.rs b/tests/authenticator.rs deleted file mode 100644 index 6899787..0000000 --- a/tests/authenticator.rs +++ /dev/null @@ -1,15 +0,0 @@ -use builder::{tasks::oauth::Authenticator, test_utils::setup_test_config}; - -#[ignore = "integration test"] -#[tokio::test] -async fn test_authenticator() -> eyre::Result<()> { - let config = setup_test_config()?; - let auth = Authenticator::new(&config)?; - - let _ = auth.fetch_oauth_token().await?; - - let token = auth.token(); - - assert!(token.is_authenticated()); - Ok(()) -} diff --git a/tests/bundle_poller_test.rs b/tests/bundle_poller_test.rs index e1e5dde..ae051d0 100644 --- a/tests/bundle_poller_test.rs +++ b/tests/bundle_poller_test.rs @@ -1,14 +1,14 @@ mod tests { - use builder::{tasks::oauth::Authenticator, test_utils}; + use builder::test_utils; use eyre::Result; #[ignore = "integration test"] #[tokio::test] async fn test_bundle_poller_roundtrip() -> Result<()> { let config = test_utils::setup_test_config().unwrap(); - let auth = Authenticator::new(&config)?; + let token = config.oauth_token(); - let mut bundle_poller = builder::tasks::cache::BundlePoller::new(&config, auth.token()); + let mut bundle_poller = builder::tasks::cache::BundlePoller::new(&config, token); let _ = bundle_poller.check_bundle_cache().await?;