76287 bc low async batcher l1 transactions permanently skip a dropped nonce after mempool deadline
Description
Code Walkthrough Step-by-Step Exploitation
2
Async nonce reservation occurs before publication
async fn send_async(&self, candidate: TxCandidate) -> SendHandle {
let (tx, rx) = oneshot::channel();
let nonce = match self.nonce_manager.reserve_nonce().await {
Ok(n) => Some(n),
Err(e) => {
let _ = tx.send(Err(e));
return SendHandle::new(rx);
}
};
let manager = self.clone();
tokio::spawn(async move {
let result = manager.send_tx(candidate, nonce).await;
let _ = tx.send(result);
});
SendHandle::new(rx)
}3
The nonce manager records a high-water mark for async reservations
pub fn consume_reserved(mut self) -> Result<u64, TxManagerError> {
let nonce = self.nonce;
if let Some(mut guard) = self.guard.take() {
let next = nonce.checked_add(1).ok_or(TxManagerError::NonceOverflow)?;
guard.reserved_high_water = guard.reserved_high_water.max(next);
}
Ok(nonce)
}4
5
After the deadline, the send loop aborts as a non-retryable failure
pub fn critical_error(&self) -> Option<TxManagerError> {
let now = Instant::now();
let inner = self.inner.lock().expect("SendState mutex poisoned");
if !inner.mined_txs.is_empty() {
return None;
}
if inner.already_reserved {
return Some(TxManagerError::AlreadyReserved);
}
if inner.nonce_too_high {
return Some(TxManagerError::NonceTooHigh);
}
if !inner.has_published && inner.nonce_too_low_count > 0 {
return Some(TxManagerError::NonceTooLow);
}
if inner.nonce_too_low_count >= self.safe_abort_nonce_too_low_count {
return Some(TxManagerError::NonceTooLow);
}
if let Some(deadline) = inner.mempool_deadline
&& now >= deadline
{
return Some(TxManagerError::MempoolDeadlineExpired);
}6
The cleanup path does not reset or return nonce N
fn should_reset_nonce_on_send_error<T>(
result: &TxManagerResult<T>,
send_state: &SendState,
nonce_override: Option<u64>,
) -> bool {
match result {
Ok(_) => false,
Err(TxManagerError::NonceTooHigh) => true,
_ if nonce_override.is_some() => false,
Err(TxManagerError::SendTimeout) => true,
Err(_) => !send_state.has_published(),
}
}
fn should_return_reserved_nonce<T>(
result: &TxManagerResult<T>,
send_state: &SendState,
) -> bool {
match result {
Ok(_) | Err(TxManagerError::NonceTooHigh | TxManagerError::NonceTooLow) => false,
Err(_) => !send_state.has_published(),
}
}7
Reset cannot recover the skipped nonce once the high-water mark moved
let effective = nonce.max(guard.reserved_high_water);
if effective != nonce {
debug!(
chain_nonce = nonce,
high_water = guard.reserved_high_water,
effective,
"high-water mark advanced nonce past chain value",
);
}
let next = effective.checked_add(1).ok_or(TxManagerError::NonceOverflow)?;
guard.nonce = Some(next);8
Batcher requeues but cannot progress
let handle = self.tx_manager.send_async(candidate).await;
// ... outcome handling ...
Err(e) => {
warn!(error = %e, "submission failed");
TxOutcome::Failed
}TxOutcome::Failed => {
let count = ids.len();
for id in ids {
pipeline.requeue(id);
}
warn!(submissions = %count, "submission failed, requeued for retry");
}As comparison with Optimism reference (op-service/txmgr)
Location
Recommendation
Proof of Concept
Previous74777 bc medium base node remote denial of serviceNext75413 sc high zk proof executor derives blobbasefee from base s da footprint header field allowing invalid output roots
Was this helpful?