Skip to content

Commit 3b45afa

Browse files
authored
feat: defensive indexing (#301)
* throw errors if L1 messages and batch commits are not sequential * add comment * fix tests * fix tests
1 parent c1762ac commit 3b45afa

File tree

3 files changed

+115
-35
lines changed

3 files changed

+115
-35
lines changed

crates/chain-orchestrator/src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ pub enum ChainOrchestratorError {
2929
/// An L1 message was not found in the database.
3030
#[error("L1 message not found at {0}")]
3131
L1MessageNotFound(L1MessageStart),
32+
/// A gap was detected in the L1 message queue: the previous message before index {0} is
33+
/// missing.
34+
#[error("L1 message queue gap detected at index {0}, previous L1 message not found")]
35+
L1MessageQueueGap(u64),
3236
/// An inconsistency was detected when trying to consolidate the chain.
3337
#[error("Chain inconsistency detected")]
3438
ChainInconsistency,
@@ -38,6 +42,9 @@ pub enum ChainOrchestratorError {
3842
/// The hash of the block header that was requested.
3943
hash: B256,
4044
},
45+
/// A gap was detected in batch commit events: the previous batch before index {0} is missing.
46+
#[error("Batch commit gap detected at index {0}, previous batch commit not found")]
47+
BatchCommitGap(u64),
4148
/// An error occurred while making a network request.
4249
#[error("Network request error: {0}")]
4350
NetworkRequestError(#[from] reth_network_p2p::error::RequestError),

crates/chain-orchestrator/src/lib.rs

Lines changed: 105 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,19 @@ impl<
636636
compute_l1_message_queue_hash(&database, &l1_message, l1_v2_message_queue_start_index)
637637
.await?;
638638
let l1_message = L1MessageEnvelope::new(l1_message, l1_block_number, None, queue_hash);
639+
640+
// Perform a consistency check to ensure the previous L1 message exists in the database.
641+
if l1_message.transaction.queue_index > 0 &&
642+
database
643+
.get_l1_message_by_index(l1_message.transaction.queue_index - 1)
644+
.await?
645+
.is_none()
646+
{
647+
return Err(ChainOrchestratorError::L1MessageQueueGap(
648+
l1_message.transaction.queue_index,
649+
))
650+
}
651+
639652
database.insert_l1_message(l1_message).await?;
640653
Ok(Some(event))
641654
}
@@ -648,6 +661,11 @@ impl<
648661
let txn = database.tx().await?;
649662
let prev_batch_index = batch.index - 1;
650663

664+
// Perform a consistency check to ensure the previous commit batch exists in the database.
665+
if txn.get_batch_by_index(prev_batch_index).await?.is_none() {
666+
return Err(ChainOrchestratorError::BatchCommitGap(batch.index))
667+
}
668+
651669
// remove any batches with an index greater than the previous batch.
652670
let affected = txn.delete_batches_gt_batch_index(prev_batch_index).await?;
653671

@@ -1152,24 +1170,28 @@ mod test {
11521170
rand::rng().fill(bytes.as_mut_slice());
11531171
let mut u = Unstructured::new(&bytes);
11541172

1155-
let batch_commit = BatchCommitData::arbitrary(&mut u).unwrap();
1156-
chain_orchestrator
1157-
.handle_l1_notification(L1Notification::BatchCommit(batch_commit.clone()));
1173+
// Insert a batch commit in the database to satisfy the chain orchestrator consistency
1174+
// checks
1175+
let batch_0 = BatchCommitData { index: 0, ..Arbitrary::arbitrary(&mut u).unwrap() };
1176+
db.insert_batch(batch_0).await.unwrap();
1177+
1178+
let batch_1 = BatchCommitData { index: 1, ..Arbitrary::arbitrary(&mut u).unwrap() };
1179+
chain_orchestrator.handle_l1_notification(L1Notification::BatchCommit(batch_1.clone()));
11581180

11591181
let event = chain_orchestrator.next().await.unwrap().unwrap();
11601182

11611183
// Verify the event structure
11621184
match event {
11631185
ChainOrchestratorEvent::BatchCommitIndexed { batch_info, safe_head, .. } => {
1164-
assert_eq!(batch_info.index, batch_commit.index);
1165-
assert_eq!(batch_info.hash, batch_commit.hash);
1186+
assert_eq!(batch_info.index, batch_1.index);
1187+
assert_eq!(batch_info.hash, batch_1.hash);
11661188
assert_eq!(safe_head, None); // No safe head since no batch revert
11671189
}
11681190
_ => panic!("Expected BatchCommitIndexed event"),
11691191
}
11701192

1171-
let batch_commit_result = db.get_batch_by_index(batch_commit.index).await.unwrap().unwrap();
1172-
assert_eq!(batch_commit, batch_commit_result);
1193+
let batch_commit_result = db.get_batch_by_index(batch_1.index).await.unwrap().unwrap();
1194+
assert_eq!(batch_1, batch_commit_result);
11731195
}
11741196

11751197
#[tokio::test]
@@ -1182,6 +1204,15 @@ mod test {
11821204
rand::rng().fill(bytes.as_mut_slice());
11831205
let mut u = Unstructured::new(&bytes);
11841206

1207+
// Insert batch 0 into the database to satisfy the consistency conditions in the chain
1208+
// orchestrator
1209+
let batch_0 = BatchCommitData {
1210+
index: 99,
1211+
calldata: Arc::new(vec![].into()),
1212+
..Arbitrary::arbitrary(&mut u).unwrap()
1213+
};
1214+
db.insert_batch(batch_0).await.unwrap();
1215+
11851216
// Create sequential batches
11861217
let batch_1 = BatchCommitData {
11871218
index: 100,
@@ -1305,22 +1336,33 @@ mod test {
13051336
rand::rng().fill(bytes.as_mut_slice());
13061337
let mut u = Unstructured::new(&bytes);
13071338

1308-
let message = TxL1Message {
1339+
// Insert an initial message in the database to satisfy the consistency checks in the chain
1340+
// orchestrator.
1341+
let message_0 = L1MessageEnvelope {
1342+
transaction: TxL1Message {
1343+
queue_index: TEST_L1_MESSAGE_QUEUE_INDEX_BOUNDARY - 2,
1344+
..Arbitrary::arbitrary(&mut u).unwrap()
1345+
},
1346+
..Arbitrary::arbitrary(&mut u).unwrap()
1347+
};
1348+
db.insert_l1_message(message_0).await.unwrap();
1349+
1350+
let message_1 = TxL1Message {
13091351
queue_index: TEST_L1_MESSAGE_QUEUE_INDEX_BOUNDARY - 1,
13101352
..Arbitrary::arbitrary(&mut u).unwrap()
13111353
};
13121354
let block_number = u64::arbitrary(&mut u).unwrap();
13131355
chain_orchestrator.handle_l1_notification(L1Notification::L1Message {
1314-
message: message.clone(),
1356+
message: message_1.clone(),
13151357
block_number,
13161358
block_timestamp: 0,
13171359
});
13181360

13191361
let _ = chain_orchestrator.next().await;
13201362

13211363
let l1_message_result =
1322-
db.get_l1_message_by_index(message.queue_index).await.unwrap().unwrap();
1323-
let l1_message = L1MessageEnvelope::new(message, block_number, None, None);
1364+
db.get_l1_message_by_index(message_1.queue_index).await.unwrap().unwrap();
1365+
let l1_message = L1MessageEnvelope::new(message_1, block_number, None, None);
13241366

13251367
assert_eq!(l1_message, l1_message_result);
13261368
}
@@ -1330,6 +1372,19 @@ mod test {
13301372
// Instantiate chain orchestrator and db
13311373
let (mut chain_orchestrator, db) = setup_test_chain_orchestrator().await;
13321374

1375+
// Insert the previous l1 message in the database to satisfy the chain orchestrator
1376+
// consistency checks.
1377+
let message = L1MessageEnvelope {
1378+
transaction: TxL1Message {
1379+
queue_index: TEST_L1_MESSAGE_QUEUE_INDEX_BOUNDARY - 1,
1380+
..Default::default()
1381+
},
1382+
l1_block_number: 1475587,
1383+
l2_block_number: None,
1384+
queue_hash: None,
1385+
};
1386+
db.insert_l1_message(message).await.unwrap();
1387+
13331388
// insert the previous L1 message in database.
13341389
chain_orchestrator.handle_l1_notification(L1Notification::L1Message {
13351390
message: TxL1Message {
@@ -1377,29 +1432,45 @@ mod test {
13771432
rand::rng().fill(bytes.as_mut_slice());
13781433
let mut u = Unstructured::new(&bytes);
13791434

1435+
// Insert batch 0 into the database to satisfy the consistency checks in the chain
1436+
// orchestrator
1437+
let batch_0 =
1438+
BatchCommitData { index: 0, block_number: 0, ..Arbitrary::arbitrary(&mut u).unwrap() };
1439+
db.insert_batch(batch_0).await.unwrap();
1440+
1441+
// Insert l1 message into the database to satisfy the consistency checks in the chain
1442+
// orchestrator
1443+
let l1_message = L1MessageEnvelope {
1444+
queue_hash: None,
1445+
l1_block_number: 0,
1446+
l2_block_number: None,
1447+
transaction: TxL1Message { queue_index: 0, ..Arbitrary::arbitrary(&mut u).unwrap() },
1448+
};
1449+
db.insert_l1_message(l1_message).await.unwrap();
1450+
13801451
// Generate a 3 random batch inputs and set their block numbers
13811452
let mut batch_commit_block_1 = BatchCommitData::arbitrary(&mut u).unwrap();
13821453
batch_commit_block_1.block_number = 1;
13831454
batch_commit_block_1.index = 1;
13841455
let batch_commit_block_1 = batch_commit_block_1;
13851456

1386-
let mut batch_commit_block_20 = BatchCommitData::arbitrary(&mut u).unwrap();
1387-
batch_commit_block_20.block_number = 20;
1388-
batch_commit_block_20.index = 20;
1389-
let batch_commit_block_20 = batch_commit_block_20;
1457+
let mut batch_commit_block_2 = BatchCommitData::arbitrary(&mut u).unwrap();
1458+
batch_commit_block_2.block_number = 2;
1459+
batch_commit_block_2.index = 2;
1460+
let batch_commit_block_2 = batch_commit_block_2;
13901461

1391-
let mut batch_commit_block_30 = BatchCommitData::arbitrary(&mut u).unwrap();
1392-
batch_commit_block_30.block_number = 30;
1393-
batch_commit_block_30.index = 30;
1394-
let batch_commit_block_30 = batch_commit_block_30;
1462+
let mut batch_commit_block_3 = BatchCommitData::arbitrary(&mut u).unwrap();
1463+
batch_commit_block_3.block_number = 3;
1464+
batch_commit_block_3.index = 3;
1465+
let batch_commit_block_3 = batch_commit_block_3;
13951466

13961467
// Index batch inputs
13971468
chain_orchestrator
13981469
.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_1.clone()));
13991470
chain_orchestrator
1400-
.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_20.clone()));
1471+
.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_2.clone()));
14011472
chain_orchestrator
1402-
.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_30.clone()));
1473+
.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_3.clone()));
14031474

14041475
// Generate 3 random L1 messages and set their block numbers
14051476
let l1_message_block_1 = L1MessageEnvelope {
@@ -1408,15 +1479,15 @@ mod test {
14081479
l2_block_number: None,
14091480
transaction: TxL1Message { queue_index: 1, ..Arbitrary::arbitrary(&mut u).unwrap() },
14101481
};
1411-
let l1_message_block_20 = L1MessageEnvelope {
1482+
let l1_message_block_2 = L1MessageEnvelope {
14121483
queue_hash: None,
1413-
l1_block_number: 20,
1484+
l1_block_number: 2,
14141485
l2_block_number: None,
14151486
transaction: TxL1Message { queue_index: 2, ..Arbitrary::arbitrary(&mut u).unwrap() },
14161487
};
1417-
let l1_message_block_30 = L1MessageEnvelope {
1488+
let l1_message_block_3 = L1MessageEnvelope {
14181489
queue_hash: None,
1419-
l1_block_number: 30,
1490+
l1_block_number: 3,
14201491
l2_block_number: None,
14211492
transaction: TxL1Message { queue_index: 3, ..Arbitrary::arbitrary(&mut u).unwrap() },
14221493
};
@@ -1428,18 +1499,18 @@ mod test {
14281499
block_timestamp: 0,
14291500
});
14301501
chain_orchestrator.handle_l1_notification(L1Notification::L1Message {
1431-
message: l1_message_block_20.clone().transaction,
1432-
block_number: l1_message_block_20.clone().l1_block_number,
1502+
message: l1_message_block_2.clone().transaction,
1503+
block_number: l1_message_block_2.clone().l1_block_number,
14331504
block_timestamp: 0,
14341505
});
14351506
chain_orchestrator.handle_l1_notification(L1Notification::L1Message {
1436-
message: l1_message_block_30.clone().transaction,
1437-
block_number: l1_message_block_30.clone().l1_block_number,
1507+
message: l1_message_block_3.clone().transaction,
1508+
block_number: l1_message_block_3.clone().l1_block_number,
14381509
block_timestamp: 0,
14391510
});
14401511

1441-
// Reorg at block 20
1442-
chain_orchestrator.handle_l1_notification(L1Notification::Reorg(20));
1512+
// Reorg at block 2
1513+
chain_orchestrator.handle_l1_notification(L1Notification::Reorg(2));
14431514

14441515
for _ in 0..7 {
14451516
chain_orchestrator.next().await.unwrap().unwrap();
@@ -1451,7 +1522,7 @@ mod test {
14511522

14521523
assert_eq!(3, batch_commits.len());
14531524
assert!(batch_commits.contains(&batch_commit_block_1));
1454-
assert!(batch_commits.contains(&batch_commit_block_20));
1525+
assert!(batch_commits.contains(&batch_commit_block_2));
14551526

14561527
// check that the L1 message at block 30 is deleted
14571528
let l1_messages = db
@@ -1461,9 +1532,9 @@ mod test {
14611532
.map(|res| res.unwrap())
14621533
.collect::<Vec<_>>()
14631534
.await;
1464-
assert_eq!(2, l1_messages.len());
1535+
assert_eq!(3, l1_messages.len());
14651536
assert!(l1_messages.contains(&l1_message_block_1));
1466-
assert!(l1_messages.contains(&l1_message_block_20));
1537+
assert!(l1_messages.contains(&l1_message_block_2));
14671538
}
14681539

14691540
// We ignore this test for now as it requires a more complex setup which leverages an L2 node

crates/manager/src/manager/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,9 +349,11 @@ where
349349
fn handle_chain_orchestrator_error(&self, err: &ChainOrchestratorError) {
350350
error!(
351351
target: "scroll::node::manager",
352-
?err,
352+
error = ?err,
353+
msg = %err,
353354
"Error occurred in the chain orchestrator"
354355
);
356+
355357
match err {
356358
ChainOrchestratorError::L1MessageMismatch { expected, actual } => {
357359
if let Some(event_sender) = self.event_sender.as_ref() {

0 commit comments

Comments
 (0)