Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c1e5cc2
feat: chain orchestrator
frisitano Jul 2, 2025
bbe0e8f
Merge branch 'main' into feat/chain-orchestrator
frisitano Jul 2, 2025
4032fa5
lint
frisitano Jul 2, 2025
39802e2
lint
frisitano Jul 2, 2025
c84dc0d
docs lint
frisitano Jul 2, 2025
ea3140c
fix: clone the in-memory chain instead of taking it
frisitano Jul 3, 2025
a28a5b2
Merge branch 'main' into feat/chain-orchestrator
frisitano Jul 3, 2025
fa6952c
update chain consolidation and l1 message validation
frisitano Jul 4, 2025
43cc2be
add missing network block error
frisitano Jul 7, 2025
5dfc813
update fork sync / reconcilliation to use network as opossed to database
frisitano Jul 8, 2025
a50558d
Merge branch 'main' into feat/chain-orchestrator
frisitano Jul 10, 2025
19458f7
fix merege
frisitano Jul 10, 2025
4578d49
refactor and add test cases
frisitano Jul 16, 2025
e6248f5
cleanup
frisitano Jul 16, 2025
626e29b
Merge branch 'main' into feat/chain-orchestrator
frisitano Jul 16, 2025
7a6ebfc
rename chain orchestrator crate
frisitano Jul 16, 2025
7d35185
cleanup
frisitano Jul 17, 2025
74fd7b8
Merge branch 'main' into feat/chain-orchestrator
frisitano Jul 18, 2025
fe75ed5
remove expect for sequencer in rnm
frisitano Jul 21, 2025
f3bab0d
sync test
frisitano Jul 21, 2025
56a0ff3
sync test
frisitano Jul 21, 2025
1320f14
sync test
frisitano Jul 21, 2025
0fa87fe
add error handling for missing paylaod id
frisitano Jul 21, 2025
cecbd03
remove networking for L1 consolidation sync test
frisitano Jul 21, 2025
af4bac9
Merge branch 'main' into feat/chain-orchestrator
frisitano Jul 29, 2025
7df1a59
improve test coverage and fix bugs
frisitano Jul 30, 2025
fa51172
lint
frisitano Jul 30, 2025
e86446d
fix cli NetworkArgs
frisitano Jul 30, 2025
9cdbdf3
add block gap to reorg integration test
frisitano Jul 31, 2025
01a10cf
Merge branch 'main' into feat/chain-orchestrator
frisitano Jul 31, 2025
2f851b1
make test more robust
frisitano Jul 31, 2025
9f88b5d
Merge branch 'main' into feat/chain-orchestrator
frisitano Aug 6, 2025
9a3339a
merge upstream
frisitano Aug 7, 2025
000ade7
refactor
frisitano Aug 7, 2025
00d18f2
address comments
frisitano Aug 11, 2025
2beb7e0
Merge branch 'main' into feat/chain-orchestrator
frisitano Aug 11, 2025
add9933
address comments
frisitano Aug 20, 2025
e3d5453
Merge branch 'main' into feat/chain-orchestrator
frisitano Aug 20, 2025
4383231
merge changes
frisitano Aug 20, 2025
1c24765
Merge branch 'main' into feat/chain-orchestrator
frisitano Aug 25, 2025
5d9aefb
add migration script
frisitano Aug 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 62 additions & 52 deletions crates/chain-orchestrator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,14 @@ impl<
));
}

// If the current header block number is less than the latest safe block number then
// we should error.
if received_chain_headers.last().expect("chain can not be empty").number <=
latest_safe_block.number
{
return Err(ChainOrchestratorError::L2SafeBlockReorgDetected);
}

// If the received header tail has a block number that is less than the current header
// tail then we should fetch more headers for the current chain to aid
// reconciliation.
Expand Down Expand Up @@ -361,14 +369,6 @@ impl<
break pos;
}

// If the current header block number is less than the latest safe block number then
// we should error.
if received_chain_headers.last().expect("chain can not be empty").number <=
latest_safe_block.number
{
return Err(ChainOrchestratorError::L2SafeBlockReorgDetected);
}

tracing::trace!(target: "scroll::chain_orchestrator", number = ?(received_chain_headers.last().expect("chain can not be empty").number - 1), "fetching block");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we store received_chain_headers.last() in a variable somewhere to avoid calling the expect dozens of times?

if let Some(header) = network_client
.get_header(BlockHashOrNumber::Hash(
Expand Down Expand Up @@ -722,15 +722,15 @@ impl<
}

/// Returns the highest finalized block for the provided batch hash. Will return [`None`] if the
/// block number has already been seen by the indexer.
/// block number has already been seen by the chain orchestrator.
async fn fetch_highest_finalized_block(
database: Arc<Database>,
batch_hash: B256,
l2_block_number: Arc<AtomicU64>,
) -> Result<Option<BlockInfo>, ChainOrchestratorError> {
let finalized_block = database.get_highest_block_for_batch_hash(batch_hash).await?;

// only return the block if the indexer hasn't seen it.
// only return the block if the chain orchestrator hasn't seen it.
// in which case also update the `l2_finalized_block_number` value.
Ok(finalized_block.filter(|info| {
let current_l2_block_number = l2_block_number.load(Ordering::Relaxed);
Expand Down Expand Up @@ -1167,18 +1167,19 @@ mod test {

#[tokio::test]
async fn test_handle_commit_batch() {
// Instantiate indexer and db
let (mut indexer, db) = setup_test_chain_orchestrator().await;
// Instantiate chain orchestrator and db
let (mut chain_orchestrator, db) = setup_test_chain_orchestrator().await;

// Generate unstructured bytes.
let mut bytes = [0u8; 1024];
rand::rng().fill(bytes.as_mut_slice());
let mut u = Unstructured::new(&bytes);

let batch_commit = BatchCommitData::arbitrary(&mut u).unwrap();
indexer.handle_l1_notification(L1Notification::BatchCommit(batch_commit.clone()));
chain_orchestrator
.handle_l1_notification(L1Notification::BatchCommit(batch_commit.clone()));

let event = indexer.next().await.unwrap().unwrap();
let event = chain_orchestrator.next().await.unwrap().unwrap();

// Verify the event structure
match event {
Expand All @@ -1196,7 +1197,7 @@ mod test {

#[tokio::test]
async fn test_handle_batch_commit_with_revert() {
// Instantiate indexer and db
// Instantiate chain orchestrator and db
let (mut chain_orchestrator, db) = setup_test_chain_orchestrator().await;

// Generate unstructured bytes.
Expand Down Expand Up @@ -1317,8 +1318,8 @@ mod test {

#[tokio::test]
async fn test_handle_l1_message() {
// Instantiate indexer and db
let (mut indexer, db) = setup_test_chain_orchestrator().await;
// Instantiate chain orchestrator and db
let (mut chain_orchestrator, db) = setup_test_chain_orchestrator().await;

// Generate unstructured bytes.
let mut bytes = [0u8; 1024];
Expand All @@ -1330,13 +1331,13 @@ mod test {
..Arbitrary::arbitrary(&mut u).unwrap()
};
let block_number = u64::arbitrary(&mut u).unwrap();
indexer.handle_l1_notification(L1Notification::L1Message {
chain_orchestrator.handle_l1_notification(L1Notification::L1Message {
message: message.clone(),
block_number,
block_timestamp: 0,
});

let _ = indexer.next().await;
let _ = chain_orchestrator.next().await;

let l1_message_result =
db.get_l1_message_by_index(message.queue_index).await.unwrap().unwrap();
Expand All @@ -1347,16 +1348,16 @@ mod test {

#[tokio::test]
async fn test_l1_message_hash_queue() {
// Instantiate indexer and db
let (mut indexer, db) = setup_test_chain_orchestrator().await;
// Instantiate chain orchestrator and db
let (mut chain_orchestrator, db) = setup_test_chain_orchestrator().await;

// insert the previous L1 message in database.
indexer.handle_l1_notification(L1Notification::L1Message {
chain_orchestrator.handle_l1_notification(L1Notification::L1Message {
message: TxL1Message { queue_index: 1062109, ..Default::default() },
block_number: 1475588,
block_timestamp: 1745305199,
});
let _ = indexer.next().await.unwrap().unwrap();
let _ = chain_orchestrator.next().await.unwrap().unwrap();

// <https://sepolia.scrollscan.com/tx/0xd80cd61ac5d8665919da19128cc8c16d3647e1e2e278b931769e986d01c6b910>
let message = TxL1Message {
Expand All @@ -1367,13 +1368,13 @@ mod test {
sender: address!("61d8d3E7F7c656493d1d76aAA1a836CEdfCBc27b"),
input: bytes!("8ef1332e000000000000000000000000323522a8de3cddeddbb67094eecaebc2436d6996000000000000000000000000323522a8de3cddeddbb67094eecaebc2436d699600000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000000000001034de00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000"),
};
indexer.handle_l1_notification(L1Notification::L1Message {
chain_orchestrator.handle_l1_notification(L1Notification::L1Message {
message: message.clone(),
block_number: 14755883,
block_timestamp: 1745305200,
});

let _ = indexer.next().await.unwrap().unwrap();
let _ = chain_orchestrator.next().await.unwrap().unwrap();

let l1_message_result =
db.get_l1_message_by_index(message.queue_index).await.unwrap().unwrap();
Expand All @@ -1386,8 +1387,8 @@ mod test {

#[tokio::test]
async fn test_handle_reorg() {
// Instantiate indexer and db
let (mut indexer, db) = setup_test_chain_orchestrator().await;
// Instantiate chain orchestrator and db
let (mut chain_orchestrator, db) = setup_test_chain_orchestrator().await;

// Generate unstructured bytes.
let mut bytes = [0u8; 1024];
Expand All @@ -1411,9 +1412,12 @@ mod test {
let batch_commit_block_30 = batch_commit_block_30;

// Index batch inputs
indexer.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_1.clone()));
indexer.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_20.clone()));
indexer.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_30.clone()));
chain_orchestrator
.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_1.clone()));
chain_orchestrator
.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_20.clone()));
chain_orchestrator
.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_30.clone()));

// Generate 3 random L1 messages and set their block numbers
let l1_message_block_1 = L1MessageEnvelope {
Expand All @@ -1436,27 +1440,27 @@ mod test {
};

// Index L1 messages
indexer.handle_l1_notification(L1Notification::L1Message {
chain_orchestrator.handle_l1_notification(L1Notification::L1Message {
message: l1_message_block_1.clone().transaction,
block_number: l1_message_block_1.clone().l1_block_number,
block_timestamp: 0,
});
indexer.handle_l1_notification(L1Notification::L1Message {
chain_orchestrator.handle_l1_notification(L1Notification::L1Message {
message: l1_message_block_20.clone().transaction,
block_number: l1_message_block_20.clone().l1_block_number,
block_timestamp: 0,
});
indexer.handle_l1_notification(L1Notification::L1Message {
chain_orchestrator.handle_l1_notification(L1Notification::L1Message {
message: l1_message_block_30.clone().transaction,
block_number: l1_message_block_30.clone().l1_block_number,
block_timestamp: 0,
});

// Reorg at block 20
indexer.handle_l1_notification(L1Notification::Reorg(20));
chain_orchestrator.handle_l1_notification(L1Notification::Reorg(20));

for _ in 0..7 {
indexer.next().await.unwrap().unwrap();
chain_orchestrator.next().await.unwrap().unwrap();
}

// Check that the batch input at block 30 is deleted
Expand Down Expand Up @@ -1485,8 +1489,8 @@ mod test {
#[ignore]
#[tokio::test]
async fn test_handle_reorg_executed_l1_messages() {
// Instantiate indexer and db
let (mut indexer, _database) = setup_test_chain_orchestrator().await;
// Instantiate chain orchestrator and db
let (mut chain_orchestrator, _database) = setup_test_chain_orchestrator().await;

// Generate unstructured bytes.
let mut bytes = [0u8; 8192];
Expand All @@ -1503,10 +1507,12 @@ mod test {
};

// Index batch inputs
indexer.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_1.clone()));
indexer.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_10.clone()));
chain_orchestrator
.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_1.clone()));
chain_orchestrator
.handle_l1_notification(L1Notification::BatchCommit(batch_commit_block_10.clone()));
for _ in 0..2 {
let _event = indexer.next().await.unwrap().unwrap();
let _event = chain_orchestrator.next().await.unwrap().unwrap();
}

let batch_1 = BatchInfo::new(batch_commit_block_1.index, batch_commit_block_1.hash);
Expand All @@ -1527,12 +1533,12 @@ mod test {
..Arbitrary::arbitrary(&mut u).unwrap()
},
};
indexer.handle_l1_notification(L1Notification::L1Message {
chain_orchestrator.handle_l1_notification(L1Notification::L1Message {
message: l1_message.transaction.clone(),
block_number: l1_message.l1_block_number,
block_timestamp: 0,
});
indexer.next().await.unwrap().unwrap();
chain_orchestrator.next().await.unwrap().unwrap();
l1_messages.push(l1_message);
}

Expand All @@ -1555,19 +1561,20 @@ mod test {
None
};
if let Some(batch_info) = batch_info {
indexer.persist_l1_consolidated_blocks(vec![l2_block.clone()], batch_info);
chain_orchestrator
.persist_l1_consolidated_blocks(vec![l2_block.clone()], batch_info);
} else {
indexer.consolidate_validated_l2_blocks(vec![l2_block.clone()]);
chain_orchestrator.consolidate_validated_l2_blocks(vec![l2_block.clone()]);
}

indexer.next().await.unwrap().unwrap();
chain_orchestrator.next().await.unwrap().unwrap();
blocks.push(l2_block);
}

// First we assert that we dont reorg the L2 or message queue hash for a higher block
// than any of the L1 messages.
indexer.handle_l1_notification(L1Notification::Reorg(17));
let event = indexer.next().await.unwrap().unwrap();
chain_orchestrator.handle_l1_notification(L1Notification::Reorg(17));
let event = chain_orchestrator.next().await.unwrap().unwrap();
assert_eq!(
event,
ChainOrchestratorEvent::ChainUnwound {
Expand All @@ -1580,8 +1587,8 @@ mod test {

// Reorg at block 7 which is one of the messages that has not been executed yet. No reorg
// but we should ensure the L1 messages have been deleted.
indexer.handle_l1_notification(L1Notification::Reorg(7));
let event = indexer.next().await.unwrap().unwrap();
chain_orchestrator.handle_l1_notification(L1Notification::Reorg(7));
let event = chain_orchestrator.next().await.unwrap().unwrap();

assert_eq!(
event,
Expand All @@ -1594,16 +1601,19 @@ mod test {
);

// Now reorg at block 5 which contains L1 messages that have been executed .
indexer.handle_l1_notification(L1Notification::Reorg(3));
let event = indexer.next().await.unwrap().unwrap();
chain_orchestrator.handle_l1_notification(L1Notification::Reorg(3));
let event = chain_orchestrator.next().await.unwrap().unwrap();

assert_eq!(
event,
ChainOrchestratorEvent::ChainUnwound {
l1_block_number: 3,
queue_index: Some(4),
l2_head_block_info: Some(blocks[3].block_info),
l2_safe_block_info: Some(BlockInfo::new(0, indexer.chain_spec.genesis_hash())),
l2_safe_block_info: Some(BlockInfo::new(
0,
chain_orchestrator.chain_spec.genesis_hash()
)),
}
);
}
Expand Down
3 changes: 2 additions & 1 deletion crates/database/db/src/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,8 @@ pub trait DatabaseOperations: DatabaseConnectionProvider {
.map(|model| model.block_info()))
}

/// Unwinds the indexer by deleting all indexed data greater than the provided L1 block number.
/// Unwinds the chain orchestrator by deleting all indexed data greater than the provided L1
/// block number.
async fn unwind(
&self,
genesis_hash: B256,
Expand Down
4 changes: 2 additions & 2 deletions crates/database/db/src/test_utils.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
//! Test utilities for the database crate.

use super::Database;
use scroll_migration::{Migrator, MigratorTrait};
use scroll_migration::{Migrator, MigratorTrait, ScrollDevMigrationInfo};

/// Instantiates a new in-memory database and runs the migrations
/// to set up the schema.
pub async fn setup_test_db() -> Database {
let database_url = "sqlite::memory:";
let connection = sea_orm::Database::connect(database_url).await.unwrap();
Migrator::<()>::up(&connection, None).await.unwrap();
Migrator::<ScrollDevMigrationInfo>::up(&connection, None).await.unwrap();
connection.into()
}
12 changes: 8 additions & 4 deletions crates/database/migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ mod m20250408_150338_load_header_metadata;
mod m20250411_072004_add_l2_block;
mod m20250616_223947_add_metadata;
mod migration_info;
pub use migration_info::{MigrationInfo, ScrollMainnetMigrationInfo, ScrollSepoliaMigrationInfo};
pub use migration_info::{
MigrationInfo, ScrollDevMigrationInfo, ScrollMainnetMigrationInfo, ScrollSepoliaMigrationInfo,
};

pub struct Migrator<MI>(pub std::marker::PhantomData<MI>);

Expand All @@ -27,8 +29,8 @@ impl<MI: MigrationInfo + Send + Sync + 'static> MigratorTrait for Migrator<MI> {

pub mod traits {
use crate::{
migration_info::ScrollMainnetTestMigrationInfo, ScrollMainnetMigrationInfo,
ScrollSepoliaMigrationInfo,
migration_info::{ScrollDevMigrationInfo, ScrollMainnetTestMigrationInfo},
ScrollMainnetMigrationInfo, ScrollSepoliaMigrationInfo,
};
use reth_chainspec::NamedChain;
use sea_orm::{prelude::async_trait::async_trait, DatabaseConnection, DbErr};
Expand All @@ -54,7 +56,9 @@ pub mod traits {
(NamedChain::ScrollSepolia, _) => {
Ok(super::Migrator::<ScrollSepoliaMigrationInfo>::up(conn, None))
}
(NamedChain::Dev, _) => Ok(super::Migrator::<()>::up(conn, None)),
(NamedChain::Dev, _) => {
Ok(super::Migrator::<ScrollDevMigrationInfo>::up(conn, None))
}
_ => Err(DbErr::Custom("expected Scroll Mainnet, Sepolia or Dev".into())),
}?
.await
Expand Down
7 changes: 4 additions & 3 deletions crates/database/migration/src/migration_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ pub trait MigrationInfo {
fn genesis_hash() -> B256;
}

impl MigrationInfo for () {
pub struct ScrollDevMigrationInfo;

impl MigrationInfo for ScrollDevMigrationInfo {
fn data_source() -> Option<DataSource> {
None
}
Expand All @@ -22,8 +24,7 @@ impl MigrationInfo for () {
}

fn genesis_hash() -> B256 {
// Todo: Update
b256!("0xc77ee681dac901672fee660088df30ef11789ec89837123cdc89690ef1fef766")
b256!("0x14844a4fc967096c628e90df3bb0c3e98941bdd31d1982c2f3e70ed17250d98b")
}
}

Expand Down
Loading