Skip to content

Commit 9e50887

Browse files
authored
refactor: break out quincey (#82)
* refactor: break out quincey * lint: fmt * fix: typo
1 parent 47d53a7 commit 9e50887

File tree

5 files changed

+140
-89
lines changed

5 files changed

+140
-89
lines changed

bin/builder.rs

+8-23
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
use builder::{
22
config::BuilderConfig,
33
service::serve_builder,
4-
tasks::{
5-
block::Simulator, bundler, metrics::MetricsTask, oauth::Authenticator, submit::SubmitTask,
6-
tx_poller,
7-
},
4+
tasks::{block::Simulator, bundler, metrics::MetricsTask, submit::SubmitTask, tx_poller},
85
};
96
use init4_bin_base::{deps::tracing, utils::from_env::FromEnv};
107
use signet_sim::SimCache;
@@ -22,35 +19,26 @@ async fn main() -> eyre::Result<()> {
2219

2320
let config = BuilderConfig::from_env()?.clone();
2421
let constants = SignetSystemConstants::pecorino();
25-
let authenticator = Authenticator::new(&config)?;
22+
let token = config.oauth_token();
2623

27-
let (host_provider, sequencer_signer) =
28-
tokio::try_join!(config.connect_host_provider(), config.connect_sequencer_signer(),)?;
24+
let (host_provider, quincey) =
25+
tokio::try_join!(config.connect_host_provider(), config.connect_quincey())?;
2926
let ru_provider = config.connect_ru_provider();
3027

3128
let zenith = config.connect_zenith(host_provider.clone());
3229

33-
let metrics = MetricsTask { host_provider: host_provider.clone() };
30+
let metrics = MetricsTask { host_provider };
3431
let (tx_channel, metrics_jh) = metrics.spawn();
3532

36-
let submit = SubmitTask {
37-
token: authenticator.token(),
38-
host_provider,
39-
zenith,
40-
client: reqwest::Client::new(),
41-
sequencer_signer,
42-
config: config.clone(),
43-
outbound_tx_channel: tx_channel,
44-
};
33+
let submit =
34+
SubmitTask { zenith, quincey, config: config.clone(), outbound_tx_channel: tx_channel };
4535

4636
let tx_poller = tx_poller::TxPoller::new(&config);
4737
let (tx_receiver, tx_poller_jh) = tx_poller.spawn();
4838

49-
let bundle_poller = bundler::BundlePoller::new(&config, authenticator.token());
39+
let bundle_poller = bundler::BundlePoller::new(&config, token);
5040
let (bundle_receiver, bundle_poller_jh) = bundle_poller.spawn();
5141

52-
let authenticator_jh = authenticator.spawn();
53-
5442
let (submit_channel, submit_jh) = submit.spawn();
5543

5644
let sim_items = SimCache::new();
@@ -94,9 +82,6 @@ async fn main() -> eyre::Result<()> {
9482
_ = server => {
9583
tracing::info!("server finished");
9684
}
97-
_ = authenticator_jh => {
98-
tracing::info!("authenticator finished");
99-
}
10085
}
10186

10287
tracing::info!("shutting down");

src/config.rs

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use crate::signer::{LocalOrAws, SignerError};
1+
use crate::{
2+
quincey::Quincey,
3+
signer::{LocalOrAws, SignerError},
4+
tasks::oauth::{Authenticator, SharedToken},
5+
};
26
use alloy::{
37
network::{Ethereum, EthereumWallet},
48
primitives::Address,
@@ -209,4 +213,31 @@ impl BuilderConfig {
209213
pub const fn connect_zenith(&self, provider: HostProvider) -> ZenithInstance {
210214
Zenith::new(self.zenith_address, provider)
211215
}
216+
217+
/// Get an oauth2 token for the builder, starting the authenticator if it
218+
// is not already running.
219+
pub fn oauth_token(&self) -> SharedToken {
220+
static ONCE: std::sync::OnceLock<SharedToken> = std::sync::OnceLock::new();
221+
222+
ONCE.get_or_init(|| {
223+
let authenticator = Authenticator::new(self).unwrap();
224+
let token = authenticator.token();
225+
authenticator.spawn();
226+
token
227+
})
228+
.clone()
229+
}
230+
231+
/// Connect to a Quincey, owned or shared.
232+
pub async fn connect_quincey(&self) -> eyre::Result<Quincey> {
233+
if let Some(signer) = self.connect_sequencer_signer().await? {
234+
return Ok(Quincey::new_owned(signer));
235+
}
236+
237+
let client = reqwest::Client::new();
238+
let url = url::Url::parse(&self.quincey_url)?;
239+
let token = self.oauth_token();
240+
241+
Ok(Quincey::new_remote(client, url, token))
242+
}
212243
}

src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub mod constants;
1818
/// Configuration for the Builder binary.
1919
pub mod config;
2020

21+
/// Quincey client for signing requests.
22+
pub mod quincey;
23+
2124
/// Implements the `/healthcheck` endpoint.
2225
pub mod service;
2326

src/quincey.rs

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use crate::{signer::LocalOrAws, tasks::oauth::SharedToken};
2+
use alloy::signers::Signer;
3+
use eyre::bail;
4+
use init4_bin_base::deps::tracing::{self, debug, info, instrument, trace};
5+
use oauth2::TokenResponse;
6+
use reqwest::Client;
7+
use signet_types::{SignRequest, SignResponse};
8+
9+
/// A quincey client for making requests to the Quincey API.
10+
#[derive(Debug, Clone)]
11+
pub enum Quincey {
12+
/// A remote quincey, this is used for production environments.
13+
/// The client will access the Quincey API over HTTP(S) via OAuth.
14+
Remote {
15+
/// The remote client.
16+
client: Client,
17+
/// The base URL for the remote API.
18+
url: reqwest::Url,
19+
/// OAuth shared token.
20+
token: SharedToken,
21+
},
22+
/// An owned quincey, either local or AWS. This is used primarily for
23+
/// testing and development environments. The client will simulate the
24+
/// Quincey API using a local or AWS KMS key.
25+
Owned(LocalOrAws),
26+
}
27+
28+
impl Quincey {
29+
/// Creates a new Quincey client from the provided URL and token.
30+
pub const fn new_remote(client: Client, url: reqwest::Url, token: SharedToken) -> Self {
31+
Self::Remote { client, url, token }
32+
}
33+
34+
/// Creates a new Quincey client for making requests to the Quincey API.
35+
pub const fn new_owned(client: LocalOrAws) -> Self {
36+
Self::Owned(client)
37+
}
38+
39+
async fn sup_owned(&self, sig_request: &SignRequest) -> eyre::Result<SignResponse> {
40+
let Self::Owned(signer) = &self else { eyre::bail!("not an owned client") };
41+
42+
info!("signing with owned quincey");
43+
signer
44+
.sign_hash(&sig_request.signing_hash())
45+
.await
46+
.map_err(Into::into)
47+
.map(|sig| SignResponse { sig, req: *sig_request })
48+
}
49+
50+
async fn sup_remote(&self, sig_request: &SignRequest) -> eyre::Result<SignResponse> {
51+
let Self::Remote { client, url, token } = &self else { bail!("not a remote client") };
52+
53+
let Some(token) = token.read() else { bail!("no token available") };
54+
55+
let resp: reqwest::Response = client
56+
.post(url.clone())
57+
.json(sig_request)
58+
.bearer_auth(token.access_token().secret())
59+
.send()
60+
.await?
61+
.error_for_status()?;
62+
63+
let body = resp.bytes().await?;
64+
65+
debug!(bytes = body.len(), "retrieved response body");
66+
trace!(body = %String::from_utf8_lossy(&body), "response body");
67+
68+
serde_json::from_slice(&body).map_err(Into::into)
69+
}
70+
71+
/// Get a signature for the provided request, by either using the owned
72+
/// or remote client.
73+
#[instrument(skip(self))]
74+
pub async fn get_signature(&self, sig_request: &SignRequest) -> eyre::Result<SignResponse> {
75+
match self {
76+
Self::Owned(_) => self.sup_owned(sig_request).await,
77+
Self::Remote { .. } => self.sup_remote(sig_request).await,
78+
}
79+
}
80+
}

src/tasks/submit.rs

+17-65
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use crate::{
22
config::{HostProvider, ZenithInstance},
3-
signer::LocalOrAws,
4-
tasks::oauth::SharedToken,
3+
quincey::Quincey,
54
utils::extract_signature_components,
65
};
76
use alloy::{
@@ -11,16 +10,14 @@ use alloy::{
1110
primitives::{FixedBytes, TxHash, U256},
1211
providers::{Provider as _, SendableTx, WalletProvider},
1312
rpc::types::eth::TransactionRequest,
14-
signers::Signer,
1513
sol_types::{SolCall, SolError},
1614
transports::TransportError,
1715
};
18-
use eyre::{Context, bail, eyre};
16+
use eyre::{bail, eyre};
1917
use init4_bin_base::deps::{
2018
metrics::{counter, histogram},
21-
tracing::{self, Instrument, debug, debug_span, error, info, instrument, trace, warn},
19+
tracing::{self, Instrument, debug, debug_span, error, info, instrument, warn},
2220
};
23-
use oauth2::TokenResponse;
2421
use signet_sim::BuiltBlock;
2522
use signet_types::{SignRequest, SignResponse};
2623
use signet_zenith::{
@@ -58,48 +55,23 @@ pub enum ControlFlow {
5855
/// Submits sidecars in ethereum txns to mainnet ethereum
5956
#[derive(Debug)]
6057
pub struct SubmitTask {
61-
/// Ethereum Provider
62-
pub host_provider: HostProvider,
6358
/// Zenith
6459
pub zenith: ZenithInstance,
65-
/// Reqwest
66-
pub client: reqwest::Client,
67-
/// Sequencer Signer
68-
pub sequencer_signer: Option<LocalOrAws>,
60+
61+
/// Quincey
62+
pub quincey: Quincey,
63+
6964
/// Config
7065
pub config: crate::config::BuilderConfig,
71-
/// Authenticator
72-
pub token: SharedToken,
66+
7367
/// Channel over which to send pending transactions
7468
pub outbound_tx_channel: mpsc::UnboundedSender<TxHash>,
7569
}
7670

7771
impl SubmitTask {
78-
#[instrument(skip(self))]
79-
async fn sup_quincey(&self, sig_request: &SignRequest) -> eyre::Result<SignResponse> {
80-
info!(
81-
host_block_number = %sig_request.host_block_number,
82-
ru_chain_id = %sig_request.ru_chain_id,
83-
"pinging quincey for signature"
84-
);
85-
86-
let Some(token) = self.token.read() else { bail!("no token available") };
87-
88-
let resp: reqwest::Response = self
89-
.client
90-
.post(self.config.quincey_url.as_ref())
91-
.json(sig_request)
92-
.bearer_auth(token.access_token().secret())
93-
.send()
94-
.await?
95-
.error_for_status()?;
96-
97-
let body = resp.bytes().await?;
98-
99-
debug!(bytes = body.len(), "retrieved response body");
100-
trace!(body = %String::from_utf8_lossy(&body), "response body");
101-
102-
serde_json::from_slice(&body).map_err(Into::into)
72+
/// Get the provider from the zenith instance
73+
const fn provider(&self) -> &HostProvider {
74+
self.zenith.provider()
10375
}
10476

10577
/// Constructs the signing request from the in-progress block passed to it and assigns the
@@ -140,7 +112,7 @@ impl SubmitTask {
140112

141113
/// Returns the next host block height
142114
async fn next_host_block_height(&self) -> eyre::Result<u64> {
143-
let result = self.host_provider.get_block_number().await?;
115+
let result = self.provider().get_block_number().await?;
144116
let next = result.checked_add(1).ok_or_else(|| eyre!("next host block height overflow"))?;
145117
Ok(next)
146118
}
@@ -164,12 +136,12 @@ impl SubmitTask {
164136
let fills = vec![]; // NB: ignored until fills are implemented
165137
let tx = self
166138
.build_blob_tx(fills, header, v, r, s, in_progress)?
167-
.with_from(self.host_provider.default_signer_address())
139+
.with_from(self.provider().default_signer_address())
168140
.with_to(self.config.builder_helper_address)
169141
.with_gas_limit(1_000_000);
170142

171143
if let Err(TransportError::ErrorResp(e)) =
172-
self.host_provider.call(tx.clone()).block(BlockNumberOrTag::Pending.into()).await
144+
self.provider().call(tx.clone()).block(BlockNumberOrTag::Pending.into()).await
173145
{
174146
error!(
175147
code = e.code,
@@ -203,12 +175,12 @@ impl SubmitTask {
203175
"sending transaction to network"
204176
);
205177

206-
let SendableTx::Envelope(tx) = self.host_provider.fill(tx).await? else {
178+
let SendableTx::Envelope(tx) = self.provider().fill(tx).await? else {
207179
bail!("failed to fill transaction")
208180
};
209181

210182
// Send the tx via the primary host_provider
211-
let fut = spawn_provider_send!(&self.host_provider, &tx);
183+
let fut = spawn_provider_send!(self.provider(), &tx);
212184

213185
// Spawn send_tx futures for all additional broadcast host_providers
214186
for host_provider in self.config.connect_additional_broadcast() {
@@ -237,26 +209,6 @@ impl SubmitTask {
237209
Ok(ControlFlow::Done)
238210
}
239211

240-
/// Sign with a local signer if available, otherwise ask quincey
241-
/// for a signature (politely).
242-
#[instrument(skip_all, fields(is_local = self.sequencer_signer.is_some()))]
243-
async fn get_signature(&self, req: SignRequest) -> eyre::Result<SignResponse> {
244-
let sig = if let Some(signer) = &self.sequencer_signer {
245-
signer.sign_hash(&req.signing_hash()).await?
246-
} else {
247-
self.sup_quincey(&req)
248-
.await
249-
.wrap_err("failed to get signature from quincey")
250-
.inspect(|_| {
251-
counter!("builder.quincey_signature_acquired").increment(1);
252-
})?
253-
.sig
254-
};
255-
256-
debug!(sig = hex::encode(sig.as_bytes()), "acquired signature");
257-
Ok(SignResponse { req, sig })
258-
}
259-
260212
#[instrument(skip_all)]
261213
async fn handle_inbound(&self, block: &BuiltBlock) -> eyre::Result<ControlFlow> {
262214
info!(txns = block.tx_count(), "handling inbound block");
@@ -272,7 +224,7 @@ impl SubmitTask {
272224
"constructed signature request for host block"
273225
);
274226

275-
let signed = self.get_signature(sig_request).await?;
227+
let signed = self.quincey.get_signature(&sig_request).await?;
276228

277229
self.submit_transaction(&signed, block).await
278230
}

0 commit comments

Comments
 (0)