Skip to content

Commit dd0cd1d

Browse files
committed
fix: retry logic bugs
1 parent 5fdff18 commit dd0cd1d

File tree

2 files changed

+81
-92
lines changed

2 files changed

+81
-92
lines changed

src/config.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ pub struct BuilderConfig {
5959
/// NOTE: should not include the host_rpc_url value
6060
#[from_env(
6161
var = "TX_BROADCAST_URLS",
62-
desc = "Additional RPC URLs to which to broadcast transactions",
63-
infallible
62+
desc = "Additional RPC URLs to which the builder broadcasts transactions",
63+
infallible,
64+
optional
6465
)]
6566
pub tx_broadcast_urls: Vec<Cow<'static, str>>,
6667
/// address of the Zenith contract on Host.

src/tasks/submit.rs

Lines changed: 78 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::{
44
utils::extract_signature_components,
55
};
66
use alloy::{
7-
consensus::{SimpleCoder, Transaction, constants::GWEI_TO_WEI},
7+
consensus::SimpleCoder,
88
eips::BlockNumberOrTag,
99
network::{TransactionBuilder, TransactionBuilder4844},
1010
primitives::{FixedBytes, TxHash, U256},
@@ -13,7 +13,7 @@ use alloy::{
1313
sol_types::{SolCall, SolError},
1414
transports::TransportError,
1515
};
16-
use eyre::{bail, eyre};
16+
use eyre::bail;
1717
use init4_bin_base::deps::{
1818
metrics::{counter, histogram},
1919
tracing::{self, Instrument, debug, debug_span, error, info, instrument, warn},
@@ -101,21 +101,11 @@ impl SubmitTask {
101101
let data = submitCall { fills, header, v, r, s }.abi_encode();
102102

103103
let sidecar = block.encode_blob::<SimpleCoder>().build()?;
104-
Ok(TransactionRequest::default()
105-
.with_blob_sidecar(sidecar)
106-
.with_input(data)
107-
.with_max_priority_fee_per_gas((GWEI_TO_WEI * 16) as u128))
108-
}
109104

110-
/// Returns the next host block height.
111-
async fn next_host_block_height(&self) -> eyre::Result<u64> {
112-
let result = self.provider().get_block_number().await?;
113-
let next = result.checked_add(1).ok_or_else(|| eyre!("next host block height overflow"))?;
114-
debug!(next, "next host block height");
115-
Ok(next)
105+
Ok(TransactionRequest::default().with_blob_sidecar(sidecar).with_input(data))
116106
}
117107

118-
/// Prepares and then sends the EIP-4844 transaction with a sidecar encoded with a rollup block to the network.
108+
/// Prepares and sends the EIP-4844 transaction with a sidecar encoded with a rollup block to the network.
119109
async fn submit_transaction(
120110
&self,
121111
retry_count: usize,
@@ -140,10 +130,7 @@ impl SubmitTask {
140130

141131
// Simulate the transaction with a call to the host provider
142132
if let Some(maybe_error) = self.sim_with_call(&tx).await {
143-
warn!(
144-
error = ?maybe_error,
145-
"error in transaction simulation"
146-
);
133+
warn!(error = ?maybe_error, "error in transaction simulation");
147134
if let Err(e) = maybe_error {
148135
return Err(e);
149136
}
@@ -160,19 +147,13 @@ impl SubmitTask {
160147
if let Err(TransportError::ErrorResp(e)) =
161148
self.provider().call(tx.clone()).block(BlockNumberOrTag::Pending.into()).await
162149
{
163-
error!(
164-
code = e.code,
165-
message = %e.message,
166-
data = ?e.data,
167-
"error in transaction submission"
168-
);
169-
150+
// NB: These errors are all handled the same way but are logged for debugging purposes
170151
if e.as_revert_data()
171152
.map(|data| data.starts_with(&IncorrectHostBlock::SELECTOR))
172153
.unwrap_or_default()
173154
{
174155
debug!(%e, "incorrect host block");
175-
return Some(Ok(ControlFlow::Retry));
156+
return Some(Ok(ControlFlow::Skip));
176157
}
177158

178159
if e.as_revert_data()
@@ -191,6 +172,12 @@ impl SubmitTask {
191172
return Some(Ok(ControlFlow::Skip));
192173
}
193174

175+
error!(
176+
code = e.code,
177+
message = %e.message,
178+
data = ?e.data,
179+
"unknown error in host transaction simulation call"
180+
);
194181
return Some(Ok(ControlFlow::Skip));
195182
}
196183

@@ -207,22 +194,14 @@ impl SubmitTask {
207194
) -> Result<TransactionRequest, eyre::Error> {
208195
// TODO: ENG-1082 Implement fills
209196
let fills = vec![];
210-
211197
// Extract the signature components from the response
212198
let (v, r, s) = extract_signature_components(&resp.sig);
213199

214-
// Bump gas with each retry to replace the previous
215-
// transaction while maintaining the same nonce
216-
// TODO: Clean this up if this works
217-
let gas_coefficient: u64 = (15 * (retry_count + 1)).try_into().unwrap();
218-
let gas_limit: u64 = 1_500_000 + (gas_coefficient * 1_000_000);
219-
let max_priority_fee_per_gas: u128 = (retry_count as u128);
220-
debug!(
221-
retry_count,
222-
gas_coefficient, gas_limit, max_priority_fee_per_gas, "calculated gas limit"
223-
);
200+
// Calculate gas limits based on retry attempts
201+
let (max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas) =
202+
calculate_gas_limits(retry_count);
224203

225-
// manually retrieve nonce
204+
// manually retrieve nonce // TODO: Maybe this should be done in Env task and passed through elsewhere
226205
let nonce =
227206
self.provider().get_transaction_count(self.provider().default_signer_address()).await?;
228207
debug!(nonce, "assigned nonce");
@@ -240,13 +219,12 @@ impl SubmitTask {
240219
// Create a blob transaction with the blob header and signature values and return it
241220
let tx = self
242221
.build_blob_tx(fills, header, v, r, s, block)?
243-
.with_from(self.provider().default_signer_address())
244222
.with_to(self.config.builder_helper_address)
245-
.with_gas_limit(gas_limit)
223+
.with_max_fee_per_gas(max_fee_per_gas)
246224
.with_max_priority_fee_per_gas(max_priority_fee_per_gas)
225+
.with_max_fee_per_blob_gas(max_fee_per_blob_gas)
247226
.with_nonce(nonce);
248227

249-
debug!(?tx, "prepared transaction request");
250228
Ok(tx)
251229
}
252230

@@ -257,23 +235,16 @@ impl SubmitTask {
257235
resp: &SignResponse,
258236
tx: TransactionRequest,
259237
) -> Result<ControlFlow, eyre::Error> {
260-
debug!(
261-
host_block_number = %resp.req.host_block_number,
262-
gas_limit = %resp.req.gas_limit,
263-
nonce = ?tx.nonce,
264-
"sending transaction to network"
265-
);
266-
267238
// assign the nonce and fill the rest of the values
268239
let SendableTx::Envelope(tx) = self.provider().fill(tx).await? else {
269240
bail!("failed to fill transaction")
270241
};
271-
debug!(tx_hash = %tx.tx_hash(), nonce = ?tx.nonce(), gas_limit = ?tx.gas_limit(), blob_gas_used = ?tx.blob_gas_used(), "filled blob transaction");
242+
debug!(tx_hash = ?tx.hash(), host_block_number = %resp.req.host_block_number, "sending transaction to network");
272243

273244
// send the tx via the primary host_provider
274245
let fut = spawn_provider_send!(self.provider(), &tx);
275246

276-
// spawn send_tx futures for all additional broadcast host_providers
247+
// spawn send_tx futures on retry attempts for all additional broadcast host_providers
277248
for host_provider in self.config.connect_additional_broadcast() {
278249
spawn_provider_send!(&host_provider, &tx);
279250
}
@@ -283,16 +254,19 @@ impl SubmitTask {
283254
error!("receipts task gone");
284255
}
285256

286-
// question mark unwraps join error, which would be an internal panic
287-
// then if let checks for rpc error
288257
if let Err(e) = fut.await? {
289-
error!(error = %e, "Primary tx broadcast failed. Skipping transaction.");
258+
// Detect and handle transaction underprice errors
259+
if matches!(e, TransportError::ErrorResp(ref err) if err.code == -32000 && err.message.contains("replacement transaction underpriced"))
260+
{
261+
debug!(?tx, "underpriced transaction error - retrying tx with gas bump");
262+
return Ok(ControlFlow::Retry);
263+
}
264+
265+
// Unknown error, log and skip
266+
error!(error = %e, "Primary tx broadcast failed");
290267
return Ok(ControlFlow::Skip);
291268
}
292269

293-
// Okay so the code gets all the way to this log
294-
// but we don't see the tx hash in the logs or in the explorer,
295-
// not even as a failed TX, just not at all.
296270
info!(
297271
tx_hash = %tx.tx_hash(),
298272
ru_chain_id = %resp.req.ru_chain_id,
@@ -341,54 +315,38 @@ impl SubmitTask {
341315

342316
// Retry loop
343317
let result = loop {
318+
// Log the retry attempt
344319
let span = debug_span!("SubmitTask::retrying_handle_inbound", retries);
345320

346-
let inbound_result = match self
347-
.handle_inbound(retries, block)
348-
.instrument(span.clone())
349-
.await
350-
{
351-
Ok(control_flow) => {
352-
debug!(?control_flow, retries, "successfully handled inbound block");
353-
control_flow
354-
}
355-
Err(err) => {
356-
// Log the retry attempt
357-
retries += 1;
358-
359-
// Delay until next slot if we get a 403 error
360-
if err.to_string().contains("403 Forbidden") {
361-
let (slot_number, _, _) = self.calculate_slot_window()?;
362-
debug!(slot_number, ?block, "403 detected - not assigned to slot");
363-
return Ok(ControlFlow::Skip);
364-
} else {
365-
error!(error = %err, "error handling inbound block");
321+
let inbound_result =
322+
match self.handle_inbound(retries, block).instrument(span.clone()).await {
323+
Ok(control_flow) => control_flow,
324+
Err(err) => {
325+
// Delay until next slot if we get a 403 error
326+
if err.to_string().contains("403 Forbidden") {
327+
let (slot_number, _, _) = self.calculate_slot_window()?;
328+
debug!(slot_number, "403 detected - skipping slot");
329+
return Ok(ControlFlow::Skip);
330+
} else {
331+
error!(error = %err, "error handling inbound block");
332+
}
333+
334+
ControlFlow::Retry
366335
}
367-
368-
ControlFlow::Retry
369-
}
370-
};
336+
};
371337

372338
let guard = span.entered();
373339

374340
match inbound_result {
375341
ControlFlow::Retry => {
342+
retries += 1;
376343
if retries > retry_limit {
377344
counter!("builder.building_too_many_retries").increment(1);
378345
debug!("retries exceeded - skipping block");
379346
return Ok(ControlFlow::Skip);
380347
}
381348
drop(guard);
382-
383-
// Detect a slot change and break out of the loop in that case too
384-
let (this_slot, start, end) = self.calculate_slot_window()?;
385-
if this_slot != current_slot {
386-
debug!("slot changed - skipping block");
387-
break inbound_result;
388-
}
389-
390-
// Otherwise retry the block
391-
debug!(retries, this_slot, start, end, "retrying block");
349+
debug!(retries, start, end, "retrying block");
392350
continue;
393351
}
394352
ControlFlow::Skip => {
@@ -424,6 +382,12 @@ impl SubmitTask {
424382
let now = std::time::SystemTime::now();
425383
now.duration_since(UNIX_EPOCH).unwrap().as_secs()
426384
}
385+
386+
/// Returns the next host block height.
387+
async fn next_host_block_height(&self) -> eyre::Result<u64> {
388+
let block_num = self.provider().get_block_number().await?;
389+
Ok(block_num + 1)
390+
}
427391

428392
/// Task future for the submit task
429393
/// NB: This task assumes that the simulator will only send it blocks for
@@ -471,3 +435,27 @@ impl SubmitTask {
471435
(sender, handle)
472436
}
473437
}
438+
439+
fn calculate_gas_limits(retry_count: usize) -> (u128, u128, u128) {
440+
let base_fee_per_gas: u128 = 100_000_000_000;
441+
let base_priority_fee_per_gas: u128 = 2_000_000_000;
442+
let base_fee_per_blob_gas: u128 = 1_000_000_000;
443+
444+
let bump_multiplier = 1150u128.pow(retry_count as u32); // 15% bump
445+
let blob_bump_multiplier = 2000u128.pow(retry_count as u32); // 100% bump (double each time) for blob gas
446+
let bump_divisor = 1000u128.pow(retry_count as u32);
447+
448+
let max_fee_per_gas = base_fee_per_gas * bump_multiplier / bump_divisor;
449+
let max_priority_fee_per_gas = base_priority_fee_per_gas * bump_multiplier / bump_divisor;
450+
let max_fee_per_blob_gas = base_fee_per_blob_gas * blob_bump_multiplier / bump_divisor;
451+
452+
debug!(
453+
retry_count,
454+
max_fee_per_gas,
455+
max_priority_fee_per_gas,
456+
max_fee_per_blob_gas,
457+
"calculated bumped gas parameters"
458+
);
459+
460+
(max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
461+
}

0 commit comments

Comments
 (0)