Skip to content

Commit a161e1b

Browse files
dylanlottprestwich
authored andcommitted
fix: retry logic bugs
1 parent 5e2c052 commit a161e1b

File tree

2 files changed

+80
-88
lines changed

2 files changed

+80
-88
lines changed

src/config.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ pub struct BuilderConfig {
7070
/// NOTE: should not include the host_rpc_url value
7171
#[from_env(
7272
var = "TX_BROADCAST_URLS",
73-
desc = "Additional RPC URLs to which to broadcast transactions",
74-
infallible
73+
desc = "Additional RPC URLs to which the builder broadcasts transactions",
74+
infallible,
75+
optional
7576
)]
7677
pub tx_broadcast_urls: Vec<Cow<'static, str>>,
7778
/// address of the Zenith contract on Host.

src/tasks/submit.rs

Lines changed: 77 additions & 86 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,
@@ -158,19 +148,13 @@ impl SubmitTask {
158148
if let Err(TransportError::ErrorResp(e)) =
159149
self.provider().call(tx.clone()).block(BlockNumberOrTag::Pending.into()).await
160150
{
161-
error!(
162-
code = e.code,
163-
message = %e.message,
164-
data = ?e.data,
165-
"error in transaction submission"
166-
);
167-
151+
// NB: These errors are all handled the same way but are logged for debugging purposes
168152
if e.as_revert_data()
169153
.map(|data| data.starts_with(&IncorrectHostBlock::SELECTOR))
170154
.unwrap_or_default()
171155
{
172156
debug!(%e, "incorrect host block");
173-
return Some(Ok(ControlFlow::Retry));
157+
return Some(Ok(ControlFlow::Skip));
174158
}
175159

176160
if e.as_revert_data()
@@ -189,6 +173,12 @@ impl SubmitTask {
189173
return Some(Ok(ControlFlow::Skip));
190174
}
191175

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

@@ -205,22 +195,14 @@ impl SubmitTask {
205195
) -> Result<TransactionRequest, eyre::Error> {
206196
// TODO: ENG-1082 Implement fills
207197
let fills = vec![];
208-
209198
// Extract the signature components from the response
210199
let (v, r, s) = extract_signature_components(&resp.sig);
211200

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

223-
// manually retrieve nonce
205+
// manually retrieve nonce // TODO: Maybe this should be done in Env task and passed through elsewhere
224206
let nonce =
225207
self.provider().get_transaction_count(self.provider().default_signer_address()).await?;
226208
debug!(nonce, "assigned nonce");
@@ -238,13 +220,12 @@ impl SubmitTask {
238220
// Create a blob transaction with the blob header and signature values and return it
239221
let tx = self
240222
.build_blob_tx(fills, header, v, r, s, block)?
241-
.with_from(self.provider().default_signer_address())
242223
.with_to(self.config.builder_helper_address)
243-
.with_gas_limit(gas_limit)
224+
.with_max_fee_per_gas(max_fee_per_gas)
244225
.with_max_priority_fee_per_gas(max_priority_fee_per_gas)
226+
.with_max_fee_per_blob_gas(max_fee_per_blob_gas)
245227
.with_nonce(nonce);
246228

247-
debug!(?tx, "prepared transaction request");
248229
Ok(tx)
249230
}
250231

@@ -255,23 +236,16 @@ impl SubmitTask {
255236
resp: &SignResponse,
256237
tx: TransactionRequest,
257238
) -> Result<ControlFlow, eyre::Error> {
258-
debug!(
259-
host_block_number = %resp.req.host_block_number,
260-
gas_limit = %resp.req.gas_limit,
261-
nonce = ?tx.nonce,
262-
"sending transaction to network"
263-
);
264-
265239
// assign the nonce and fill the rest of the values
266240
let SendableTx::Envelope(tx) = self.provider().fill(tx).await? else {
267241
bail!("failed to fill transaction")
268242
};
269-
debug!(tx_hash = %tx.tx_hash(), nonce = ?tx.nonce(), gas_limit = ?tx.gas_limit(), blob_gas_used = ?tx.blob_gas_used(), "filled blob transaction");
243+
debug!(tx_hash = ?tx.hash(), host_block_number = %resp.req.host_block_number, "sending transaction to network");
270244

271245
// send the tx via the primary host_provider
272246
let fut = spawn_provider_send!(self.provider(), &tx);
273247

274-
// spawn send_tx futures for all additional broadcast host_providers
248+
// spawn send_tx futures on retry attempts for all additional broadcast host_providers
275249
for host_provider in self.config.connect_additional_broadcast() {
276250
spawn_provider_send!(&host_provider, &tx);
277251
}
@@ -281,16 +255,19 @@ impl SubmitTask {
281255
error!("receipts task gone");
282256
}
283257

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

291-
// Okay so the code gets all the way to this log
292-
// but we don't see the tx hash in the logs or in the explorer,
293-
// not even as a failed TX, just not at all.
294271
info!(
295272
tx_hash = %tx.tx_hash(),
296273
ru_chain_id = %resp.req.ru_chain_id,
@@ -339,54 +316,38 @@ impl SubmitTask {
339316

340317
// Retry loop
341318
let result = loop {
319+
// Log the retry attempt
342320
let span = debug_span!("SubmitTask::retrying_handle_inbound", retries);
343321

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

370339
let guard = span.entered();
371340

372341
match inbound_result {
373342
ControlFlow::Retry => {
343+
retries += 1;
374344
if retries > retry_limit {
375345
counter!("builder.building_too_many_retries").increment(1);
376346
debug!("retries exceeded - skipping block");
377347
return Ok(ControlFlow::Skip);
378348
}
379349
drop(guard);
380-
381-
// Detect a slot change and break out of the loop in that case too
382-
let (this_slot, start, end) = self.calculate_slot_window()?;
383-
if this_slot != current_slot {
384-
debug!("slot changed - skipping block");
385-
break inbound_result;
386-
}
387-
388-
// Otherwise retry the block
389-
debug!(retries, this_slot, start, end, "retrying block");
350+
debug!(retries, start, end, "retrying block");
390351
continue;
391352
}
392353
ControlFlow::Skip => {
@@ -423,6 +384,12 @@ impl SubmitTask {
423384
now.duration_since(UNIX_EPOCH).unwrap().as_secs()
424385
}
425386

387+
/// Returns the next host block height.
388+
async fn next_host_block_height(&self) -> eyre::Result<u64> {
389+
let block_num = self.provider().get_block_number().await?;
390+
Ok(block_num + 1)
391+
}
392+
426393
/// Task future for the submit task
427394
/// NB: This task assumes that the simulator will only send it blocks for
428395
/// slots that it's assigned.
@@ -469,3 +436,27 @@ impl SubmitTask {
469436
(sender, handle)
470437
}
471438
}
439+
440+
fn calculate_gas_limits(retry_count: usize) -> (u128, u128, u128) {
441+
let base_fee_per_gas: u128 = 100_000_000_000;
442+
let base_priority_fee_per_gas: u128 = 2_000_000_000;
443+
let base_fee_per_blob_gas: u128 = 1_000_000_000;
444+
445+
let bump_multiplier = 1150u128.pow(retry_count as u32); // 15% bump
446+
let blob_bump_multiplier = 2000u128.pow(retry_count as u32); // 100% bump (double each time) for blob gas
447+
let bump_divisor = 1000u128.pow(retry_count as u32);
448+
449+
let max_fee_per_gas = base_fee_per_gas * bump_multiplier / bump_divisor;
450+
let max_priority_fee_per_gas = base_priority_fee_per_gas * bump_multiplier / bump_divisor;
451+
let max_fee_per_blob_gas = base_fee_per_blob_gas * blob_bump_multiplier / bump_divisor;
452+
453+
debug!(
454+
retry_count,
455+
max_fee_per_gas,
456+
max_priority_fee_per_gas,
457+
max_fee_per_blob_gas,
458+
"calculated bumped gas parameters"
459+
);
460+
461+
(max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
462+
}

0 commit comments

Comments
 (0)