Skip to content

Commit 0a6647c

Browse files
authored
fix: Drop transaction if we cannot resubmit it (#7240)
Co-authored-by: Danil Nemirovsky <[email protected]>
1 parent 8fd3bf7 commit 0a6647c

File tree

5 files changed

+196
-9
lines changed

5 files changed

+196
-9
lines changed

rust/main/lander/src/adapter/chains/ethereum/adapter.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ impl EthereumAdapter {
164164
tx: &Transaction,
165165
gas_price: &GasPrice,
166166
) -> Result<(), LanderError> {
167+
use TransactionStatus::*;
168+
167169
let precursor = tx.precursor();
168170
let tx_gas_price = precursor.extract_gas_price();
169171

@@ -181,7 +183,11 @@ impl EthereumAdapter {
181183
?tx,
182184
"not resubmitting transaction since new gas price is the same as the old one"
183185
);
184-
return Err(LanderError::TxAlreadyExists);
186+
187+
return match tx.status {
188+
PendingInclusion | Dropped(_) => Err(LanderError::TxWontBeResubmitted),
189+
Mempool | Included | Finalized => Err(LanderError::TxAlreadyExists),
190+
};
185191
}
186192

187193
Ok(())

rust/main/lander/src/adapter/chains/ethereum/adapter/tests.rs

Lines changed: 174 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ mod check_if_resubmission_makes_sense {
174174
}
175175

176176
#[test]
177-
fn resubmission_with_same_gas_price_is_rejected() {
178-
// Transaction with existing gas price
177+
fn resubmission_with_same_gas_price_is_rejected_for_included() {
178+
// Transaction with existing gas price in Included status
179179
let mut tx = dummy_evm_tx(
180180
ExpectedTxType::Eip1559,
181181
vec![],
@@ -207,6 +207,144 @@ mod check_if_resubmission_makes_sense {
207207
assert!(matches!(result, Err(LanderError::TxAlreadyExists)));
208208
}
209209

210+
#[test]
211+
fn resubmission_with_same_gas_price_is_rejected_for_pending_inclusion() {
212+
// Transaction with existing gas price in PendingInclusion status
213+
let mut tx = dummy_evm_tx(
214+
ExpectedTxType::Eip1559,
215+
vec![],
216+
crate::TransactionStatus::PendingInclusion,
217+
H160::random(),
218+
);
219+
220+
// Set existing gas price
221+
if let VmSpecificTxData::Evm(ethereum_tx_precursor) = &mut tx.vm_specific_data {
222+
ethereum_tx_precursor.tx = TypedTransaction::Eip1559(Eip1559TransactionRequest {
223+
from: Some(H160::random()),
224+
to: Some(H160::random().into()),
225+
nonce: Some(0.into()),
226+
gas: Some(21000.into()),
227+
max_fee_per_gas: Some(1000000000.into()),
228+
max_priority_fee_per_gas: Some(1000000.into()),
229+
value: Some(1.into()),
230+
..Default::default()
231+
});
232+
}
233+
234+
// New gas price is the same
235+
let new_gas_price = GasPrice::Eip1559 {
236+
max_fee: 1000000000u64.into(),
237+
max_priority_fee: 1000000u64.into(),
238+
};
239+
240+
let result = EthereumAdapter::check_if_resubmission_makes_sense(&tx, &new_gas_price);
241+
assert!(matches!(result, Err(LanderError::TxWontBeResubmitted)));
242+
}
243+
244+
#[test]
245+
fn resubmission_with_same_gas_price_is_rejected_for_mempool() {
246+
// Transaction with existing gas price in Mempool status
247+
let mut tx = dummy_evm_tx(
248+
ExpectedTxType::Eip1559,
249+
vec![],
250+
crate::TransactionStatus::Mempool,
251+
H160::random(),
252+
);
253+
254+
// Set existing gas price
255+
if let VmSpecificTxData::Evm(ethereum_tx_precursor) = &mut tx.vm_specific_data {
256+
ethereum_tx_precursor.tx = TypedTransaction::Eip1559(Eip1559TransactionRequest {
257+
from: Some(H160::random()),
258+
to: Some(H160::random().into()),
259+
nonce: Some(0.into()),
260+
gas: Some(21000.into()),
261+
max_fee_per_gas: Some(1000000000.into()),
262+
max_priority_fee_per_gas: Some(1000000.into()),
263+
value: Some(1.into()),
264+
..Default::default()
265+
});
266+
}
267+
268+
// New gas price is the same
269+
let new_gas_price = GasPrice::Eip1559 {
270+
max_fee: 1000000000u64.into(),
271+
max_priority_fee: 1000000u64.into(),
272+
};
273+
274+
let result = EthereumAdapter::check_if_resubmission_makes_sense(&tx, &new_gas_price);
275+
assert!(matches!(result, Err(LanderError::TxAlreadyExists)));
276+
}
277+
278+
#[test]
279+
fn resubmission_with_same_gas_price_is_rejected_for_finalized() {
280+
// Transaction with existing gas price in Finalized status
281+
let mut tx = dummy_evm_tx(
282+
ExpectedTxType::Eip1559,
283+
vec![],
284+
crate::TransactionStatus::Finalized,
285+
H160::random(),
286+
);
287+
288+
// Set existing gas price
289+
if let VmSpecificTxData::Evm(ethereum_tx_precursor) = &mut tx.vm_specific_data {
290+
ethereum_tx_precursor.tx = TypedTransaction::Eip1559(Eip1559TransactionRequest {
291+
from: Some(H160::random()),
292+
to: Some(H160::random().into()),
293+
nonce: Some(0.into()),
294+
gas: Some(21000.into()),
295+
max_fee_per_gas: Some(1000000000.into()),
296+
max_priority_fee_per_gas: Some(1000000.into()),
297+
value: Some(1.into()),
298+
..Default::default()
299+
});
300+
}
301+
302+
// New gas price is the same
303+
let new_gas_price = GasPrice::Eip1559 {
304+
max_fee: 1000000000u64.into(),
305+
max_priority_fee: 1000000u64.into(),
306+
};
307+
308+
let result = EthereumAdapter::check_if_resubmission_makes_sense(&tx, &new_gas_price);
309+
assert!(matches!(result, Err(LanderError::TxAlreadyExists)));
310+
}
311+
312+
#[test]
313+
fn resubmission_with_same_gas_price_is_rejected_for_dropped() {
314+
use crate::transaction::DropReason;
315+
316+
// Transaction with existing gas price in Dropped status
317+
let mut tx = dummy_evm_tx(
318+
ExpectedTxType::Eip1559,
319+
vec![],
320+
crate::TransactionStatus::Dropped(DropReason::DroppedByChain),
321+
H160::random(),
322+
);
323+
324+
// Set existing gas price
325+
if let VmSpecificTxData::Evm(ethereum_tx_precursor) = &mut tx.vm_specific_data {
326+
ethereum_tx_precursor.tx = TypedTransaction::Eip1559(Eip1559TransactionRequest {
327+
from: Some(H160::random()),
328+
to: Some(H160::random().into()),
329+
nonce: Some(0.into()),
330+
gas: Some(21000.into()),
331+
max_fee_per_gas: Some(1000000000.into()),
332+
max_priority_fee_per_gas: Some(1000000.into()),
333+
value: Some(1.into()),
334+
..Default::default()
335+
});
336+
}
337+
338+
// New gas price is the same
339+
let new_gas_price = GasPrice::Eip1559 {
340+
max_fee: 1000000000u64.into(),
341+
max_priority_fee: 1000000u64.into(),
342+
};
343+
344+
let result = EthereumAdapter::check_if_resubmission_makes_sense(&tx, &new_gas_price);
345+
assert!(matches!(result, Err(LanderError::TxWontBeResubmitted)));
346+
}
347+
210348
#[test]
211349
fn legacy_tx_resubmission_with_higher_gas_price_is_allowed() {
212350
// Transaction with existing legacy gas price
@@ -240,8 +378,8 @@ mod check_if_resubmission_makes_sense {
240378
}
241379

242380
#[test]
243-
fn legacy_tx_resubmission_with_same_gas_price_is_rejected() {
244-
// Transaction with existing legacy gas price
381+
fn legacy_tx_resubmission_with_same_gas_price_is_rejected_for_included() {
382+
// Transaction with existing legacy gas price in Included status
245383
let mut tx = dummy_evm_tx(
246384
ExpectedTxType::Legacy,
247385
vec![],
@@ -271,6 +409,38 @@ mod check_if_resubmission_makes_sense {
271409
assert!(matches!(result, Err(LanderError::TxAlreadyExists)));
272410
}
273411

412+
#[test]
413+
fn legacy_tx_resubmission_with_same_gas_price_is_rejected_for_pending_inclusion() {
414+
// Transaction with existing legacy gas price in PendingInclusion status
415+
let mut tx = dummy_evm_tx(
416+
ExpectedTxType::Legacy,
417+
vec![],
418+
crate::TransactionStatus::PendingInclusion,
419+
H160::random(),
420+
);
421+
422+
// Set existing gas price
423+
if let VmSpecificTxData::Evm(ethereum_tx_precursor) = &mut tx.vm_specific_data {
424+
ethereum_tx_precursor.tx = TypedTransaction::Legacy(TransactionRequest {
425+
from: Some(H160::random()),
426+
to: Some(H160::random().into()),
427+
nonce: Some(0.into()),
428+
gas: Some(21000.into()),
429+
gas_price: Some(1000000000.into()),
430+
value: Some(1.into()),
431+
..Default::default()
432+
});
433+
}
434+
435+
// New gas price is the same
436+
let new_gas_price = GasPrice::NonEip1559 {
437+
gas_price: 1000000000u64.into(),
438+
};
439+
440+
let result = EthereumAdapter::check_if_resubmission_makes_sense(&tx, &new_gas_price);
441+
assert!(matches!(result, Err(LanderError::TxWontBeResubmitted)));
442+
}
443+
274444
#[test]
275445
fn eip1559_resubmission_with_only_max_fee_increased_is_allowed() {
276446
// Transaction with existing gas price

rust/main/lander/src/dispatcher/db/transaction/loader.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,26 @@ impl LoadableFromDb for TransactionDbLoader {
4141
type Item = Transaction;
4242

4343
async fn highest_index(&self) -> Result<u32, LanderError> {
44-
Ok(self.db.retrieve_highest_transaction_index().await?)
44+
let index = self.db.retrieve_highest_transaction_index().await?;
45+
debug!(?index, "Highest transaction index");
46+
Ok(index)
4547
}
4648

4749
async fn retrieve_by_index(&self, index: u32) -> Result<Option<Self::Item>, LanderError> {
48-
Ok(self.db.retrieve_transaction_by_index(index).await?)
50+
let transaction = self.db.retrieve_transaction_by_index(index).await?;
51+
debug!(?transaction, ?index, "Retrieved transaction by index");
52+
Ok(transaction)
4953
}
5054

5155
async fn load(&self, item: Self::Item) -> Result<LoadingOutcome, LanderError> {
5256
match item.status {
5357
TransactionStatus::PendingInclusion | TransactionStatus::Mempool => {
58+
debug!(?item, "Send transaction to inclusion stage");
5459
self.inclusion_stage_sender.send(item).await?;
5560
Ok(LoadingOutcome::Loaded)
5661
}
5762
TransactionStatus::Included => {
63+
debug!(?item, "Send transaction to finality stage");
5864
self.finality_stage_sender.send(item).await?;
5965
Ok(LoadingOutcome::Loaded)
6066
}

rust/main/lander/src/dispatcher/stages/inclusion_stage.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ impl InclusionStage {
274274
match tx_status {
275275
TransactionStatus::PendingInclusion | TransactionStatus::Mempool => {
276276
info!(tx_uuid = ?tx.uuid, ?tx_status, "Transaction is pending inclusion");
277+
update_tx_status(state, &mut tx, tx_status.clone()).await?;
277278
if !state.adapter.tx_ready_for_resubmission(&tx).await {
278279
info!(?tx, "Transaction is not ready for resubmission");
279280
return Ok(());

rust/main/lander/src/error.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub enum LanderError {
1616
TxReverted,
1717
#[error("The transaction hash was not found: {0}")]
1818
TxHashNotFound(String),
19+
#[error("Transaction won't be resubmitted")]
20+
TxWontBeResubmitted,
1921
#[error("Failed to send over a channel {0}")]
2022
ChannelSendFailure(#[from] tokio::sync::mpsc::error::SendError<Transaction>),
2123
#[error("Channel closed")]
@@ -47,6 +49,8 @@ impl LanderError {
4749
TxSubmissionError(_) => "TxSubmissionError".to_string(),
4850
TxAlreadyExists => "TxAlreadyExists".to_string(),
4951
TxReverted => "TxReverted".to_string(),
52+
TxHashNotFound(_) => "TxHashNotFound".to_string(),
53+
TxWontBeResubmitted => "TxWontBeResubmitted".to_string(),
5054
ChannelSendFailure(_) => "ChannelSendFailure".to_string(),
5155
ChannelClosed => "ChannelClosed".to_string(),
5256
EyreError(_) => "EyreError".to_string(),
@@ -56,7 +60,6 @@ impl LanderError {
5660
NonRetryableError(_) => "NonRetryableError".to_string(),
5761
DbError(_) => "DbError".to_string(),
5862
ChainCommunicationError(_) => "ChainCommunicationError".to_string(),
59-
TxHashNotFound(_) => "TxHashNotFound".to_string(),
6063
}
6164
}
6265
}
@@ -128,7 +131,8 @@ impl IsRetryable for LanderError {
128131
| PayloadNotFound
129132
| TxAlreadyExists
130133
| DbError(_)
131-
| TxHashNotFound(_) => false,
134+
| TxHashNotFound(_)
135+
| TxWontBeResubmitted => false,
132136
}
133137
}
134138
}

0 commit comments

Comments
 (0)