Skip to content
This repository was archived by the owner on Feb 23, 2026. It is now read-only.

Commit 54cb18b

Browse files
committed
test(sidecar): balance increase dropped, chained preconfs dropped
1 parent 042f138 commit 54cb18b

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

bolt-sidecar/src/state/execution.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,8 @@ mod tests {
894894
Ok(())
895895
}
896896

897+
/// Tests that a balance increase allows the recipient to send a transaction using preconfirmed
898+
/// state.
897899
#[tokio::test]
898900
async fn test_balance_increase() -> eyre::Result<()> {
899901
let _ = tracing_subscriber::fmt::try_init();
@@ -954,6 +956,142 @@ mod tests {
954956
Ok(())
955957
}
956958

959+
/// Tests that a balance increase is dropped if the preconfirmation is cancelled.
960+
#[tokio::test]
961+
async fn test_balance_increase_dropped() -> eyre::Result<()> {
962+
let _ = tracing_subscriber::fmt::try_init();
963+
964+
let anvil = launch_anvil();
965+
let client = StateClient::new(anvil.endpoint_url());
966+
967+
let mut state = ExecutionState::new(client.clone(), LimitsOpts::default()).await?;
968+
969+
let sender = anvil.addresses().first().unwrap();
970+
let sender_pk = anvil.keys().first().unwrap();
971+
let signer = LocalSigner::random();
972+
973+
// initialize the state by updating the head once
974+
let slot = client.get_head().await?;
975+
state.update_head(None, slot).await?;
976+
let target_slot = slot;
977+
978+
let recipient_sk = PrivateKeySigner::random();
979+
let recipient_pk = recipient_sk.address();
980+
981+
// Create a transfer of 1 ETH to the recipient
982+
let tx = default_test_transaction(*sender, Some(0))
983+
.with_to(recipient_pk)
984+
.with_value(WEI_IN_ETHER);
985+
let mut request =
986+
create_signed_inclusion_request(&[tx.clone()], &sender_pk.to_bytes(), 10).await?;
987+
988+
let validation = state.validate_request(&mut request).await;
989+
assert!(validation.is_ok(), "Validation failed: {validation:?}");
990+
991+
add_constraint(request.clone(), &mut state, &signer, target_slot)?;
992+
993+
// Send a cancel tx
994+
let tx = default_test_transaction(*sender, Some(0))
995+
.with_to(*sender)
996+
.with_max_priority_fee_per_gas(tx.max_priority_fee_per_gas.unwrap() + 1);
997+
998+
let _ = client.inner().send_transaction(tx).await?;
999+
1000+
// Wait 1s, update the head to include the cancel tx; the constraint should be dropped
1001+
tokio::time::sleep(Duration::from_secs(1)).await;
1002+
state.update_head(None, slot + 1).await?;
1003+
1004+
// Now the recipient should not have balance
1005+
let tx = default_test_transaction(recipient_pk, Some(0)).with_value(U256::from(1));
1006+
let mut request = create_signed_inclusion_request(
1007+
&[tx],
1008+
recipient_sk.to_bytes().as_slice(),
1009+
target_slot + 1,
1010+
)
1011+
.await?;
1012+
1013+
let validation_result = state.validate_request(&mut request).await;
1014+
1015+
assert!(
1016+
matches!(validation_result, Err(ValidationError::InsufficientBalance)),
1017+
"Expected InsufficientBalance error, got {:?}",
1018+
validation_result
1019+
);
1020+
1021+
Ok(())
1022+
}
1023+
1024+
/// Tests that a chained preconfirmation is dropped if the previous one is cancelled.
1025+
#[tokio::test]
1026+
async fn test_chained_preconfs_dropped() -> eyre::Result<()> {
1027+
let _ = tracing_subscriber::fmt::try_init();
1028+
1029+
let anvil = launch_anvil();
1030+
let client = StateClient::new(anvil.endpoint_url());
1031+
1032+
let mut state = ExecutionState::new(client.clone(), LimitsOpts::default()).await?;
1033+
1034+
let sender = anvil.addresses().first().unwrap();
1035+
let sender_pk = anvil.keys().first().unwrap();
1036+
let signer = LocalSigner::random();
1037+
1038+
// initialize the state by updating the head once
1039+
let slot = client.get_head().await?;
1040+
state.update_head(None, slot).await?;
1041+
let target_slot = slot + 2;
1042+
1043+
let recipient_sk = PrivateKeySigner::random();
1044+
let recipient_pk = recipient_sk.address();
1045+
1046+
// Create a transfer of 1 ETH to the recipient
1047+
let tx_1 = default_test_transaction(*sender, Some(0))
1048+
.with_to(recipient_pk)
1049+
.with_value(WEI_IN_ETHER);
1050+
let mut request =
1051+
create_signed_inclusion_request(&[tx_1.clone()], &sender_pk.to_bytes(), 10).await?;
1052+
1053+
let validation = state.validate_request(&mut request).await;
1054+
assert!(validation.is_ok(), "Validation failed: {validation:?}");
1055+
1056+
add_constraint(request.clone(), &mut state, &signer, target_slot)?;
1057+
1058+
// Now the sender should have enough balance to send a transaction
1059+
let tx_2 = default_test_transaction(recipient_pk, Some(0))
1060+
.with_value(WEI_IN_ETHER.div_ceil(U256::from(2)));
1061+
let mut request =
1062+
create_signed_inclusion_request(&[tx_2], recipient_sk.to_bytes().as_slice(), 10)
1063+
.await?;
1064+
1065+
let validation_result = state.validate_request(&mut request).await;
1066+
1067+
add_constraint(request, &mut state, &signer, target_slot)?;
1068+
1069+
assert!(validation_result.is_ok(), "validation failed: {validation_result:?}");
1070+
1071+
// Cancel the first preconfirmation request so that both preconfs are dropped.
1072+
1073+
// Send a cancel tx
1074+
let tx_3 = default_test_transaction(*sender, Some(0))
1075+
.with_to(*sender)
1076+
.with_max_priority_fee_per_gas(tx_1.max_priority_fee_per_gas.unwrap() + 1);
1077+
1078+
let _ = client.inner().send_transaction(tx_3).await?;
1079+
1080+
// Wait 1s, update the head to include the cancel tx; the constraint should be dropped
1081+
tokio::time::sleep(Duration::from_secs(1)).await;
1082+
state.update_head(None, slot + 1).await?;
1083+
1084+
// Check that both preconfs have been dropped
1085+
1086+
let template = state.block_templates.get(&target_slot).unwrap();
1087+
assert!(
1088+
template.signed_constraints_list.is_empty(),
1089+
"block template should be empty, but got: {template:?}"
1090+
);
1091+
1092+
Ok(())
1093+
}
1094+
9571095
#[tokio::test]
9581096
async fn test_invalid_inclusion_request_basefee() -> eyre::Result<()> {
9591097
let _ = tracing_subscriber::fmt::try_init();

0 commit comments

Comments
 (0)