Skip to content

Commit 73f707b

Browse files
authored
feat: batched eoa refunds (#650)
## 📝 Summary Introduce batched end of block refunds for EOA refunds recipients. ## ✅ I have completed the following steps: * [x] Run `make lint` * [x] Run `make test` * [x] Added tests (if applicable)
1 parent 63c8f52 commit 73f707b

File tree

8 files changed

+274
-34
lines changed

8 files changed

+274
-34
lines changed

crates/rbuilder/src/building/builders/block_building_helper.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ impl BlockBuildingHelperFromProvider {
288288
(self.payout_tx_gas, payout_tx_value)
289289
{
290290
use_last_tx_payment = true;
291-
match self.partial_block.insert_proposer_payout_tx(
291+
match self.partial_block.insert_refunds_and_proposer_payout_tx(
292292
payout_tx_gas,
293293
payout_tx_value,
294294
&self.building_ctx,

crates/rbuilder/src/building/conflict.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@ pub fn find_conflict_slow(
3131
) -> eyre::Result<HashMap<(OrderId, OrderId), Conflict>> {
3232
let mut state_provider = Arc::<dyn StateProvider>::from(state_provider);
3333
let mut local_ctx = ThreadBlockBuildingContext::default();
34+
// We use empty combined refunds because the value of the bundle will
35+
// not change from batching.
36+
let combined_refunds = std::collections::HashMap::default();
3437
let profits_alone = {
3538
let mut profits_alone = HashMap::new();
3639
for order in orders {
3740
let mut state = BlockState::new_arc(state_provider);
3841
let mut fork = PartialBlockFork::new(&mut state, ctx, &mut local_ctx);
39-
if let Ok(res) = fork.commit_order(order, 0, 0, 0, true)? {
42+
if let Ok(res) = fork.commit_order(order, 0, 0, 0, true, &combined_refunds)? {
4043
profits_alone.insert(order.id(), res.coinbase_profit);
4144
};
4245
state_provider = state.into_provider();
@@ -76,7 +79,7 @@ pub fn find_conflict_slow(
7679
let mut fork = PartialBlockFork::new(&mut state, ctx, &mut local_ctx);
7780
let mut gas_used = 0;
7881
let mut blob_gas_used = 0;
79-
match fork.commit_order(order1, gas_used, 0, blob_gas_used, true)? {
82+
match fork.commit_order(order1, gas_used, 0, blob_gas_used, true, &combined_refunds)? {
8083
Ok(res) => {
8184
gas_used += res.gas_used;
8285
blob_gas_used += res.blob_gas_used;
@@ -85,7 +88,7 @@ pub fn find_conflict_slow(
8588
results.insert(pair, Conflict::Fatal);
8689
}
8790
};
88-
match fork.commit_order(order2, gas_used, 0, blob_gas_used, true)? {
91+
match fork.commit_order(order2, gas_used, 0, blob_gas_used, true, &combined_refunds)? {
8992
Ok(re) => {
9093
let profit_alone = *profits_alone.get(&order2.id()).unwrap();
9194
let profit_with_conflict = re.coinbase_profit;

crates/rbuilder/src/building/mod.rs

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ use crate::{
77
provider::RootHasher,
88
roothash::RootHashError,
99
utils::{
10-
a2r_withdrawal, default_cfg_env, elapsed_ms,
10+
a2r_withdrawal,
11+
constants::BASE_TX_GAS,
12+
default_cfg_env, elapsed_ms,
1113
receipts::{
1214
calculate_receipt_root_and_block_logs_bloom, calculate_transactions_root, BloomCache,
1315
TransactionRootCache,
1416
},
1517
timestamp_as_u64, Signer,
1618
},
1719
};
18-
use alloy_consensus::{Header, EMPTY_OMMER_ROOT_HASH};
20+
use alloy_consensus::{constants::KECCAK_EMPTY, Header, EMPTY_OMMER_ROOT_HASH};
1921
use alloy_eips::{
2022
eip1559::{calculate_block_gas_limit, ETHEREUM_BLOCK_GAS_LIMIT_30M},
2123
eip4844::BlobTransactionSidecar,
@@ -52,7 +54,7 @@ use revm::{
5254
};
5355
use serde::Deserialize;
5456
use std::{
55-
collections::HashMap,
57+
collections::{hash_map, HashMap},
5658
hash::Hash,
5759
str::FromStr,
5860
sync::Arc,
@@ -427,6 +429,8 @@ pub struct PartialBlock<Tracer: SimulationTracer> {
427429
pub coinbase_profit: U256,
428430
/// Tx execution info belonging to successfully executed orders.
429431
pub executed_tx_infos: Vec<TransactionExecutionInfo>,
432+
/// Combined refunds.
433+
pub combined_refunds: HashMap<Address, U256>,
430434
pub tracer: Tracer,
431435
}
432436

@@ -450,6 +454,8 @@ pub enum InsertPayoutTxErr {
450454
CriticalCommitError(#[from] CriticalCommitOrderError),
451455
#[error("Profit too low to insert payout tx")]
452456
ProfitTooLow,
457+
#[error("Combined refund tx reverted")]
458+
CombinedRefundTxReverted,
453459
#[error("Payout tx reverted")]
454460
PayoutTxReverted,
455461
#[error("Signer error: {0}")]
@@ -542,6 +548,7 @@ impl<Tracer: SimulationTracer> PartialBlock<Tracer> {
542548
blob_gas_used: self.blob_gas_used,
543549
coinbase_profit: self.coinbase_profit,
544550
executed_tx_infos: self.executed_tx_infos,
551+
combined_refunds: self.combined_refunds,
545552
tracer,
546553
}
547554
}
@@ -580,6 +587,7 @@ impl<Tracer: SimulationTracer> PartialBlock<Tracer> {
580587
self.gas_reserved,
581588
self.blob_gas_used,
582589
self.discard_txs,
590+
&self.combined_refunds,
583591
)?;
584592
let ok_result = match exec_result {
585593
Ok(ok) => ok,
@@ -603,6 +611,18 @@ impl<Tracer: SimulationTracer> PartialBlock<Tracer> {
603611
self.blob_gas_used += ok_result.blob_gas_used;
604612
self.coinbase_profit += ok_result.coinbase_profit;
605613
self.executed_tx_infos.extend(ok_result.tx_infos.clone());
614+
615+
// Update combined refunds
616+
if let Some((address, refund_value)) = ok_result.delayed_kickback {
617+
let entry = self.combined_refunds.entry(address);
618+
if matches!(entry, hash_map::Entry::Vacant(_)) {
619+
// This is the first refund for the recipient,
620+
// so we need to reserve the gas for the refund tx.
621+
self.gas_reserved += 21_000;
622+
}
623+
*entry.or_default() += refund_value;
624+
}
625+
606626
Ok(Ok(ExecutionResult {
607627
coinbase_profit: ok_result.coinbase_profit,
608628
inplace_sim: inplace_sim_result,
@@ -628,7 +648,7 @@ impl<Tracer: SimulationTracer> PartialBlock<Tracer> {
628648

629649
/// Inserts payout tx to ctx.attributes.suggested_fee_recipient (should be called at the end of the block)
630650
/// Returns the paid value (block profit after subtracting the burned basefee of the payout tx)
631-
pub fn insert_proposer_payout_tx(
651+
pub fn insert_refunds_and_proposer_payout_tx(
632652
&mut self,
633653
gas_limit: u64,
634654
value: U256,
@@ -641,13 +661,53 @@ impl<Tracer: SimulationTracer> PartialBlock<Tracer> {
641661
.as_ref()
642662
.ok_or(InsertPayoutTxErr::NoSigner)?;
643663
self.free_reserved_gas();
644-
let nonce = state
664+
let mut nonce = state
645665
.nonce(
646666
builder_signer.address,
647667
&ctx.shared_cached_reads,
648668
&mut local_ctx.cached_reads,
649669
)
650670
.map_err(CriticalCommitOrderError::Reth)?;
671+
672+
let mut fork = PartialBlockFork::new(state, ctx, local_ctx).with_tracer(&mut self.tracer);
673+
674+
for (refund_recipient, refund_amount) in &self.combined_refunds {
675+
let refund_recipient_code_hash = fork
676+
.state
677+
.code_hash(
678+
*refund_recipient,
679+
&ctx.shared_cached_reads,
680+
&mut fork.local_ctx.cached_reads,
681+
)
682+
.map_err(CriticalCommitOrderError::Reth)?;
683+
if refund_recipient_code_hash != KECCAK_EMPTY {
684+
error!(%refund_recipient_code_hash, %refund_recipient, %refund_amount, "Refund recipient has code, skipping refund");
685+
continue;
686+
}
687+
688+
let refund_tx = TransactionSignedEcRecoveredWithBlobs::new_no_blobs(create_payout_tx(
689+
ctx.chain_spec.as_ref(),
690+
ctx.evm_env.block_env.basefee,
691+
builder_signer,
692+
nonce,
693+
*refund_recipient,
694+
BASE_TX_GAS,
695+
*refund_amount,
696+
)?)
697+
.unwrap();
698+
let refund_result =
699+
fork.commit_tx(&refund_tx, self.gas_used, 0, self.blob_gas_used)??;
700+
if !refund_result.tx_info.receipt.success {
701+
return Err(InsertPayoutTxErr::CombinedRefundTxReverted);
702+
}
703+
704+
self.gas_used += refund_result.tx_info.gas_used;
705+
self.blob_gas_used += refund_result.blob_gas_used;
706+
self.executed_tx_infos.push(refund_result.tx_info);
707+
708+
nonce += 1;
709+
}
710+
651711
let tx = create_payout_tx(
652712
ctx.chain_spec.as_ref(),
653713
ctx.evm_env.block_env.basefee,
@@ -659,7 +719,6 @@ impl<Tracer: SimulationTracer> PartialBlock<Tracer> {
659719
)?;
660720
// payout tx has no blobs so it's safe to unwrap
661721
let tx = TransactionSignedEcRecoveredWithBlobs::new_no_blobs(tx).unwrap();
662-
let mut fork = PartialBlockFork::new(state, ctx, local_ctx).with_tracer(&mut self.tracer);
663722
let exec_result = fork.commit_tx(&tx, self.gas_used, 0, self.blob_gas_used)?;
664723
let ok_result = exec_result?;
665724
if !ok_result.tx_info.receipt.success {
@@ -919,6 +978,7 @@ impl PartialBlock<()> {
919978
blob_gas_used: 0,
920979
coinbase_profit: U256::ZERO,
921980
executed_tx_infos: Vec::new(),
981+
combined_refunds: HashMap::default(),
922982
tracer: (),
923983
}
924984
}
@@ -1014,6 +1074,7 @@ mod test {
10141074
coinbase_profit: profit_2,
10151075
},
10161076
],
1077+
delayed_kickback: None,
10171078
original_order_ids: Default::default(),
10181079
nonces_updated: Default::default(),
10191080
paid_kickbacks: Default::default(),
@@ -1056,6 +1117,7 @@ mod test {
10561117
gas_used: Default::default(),
10571118
coinbase_profit: profit,
10581119
}],
1120+
delayed_kickback: None,
10591121
original_order_ids: Default::default(),
10601122
nonces_updated: Default::default(),
10611123
paid_kickbacks: Default::default(),

0 commit comments

Comments
 (0)