Skip to content

refactor: break out quincey #82

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

Merged
merged 1 commit into from
May 12, 2025
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
31 changes: 8 additions & 23 deletions bin/builder.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -94,9 +82,6 @@ async fn main() -> eyre::Result<()> {
_ = server => {
tracing::info!("server finished");
}
_ = authenticator_jh => {
tracing::info!("authenticator finished");
}
}

tracing::info!("shutting down");
Expand Down
33 changes: 32 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<SharedToken> = 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<Quincey> {
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))
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ 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;

Expand Down
80 changes: 80 additions & 0 deletions src/quincey.rs
Original file line number Diff line number Diff line change
@@ -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<SignResponse> {
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<SignResponse> {
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<SignResponse> {
match self {
Self::Owned(_) => self.sup_owned(sig_request).await,
Self::Remote { .. } => self.sup_remote(sig_request).await,
}
}
}
82 changes: 17 additions & 65 deletions src/tasks/submit.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use crate::{
config::{HostProvider, ZenithInstance},
signer::LocalOrAws,
tasks::oauth::SharedToken,
quincey::Quincey,
utils::extract_signature_components,
};
use alloy::{
Expand All @@ -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::{
Expand Down Expand Up @@ -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<LocalOrAws>,

/// 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<TxHash>,
}

impl SubmitTask {
#[instrument(skip(self))]
async fn sup_quincey(&self, sig_request: &SignRequest) -> eyre::Result<SignResponse> {
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
Expand Down Expand Up @@ -140,7 +112,7 @@ impl SubmitTask {

/// Returns the next host block height
async fn next_host_block_height(&self) -> eyre::Result<u64> {
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)
}
Expand All @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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<SignResponse> {
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<ControlFlow> {
info!(txns = block.tx_count(), "handling inbound block");
Expand All @@ -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
}
Expand Down