diff --git a/Cargo.lock b/Cargo.lock index c6b29bad..0d90a22e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2677,7 +2677,7 @@ checksum = "c3b847e05a34be5c38f3f2a5052178a3bd32e6b5702f3ea775efde95c483a539" dependencies = [ "anyhow", "cc", - "colored", + "colored 2.2.0", "getrandom 0.2.16", "glob", "libc", @@ -2696,7 +2696,7 @@ dependencies = [ "clap", "codspeed", "codspeed-criterion-compat-walltime", - "colored", + "colored 2.2.0", "futures", "regex", "tokio", @@ -2825,6 +2825,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -11092,7 +11101,10 @@ name = "rollup-node" version = "1.0.5" dependencies = [ "alloy-chains", + "alloy-consensus", "alloy-eips", + "alloy-genesis", + "alloy-network", "alloy-primitives", "alloy-provider", "alloy-rpc-client", @@ -11101,6 +11113,7 @@ dependencies = [ "alloy-signer", "alloy-signer-aws", "alloy-signer-local", + "alloy-sol-types", "alloy-transport", "async-trait", "auto_impl", @@ -11108,17 +11121,22 @@ dependencies = [ "aws-sdk-kms", "clap", "color-eyre", + "colored 3.0.0", "console-subscriber", + "crossterm 0.28.1", "eyre", "futures", + "glob", "http-body-util", "hyper 1.8.1", "hyper-util", "jsonrpsee", "pprof", "rayon", + "regex-lite", "reqwest", "reth-chainspec", + "reth-cli", "reth-cli-util", "reth-e2e-test-utils", "reth-engine-local", @@ -11127,6 +11145,7 @@ dependencies = [ "reth-network", "reth-network-api", "reth-network-p2p", + "reth-network-peers", "reth-node-api", "reth-node-builder", "reth-node-core", @@ -11173,9 +11192,11 @@ dependencies = [ "scroll-migration", "scroll-network", "scroll-wire", + "serde", "serde_json", "tokio", "tracing", + "tracing-subscriber 0.3.20", ] [[package]] @@ -13688,6 +13709,17 @@ dependencies = [ "tracing-subscriber 0.3.20", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-logfmt" version = "0.3.5" @@ -13732,9 +13764,11 @@ dependencies = [ "serde", "serde_json", "sharded-slab", + "smallvec", "thread_local", "tracing", "tracing-core", + "tracing-log", "tracing-serde", ] diff --git a/Cargo.toml b/Cargo.toml index 492af659..ce3f6db1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,6 +127,7 @@ strip = "none" alloy-chains = { version = "0.2.5", default-features = false } alloy-consensus = { version = "1.0.37", default-features = false } alloy-eips = { version = "1.0.37", default-features = false } +alloy-genesis = { version = "1.0.37", default-features = false } alloy-json-rpc = { version = "1.0.37", default-features = false } alloy-network = { version = "1.0.37", default-features = false } alloy-primitives = { version = "1.4.1", default-features = false } @@ -181,6 +182,7 @@ reth-testing-utils = { git = "https://github.com/scroll-tech/reth.git", tag = "s reth-revm = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll-v91.4", default-features = false } reth-evm = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll-v91.4", default-features = false } reth-engine-local = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll-v91.4", default-features = false } +reth-cli = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll-v91.4", default-features = false } reth-cli-util = { git = "https://github.com/scroll-tech/reth.git", tag = "scroll-v91.4", default-features = false } # reth-scroll diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index e63b526d..cc712023 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -4,3 +4,4 @@ - [Running a Node](./running-a-node.md) - [Running a Sequencer](./running-a-sequencer.md) - [Running with Docker Compose](./docker-operations.md) +- [Debug Toolkit](./debug-toolkit.md) diff --git a/book/src/debug-toolkit.md b/book/src/debug-toolkit.md new file mode 100644 index 00000000..7ca17d95 --- /dev/null +++ b/book/src/debug-toolkit.md @@ -0,0 +1,514 @@ +# Debug Toolkit + +The Debug Toolkit is an interactive REPL (Read-Eval-Print Loop) for debugging, development, and hackathon scenarios. + +It supports two modes: + +- **Attach mode**: connect to an already-running node over JSON-RPC. +- **Local (spawn) mode**: spin up a local test network and interact with in-process nodes. + +## Getting Started + +### Source Code + +The debug toolkit is available on the `feat/debug-toolkit` branch: + +**Repository:** [https://github.com/scroll-tech/rollup-node/tree/feat/debug-toolkit](https://github.com/scroll-tech/rollup-node/tree/feat/debug-toolkit) + +```bash +git clone https://github.com/scroll-tech/rollup-node.git +cd rollup-node +git checkout feat/debug-toolkit +``` + +### Building + +Build with the `debug-toolkit` feature flag: + +```bash +cargo build -p rollup-node --features debug-toolkit --release +``` + +## Attach Mode + +Use attach mode when you want to inspect or control an already-running node: + +```bash +cargo run --features debug-toolkit --bin scroll-debug -- \ + --attach http://localhost:8545 \ + --private-key +``` + +Notes: + +- `--private-key` is optional, but required for `tx send` and `tx inject`. +- Commands that depend on local fixtures (`build`, `run`, `node`, `db`, L1 mock injection) are only available in local (spawn) mode. + +## Connecting to a Remote Network (Local (spawn) Mode) + +The primary use case is connecting local follower nodes to a remote sequencer and L1. This allows you to run tests and scripts against a live network. + +### Network Connection Info + +``` +L1 RPC: http://ec2-54-167-214-30.compute-1.amazonaws.com:8545 +L1 Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +Sequencer HTTP: http://ec2-54-175-126-206.compute-1.amazonaws.com:8545 +Sequencer Enode: enode://3322bb29bba1f30f3bb40e816779f1be8ab3c14a5d14ff6c76d0585a63bdcc4ba25008be138780dafd03cb3e3ae4546da9a566b4ff9f1d237fa5d3d79bfdd219@54.175.126.206:30303 +Signer: 0xb674ff99cca262c99d3eab5b32796a99188543da +Genesis: tests/l2reth-genesis-e2e.json +``` + +### Connect to Remote Network + +```bash +cargo run --features debug-toolkit --bin scroll-debug -- \ + --followers 2 \ + --chain tests/l2reth-genesis-e2e.json \ + --bootnodes enode://3322bb29bba1f30f3bb40e816779f1be8ab3c14a5d14ff6c76d0585a63bdcc4ba25008be138780dafd03cb3e3ae4546da9a566b4ff9f1d237fa5d3d79bfdd219@54.175.126.206:30303 \ + --l1-url http://ec2-54-167-214-30.compute-1.amazonaws.com:8545 \ + --valid-signer 0xb674ff99cca262c99d3eab5b32796a99188543da +``` + +This creates local follower nodes that: +- Connect to the remote sequencer via P2P (`--bootnodes`) +- Sync L1 state from the remote L1 RPC (`--l1-url`) +- Validate blocks using the network's authorized signer (`--valid-signer`) +- Use the matching genesis configuration (`--chain`) + +### CLI Options Explained + +| Option | Description | +|--------|-------------| +| `--chain ` | Genesis configuration: `dev`, `scroll-sepolia`, `scroll-mainnet`, or path to JSON file | +| `--sequencer` | Enable local sequencer mode | +| `--followers ` | Number of local follower nodes to spin up (can be any number) | +| `--bootnodes ` | Remote sequencer enode URL to connect to | +| `--l1-url ` | Remote L1 RPC endpoint | +| `--valid-signer ` | Authorized block signer address for consensus validation | +| `--log-file ` | Path to log file (default: `./scroll-debug-.log`) | + +## Local (Spawn) Mode + +You can also run a fully local environment with a mock L1 and local sequencer for offline development: + +```bash +cargo run --features debug-toolkit --bin scroll-debug -- \ + --chain dev \ + --sequencer \ + --followers 2 +``` + +This creates: +- **Node 0**: Local sequencer (produces blocks) +- **Node 1-N**: Local followers (receive blocks via P2P) + +With mock L1, you must manually sync before building blocks: + +``` +scroll-debug [seq:0]> l1 sync +L1 synced event sent + +scroll-debug [seq:0]> build +Block build triggered! +``` + +## Using the Network Handle + +The `TestFixture` provides access to network handles for programmatic control. This is useful for writing custom actions and tests: + +```rust +use rollup_node::test_utils::TestFixture; + +async fn example(fixture: &TestFixture) -> eyre::Result<()> { + // Access a node's network handle + let node = &fixture.nodes[0]; + let network_handle = node.rollup_manager_handle.get_network_handle().await?; + + // Get local node info + let local_record = network_handle.local_node_record(); + println!("Local enode: {}", local_record); + + // Access the inner network handle for P2P operations + let inner = network_handle.inner(); + + // Get connected peers + let peers = inner.get_all_peers().await?; + println!("Connected to {} peers", peers.len()); + + // Add a peer + inner.add_peer(peer_id, socket_addr); + + Ok(()) +} +``` + +## Commands + +### Status & Inspection + +| Command | Description | +|---------|-------------| +| `status` | Show node status (L2 head/safe/finalized, L1 state, sync status) | +| `block [n\|latest]` | Display block details | +| `blocks ` | List blocks in range | +| `fcs` | Show forkchoice state | + +**Example:** + +``` +scroll-debug [fol:0]> status +=== Node 0 (Follower) === +Node: + Database: /tmp/.tmpXYZ/db/scroll.db + HTTP RPC: http://127.0.0.1:62491 +L2: + Head: #42 (0x1234abcd...) + Safe: #40 (0x5678efgh...) + Finalized: #35 (0x9abc1234...) + Synced: true +L1: + Head: #18923456 + Finalized: #18923400 + Processed: #18923450 + Synced: true +``` + +### L1 Commands + +These commands allow you to simulate L1 events (useful in local mode with mock L1): + +| Command | Description | +|---------|-------------| +| `l1 status` | Show L1 sync state | +| `l1 sync` | Inject L1 synced event | +| `l1 block ` | Inject new L1 block notification | +| `l1 reorg ` | Inject L1 reorg | + +### Block & Transaction + +| Command | Description | +|---------|-------------| +| `build` | Build a new block (local sequencer mode only) | +| `tx send [idx]` | Send ETH transfer (value in wei, idx = wallet index) | +| `tx pending` | List pending transactions | +| `tx inject ` | Inject raw transaction | + +**Example:** + +``` +scroll-debug [seq:0]> tx send 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2 1000000000000000000 +Transaction sent! + Hash: 0xabcd... + From: 0x1234... + To: 0x742d... + Value: 1000000000000000000 wei +``` + +### Wallet + +| Command | Description | +|---------|-------------| +| `wallet` | Show wallet address, balance, and nonce | +| `wallet gen` | Generate and list all available wallets | + +The toolkit includes pre-funded test wallets: + +``` +scroll-debug [fol:0]> wallet gen +Generated Wallets (10): + Chain ID: 222222 + + [0] 0x1234567890abcdef... + Balance: 1000000000000000000000 wei (1000.000000 ETH) + + [1] 0xabcdef1234567890... + Balance: 1000000000000000000000 wei (1000.000000 ETH) + ... +``` + +### Network + +| Command | Description | +|---------|-------------| +| `peers` | List connected peers and show local enode | +| `peers connect ` | Connect to a peer (enode://...) | + +**Example:** + +``` +scroll-debug [fol:0]> peers +Local Node: + Peer ID: 0x1234... + Enode: enode://abcd...@127.0.0.1:30303 + +Connected Peers (1): + 0x3322bb29... + Address: 54.175.126.206:30303 + Client: scroll-reth/v1.0.0 +``` + +### Events + +The REPL streams chain events in real-time: + +| Command | Description | +|---------|-------------| +| `events on` | Enable background event stream | +| `events off` | Disable background event stream | +| `events filter ` | Filter events by type (e.g., `Block*`, `L1*`) | +| `events history [n]` | Show last N events (default: 20) | + +### Custom Actions + +Run pre-built or custom actions: + +| Command | Description | +|---------|-------------| +| `run list` | List available custom actions | +| `run [args]` | Execute a custom action | + +**Built-in Actions:** + +| Action | Description | +|--------|-------------| +| `build-blocks [delay_ms]` | Build multiple blocks in sequence | +| `stress-test [build_every]` | Send multiple transactions and build blocks | +| `sync-all` | Send L1 sync event to all nodes | + +### Node Management + +Switch between nodes when running multiple followers: + +| Command | Description | +|---------|-------------| +| `node ` | Switch active node context | +| `nodes` | List all nodes in fixture | + +``` +scroll-debug [fol:0]> nodes +Nodes: + [0] Follower * + [1] Follower + [2] Follower + +scroll-debug [fol:0]> node 1 +Switched to node 1 (Follower) +``` + +### Database + +| Command | Description | +|---------|-------------| +| `db` | Show database path and access command | + +``` +scroll-debug [fol:0]> db +Database Info: + Path: /path/to/datadir/db/scroll.db + +Access from another terminal: + sqlite3 /path/to/datadir/db/scroll.db + +Useful queries: + .tables -- List all tables + .schema -- Show table schema + SELECT * FROM metadata; -- View metadata + SELECT * FROM l2_block ORDER BY number DESC LIMIT 10; +``` + +### Logs + +| Command | Description | +|---------|-------------| +| `logs` | Show log file path and tail command | + +Tracing logs are written to a file to keep the REPL display clean: + +``` +scroll-debug [fol:0]> logs +Log File: + Path: ./scroll-debug-12345.log + +View logs in another terminal: + tail -f ./scroll-debug-12345.log +``` + +### Admin + +| Command | Description | +|---------|-------------| +| `admin enable-seq` | Enable automatic sequencing | +| `admin disable-seq` | Disable automatic sequencing | +| `admin revert ` | Revert node state to L1 block number `n` | + +### Raw RPC + +| Command | Description | +|---------|-------------| +| `rpc [params]` | Execute any JSON-RPC call and print result | + +**Examples:** + +```bash +rpc eth_blockNumber +rpc eth_getBlockByNumber ["latest",false] +``` + +### Other + +| Command | Description | +|---------|-------------| +| `help` | Show available commands | +| `exit` | Exit the REPL | + +## Creating Custom Actions + +You can create custom actions by implementing the `Action` trait. Actions have full access to the `TestFixture`: + +```rust +use rollup_node::debug_toolkit::actions::{Action, ActionRegistry}; +use rollup_node::test_utils::TestFixture; +use async_trait::async_trait; + +struct MyCustomAction; + +#[async_trait] +impl Action for MyCustomAction { + fn name(&self) -> &'static str { + "my-action" + } + + fn description(&self) -> &'static str { + "Does something cool with the fixture" + } + + fn usage(&self) -> Option<&'static str> { + Some("run my-action [arg1] [arg2]") + } + + async fn execute( + &self, + fixture: &mut TestFixture, + args: &[String], + ) -> eyre::Result<()> { + // Access nodes + println!("Fixture has {} nodes", fixture.nodes.len()); + + // Access network handle + let node = &fixture.nodes[0]; + let network_handle = node.rollup_manager_handle.get_network_handle().await?; + println!("Connected peers: {}", network_handle.inner().num_connected_peers()); + + // Access wallet + let wallet = fixture.wallet.lock().await; + println!("Wallet address: {:?}", wallet.inner.address()); + drop(wallet); + + // Query chain state + let status = node.rollup_manager_handle.status().await?; + println!("Head block: {}", status.l2.fcs.head_block_info().number); + + Ok(()) + } +} +``` + +### Registering Actions + +Add your action to the registry in `crates/node/src/debug_toolkit/actions.rs`: + +```rust +impl ActionRegistry { + pub fn new() -> Self { + let mut registry = Self { actions: Vec::new() }; + + // Built-in actions + registry.register(Box::new(BuildBlocksAction)); + registry.register(Box::new(StressTestAction)); + registry.register(Box::new(SyncAllAction)); + + // Add your custom action here: + registry.register(Box::new(MyCustomAction)); + + registry + } +} +``` + +## Useful Cast Commands + +Use [Foundry's `cast`](https://book.getfoundry.sh/cast/) to interact with the network. The `status` command in the REPL shows the HTTP RPC endpoint for your local nodes. + +### Check Block Status + +```bash +# Check latest L1 block +cast block latest --rpc-url http://ec2-54-167-214-30.compute-1.amazonaws.com:8545 + +# Check latest L2 block (sequencer) +cast block latest --rpc-url http://ec2-54-175-126-206.compute-1.amazonaws.com:8545 +``` + +### Check Sync Status + +```bash +# L2 sequencer sync status +cast rpc rollupNode_status --rpc-url http://ec2-54-175-126-206.compute-1.amazonaws.com:8545 | jq +``` + +### Check and Manage Peers + +```bash +# Check connected peers +cast rpc admin_peers --rpc-url http://ec2-54-175-126-206.compute-1.amazonaws.com:8545 + +# Get node info (enode URL) +cast rpc admin_nodeInfo --rpc-url http://ec2-54-175-126-206.compute-1.amazonaws.com:8545 | jq -r '.enode' +``` + +### Enable Sequencing + +```bash +cast rpc rollupNodeAdmin_enableAutomaticSequencing --rpc-url http://ec2-54-175-126-206.compute-1.amazonaws.com:8545 +``` + +### Send Transactions + +```bash +# Get wallet address from private key +cast wallet address --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + +# Check balance on L2 +cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --ether --rpc-url http://ec2-54-175-126-206.compute-1.amazonaws.com:8545 + +# Send L2 transaction +cast send 0x0000000000000000000000000000000000000002 \ + --rpc-url http://ec2-54-175-126-206.compute-1.amazonaws.com:8545 \ + --value 0.00001ether \ + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +``` + +### L1 to L2 Bridge (Send L1 Message) + +```bash +# Send message from L1 to L2 via the messenger contract +cast send --rpc-url http://ec2-54-167-214-30.compute-1.amazonaws.com:8545 \ + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ + --legacy --gas-price 0.1gwei --gas-limit 200000 --value 0.001ether \ + "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" \ + "sendMessage(address _to, uint256 _value, bytes memory _message, uint256 _gasLimit)" \ + 0x0000000000000000000000000000000000000002 0x1 0x 200000 + +# Check L1 message queue index +cast call --rpc-url http://ec2-54-167-214-30.compute-1.amazonaws.com:8545 \ + "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" \ + "nextCrossDomainMessageIndex()(uint256)" +``` + +### Contract Addresses + +| Contract | Address | +|----------|---------| +| L1 Messenger | `0x8A791620dd6260079BF849Dc5567aDC3F2FdC318` | +| L1 Message Queue | `0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9` | diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index b90e609b..ccade99e 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -10,6 +10,11 @@ exclude.workspace = true name = "rollup-node" path = "src/main.rs" +[[bin]] +name = "scroll-debug" +path = "src/bin/scroll_debug.rs" +required-features = ["debug-toolkit"] + [lints] workspace = true @@ -19,6 +24,9 @@ async-trait.workspace = true # alloy alloy-chains.workspace = true +alloy-consensus.workspace = true +alloy-genesis.workspace = true +alloy-network.workspace = true alloy-primitives.workspace = true alloy-provider.workspace = true alloy-rpc-client.workspace = true @@ -44,6 +52,7 @@ reth-scroll-node.workspace = true reth-scroll-rpc.workspace = true reth-chainspec.workspace = true +reth-cli.workspace = true reth-cli-util.workspace = true reth-eth-wire-types.workspace = true reth-evm.workspace = true @@ -54,6 +63,7 @@ reth-node-types.workspace = true reth-network.workspace = true reth-network-api.workspace = true reth-network-p2p.workspace = true +reth-network-peers.workspace = true reth-revm.workspace = true reth-rpc-api.workspace = true reth-rpc-eth-api.workspace = true @@ -89,6 +99,15 @@ reth-tokio-util = { workspace = true, optional = true } scroll-alloy-rpc-types-engine = { workspace = true, optional = true } scroll-alloy-rpc-types.workspace = true +# debug-toolkit dependencies +alloy-sol-types = { workspace = true, optional = true } +colored = { version = "3.0", optional = true } +crossterm = { version = "0.28", optional = true } +glob = { version = "0.3", optional = true } +regex-lite = { version = "0.1", optional = true } +serde = { workspace = true, optional = true } +tracing-subscriber = { version = "0.3", optional = true } + scroll-db.workspace = true scroll-engine.workspace = true scroll-migration.workspace = true @@ -99,6 +118,7 @@ auto_impl.workspace = true clap = { workspace = true, features = ["derive", "env"] } eyre.workspace = true futures.workspace = true +serde_json.workspace = true jsonrpsee = { version = "0.26.0", features = ["server", "client", "macros"] } rayon.workspace = true reqwest.workspace = true @@ -135,6 +155,16 @@ alloy-rpc-types-eth = { workspace = true } [features] js-tracer = ["reth-scroll-node/js-tracer", "reth-scroll-rpc/js-tracer"] +debug-toolkit = [ + "test-utils", + "dep:alloy-sol-types", + "dep:colored", + "dep:crossterm", + "dep:glob", + "dep:regex-lite", + "dep:serde", + "dep:tracing-subscriber", +] test-utils = [ "reth-engine-local", "reth-trie-db/test-utils", diff --git a/crates/node/src/args.rs b/crates/node/src/args.rs index a6f006bc..15db70a9 100644 --- a/crates/node/src/args.rs +++ b/crates/node/src/args.rs @@ -5,6 +5,7 @@ use crate::{ pprof::PprofConfig, }; use alloy_chains::NamedChain; +use alloy_consensus::BlockHeader; use alloy_primitives::{hex, Address, U128}; use alloy_provider::{layers::CacheLayer, Provider, ProviderBuilder}; use alloy_rpc_client::RpcClient; @@ -19,7 +20,6 @@ use reth_network::NetworkProtocols; use reth_network_api::FullNetwork; use reth_network_p2p::FullBlockClient; use reth_node_builder::{rpc::RethRpcServerHandles, NodeConfig as RethNodeConfig}; -use reth_node_core::primitives::BlockHeader; use reth_scroll_chainspec::{ ChainConfig, ScrollChainConfig, ScrollChainSpec, SCROLL_FEE_VAULT_ADDRESS, }; @@ -668,7 +668,7 @@ impl RollupNodeNetworkArgs { } /// The arguments for the L1 provider. -#[derive(Debug, Default, Clone, clap::Args)] +#[derive(Debug, Clone, clap::Args)] pub struct L1ProviderArgs { /// The URL for the L1 RPC. #[arg(long = "l1.url", id = "l1_url", value_name = "L1_URL")] @@ -690,6 +690,19 @@ pub struct L1ProviderArgs { pub cache_max_items: u32, } +impl Default for L1ProviderArgs { + fn default() -> Self { + Self { + url: None, + compute_units_per_second: constants::PROVIDER_COMPUTE_UNITS_PER_SECOND, + max_retries: constants::L1_PROVIDER_MAX_RETRIES, + initial_backoff: constants::L1_PROVIDER_INITIAL_BACKOFF, + logs_query_block_range: constants::LOGS_QUERY_BLOCK_RANGE, + cache_max_items: constants::L1_PROVIDER_CACHE_MAX_ITEMS, + } + } +} + /// The arguments for the Beacon provider. #[derive(Debug, Default, Clone, clap::Args)] pub struct BlobProviderArgs { diff --git a/crates/node/src/bin/scroll_debug.rs b/crates/node/src/bin/scroll_debug.rs new file mode 100644 index 00000000..edb27057 --- /dev/null +++ b/crates/node/src/bin/scroll_debug.rs @@ -0,0 +1,71 @@ +//! Scroll Debug Toolkit - Interactive REPL for debugging rollup nodes. +//! +//! Usage: +//! ```bash +//! # Start REPL with dev chain and sequencer mode +//! cargo run --features debug-toolkit --bin scroll-debug -- --chain dev --sequencer +//! +//! # Start with persistent storage +//! cargo run --features debug-toolkit --bin scroll-debug -- --chain dev --sequencer --datadir ./data +//! +//! # Start with followers +//! cargo run --features debug-toolkit --bin scroll-debug -- --chain dev --sequencer --followers 2 +//! +//! # Start with a real L1 endpoint +//! cargo run --features debug-toolkit --bin scroll-debug -- --chain dev --sequencer --l1-url https://eth.llamarpc.com +//! +//! # See all available options +//! cargo run --features debug-toolkit --bin scroll-debug -- --help +//! ``` + +#[cfg(feature = "debug-toolkit")] +fn main() -> eyre::Result<()> { + use clap::Parser; + use rollup_node::debug_toolkit::DebugArgs; + use std::fs::File; + use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + + // Parse args first so we can use log_file option + let args = DebugArgs::parse(); + + // Set default log level + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "info"); + } + + // Determine log file path (default to current directory) + let log_path = args.log_file.clone().unwrap_or_else(|| { + std::env::current_dir() + .unwrap_or_else(|_| std::path::PathBuf::from(".")) + .join(format!("scroll-debug-{}.log", std::process::id())) + }); + + // Initialize tracing to write to file + let file = File::create(&log_path)?; + tracing_subscriber::registry() + .with(EnvFilter::from_default_env()) + .with(fmt::layer().with_writer(file).with_ansi(false)) + .init(); + + eprintln!("Logs: {}", log_path.display()); + eprintln!("Tail: tail -f {}", log_path.display()); + eprintln!(); + if let Some(url) = &args.attach { + eprintln!("Attaching to node at {}...", url); + } else { + eprintln!("Starting nodes (this may take a moment)..."); + } + + // Create tokio runtime and run + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()? + .block_on(async { args.run(Some(log_path)).await }) +} + +#[cfg(not(feature = "debug-toolkit"))] +fn main() { + eprintln!("Error: scroll-debug requires the 'debug-toolkit' feature."); + eprintln!("Run with: cargo run --features debug-toolkit --bin scroll-debug"); + std::process::exit(1); +} diff --git a/crates/node/src/debug_toolkit/actions.rs b/crates/node/src/debug_toolkit/actions.rs new file mode 100644 index 00000000..b84ad7a1 --- /dev/null +++ b/crates/node/src/debug_toolkit/actions.rs @@ -0,0 +1,370 @@ +//! Custom action framework for the debug toolkit. +//! +//! Users can implement the [`Action`] trait to create custom commands +//! that have full access to the [`TestFixture`]. +//! +//! # Example +//! +//! ```rust,ignore +//! use rollup_node::debug_toolkit::actions::{Action, ActionRegistry}; +//! use rollup_node::test_utils::TestFixture; +//! use async_trait::async_trait; +//! +//! struct MyAction; +//! +//! #[async_trait] +//! impl Action for MyAction { +//! fn name(&self) -> &'static str { +//! "my-action" +//! } +//! +//! fn description(&self) -> &'static str { +//! "Does something cool with the fixture" +//! } +//! +//! async fn execute( +//! &self, +//! fixture: &mut TestFixture, +//! args: &[String], +//! ) -> eyre::Result<()> { +//! // Your custom logic here +//! println!("Running my action with {} nodes!", fixture.nodes.len()); +//! Ok(()) +//! } +//! } +//! +//! // Register in ActionRegistry::new() +//! ``` + +use crate::test_utils::TestFixture; +use async_trait::async_trait; +use colored::Colorize; +use futures::StreamExt; +use rollup_node_chain_orchestrator::ChainOrchestratorEvent; + +/// Trait for custom debug actions. +/// +/// Implement this trait to create actions that can be invoked via `run `. +#[async_trait] +pub trait Action: Send + Sync { + /// Name of the action (used in `run `). + fn name(&self) -> &'static str; + + /// Short description shown in `run list`. + fn description(&self) -> &'static str; + + /// Optional usage string for help. + fn usage(&self) -> Option<&'static str> { + None + } + + /// Execute the action with full access to the fixture. + /// + /// # Arguments + /// * `fixture` - Mutable reference to the test fixture + /// * `args` - Arguments passed after the action name + async fn execute(&self, fixture: &mut TestFixture, args: &[String]) -> eyre::Result<()>; +} + +/// Registry of available actions. +pub struct ActionRegistry { + actions: Vec>, +} + +impl std::fmt::Debug for ActionRegistry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ActionRegistry").field("action_count", &self.actions.len()).finish() + } +} + +impl Default for ActionRegistry { + fn default() -> Self { + Self::new() + } +} + +impl ActionRegistry { + /// Create a new registry with built-in actions. + pub fn new() -> Self { + let mut registry = Self { actions: Vec::new() }; + + // Register built-in example actions + registry.register(Box::new(BuildBlocksAction)); + registry.register(Box::new(StressTestAction)); + registry.register(Box::new(SyncAllAction)); + + // ======================================== + // ADD YOUR CUSTOM ACTIONS HERE: + // registry.register(Box::new(MyCustomAction)); + // ======================================== + + registry + } + + /// Register a new action. + pub fn register(&mut self, action: Box) { + self.actions.push(action); + } + + /// Get an action by name. + pub fn get(&self, name: &str) -> Option<&dyn Action> { + self.actions.iter().find(|a| a.name() == name).map(|a| a.as_ref()) + } + + /// List all registered actions. + pub fn list(&self) -> impl Iterator { + self.actions.iter().map(|a| a.as_ref()) + } +} + +// ============================================================================ +// Built-in Example Actions +// ============================================================================ + +/// Build multiple blocks in sequence. +struct BuildBlocksAction; + +#[async_trait] +impl Action for BuildBlocksAction { + fn name(&self) -> &'static str { + "build-blocks" + } + + fn description(&self) -> &'static str { + "Build multiple blocks in sequence" + } + + fn usage(&self) -> Option<&'static str> { + Some("run build-blocks [timeout_ms]") + } + + async fn execute(&self, fixture: &mut TestFixture, args: &[String]) -> eyre::Result<()> { + let count: usize = args.first().and_then(|s| s.parse().ok()).unwrap_or(5); + let timeout_ms: u64 = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(5000); + + println!("Building {} blocks (timeout: {}ms per block)...", count, timeout_ms); + + let sequencer_idx = fixture + .nodes + .iter() + .position(|n| n.is_sequencer()) + .ok_or_else(|| eyre::eyre!("No sequencer node found"))?; + + // Get an event listener for the sequencer + let mut event_rx = fixture.nodes[sequencer_idx] + .rollup_manager_handle + .get_event_listener() + .await + .map_err(|e| eyre::eyre!("Failed to get event listener: {}", e))?; + + for i in 1..=count { + fixture.nodes[sequencer_idx].rollup_manager_handle.build_block(); + print!(" Block {} triggered, waiting...", i); + let _ = std::io::Write::flush(&mut std::io::stdout()); + + // Wait for BlockSequenced event + let timeout = tokio::time::sleep(std::time::Duration::from_millis(timeout_ms)); + tokio::pin!(timeout); + + loop { + tokio::select! { + event = event_rx.next() => { + if let Some(ChainOrchestratorEvent::BlockSequenced(block)) = event { + println!(" sequenced at #{}", block.header.number); + break; + } + // Continue waiting for BlockSequenced event + } + _ = &mut timeout => { + println!(" timeout!"); + return Err(eyre::eyre!("Timeout waiting for block {} to be sequenced", i)); + } + } + } + } + + let status = fixture.nodes[sequencer_idx].rollup_manager_handle.status().await?; + println!( + "{}", + format!("Done! Head is now at block #{}", status.l2.fcs.head_block_info().number) + .green() + ); + + Ok(()) + } +} + +/// Stress test by sending many transactions. +struct StressTestAction; + +#[async_trait] +impl Action for StressTestAction { + fn name(&self) -> &'static str { + "stress-test" + } + + fn description(&self) -> &'static str { + "Send multiple transactions and build blocks" + } + + fn usage(&self) -> Option<&'static str> { + Some("run stress-test [build_every_n]") + } + + async fn execute(&self, fixture: &mut TestFixture, args: &[String]) -> eyre::Result<()> { + use alloy_consensus::{SignableTransaction, TxEip1559}; + use alloy_eips::eip2718::Encodable2718; + use alloy_network::TxSignerSync; + use alloy_primitives::{TxKind, U256}; + + let tx_count: usize = args.first().and_then(|s| s.parse().ok()).unwrap_or(10); + let build_every: usize = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(5); + + println!("Stress test: {} txs, build every {} txs", tx_count, build_every); + + let sequencer_idx = fixture + .nodes + .iter() + .position(|n| n.is_sequencer()) + .ok_or_else(|| eyre::eyre!("No sequencer node found"))?; + + let mut wallet = fixture.wallet.lock().await; + let chain_id = wallet.chain_id; + let signer = wallet.inner.clone(); + let to_address = signer.address(); // Send to self + + for i in 0..tx_count { + let nonce = wallet.inner_nonce; + wallet.inner_nonce += 1; + + let mut tx = TxEip1559 { + chain_id, + nonce, + gas_limit: 21000, + max_fee_per_gas: 1_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + to: TxKind::Call(to_address), + value: U256::from(1), + access_list: Default::default(), + input: Default::default(), + }; + + let signature = signer.sign_transaction_sync(&mut tx)?; + let signed = tx.into_signed(signature); + let raw_tx = alloy_primitives::Bytes::from(signed.encoded_2718()); + + // Need to drop wallet lock to inject + drop(wallet); + + fixture.inject_tx_on(sequencer_idx, raw_tx).await?; + print!("."); + + // Re-acquire wallet lock + wallet = fixture.wallet.lock().await; + + // Build block periodically + if (i + 1) % build_every == 0 { + drop(wallet); + fixture.nodes[sequencer_idx].rollup_manager_handle.build_block(); + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + wallet = fixture.wallet.lock().await; + print!("B"); + } + } + + drop(wallet); + + // Final build + fixture.nodes[sequencer_idx].rollup_manager_handle.build_block(); + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + println!(); + let status = fixture.nodes[sequencer_idx].rollup_manager_handle.status().await?; + println!( + "{}", + format!( + "Done! Sent {} txs, head at block #{}", + tx_count, + status.l2.fcs.head_block_info().number + ) + .green() + ); + + Ok(()) + } +} + +/// Ensure L1 is synced on all nodes. +struct SyncAllAction; + +#[async_trait] +impl Action for SyncAllAction { + fn name(&self) -> &'static str { + "sync-all" + } + + fn description(&self) -> &'static str { + "Send L1 sync event to all nodes" + } + + fn usage(&self) -> Option<&'static str> { + None + } + + async fn execute(&self, fixture: &mut TestFixture, _args: &[String]) -> eyre::Result<()> { + println!("Syncing L1 on all {} nodes...", fixture.nodes.len()); + + fixture.l1().sync().await?; + + println!("{}", "All nodes synced!".green()); + Ok(()) + } +} + +// ============================================================================ +// Template for custom actions - copy this to create your own! +// ============================================================================ + +#[allow(dead_code)] +struct TemplateAction; + +#[allow(dead_code)] +#[async_trait] +impl Action for TemplateAction { + fn name(&self) -> &'static str { + "template" + } + + fn description(&self) -> &'static str { + "Template action - copy and modify!" + } + + fn usage(&self) -> Option<&'static str> { + Some("run template [arg1] [arg2]") + } + + async fn execute(&self, fixture: &mut TestFixture, args: &[String]) -> eyre::Result<()> { + // Access nodes + println!("Fixture has {} nodes", fixture.nodes.len()); + + // Access wallet + let wallet = fixture.wallet.lock().await; + println!("Wallet address: {:?}", wallet.inner.address()); + drop(wallet); + + // Access L1 provider + // fixture.l1().sync().await?; + + // Access specific node + let node = &fixture.nodes[0]; + let status = node.rollup_manager_handle.status().await?; + println!("Head block: {}", status.l2.fcs.head_block_info().number); + + // Process arguments + for (i, arg) in args.iter().enumerate() { + println!("Arg {}: {}", i, arg); + } + + Ok(()) + } +} diff --git a/crates/node/src/debug_toolkit/cli.rs b/crates/node/src/debug_toolkit/cli.rs new file mode 100644 index 00000000..ed4ef710 --- /dev/null +++ b/crates/node/src/debug_toolkit/cli.rs @@ -0,0 +1,179 @@ +//! CLI subcommand for the debug toolkit. + +use crate::{test_utils::TestFixtureBuilder, L1ProviderArgs}; +use alloy_primitives::Address; +use alloy_provider::{layers::CacheLayer, ProviderBuilder}; +use alloy_rpc_client::RpcClient; +use alloy_transport::layers::RetryBackoffLayer; +use clap::Parser; +use reth_network_peers::TrustedPeer; +use std::{path::PathBuf, str::FromStr}; + +/// Debug toolkit CLI arguments. +#[derive(Debug, Parser)] +#[command( + name = "scroll-debug", + about = "Scroll Debug Toolkit - Interactive REPL for debugging.\n\ + \n\ + Two modes:\n \ + Spawn: scroll-debug --chain dev --sequencer (starts a local test network)\n \ + Attach: scroll-debug --attach http://localhost:8545 (connects to a running node)" +)] +pub struct DebugArgs { + // ── Attach mode ────────────────────────────────────────────────────────── + /// Attach to an already-running node at this RPC URL instead of spawning a test network. + /// + /// Example: `--attach ` + #[arg( + long, + conflicts_with_all = ["chain", "sequencer", "followers", "l1_url", "bootnodes", "valid_signer"] + )] + pub attach: Option, + + /// Private key (hex, with or without 0x prefix) used for signing transactions in attach mode. + /// + /// If omitted, tx send/inject commands will fail with an explanatory error. + #[arg(long, requires = "attach")] + pub private_key: Option, + + // ── Spawn mode ─────────────────────────────────────────────────────────── + /// Chain to use (dev, scroll-sepolia, scroll-mainnet) or path to genesis file. + #[arg(long, default_value = "dev")] + pub chain: String, + + /// Enable sequencer mode. + #[arg(long)] + pub sequencer: bool, + + /// Number of follower nodes. + #[arg(long, default_value = "0")] + pub followers: usize, + + /// Persistent data directory (uses temp dir if not specified). + #[arg(long)] + pub datadir: Option, + + /// L1 RPC endpoint URL (optional, uses mock L1 if not specified). + #[arg(long)] + pub l1_url: Option, + + /// Comma-separated list of bootnode enode URLs to connect to. + #[arg(long, value_delimiter = ',')] + pub bootnodes: Option>, + + /// The valid signer address for the network. + #[arg(long)] + pub valid_signer: Option
, + + // ── Common ─────────────────────────────────────────────────────────────── + /// Path to log file. Defaults to ./scroll-debug-.log + #[arg(long)] + pub log_file: Option, +} + +impl DebugArgs { + /// Run the debug toolkit with these arguments. + pub async fn run(self, log_path: Option) -> eyre::Result<()> { + use super::{AttachRepl, DebugRepl}; + + // ── Attach mode ────────────────────────────────────────────────────── + if let Some(url) = self.attach { + let mut repl = AttachRepl::new(url, self.private_key).await?; + if let Some(path) = log_path { + repl.set_log_path(path); + } + return repl.run().await; + } + + // ── Spawn mode ─────────────────────────────────────────────────────── + // Build the fixture + let mut builder = TestFixtureBuilder::new().with_chain(&self.chain)?; + + if self.sequencer { + builder = builder.sequencer(); + } + + if self.followers > 0 { + builder = builder.followers(self.followers); + } + + if self.valid_signer.is_some() { + builder = builder.with_consensus_system_contract(self.valid_signer); + builder = builder.with_network_valid_signer(self.valid_signer); + } + + if self.bootnodes.as_ref().map(|b| !b.is_empty()).unwrap_or(false) || + self.l1_url.is_some() || + self.valid_signer.is_some() + { + // Disable test mode if bootnodes or l1 url are specified + builder.config_mut().test = false; + } + + // Apply L1 URL if provided - build provider for REPL access + if let Some(l1_url) = self.l1_url { + builder.config_mut().l1_provider_args.url = Some(l1_url.clone()); + + // Build the L1 provider with retry and cache layers + let L1ProviderArgs { + max_retries, + initial_backoff, + compute_units_per_second, + cache_max_items, + .. + } = L1ProviderArgs::default(); + + let client = RpcClient::builder() + .layer(RetryBackoffLayer::new( + max_retries, + initial_backoff, + compute_units_per_second, + )) + .http(l1_url); + let cache_layer = CacheLayer::new(cache_max_items); + let provider = ProviderBuilder::new().layer(cache_layer).connect_client(client); + + builder = builder.with_l1_provider(Box::new(provider)); + } + + // Parse and apply bootnodes if provided + if let Some(bootnode_strs) = self.bootnodes { + let mut bootnodes = Vec::with_capacity(bootnode_strs.len()); + for enode in bootnode_strs { + match TrustedPeer::from_str(&enode) { + Ok(peer) => bootnodes.push(peer), + Err(e) => { + return Err(eyre::eyre!("Failed to parse bootnode '{}': {}", enode, e)); + } + } + } + if !bootnodes.is_empty() { + builder = builder.bootnodes(bootnodes); + } + } + + let fixture = builder.build().await?; + + // Create and run REPL + let mut repl = DebugRepl::new(fixture); + if let Some(path) = log_path { + repl.set_log_path(path); + } + repl.run().await + } +} + +/// Entry point for the debug toolkit. +/// +/// Usage: +/// ```bash +/// cargo run --features debug-toolkit --bin scroll-debug -- --chain dev --sequencer +/// ``` +pub async fn main() -> eyre::Result<()> { + // Initialize tracing + tracing_subscriber::fmt::init(); + + // Parse arguments and run + let args = DebugArgs::parse(); + args.run(None).await +} diff --git a/crates/node/src/debug_toolkit/commands.rs b/crates/node/src/debug_toolkit/commands.rs new file mode 100644 index 00000000..07768c5b --- /dev/null +++ b/crates/node/src/debug_toolkit/commands.rs @@ -0,0 +1,453 @@ +//! Command parsing and execution for the debug REPL. + +use alloy_primitives::{Address, Bytes, U256}; +use colored::Colorize; +use std::str::FromStr; + +/// A parsed REPL command. +#[derive(Debug, Clone)] +pub enum Command { + /// Show node status. + Status, + /// Show detailed sync status. + SyncStatus, + /// Show block details. + Block(BlockArg), + /// List blocks in range. + Blocks { + /// Starting block number. + from: u64, + /// Ending block number. + to: u64, + }, + /// Show forkchoice state. + Fcs, + /// L1 commands. + L1(L1Command), + /// Build a new block. + Build, + /// Transaction commands. + Tx(TxCommand), + /// Wallet commands. + Wallet(WalletCommand), + /// Peer commands. + Peers(PeersCommand), + /// Event commands. + Events(EventsCommand), + /// Run a custom action. + Run(RunCommand), + /// Switch to a different node. + Node(usize), + /// List all nodes. + Nodes, + /// Show database path and access command. + Db, + /// Show log file path. + Logs, + /// Admin commands. + Admin(AdminCommand), + /// Execute a raw JSON-RPC call and print the result. + Rpc { + /// The RPC method name (e.g. `eth_blockNumber`). + method: String, + /// Raw JSON params string (e.g. `["latest", false]`). + params: Option, + }, + /// Show help. + Help, + /// Exit the REPL. + Exit, + /// Unknown command. + Unknown(String), +} + +/// Admin commands. +#[derive(Debug, Clone)] +pub enum AdminCommand { + /// Enable automatic sequencing. + EnableSequencing, + /// Disable automatic sequencing. + DisableSequencing, + /// Revert the node state to a specified L1 block number. + RevertToL1Block(u64), +} + +/// Run command variants. +#[derive(Debug, Clone)] +pub enum RunCommand { + /// List available actions. + List, + /// Execute an action by name. + Execute { + /// Action name. + name: String, + /// Arguments to pass to the action. + args: Vec, + }, +} + +/// Block argument: either a number or "latest". +#[derive(Debug, Clone)] +pub enum BlockArg { + /// Latest block. + Latest, + /// Block by number. + Number(u64), +} + +/// L1-related commands. +#[derive(Debug, Clone)] +pub enum L1Command { + /// Show L1 status. + Status, + /// Inject L1 synced event. + Sync, + /// Inject new L1 block. + Block(u64), + /// Inject L1 reorg. + Reorg(u64), + /// Show L1 message queue status. + Messages, + /// Send L1 message (bridge to L2). + Send { + /// Recipient address on L2. + to: Address, + /// Value to send. + value: U256, + }, +} + +/// Transaction-related commands. +#[derive(Debug, Clone)] +pub enum TxCommand { + /// Send a transfer. + Send { + /// Recipient address. + to: Address, + /// Transfer value. + value: U256, + /// Wallet index to send from (from `wallet gen` list). + from: Option, + }, + /// List pending transactions. + Pending, + /// Inject raw transaction. + Inject(Bytes), +} + +/// Peer-related commands. +#[derive(Debug, Clone)] +pub enum PeersCommand { + /// List connected peers. + List, + /// Connect to a peer. + Connect(String), +} + +/// Wallet-related commands. +#[derive(Debug, Clone)] +pub enum WalletCommand { + /// Show wallet info (address, balance, nonce). + Info, + /// Generate and list available wallets. + Gen, +} + +/// Event-related commands. +#[derive(Debug, Clone)] +pub enum EventsCommand { + /// Enable background event stream. + On, + /// Disable background event stream. + Off, + /// Set event filter. + Filter(Option), + /// Show event history. + History(usize), +} + +impl Command { + /// Parse a command from input string. + pub fn parse(input: &str) -> Self { + let input = input.trim(); + if input.is_empty() { + return Self::Unknown(String::new()); + } + + let parts: Vec<&str> = input.split_whitespace().collect(); + let cmd = parts[0].to_lowercase(); + let args = &parts[1..]; + + match cmd.as_str() { + "status" => Self::Status, + "sync-status" | "syncstatus" => Self::SyncStatus, + "block" => Self::parse_block(args), + "blocks" => Self::parse_blocks(args), + "fcs" | "forkchoice" => Self::Fcs, + "l1" => Self::parse_l1(args), + "build" => Self::Build, + "tx" => Self::parse_tx(args), + "wallet" => Self::parse_wallet(args), + "peers" | "peer" => Self::parse_peers(args), + "events" | "event" => Self::parse_events(args), + "run" => Self::parse_run(args), + "node" => Self::parse_node(args), + "nodes" => Self::Nodes, + "db" | "database" => Self::Db, + "logs" | "log" => Self::Logs, + "admin" => Self::parse_admin(args), + "rpc" => Self::parse_rpc(args), + "help" | "?" => Self::Help, + "exit" | "quit" | "q" => Self::Exit, + _ => Self::Unknown(cmd), + } + } + + fn parse_block(args: &[&str]) -> Self { + let arg = args.first().copied().unwrap_or("latest"); + if arg == "latest" { + Self::Block(BlockArg::Latest) + } else { + match arg.parse::() { + Ok(n) => Self::Block(BlockArg::Number(n)), + Err(_) => Self::Unknown(format!("block {}", arg)), + } + } + } + + fn parse_blocks(args: &[&str]) -> Self { + if args.len() < 2 { + return Self::Unknown("blocks requires arguments".to_string()); + } + match (args[0].parse::(), args[1].parse::()) { + (Ok(from), Ok(to)) => Self::Blocks { from, to }, + _ => Self::Unknown("blocks requires numeric arguments".to_string()), + } + } + + fn parse_l1(args: &[&str]) -> Self { + let subcmd = args.first().copied().unwrap_or("status"); + let subargs = if args.len() > 1 { &args[1..] } else { &[] }; + + match subcmd { + "status" => Self::L1(L1Command::Status), + "sync" | "synced" => Self::L1(L1Command::Sync), + "block" => { + if let Some(n) = subargs.first().and_then(|s| s.parse::().ok()) { + Self::L1(L1Command::Block(n)) + } else { + Self::Unknown("l1 block requires a block number".to_string()) + } + } + "reorg" => { + if let Some(n) = subargs.first().and_then(|s| s.parse::().ok()) { + Self::L1(L1Command::Reorg(n)) + } else { + Self::Unknown("l1 reorg requires a block number".to_string()) + } + } + "messages" | "msg" | "queue" => Self::L1(L1Command::Messages), + "send" => { + if subargs.len() < 2 { + return Self::Unknown("l1 send requires ".to_string()); + } + match (Address::from_str(subargs[0]), U256::from_str(subargs[1])) { + (Ok(to), Ok(value)) => Self::L1(L1Command::Send { to, value }), + _ => Self::Unknown("l1 send: invalid address or value".to_string()), + } + } + _ => Self::Unknown(format!("l1 {}", subcmd)), + } + } + + fn parse_tx(args: &[&str]) -> Self { + let subcmd = args.first().copied().unwrap_or("pending"); + let subargs = if args.len() > 1 { &args[1..] } else { &[] }; + + match subcmd { + "pending" => Self::Tx(TxCommand::Pending), + "send" => { + if subargs.len() < 2 { + return Self::Unknown( + "tx send requires [wallet_index]".to_string(), + ); + } + match (Address::from_str(subargs[0]), U256::from_str(subargs[1])) { + (Ok(to), Ok(value)) => { + let from = subargs.get(2).and_then(|s| s.parse::().ok()); + Self::Tx(TxCommand::Send { to, value, from }) + } + _ => Self::Unknown("tx send: invalid address or value".to_string()), + } + } + "inject" => { + if let Some(hex) = subargs.first() { + match Bytes::from_str(hex) { + Ok(bytes) => Self::Tx(TxCommand::Inject(bytes)), + Err(_) => Self::Unknown("tx inject: invalid hex data".to_string()), + } + } else { + Self::Unknown("tx inject requires hex data".to_string()) + } + } + _ => Self::Unknown(format!("tx {}", subcmd)), + } + } + + fn parse_wallet(args: &[&str]) -> Self { + let subcmd = args.first().copied().unwrap_or("info"); + match subcmd { + "info" | "" => Self::Wallet(WalletCommand::Info), + "gen" | "generate" => Self::Wallet(WalletCommand::Gen), + _ => Self::Unknown("wallet command not recognized".to_string()), + } + } + + fn parse_peers(args: &[&str]) -> Self { + let subcmd = args.first().copied().unwrap_or("list"); + let subargs = if args.len() > 1 { &args[1..] } else { &[] }; + + match subcmd { + "list" | "" => Self::Peers(PeersCommand::List), + "connect" => { + if let Some(enode) = subargs.first() { + Self::Peers(PeersCommand::Connect(enode.to_string())) + } else { + Self::Unknown("peers connect requires enode URL".to_string()) + } + } + _ => Self::Unknown("peers command not recognized".to_string()), + } + } + + fn parse_events(args: &[&str]) -> Self { + let subcmd = args.first().copied().unwrap_or("history"); + let subargs = if args.len() > 1 { &args[1..] } else { &[] }; + + match subcmd { + "on" => Self::Events(EventsCommand::On), + "off" => Self::Events(EventsCommand::Off), + "filter" => { + let pattern = subargs.first().map(|s| s.to_string()); + Self::Events(EventsCommand::Filter(pattern)) + } + "history" => { + let count = subargs.first().and_then(|s| s.parse().ok()).unwrap_or(20); + Self::Events(EventsCommand::History(count)) + } + _ => Self::Unknown("Unknown events command".to_string()), + } + } + + fn parse_node(args: &[&str]) -> Self { + if let Some(n) = args.first().and_then(|s| s.parse::().ok()) { + Self::Node(n) + } else { + Self::Unknown("node requires an index".to_string()) + } + } + + fn parse_run(args: &[&str]) -> Self { + if args.is_empty() || args[0] == "list" { + Self::Run(RunCommand::List) + } else { + let name = args[0].to_string(); + let action_args: Vec = args.iter().skip(1).map(|s| s.to_string()).collect(); + Self::Run(RunCommand::Execute { name, args: action_args }) + } + } + + fn parse_admin(args: &[&str]) -> Self { + let subcmd = args.first().copied().unwrap_or("help"); + let subargs = if args.len() > 1 { &args[1..] } else { &[] }; + + match subcmd { + "enable-seq" | "enable-sequencing" => Self::Admin(AdminCommand::EnableSequencing), + "disable-seq" | "disable-sequencing" => Self::Admin(AdminCommand::DisableSequencing), + "revert" | "revert-to-l1" => { + if let Some(n) = subargs.first().and_then(|s| s.parse::().ok()) { + Self::Admin(AdminCommand::RevertToL1Block(n)) + } else { + Self::Unknown("admin revert requires a block number".to_string()) + } + } + _ => Self::Unknown(format!("admin {}", subcmd)), + } + } + + fn parse_rpc(args: &[&str]) -> Self { + let Some(method) = args.first() else { + return Self::Unknown("rpc requires a method name".to_string()); + }; + let params = (args.len() > 1).then(|| args[1..].join(" ")); + Self::Rpc { method: method.to_string(), params } + } +} + +/// Print the help message. +pub fn print_help() { + println!("{}", "Scroll Debug Toolkit - Commands".bold()); + println!(); + println!("{}", "Status & Inspection:".underline()); + println!(" status Show node status (head, safe, finalized, L1 state)"); + println!(" sync-status Show detailed sync status (L1/L2 sync state)"); + println!(" block [n|latest] Display block details"); + println!(" blocks List blocks in range"); + println!(" fcs Show forkchoice state"); + println!(); + println!("{}", "L1 Commands:".underline()); + println!(" l1 status Show L1 sync state"); + println!(" l1 sync Inject L1 synced event"); + println!(" l1 block Inject new L1 block notification"); + println!(" l1 reorg Inject L1 reorg"); + println!(" l1 messages Show L1 message queue info (requires --l1-url)"); + println!(" l1 send Show cast command for L1->L2 bridge transfer"); + println!(); + println!("{}", "Block & Transaction:".underline()); + println!(" build Build a new block (sequencer mode)"); + println!(" tx send [idx] Send ETH transfer (idx = wallet index from gen)"); + println!(" tx pending List pending transactions"); + println!(" tx inject Inject raw transaction"); + println!(); + println!("{}", "Wallet:".underline()); + println!(" wallet Show wallet address, balance, and nonce"); + println!(" wallet gen Generate and list all available wallets"); + println!(); + println!("{}", "Network:".underline()); + println!(" peers List connected peers and show local enode"); + println!(" peers connect Connect to a peer (enode://...)"); + println!(); + println!("{}", "Events:".underline()); + println!(" events on Enable background event stream"); + println!(" events off Disable background event stream"); + println!(" events filter Filter events by type (e.g., Block*, L1*)"); + println!(" events history [n] Show last N events (default: 20)"); + println!(); + println!("{}", "Custom Actions:".underline()); + println!(" run list List available custom actions"); + println!(" run [args] Execute a custom action"); + println!(); + println!("{}", "Node Management:".underline()); + println!(" node Switch active node context"); + println!(" nodes List all nodes in fixture"); + println!(); + println!("{}", "Database:".underline()); + println!(" db Show database path and access command"); + println!(); + println!("{}", "Logs:".underline()); + println!(" logs Show log file path and tail command"); + println!(); + println!("{}", "Admin:".underline()); + println!(" admin enable-seq Enable automatic sequencing"); + println!(" admin disable-seq Disable automatic sequencing"); + println!(" admin revert Revert node state to L1 block number "); + println!(); + println!("{}", "Raw RPC:".underline()); + println!(" rpc [params] Execute any JSON-RPC call and print result"); + println!(" rpc eth_blockNumber"); + println!(" rpc eth_getBlockByNumber [\"latest\",false]"); + println!(); + println!("{}", "Other:".underline()); + println!(" help Show this help message"); + println!(" exit Exit the REPL"); +} diff --git a/crates/node/src/debug_toolkit/event/mod.rs b/crates/node/src/debug_toolkit/event/mod.rs new file mode 100644 index 00000000..f1d68c65 --- /dev/null +++ b/crates/node/src/debug_toolkit/event/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod stream; + +pub use stream::*; diff --git a/crates/node/src/debug_toolkit/event/stream.rs b/crates/node/src/debug_toolkit/event/stream.rs new file mode 100644 index 00000000..057e9a39 --- /dev/null +++ b/crates/node/src/debug_toolkit/event/stream.rs @@ -0,0 +1,297 @@ +/// Background event streaming for the debug REPL. +use colored::Colorize; +use rollup_node_chain_orchestrator::ChainOrchestratorEvent; +use std::{ + collections::VecDeque, + time::{Duration, Instant}, +}; + +/// Maximum number of events to keep in history. +const DEFAULT_HISTORY_CAPACITY: usize = 100; + +/// State for background event streaming. +#[derive(Debug)] +pub struct EventStreamState { + /// Whether background streaming is enabled. + enabled: bool, + /// Event type filter (glob pattern). + filter: Option, + /// Ring buffer of recent events for `events history`. + history: VecDeque<(Instant, ChainOrchestratorEvent)>, + /// Max history size. + history_capacity: usize, + /// Counter for event numbering. + event_counter: usize, +} + +impl Default for EventStreamState { + fn default() -> Self { + Self::new() + } +} + +impl EventStreamState { + /// Create a new event stream state. + pub fn new() -> Self { + Self { + enabled: false, + filter: None, + history: VecDeque::with_capacity(DEFAULT_HISTORY_CAPACITY), + history_capacity: DEFAULT_HISTORY_CAPACITY, + event_counter: 0, + } + } + + /// Enable background event streaming. + pub const fn enable(&mut self) { + self.enabled = true; + } + + /// Disable background event streaming. + pub const fn disable(&mut self) { + self.enabled = false; + } + + /// Check if streaming is enabled. + pub const fn is_enabled(&self) -> bool { + self.enabled + } + + /// Set the event filter pattern. + pub fn set_filter(&mut self, pattern: Option) { + self.filter = pattern; + } + + /// Get the current filter pattern. + pub fn filter(&self) -> Option<&str> { + self.filter.as_deref() + } + + /// Record an event in history and optionally display it. + pub fn record_event(&mut self, event: ChainOrchestratorEvent) -> Option { + let now = Instant::now(); + + // Add to history + if self.history.len() >= self.history_capacity { + self.history.pop_front(); + } + self.history.push_back((now, event.clone())); + self.event_counter += 1; + + // Check if we should display this event + if !self.enabled { + return None; + } + + let event_name = event_type_name(&event); + if !self.matches_filter(&event_name) { + return None; + } + + Some(self.format_event(&event)) + } + + /// Check if an event name matches the filter. + fn matches_filter(&self, event_name: &str) -> bool { + match &self.filter { + None => true, + Some(pattern) => { + // Simple glob matching: * matches any sequence of characters + let pattern = pattern.replace('*', ".*"); + regex_lite::Regex::new(&format!("^{}$", pattern)) + .map(|re| re.is_match(event_name)) + .unwrap_or(true) + } + } + } + + /// Format an event for display. + pub fn format_event(&self, event: &ChainOrchestratorEvent) -> String { + let prefix = " [EVENT]".cyan(); + let event_str = format_event_short(event); + format!("{} {}", prefix, event_str) + } + + /// Get recent events from history. + pub fn get_history(&self, count: usize) -> Vec<(Duration, &ChainOrchestratorEvent)> { + let start = Instant::now(); + self.history + .iter() + .rev() + .take(count) + .map(|(t, e)| (start.duration_since(*t), e)) + .collect::>() + .into_iter() + .rev() + .collect() + } + + /// Get the total number of events recorded. + pub const fn total_events(&self) -> usize { + self.event_counter + } +} + +/// Get the type name of an event for filtering. +pub fn event_type_name(event: &ChainOrchestratorEvent) -> String { + match event { + ChainOrchestratorEvent::BlockSequenced(_) => "BlockSequenced".to_string(), + ChainOrchestratorEvent::ChainConsolidated { .. } => "ChainConsolidated".to_string(), + ChainOrchestratorEvent::ChainExtended(_) => "ChainExtended".to_string(), + ChainOrchestratorEvent::ChainReorged(_) => "ChainReorged".to_string(), + ChainOrchestratorEvent::L1Synced => "L1Synced".to_string(), + ChainOrchestratorEvent::OptimisticSync(_) => "OptimisticSync".to_string(), + ChainOrchestratorEvent::NewL1Block(_) => "NewL1Block".to_string(), + ChainOrchestratorEvent::L1MessageCommitted(_) => "L1MessageCommitted".to_string(), + ChainOrchestratorEvent::L1Reorg { .. } => "L1Reorg".to_string(), + ChainOrchestratorEvent::BatchConsolidated(_) => "BatchConsolidated".to_string(), + ChainOrchestratorEvent::UnwoundToL1Block(_) => "UnwoundToL1Block".to_string(), + ChainOrchestratorEvent::BlockConsolidated(_) => "BlockConsolidated".to_string(), + ChainOrchestratorEvent::BatchReverted { .. } => "BatchReverted".to_string(), + ChainOrchestratorEvent::L1BlockFinalized(_, _) => "L1BlockFinalized".to_string(), + ChainOrchestratorEvent::NewBlockReceived(_) => "NewBlockReceived".to_string(), + ChainOrchestratorEvent::L1MessageNotFoundInDatabase(_) => { + "L1MessageNotFoundInDatabase".to_string() + } + ChainOrchestratorEvent::BlockFailedConsensusChecks(_, _) => { + "BlockFailedConsensusChecks".to_string() + } + ChainOrchestratorEvent::InsufficientDataForReceivedBlock(_) => { + "InsufficientDataForReceivedBlock".to_string() + } + ChainOrchestratorEvent::BlockAlreadyKnown(_, _) => "BlockAlreadyKnown".to_string(), + ChainOrchestratorEvent::OldForkReceived { .. } => "OldForkReceived".to_string(), + ChainOrchestratorEvent::BatchCommitIndexed { .. } => "BatchCommitIndexed".to_string(), + ChainOrchestratorEvent::BatchFinalized { .. } => "BatchFinalized".to_string(), + ChainOrchestratorEvent::L2ChainCommitted(_, _, _) => "L2ChainCommitted".to_string(), + ChainOrchestratorEvent::L2ConsolidatedBlockCommitted(_) => { + "L2ConsolidatedBlockCommitted".to_string() + } + ChainOrchestratorEvent::SignedBlock { .. } => "SignedBlock".to_string(), + ChainOrchestratorEvent::L1MessageMismatch { .. } => "L1MessageMismatch".to_string(), + ChainOrchestratorEvent::FcsHeadUpdated(_) => "FcsHeadUpdated".to_string(), + } +} + +/// Format an event for short display. +pub fn format_event_short(event: &ChainOrchestratorEvent) -> String { + match event { + ChainOrchestratorEvent::BlockSequenced(block) => { + format!( + "BlockSequenced {{ block: {}, hash: {:.8}... }}", + block.header.number, + format!("{:?}", block.header.hash_slow()) + ) + } + ChainOrchestratorEvent::ChainConsolidated { from, to } => { + format!("ChainConsolidated {{ from: {}, to: {} }}", from, to) + } + ChainOrchestratorEvent::ChainExtended(import) => { + format!("ChainExtended {{ blocks: {} }}", import.chain.len()) + } + ChainOrchestratorEvent::ChainReorged(import) => { + format!("ChainReorged {{ blocks: {} }}", import.chain.len()) + } + ChainOrchestratorEvent::L1Synced => "L1Synced".to_string(), + ChainOrchestratorEvent::OptimisticSync(info) => { + format!("OptimisticSync {{ block: {} }}", info.number) + } + ChainOrchestratorEvent::NewL1Block(num) => format!("NewL1Block {{ block: {} }}", num), + ChainOrchestratorEvent::L1MessageCommitted(queue_index) => { + format!("L1MessageCommitted {{ queue_index: {} }}", queue_index) + } + ChainOrchestratorEvent::L1Reorg { l1_block_number, .. } => { + format!("L1Reorg {{ l1_block: {} }}", l1_block_number) + } + ChainOrchestratorEvent::BatchConsolidated(outcome) => { + format!("BatchConsolidated {{ blocks: {} }}", outcome.blocks.len()) + } + ChainOrchestratorEvent::UnwoundToL1Block(num) => { + format!("UnwoundToL1Block {{ block: {} }}", num) + } + ChainOrchestratorEvent::BlockConsolidated(outcome) => { + format!("BlockConsolidated {{ block: {} }}", outcome.block_info().block_info.number) + } + ChainOrchestratorEvent::BatchReverted { batch_info, safe_head } => { + format!("BatchReverted {{ index: {}, safe: {} }}", batch_info.index, safe_head.number) + } + ChainOrchestratorEvent::L1BlockFinalized(num, batches) => { + format!("L1BlockFinalized {{ block: {}, batches: {} }}", num, batches.len()) + } + ChainOrchestratorEvent::NewBlockReceived(nbwp) => { + format!( + "NewBlockReceived {{ block: {}, peer: {:.8}... }}", + nbwp.block.header.number, + format!("{:?}", nbwp.peer_id) + ) + } + ChainOrchestratorEvent::L1MessageNotFoundInDatabase(key) => { + format!("L1MessageNotFoundInDatabase {{ key: {:?} }}", key) + } + ChainOrchestratorEvent::BlockFailedConsensusChecks(hash, peer) => { + format!( + "BlockFailedConsensusChecks {{ hash: {:.8}..., peer: {:.8}... }}", + format!("{:?}", hash), + format!("{:?}", peer) + ) + } + ChainOrchestratorEvent::InsufficientDataForReceivedBlock(hash) => { + format!("InsufficientDataForReceivedBlock {{ hash: {:.8}... }}", format!("{:?}", hash)) + } + ChainOrchestratorEvent::BlockAlreadyKnown(hash, peer) => { + format!( + "BlockAlreadyKnown {{ hash: {:.8}..., peer: {:.8}... }}", + format!("{:?}", hash), + format!("{:?}", peer) + ) + } + ChainOrchestratorEvent::OldForkReceived { headers, peer_id, .. } => { + format!( + "OldForkReceived {{ headers: {}, peer: {:.8}... }}", + headers.len(), + format!("{:?}", peer_id) + ) + } + ChainOrchestratorEvent::BatchCommitIndexed { batch_info, l1_block_number } => { + format!( + "BatchCommitIndexed {{ index: {}, l1_block: {} }}", + batch_info.index, l1_block_number + ) + } + ChainOrchestratorEvent::BatchFinalized { l1_block_info, triggered_batches } => { + format!( + "BatchFinalized {{ l1_block: {}, batches: {} }}", + l1_block_info.number, + triggered_batches.len() + ) + } + ChainOrchestratorEvent::L2ChainCommitted(info, batch, is_consolidated) => { + format!( + "L2ChainCommitted {{ block: {}, batch: {:?}, consolidated: {} }}", + info.block_info.number, + batch.as_ref().map(|b| b.index), + is_consolidated + ) + } + ChainOrchestratorEvent::L2ConsolidatedBlockCommitted(info) => { + format!("L2ConsolidatedBlockCommitted {{ block: {} }}", info.block_info.number) + } + ChainOrchestratorEvent::SignedBlock { block, .. } => { + format!( + "SignedBlock {{ block: {}, hash: {:.8}... }}", + block.header.number, + format!("{:?}", block.header.hash_slow()) + ) + } + ChainOrchestratorEvent::L1MessageMismatch { expected, actual } => { + format!( + "L1MessageMismatch {{ expected: {:.8}..., actual: {:.8}... }}", + format!("{:?}", expected), + format!("{:?}", actual) + ) + } + ChainOrchestratorEvent::FcsHeadUpdated(info) => { + format!("FcsHeadUpdated {{ block: {} }}", info.number) + } + } +} diff --git a/crates/node/src/debug_toolkit/mod.rs b/crates/node/src/debug_toolkit/mod.rs new file mode 100644 index 00000000..22794875 --- /dev/null +++ b/crates/node/src/debug_toolkit/mod.rs @@ -0,0 +1,75 @@ +//! Debug Toolkit for Scroll Rollup Node +//! +//! This module provides an interactive REPL and debugging utilities for +//! hackathons, development, and debugging scenarios. +//! +//! # Quick Start +//! +//! ```rust,ignore +//! use rollup_node::debug_toolkit::prelude::*; +//! use rollup_node::test_utils::TestFixture; +//! +//! #[tokio::main] +//! async fn main() -> eyre::Result<()> { +//! // Create a test fixture +//! let fixture = TestFixture::builder() +//! .with_chain("dev") +//! .sequencer() +//! .with_noop_consensus() +//! .build() +//! .await?; +//! +//! // Start the REPL +//! let mut repl = DebugRepl::new(fixture); +//! repl.run().await?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! # Custom Actions +//! +//! You can create custom actions by implementing the [`actions::Action`] trait: +//! +//! ```rust,ignore +//! use rollup_node::debug_toolkit::actions::{Action, ActionRegistry}; +//! use rollup_node::test_utils::TestFixture; +//! use async_trait::async_trait; +//! +//! struct MyAction; +//! +//! #[async_trait] +//! impl Action for MyAction { +//! fn name(&self) -> &'static str { "my-action" } +//! fn description(&self) -> &'static str { "Does something cool" } +//! +//! async fn execute( +//! &self, +//! fixture: &mut TestFixture, +//! args: &[String], +//! ) -> eyre::Result<()> { +//! // Your logic here with full fixture access +//! Ok(()) +//! } +//! } +//! ``` + +pub mod actions; +pub mod cli; +mod commands; +mod event; +mod repl; +mod shared; + +pub use cli::DebugArgs; +pub use commands::*; +pub use event::*; +pub use repl::{AttachRepl, DebugRepl}; + +/// Prelude for convenient imports. +pub mod prelude { + pub use super::{ + actions::{Action, ActionRegistry}, + AttachRepl, DebugRepl, EventStreamState, + }; +} diff --git a/crates/node/src/debug_toolkit/repl/attach.rs b/crates/node/src/debug_toolkit/repl/attach.rs new file mode 100644 index 00000000..0f28382a --- /dev/null +++ b/crates/node/src/debug_toolkit/repl/attach.rs @@ -0,0 +1,569 @@ +/// REPL for attaching to an already-running scroll node via JSON-RPC. +use crate::debug_toolkit::commands::{ + print_help, AdminCommand, BlockArg, Command, EventsCommand, L1Command, PeersCommand, TxCommand, +}; +use alloy_consensus::{SignableTransaction, TxEip1559}; +use alloy_eips::{eip2718::Encodable2718, BlockId, BlockNumberOrTag}; +use alloy_network::TxSignerSync; +use alloy_primitives::TxKind; +use alloy_provider::{Provider, ProviderBuilder}; +use alloy_signer_local::PrivateKeySigner; +use colored::Colorize; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; +use reqwest::Url; +use rollup_node_chain_orchestrator::ChainOrchestratorStatus; +use scroll_alloy_network::Scroll; +use std::{io::Write, path::PathBuf, time::Duration}; + +/// Interactive REPL that attaches to a running node via JSON-RPC. +#[derive(Debug)] +pub struct AttachRepl { + /// The RPC URL of the target node. + url: Url, + /// Alloy provider — all RPC calls including custom namespaces go through `raw_request`. + provider: alloy_provider::RootProvider, + /// Optional private key for signing transactions locally. + signer: Option, + /// Whether the REPL is running. + running: bool, + /// Whether background head-block polling is enabled. + events_enabled: bool, + /// Most recently seen block number (for head-block polling). + last_seen_block: u64, + /// Path to the log file (for `logs` command). + log_path: Option, +} + +impl AttachRepl { + /// Connect to a node at the given URL and build the REPL. + pub async fn new(url: Url, private_key: Option) -> eyre::Result { + // Use `default()` (no fillers) to get a plain `RootProvider`. + // We don't need gas/nonce fillers since we build transactions manually. + let provider = ProviderBuilder::default() + .connect(url.as_str()) + .await + .map_err(|e| eyre::eyre!("Failed to connect to {}: {}", url, e))?; + + let signer = if let Some(pk) = private_key { + let pk = pk.trim_start_matches("0x"); + let signer: PrivateKeySigner = + pk.parse().map_err(|e| eyre::eyre!("Invalid private key: {}", e))?; + Some(signer) + } else { + None + }; + + let last_seen_block = provider.get_block_number().await.unwrap_or(0); + + Ok(Self { + url, + provider, + signer, + running: false, + events_enabled: false, + last_seen_block, + log_path: None, + }) + } + + /// Set the log file path (shown by `logs` command). + pub fn set_log_path(&mut self, path: PathBuf) { + self.log_path = Some(path); + } + + /// Get the REPL prompt string. + fn get_prompt(&self) -> String { + let host = self.url.host_str().unwrap_or("?"); + let port = self.url.port().map(|p| format!(":{}", p)).unwrap_or_default(); + format!("{} [{}{}]> ", "scroll-debug".cyan(), host, port) + } + + /// Run the REPL loop. + pub async fn run(&mut self) -> eyre::Result<()> { + self.running = true; + + let _guard = super::terminal::RawModeGuard::new()?; + + let _ = disable_raw_mode(); + println!(); + println!("{}", "Scroll Debug Toolkit (attach mode)".bold().cyan()); + println!("Connected to: {}", self.url.as_str().green()); + if let Some(signer) = &self.signer { + println!("Signer: {:?}", signer.address()); + } else { + println!("{}", "No signer, tx send/inject require --private-key".yellow()); + } + println!("Type 'help' for available commands, 'exit' to quit."); + println!(); + if let Err(e) = self.cmd_status().await { + println!("{}: {}", "Warning: could not fetch initial status".yellow(), e); + } + let _ = enable_raw_mode(); + + let mut input_buffer = String::new(); + let mut stdout = std::io::stdout(); + let mut head_poll_tick = tokio::time::interval(Duration::from_secs(2)); + head_poll_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + let mut input_tick = tokio::time::interval(Duration::from_millis(50)); + input_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + print!("{}", self.get_prompt()); + let _ = stdout.flush(); + + while self.running { + tokio::select! { + biased; + + // Head-block polling (only when events are enabled). + _ = head_poll_tick.tick(), if self.events_enabled => { + if let Ok(number) = self.provider.get_block_number().await { + if number > self.last_seen_block { + for n in (self.last_seen_block + 1)..=number { + let id = BlockId::Number(BlockNumberOrTag::Number(n)); + if let Ok(Some(block)) = self.provider.get_block(id).await { + let msg = format!( + "[new block] #{} hash={:.12}... txs={}", + block.header.number, + format!("{:?}", block.header.hash), + block.transactions.len(), + ); + print!("\r\x1b[K{}\r\n{}{}", msg.cyan(), self.get_prompt(), input_buffer); + let _ = stdout.flush(); + } + } + self.last_seen_block = number; + } + } + } + + // Check for keyboard input (non-blocking) + _ = input_tick.tick() => { + match super::terminal::poll_keyboard(&mut input_buffer, &self.get_prompt())? { + super::terminal::InputAction::Command(line) => { + let _ = disable_raw_mode(); + if let Err(e) = self.execute_command(&line).await { + println!("{}: {}", "Error".red(), e); + } + let _ = enable_raw_mode(); + if self.running { + print!("{}", self.get_prompt()); + let _ = stdout.flush(); + } + } + super::terminal::InputAction::Quit => self.running = false, + super::terminal::InputAction::None => {} + } + } + } + } + + print!("Goodbye!\r\n"); + Ok(()) + } + + /// Dispatch a parsed command. + async fn execute_command(&mut self, input: &str) -> eyre::Result<()> { + let cmd = Command::parse(input); + match cmd { + Command::Status => self.cmd_status().await, + Command::SyncStatus => self.cmd_sync_status().await, + Command::Block(arg) => self.cmd_block(arg).await, + Command::Blocks { from, to } => self.cmd_blocks(from, to).await, + Command::Fcs => self.cmd_fcs().await, + Command::L1(l1_cmd) => self.cmd_l1(l1_cmd).await, + Command::Tx(tx_cmd) => self.cmd_tx(tx_cmd).await, + Command::Peers(peers_cmd) => self.cmd_peers(peers_cmd).await, + Command::Events(events_cmd) => self.cmd_events(events_cmd), + Command::Admin(admin_cmd) => self.cmd_admin(admin_cmd).await, + Command::Rpc { method, params } => self.cmd_rpc(&method, params.as_deref()).await, + Command::Logs => self.cmd_logs(), + Command::Help => { + print_help(); + Ok(()) + } + Command::Exit => { + self.running = false; + Ok(()) + } + // Spawn-mode-only commands — give informative errors + Command::Build => { + println!( + "{}", + "build is only available in spawn mode (--chain / --sequencer).".yellow() + ); + Ok(()) + } + Command::Wallet(_) => { + println!( + "{}", + "wallet gen is only available in spawn mode. Use --private-key to set a signer." + .yellow() + ); + Ok(()) + } + Command::Run(_) => { + println!("{}", "run actions are only available in spawn mode.".yellow()); + Ok(()) + } + Command::Node(_) | Command::Nodes => { + println!( + "{}", + "node switching is only available in spawn mode (multiple nodes).".yellow() + ); + Ok(()) + } + Command::Db => { + println!("{}", "db path is only available in spawn mode.".yellow()); + Ok(()) + } + Command::Unknown(s) => { + if !s.is_empty() { + println!("Unknown command: {}. Type 'help' for available commands.", s); + } + Ok(()) + } + } + } + + // ------------------------------------------------------------------------- + // Helper + // ------------------------------------------------------------------------- + + /// Call a custom-namespace JSON-RPC method and deserialize the response. + /// + /// Uses `raw_request_dyn` (no trait bounds on P/R) combined with `serde_json` for + /// maximum compatibility regardless of the provider's network/transport generics. + async fn raw( + &self, + method: &'static str, + params: impl serde::Serialize, + ) -> eyre::Result { + crate::debug_toolkit::shared::rpc::raw_typed(&self.provider, method, params).await + } + + // ------------------------------------------------------------------------- + // Command implementations + // ------------------------------------------------------------------------- + + /// `status` — show node status via `rollupNode_status`. + async fn cmd_status(&self) -> eyre::Result<()> { + let status: ChainOrchestratorStatus = self.raw("rollupNode_status", ()).await?; + + println!("{}", "=== Node Status ===".bold()); + println!("{}", "Node:".underline()); + println!(" RPC: {}", self.url.as_str()); + if let Some(signer) = &self.signer { + println!(" From: {:?}", signer.address()); + } + crate::debug_toolkit::shared::status::print_status_overview(&status); + + Ok(()) + } + + /// `sync-status` — detailed sync status. + async fn cmd_sync_status(&self) -> eyre::Result<()> { + let status: ChainOrchestratorStatus = self.raw("rollupNode_status", ()).await?; + crate::debug_toolkit::shared::status::print_sync_status(&status); + Ok(()) + } + + /// `fcs` — show forkchoice state. + async fn cmd_fcs(&self) -> eyre::Result<()> { + let status: ChainOrchestratorStatus = self.raw("rollupNode_status", ()).await?; + crate::debug_toolkit::shared::status::print_forkchoice(&status); + Ok(()) + } + + /// `block [n|latest]` — show block details. + async fn cmd_block(&self, arg: BlockArg) -> eyre::Result<()> { + let tag = match arg { + BlockArg::Latest => BlockNumberOrTag::Latest, + BlockArg::Number(n) => BlockNumberOrTag::Number(n), + }; + + let block: Option = + self.raw("eth_getBlockByNumber", (tag, false)).await?; + let block = block.ok_or_else(|| eyre::eyre!("Block not found"))?; + + let number = block["number"].as_str().unwrap_or("?"); + let hash = block["hash"].as_str().unwrap_or("?"); + let parent = block["parentHash"].as_str().unwrap_or("?"); + let timestamp = block["timestamp"].as_str().unwrap_or("?"); + let gas_used = block["gasUsed"].as_str().unwrap_or("?"); + let gas_limit = block["gasLimit"].as_str().unwrap_or("?"); + let txs = block["transactions"].as_array(); + + println!("{}", format!("Block {}", number).bold()); + println!(" Hash: {}", hash); + println!(" Parent: {}", parent); + println!(" Timestamp: {}", timestamp); + println!(" Gas Used: {}", gas_used); + println!(" Gas Limit: {}", gas_limit); + + if let Some(txs) = txs { + println!(" Txs: {}", txs.len()); + for (i, tx) in txs.iter().enumerate() { + let tx_hash = tx.as_str().or_else(|| tx["hash"].as_str()).unwrap_or("?"); + println!(" [{}] hash={}", i, tx_hash); + } + } + + Ok(()) + } + + /// `blocks ` — list blocks in a range. + async fn cmd_blocks(&self, from: u64, to: u64) -> eyre::Result<()> { + println!("{}", format!("Blocks {} to {}:", from, to).bold()); + for n in from..=to { + let tag = BlockNumberOrTag::Number(n); + let block: Option = + self.raw("eth_getBlockByNumber", (tag, false)).await?; + if let Some(block) = block { + let hash = block["hash"].as_str().unwrap_or("?"); + let gas = block["gasUsed"].as_str().unwrap_or("?"); + let tx_count = block["transactions"].as_array().map(|a| a.len()).unwrap_or(0); + println!(" #{}: {} txs, gas: {}, hash: {:.12}...", n, tx_count, gas, hash); + } else { + println!(" #{}: {}", n, "not found".dimmed()); + } + } + Ok(()) + } + + /// `l1 status` / `l1 messages` — L1-related queries. + async fn cmd_l1(&self, cmd: L1Command) -> eyre::Result<()> { + match cmd { + L1Command::Status => { + let status: ChainOrchestratorStatus = self.raw("rollupNode_status", ()).await?; + println!("{}", "L1 Status:".bold()); + println!( + " Synced: {}", + if status.l1.status.is_synced() { "true".green() } else { "false".red() } + ); + println!(" L1 Head: #{}", status.l1.latest); + println!(" L1 Final: #{}", status.l1.finalized); + println!(" Processed: #{}", status.l1.processed); + } + L1Command::Messages => { + let msg: Option = + self.raw("rollupNode_getL1MessageByIndex", [0u64]).await?; + println!("{}", "L1 Message Queue (index 0):".bold()); + match msg { + Some(m) => println!("{}", serde_json::to_string_pretty(&m)?), + None => println!(" {}", "No message at index 0".dimmed()), + } + println!( + "{}", + "Hint: use 'rpc rollupNode_getL1MessageByIndex []' for specific indices" + .dimmed() + ); + } + L1Command::Sync | L1Command::Block(_) | L1Command::Reorg(_) => { + println!( + "{}", + "l1 sync/block/reorg are only available in spawn mode (mock L1).".yellow() + ); + } + L1Command::Send { .. } => { + println!( + "{}", + "l1 send is only available in spawn mode. Use cast or a wallet to bridge." + .yellow() + ); + } + } + Ok(()) + } + + /// `tx pending` / `tx send` / `tx inject`. + async fn cmd_tx(&self, cmd: TxCommand) -> eyre::Result<()> { + match cmd { + TxCommand::Pending => { + let result: serde_json::Value = self.raw("txpool_content", ()).await?; + println!("{}", "Pending Transactions:".bold()); + println!("{}", serde_json::to_string_pretty(&result)?); + } + TxCommand::Send { to, value, from: _ } => { + let signer = self.signer.as_ref().ok_or_else(|| { + eyre::eyre!("No signer configured. Start with --private-key .") + })?; + let from_address = signer.address(); + + let chain_id: serde_json::Value = self.raw("eth_chainId", ()).await?; + let chain_id: u64 = u64::from_str_radix( + chain_id.as_str().unwrap_or("0x1").trim_start_matches("0x"), + 16, + ) + .unwrap_or(1); + + let nonce_val: serde_json::Value = + self.raw("eth_getTransactionCount", (from_address, "latest")).await?; + let nonce: u64 = u64::from_str_radix( + nonce_val.as_str().unwrap_or("0x0").trim_start_matches("0x"), + 16, + ) + .unwrap_or(0); + + let latest: serde_json::Value = + self.raw("eth_getBlockByNumber", ("latest", false)).await?; + let base_fee = latest["baseFeePerGas"] + .as_str() + .and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok()) + .unwrap_or(1_000_000_000); + let base_fee_u128 = base_fee as u128; + // Keep priority tip conservative on low-fee chains and always satisfy: + // max_fee_per_gas >= max_priority_fee_per_gas. + let max_priority_fee_per_gas = (base_fee_u128 / 2).max(1); + let max_fee_per_gas = (base_fee_u128 * 2).max(max_priority_fee_per_gas); + let gas_limit = match self + .raw::( + "eth_estimateGas", + [serde_json::json!({ + "from": format!("{:#x}", from_address), + "to": format!("{:#x}", to), + "value": format!("0x{value:x}"), + })], + ) + .await + { + Ok(v) => v + .as_str() + .and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok()) + // Add a small safety buffer on top of estimate. + .map(|g| g.saturating_mul(12) / 10) + .filter(|g| *g > 0) + .unwrap_or(21_000), + Err(e) => { + println!( + "{}", + format!( + "Warning: eth_estimateGas failed ({}), falling back to 21000", + e + ) + .yellow() + ); + 21_000 + } + }; + + let mut tx = TxEip1559 { + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to: TxKind::Call(to), + value, + access_list: Default::default(), + input: Default::default(), + }; + + let signature = signer.sign_transaction_sync(&mut tx)?; + let signed = tx.into_signed(signature); + let raw_tx = alloy_primitives::hex::encode_prefixed(signed.encoded_2718()); + + let tx_hash: serde_json::Value = + self.raw("eth_sendRawTransaction", [raw_tx]).await?; + let tx_hash_str = + tx_hash.as_str().map(ToOwned::to_owned).unwrap_or_else(|| tx_hash.to_string()); + crate::debug_toolkit::shared::output::print_tx_sent( + &tx_hash_str, + &format!("{:?}", from_address), + &format!("{:?}", to), + value, + false, + ); + } + TxCommand::Inject(bytes) => { + let hex = alloy_primitives::hex::encode_prefixed(&bytes); + let tx_hash: serde_json::Value = self.raw("eth_sendRawTransaction", [hex]).await?; + let tx_hash_str = + tx_hash.as_str().map(ToOwned::to_owned).unwrap_or_else(|| tx_hash.to_string()); + crate::debug_toolkit::shared::output::print_tx_injected(&tx_hash_str); + } + } + Ok(()) + } + + /// `peers` / `peers connect`. + async fn cmd_peers(&self, cmd: PeersCommand) -> eyre::Result<()> { + match cmd { + PeersCommand::List => { + let peers: serde_json::Value = self.raw("admin_peers", ()).await?; + println!("{}", "Connected Peers:".bold()); + println!("{}", serde_json::to_string_pretty(&peers)?); + + println!(); + let node_info: serde_json::Value = self.raw("admin_nodeInfo", ()).await?; + println!("{}", "Local Node Info:".bold()); + println!("{}", serde_json::to_string_pretty(&node_info)?); + } + PeersCommand::Connect(enode_url) => { + let result: bool = self.raw("admin_addPeer", [enode_url.as_str()]).await?; + if result { + println!("{}", format!("Peer add request sent: {}", enode_url).green()); + } else { + println!("{}", "admin_addPeer returned false".yellow()); + } + } + } + Ok(()) + } + + /// `events on` / `events off` — toggle head-block polling. + fn cmd_events(&mut self, cmd: EventsCommand) -> eyre::Result<()> { + match cmd { + EventsCommand::On => { + self.events_enabled = true; + println!("{}", "Head-block polling enabled (2s interval)".green()); + } + EventsCommand::Off => { + self.events_enabled = false; + println!("{}", "Head-block polling disabled".yellow()); + } + EventsCommand::Filter(_) | EventsCommand::History(_) => { + println!("{}", "events filter/history are only available in spawn mode.".yellow()); + } + } + Ok(()) + } + + /// `admin enable-seq` / `admin disable-seq` / `admin revert `. + async fn cmd_admin(&self, cmd: AdminCommand) -> eyre::Result<()> { + match cmd { + AdminCommand::EnableSequencing => { + let result: bool = + self.raw("rollupNodeAdmin_enableAutomaticSequencing", ()).await?; + crate::debug_toolkit::shared::output::print_admin_enable_result(result); + } + AdminCommand::DisableSequencing => { + let result: bool = + self.raw("rollupNodeAdmin_disableAutomaticSequencing", ()).await?; + crate::debug_toolkit::shared::output::print_admin_disable_result(result); + } + AdminCommand::RevertToL1Block(block_number) => { + crate::debug_toolkit::shared::output::print_admin_revert_start(block_number); + let result: bool = + self.raw("rollupNodeAdmin_revertToL1Block", [block_number]).await?; + crate::debug_toolkit::shared::output::print_admin_revert_result( + block_number, + result, + ); + } + } + Ok(()) + } + + /// `rpc [params]` — call any JSON-RPC method and pretty-print the result. + async fn cmd_rpc(&self, method: &str, params: Option<&str>) -> eyre::Result<()> { + let pretty = + crate::debug_toolkit::shared::rpc::raw_value(&self.provider, method, params).await?; + crate::debug_toolkit::shared::output::print_pretty_json(&pretty) + } + + /// `logs` — show log file path. + fn cmd_logs(&self) -> eyre::Result<()> { + crate::debug_toolkit::shared::output::print_log_file(&self.log_path); + Ok(()) + } +} diff --git a/crates/node/src/debug_toolkit/repl/local.rs b/crates/node/src/debug_toolkit/repl/local.rs new file mode 100644 index 00000000..dd10a81f --- /dev/null +++ b/crates/node/src/debug_toolkit/repl/local.rs @@ -0,0 +1,975 @@ +/// Interactive REPL for debugging rollup nodes. +use crate::debug_toolkit::{ + actions::ActionRegistry, + commands::{ + print_help, AdminCommand, BlockArg, Command, EventsCommand, L1Command, PeersCommand, + RunCommand, TxCommand, WalletCommand, + }, + event::stream::EventStreamState, +}; +use crate::test_utils::{fixture::NodeType, TestFixture}; +use alloy_consensus::{SignableTransaction, TxEip1559, TxLegacy}; +use alloy_eips::{eip2718::Encodable2718, BlockNumberOrTag}; +use alloy_network::{TransactionResponse, TxSignerSync}; +use alloy_primitives::{address, Address, Bytes, TxKind, U256}; +use alloy_provider::ProviderBuilder; +use alloy_rpc_types_eth::TransactionRequest; +use alloy_signer_local::PrivateKeySigner; +use alloy_sol_types::{sol, SolCall}; +use colored::Colorize; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; +use futures::StreamExt; +use reth_network::PeersInfo; +use reth_network_api::Peers; +use reth_network_peers::NodeRecord; +use reth_rpc_api::EthApiServer; +use reth_transaction_pool::TransactionPool; +use scroll_alloy_network::Scroll; +use std::{io::Write, path::PathBuf, str::FromStr, time::Duration}; + +// L1 contract addresses +const L1_MESSENGER_ADDRESS: Address = address!("8A791620dd6260079BF849Dc5567aDC3F2FdC318"); +const L1_MESSAGE_QUEUE_ADDRESS: Address = address!("Dc64a140Aa3E981100a9becA4E685f962f0cF6C9"); + +// L1 contract interfaces +sol! { + /// L1 Message Queue contract interface + interface IL1MessageQueue { + function nextCrossDomainMessageIndex() external view returns (uint256); + } + + /// L1 Messenger contract interface + interface IL1ScrollMessenger { + function sendMessage( + address _to, + uint256 _value, + bytes memory _message, + uint256 _gasLimit + ) external payable; + } +} + +/// Interactive REPL for debugging rollup nodes. +pub struct DebugRepl { + /// The test fixture containing nodes. + fixture: TestFixture, + /// Whether the REPL is running. + running: bool, + /// Current active node index. + active_node: usize, + /// Event stream state per node. + event_streams: Vec, + /// Registry of custom actions. + action_registry: ActionRegistry, + /// Path to the log file. + log_path: Option, +} + +impl std::fmt::Debug for DebugRepl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DebugRepl") + .field("running", &self.running) + .field("active_node", &self.active_node) + .field("event_streams", &self.event_streams) + .field("action_registry", &"ActionRegistry { ... }") + .field("log_path", &self.log_path) + .finish_non_exhaustive() + } +} + +impl DebugRepl { + /// Create a new REPL with the given fixture. + pub fn new(fixture: TestFixture) -> Self { + // Create one event stream per node, enabled by default + let event_streams = (0..fixture.nodes.len()) + .map(|_| { + let mut es = EventStreamState::new(); + es.enable(); + es + }) + .collect(); + + Self { + fixture, + running: false, + active_node: 0, + event_streams, + action_registry: ActionRegistry::new(), + log_path: None, + } + } + + /// Create a new REPL with a custom action registry. + pub fn with_action_registry(fixture: TestFixture, action_registry: ActionRegistry) -> Self { + let event_streams = (0..fixture.nodes.len()) + .map(|_| { + let mut es = EventStreamState::new(); + es.enable(); + es + }) + .collect(); + + Self { + fixture, + running: false, + active_node: 0, + event_streams, + action_registry, + log_path: None, + } + } + + /// Set the log file path. + pub fn set_log_path(&mut self, path: PathBuf) { + self.log_path = Some(path); + } + + /// Get mutable access to the action registry to register custom actions. + pub const fn action_registry_mut(&mut self) -> &mut ActionRegistry { + &mut self.action_registry + } + + /// Run the REPL loop. + pub async fn run(&mut self) -> eyre::Result<()> { + self.running = true; + + let _guard = super::terminal::RawModeGuard::new()?; + + // Print welcome message and initial status + // Disable raw mode temporarily so println! works correctly + let _ = disable_raw_mode(); + println!(); + println!("{}", "Scroll Debug Toolkit".bold().cyan()); + println!("Type 'help' for available commands, 'exit' to quit."); + println!(); + + // Show initial status + self.cmd_status().await?; + + // Re-enable raw mode for input handling + let _ = enable_raw_mode(); + + // Current input line buffer + let mut input_buffer = String::new(); + let mut stdout = std::io::stdout(); + + // Print initial prompt + print!("{}", self.get_prompt()); + let _ = stdout.flush(); + + while self.running { + // Poll for events and input + tokio::select! { + biased; + + // Check for events from the node + Some(event) = self.fixture.nodes[self.active_node].chain_orchestrator_rx.next() => { + // Display if streaming is enabled + if let Some(formatted) = self.event_streams[self.active_node].record_event(event) { + // Clear current line, print event, reprint prompt with input buffer + print!("\r\x1b[K{}\r\n{}{}", formatted, self.get_prompt(), input_buffer); + let _ = stdout.flush(); + } + } + + // Check for keyboard input (non-blocking) + _ = tokio::time::sleep(Duration::from_millis(50)) => { + match super::terminal::poll_keyboard(&mut input_buffer, &self.get_prompt())? { + super::terminal::InputAction::Command(line) => { + let _ = disable_raw_mode(); + if let Err(e) = self.execute_command(&line).await { + println!("{}: {}", "Error".red(), e); + } + let _ = enable_raw_mode(); + if self.running { + print!("{}", self.get_prompt()); + let _ = stdout.flush(); + } + } + super::terminal::InputAction::Quit => self.running = false, + super::terminal::InputAction::None => {} + } + } + } + } + + print!("Goodbye!\r\n"); + Ok(()) + } + + /// Get the REPL prompt. + fn get_prompt(&self) -> String { + let node_type = match self.fixture.nodes[self.active_node].typ { + NodeType::Sequencer => "seq", + NodeType::Follower => "fol", + }; + format!("{} [{}:{}]> ", "scroll-debug".cyan(), node_type, self.active_node) + } + + /// Execute a command. + async fn execute_command(&mut self, input: &str) -> eyre::Result<()> { + let cmd = Command::parse(input); + + match cmd { + Command::Status => self.cmd_status().await, + Command::SyncStatus => self.cmd_sync_status().await, + Command::Block(arg) => self.cmd_block(arg).await, + Command::Blocks { from, to } => self.cmd_blocks(from, to).await, + Command::Fcs => self.cmd_fcs().await, + Command::L1(l1_cmd) => self.cmd_l1(l1_cmd).await, + Command::Build => self.cmd_build().await, + Command::Tx(tx_cmd) => self.cmd_tx(tx_cmd).await, + Command::Wallet(wallet_cmd) => self.cmd_wallet(wallet_cmd).await, + Command::Peers(peers_cmd) => self.cmd_peers(peers_cmd).await, + Command::Events(events_cmd) => self.cmd_events(events_cmd).await, + Command::Run(run_cmd) => self.cmd_run(run_cmd).await, + Command::Node(idx) => self.cmd_switch_node(idx), + Command::Nodes => self.cmd_list_nodes(), + Command::Db => self.cmd_db(), + Command::Logs => self.cmd_logs(), + Command::Admin(admin_cmd) => self.cmd_admin(admin_cmd).await, + Command::Rpc { method, params } => self.cmd_rpc(&method, params.as_deref()).await, + Command::Help => { + print_help(); + Ok(()) + } + Command::Exit => { + self.running = false; + Ok(()) + } + Command::Unknown(s) => { + if !s.is_empty() { + println!("Unknown command: {}. Type 'help' for available commands.", s); + } + Ok(()) + } + } + } + + /// Show node status. + async fn cmd_status(&self) -> eyre::Result<()> { + let node = &self.fixture.nodes[self.active_node]; + let node_type = match node.typ { + NodeType::Sequencer => "Sequencer", + NodeType::Follower => "Follower", + }; + + let status = node.rollup_manager_handle.status().await?; + + println!("{}", format!("=== Node {} ({}) ===", self.active_node, node_type).bold()); + + // Node Info + let db_path = node.node.inner.config.datadir().db().join("scroll.db"); + let http_addr = node.node.inner.rpc_server_handle().http_local_addr(); + println!("{}", "Node:".underline()); + println!(" Database: {}", db_path.display()); + if let Some(addr) = http_addr { + println!(" HTTP RPC: http://{}", addr); + } + crate::debug_toolkit::shared::status::print_status_overview(&status); + + Ok(()) + } + + /// Show detailed sync status (`rollupNode_status` RPC equivalent). + async fn cmd_sync_status(&self) -> eyre::Result<()> { + let node = &self.fixture.nodes[self.active_node]; + let status = node.rollup_manager_handle.status().await?; + crate::debug_toolkit::shared::status::print_sync_status(&status); + Ok(()) + } + + /// Show block details. + async fn cmd_block(&self, arg: BlockArg) -> eyre::Result<()> { + let node = &self.fixture.nodes[self.active_node]; + + let block_id = match arg { + BlockArg::Latest => BlockNumberOrTag::Latest, + BlockArg::Number(n) => BlockNumberOrTag::Number(n), + }; + + let block = node + .node + .rpc + .inner + .eth_api() + .block_by_number(block_id, true) + .await? + .ok_or_else(|| eyre::eyre!("Block not found"))?; + + println!("{}", format!("Block #{}", block.header.number).bold()); + println!(" Hash: {:?}", block.header.hash); + println!(" Parent: {:?}", block.header.parent_hash); + println!(" Timestamp: {}", block.header.timestamp); + println!(" Gas Used: {}", block.header.gas_used); + println!(" Gas Limit: {}", block.header.gas_limit); + println!(" Txs: {}", block.transactions.len()); + + if let Some(txs) = block.transactions.as_transactions() { + for (i, tx) in txs.iter().enumerate() { + println!(" [{}] hash={:?}", i, tx.inner.tx_hash()); + } + } + + Ok(()) + } + + /// List blocks in range. + async fn cmd_blocks(&self, from: u64, to: u64) -> eyre::Result<()> { + let node = &self.fixture.nodes[self.active_node]; + + println!("{}", format!("Blocks {} to {}:", from, to).bold()); + + for n in from..=to { + let block = node + .node + .rpc + .inner + .eth_api() + .block_by_number(BlockNumberOrTag::Number(n), false) + .await?; + + if let Some(block) = block { + println!( + " #{}: {} txs, gas: {}, hash: {:.12}...", + n, + block.transactions.len(), + block.header.gas_used, + format!("{:?}", block.header.hash) + ); + } else { + println!(" #{}: {}", n, "not found".dimmed()); + } + } + + Ok(()) + } + + /// Show forkchoice state. + async fn cmd_fcs(&self) -> eyre::Result<()> { + let node = &self.fixture.nodes[self.active_node]; + let status = node.rollup_manager_handle.status().await?; + crate::debug_toolkit::shared::status::print_forkchoice(&status); + Ok(()) + } + + /// Execute L1 commands. + async fn cmd_l1(&mut self, cmd: L1Command) -> eyre::Result<()> { + match cmd { + L1Command::Status => { + let node = &self.fixture.nodes[self.active_node]; + let status = node.rollup_manager_handle.status().await?; + + println!("{}", "L1 Status:".bold()); + println!( + " Synced: {}", + if status.l1.status.is_synced() { "true".green() } else { "false".red() } + ); + println!(" L1 Head: #{}", status.l1.latest); + println!(" L1 Final: #{}", status.l1.finalized); + } + L1Command::Sync => { + self.fixture.l1().sync().await?; + println!("{}", "L1 synced event sent".green()); + } + L1Command::Block(n) => { + self.fixture.l1().new_block(n).await?; + println!("{}", format!("L1 block {} notification sent", n).green()); + } + L1Command::Reorg(block) => { + self.fixture.l1().reorg_to(block).await?; + println!("{}", format!("L1 reorg to block {} sent", block).green()); + } + L1Command::Messages => { + println!("{}", "L1 Message Queue:".bold()); + + let Some(provider) = &self.fixture.l1_provider else { + println!( + "{}", + "No L1 provider available. Start with --l1-url to enable L1 commands." + .yellow() + ); + return Ok(()); + }; + + // Use sol! generated call type for encoding + let call = IL1MessageQueue::nextCrossDomainMessageIndexCall {}; + let call_request = TransactionRequest::default() + .to(L1_MESSAGE_QUEUE_ADDRESS) + .input(call.abi_encode().into()); + + match provider.call(call_request).await { + Ok(result) => { + // Decode uint256 from result using sol! generated return type + match IL1MessageQueue::nextCrossDomainMessageIndexCall::abi_decode_returns( + &result, + ) { + Ok(index) => { + println!(" Next Message Index: {}", index.to_string().green()); + } + Err(e) => { + println!("{}", format!("Failed to decode response: {}", e).red()); + } + } + } + Err(e) => { + println!("{}", format!("Failed to query message queue: {}", e).red()); + } + } + } + L1Command::Send { to, value } => { + println!("{}", "L1 -> L2 Bridge Transfer:".bold()); + + let Some(provider) = &self.fixture.l1_provider else { + println!( + "{}", + "No L1 provider available. Start with --l1-url to enable L1 commands." + .yellow() + ); + return Ok(()); + }; + + println!(" To: {:?}", to); + println!(" Value: {} wei", value); + println!(); + + // Use sol! generated call type for encoding + let call = IL1ScrollMessenger::sendMessageCall { + _to: to, + _value: value, + _message: Bytes::new(), + _gasLimit: U256::from(200000u64), + }; + let calldata = call.abi_encode(); + + // Use the default private key for L1 transactions + let private_key = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; + let signer: PrivateKeySigner = private_key.parse().expect("valid private key"); + let from_address = signer.address(); + + // Get chain ID, nonce from L1 + let chain_id = provider.get_chain_id().await?; + let nonce = provider.get_transaction_count(from_address).await?; + + // Build a legacy transaction (for compatibility with local L1) + // Use fixed 0.1 gwei gas price like the cast command + let mut tx = TxLegacy { + chain_id: Some(chain_id), + nonce, + gas_price: 100_000_000, // 0.1 gwei + gas_limit: 200_000, + to: TxKind::Call(L1_MESSENGER_ADDRESS), + value, + input: calldata.into(), + }; + + // Sign the transaction + let signature = signer.sign_transaction_sync(&mut tx)?; + let signed = tx.into_signed(signature); + let raw_tx = signed.encoded_2718(); + + // Send the transaction and wait for receipt + println!("{}", "Sending transaction...".dimmed()); + match provider.send_raw_transaction(&raw_tx).await { + Ok(pending) => { + let tx_hash = *pending.tx_hash(); + println!("{}", "Transaction sent!".green()); + println!(" Hash: {:?}", tx_hash); + println!(" From: {:?}", from_address); + println!(); + println!("{}", "Waiting for receipt...".dimmed()); + match pending.get_receipt().await { + Ok(receipt) => { + let status_str = if receipt.status() { + "Success".green() + } else { + "Failed".red() + }; + println!(" Status: {}", status_str); + println!(" Block: #{}", receipt.block_number.unwrap_or(0)); + println!(); + println!( + "{}", + "The L1 message will be included in L2 after L1 block finalization." + .dimmed() + ); + } + Err(e) => { + println!("{}", format!("Failed to get receipt: {}", e).yellow()); + } + } + } + Err(e) => { + println!("{}", format!("Failed to send transaction: {}", e).red()); + } + } + } + } + Ok(()) + } + + /// Build a new block. + async fn cmd_build(&self) -> eyre::Result<()> { + if !self.fixture.nodes[self.active_node].is_sequencer() { + println!("{}", "Error: build command requires sequencer node".red()); + return Ok(()); + } + + let handle = &self.fixture.nodes[self.active_node].rollup_manager_handle; + + // Check if L1 is synced + let status = handle.status().await?; + if !status.l1.status.is_synced() { + println!("{}", "Error: L1 is not synced".red()); + println!( + "{}", + "Hint: Run 'l1 sync' to mark the mock L1 as synced before building blocks".yellow() + ); + return Ok(()); + } + + // Trigger block building - events will be displayed through normal event stream + handle.build_block(); + println!("{}", "Block build triggered!".green()); + + Ok(()) + } + + /// Execute transaction commands. + async fn cmd_tx(&mut self, cmd: TxCommand) -> eyre::Result<()> { + match cmd { + TxCommand::Pending => { + let node = &self.fixture.nodes[self.active_node]; + let pending_txs = node.node.inner.pool.pooled_transactions(); + + if pending_txs.is_empty() { + println!("{}", "No pending transactions".dimmed()); + } else { + println!("{}", format!("Pending Transactions ({}):", pending_txs.len()).bold()); + for (i, tx) in pending_txs.iter().enumerate() { + let hash = tx.hash(); + let from = tx.sender(); + let nonce = tx.nonce(); + let gas_price = tx.max_fee_per_gas(); + println!( + " [{}] hash={:.16}... from={:.12}... nonce={} gas_price={}", + i, + format!("{:?}", hash), + format!("{:?}", from), + nonce, + gas_price + ); + } + } + } + TxCommand::Send { to, value, from } => { + // Get wallet info + let mut wallet = self.fixture.wallet.lock().await; + let chain_id = wallet.chain_id; + + // If a wallet index is specified, use that wallet from wallet_gen() + let (signer, nonce, from_address) = if let Some(idx) = from { + let wallets = wallet.wallet_gen(); + if idx >= wallets.len() { + println!( + "{}", + format!( + "Invalid wallet index {}. Valid range: 0-{}", + idx, + wallets.len() - 1 + ) + .red() + ); + return Ok(()); + } + let signer = wallets[idx].clone(); + let address = signer.address(); + + // Get the nonce from the chain for this wallet + let node = &self.fixture.nodes[self.active_node]; + let nonce = node + .node + .rpc + .inner + .eth_api() + .transaction_count(address, Some(BlockNumberOrTag::Latest.into())) + .await? + .to::(); + + (signer, nonce, address) + } else { + // Use the default wallet + let signer = wallet.inner.clone(); + let nonce = wallet.inner_nonce; + let address = signer.address(); + + // Update nonce for the default wallet + wallet.inner_nonce += 1; + + (signer, nonce, address) + }; + drop(wallet); + + // Build an EIP-1559 transaction + let mut tx = TxEip1559 { + chain_id, + nonce, + gas_limit: 21000, + max_fee_per_gas: 1_000_000_000, // 1 gwei + max_priority_fee_per_gas: 1_000_000_000, // 1 gwei + to: TxKind::Call(to), + value, + access_list: Default::default(), + input: Default::default(), + }; + + // Sign the transaction + let signature = signer.sign_transaction_sync(&mut tx)?; + let signed = tx.into_signed(signature); + + // Encode as raw bytes (EIP-2718 envelope) + let raw_tx = alloy_primitives::Bytes::from(signed.encoded_2718()); + + // Inject the transaction + let node = &self.fixture.nodes[self.active_node]; + let tx_hash = node.node.rpc.inject_tx(raw_tx.clone()).await?; + + crate::debug_toolkit::shared::output::print_tx_sent( + &format!("{:?}", tx_hash), + &format!("{:?}", from_address), + &format!("{:?}", to), + value, + true, + ); + } + TxCommand::Inject(bytes) => { + let tx_hash = self.fixture.inject_tx_on(self.active_node, bytes.clone()).await?; + crate::debug_toolkit::shared::output::print_tx_injected(&format!("{:?}", tx_hash)); + } + } + Ok(()) + } + + /// Execute wallet commands. + async fn cmd_wallet(&self, cmd: WalletCommand) -> eyre::Result<()> { + match cmd { + WalletCommand::Info => { + let wallet = self.fixture.wallet.lock().await; + let address = wallet.inner.address(); + let chain_id = wallet.chain_id; + let nonce = wallet.inner_nonce; + drop(wallet); + + // Get balance from the node + let node = &self.fixture.nodes[self.active_node]; + let balance = node + .node + .rpc + .inner + .eth_api() + .balance(address, Some(BlockNumberOrTag::Latest.into())) + .await?; + + println!("{}", "Wallet Info:".bold()); + println!(" Address: {:?}", address); + println!(" Chain ID: {}", chain_id); + println!(" Nonce: {}", nonce); + println!( + " Balance: {} wei ({:.6} ETH)", + balance, + balance.to::() as f64 / 1e18 + ); + } + WalletCommand::Gen => { + let wallet = self.fixture.wallet.lock().await; + let wallets = wallet.wallet_gen(); + let chain_id = wallet.chain_id; + drop(wallet); + + println!("{}", format!("Generated Wallets ({}):", wallets.len()).bold()); + println!(" Chain ID: {}", chain_id); + println!(); + + for (i, signer) in wallets.iter().enumerate() { + let address = signer.address(); + // Get balance for each wallet + let node = &self.fixture.nodes[self.active_node]; + let balance = node + .node + .rpc + .inner + .eth_api() + .balance(address, Some(BlockNumberOrTag::Latest.into())) + .await + .unwrap_or_default(); + + println!(" [{}] {:?}", format!("{}", i).cyan(), address); + println!( + " Balance: {} wei ({:.6} ETH)", + balance, + balance.to::() as f64 / 1e18 + ); + } + } + } + Ok(()) + } + + /// Execute peer commands. + async fn cmd_peers(&self, cmd: PeersCommand) -> eyre::Result<()> { + let node = &self.fixture.nodes[self.active_node]; + let network_handle = node.rollup_manager_handle.get_network_handle().await?; + + match cmd { + PeersCommand::List => { + // Get this node's info + let local_record = network_handle.local_node_record(); + let peer_count = network_handle.inner().num_connected_peers(); + + println!("{}", "Local Node:".bold()); + println!(" Peer ID: {:?}", local_record.id); + println!(" Enode: {}", local_record); + println!(); + + // Get connected peers + let peers = network_handle + .inner() + .get_all_peers() + .await + .map_err(|e| eyre::eyre!("Failed to get peers: {}", e))?; + + println!("{}", format!("Connected Peers ({}):", peer_count).bold()); + + if peers.is_empty() { + println!(" {}", "No peers connected".dimmed()); + } else { + for peer in &peers { + println!(" {:?}", peer.remote_id); + println!(" Address: {}", peer.remote_addr); + println!(" Client: {}", peer.client_version); + println!(" Enode: {}", peer.enode); + println!(); + } + } + + // Show other nodes in fixture for convenience + if self.fixture.nodes.len() > 1 { + println!("{}", "Other Nodes in Fixture:".bold()); + for (i, other_node) in self.fixture.nodes.iter().enumerate() { + if i == self.active_node { + continue; + } + let other_handle = + other_node.rollup_manager_handle.get_network_handle().await?; + let other_record = other_handle.local_node_record(); + let node_type = match other_node.typ { + NodeType::Sequencer => "Sequencer", + NodeType::Follower => "Follower", + }; + println!(" [{}] {} - {}", i, node_type, other_record); + } + } + } + PeersCommand::Connect(enode_url) => { + // Parse the enode URL + match NodeRecord::from_str(&enode_url) { + Ok(record) => { + network_handle.inner().add_peer(record.id, record.tcp_addr()); + println!("{}", format!("Connecting to peer: {:?}", record.id).green()); + println!(" Address: {}", record.tcp_addr()); + println!("{}", "Note: Use 'peers' to check connection status".dimmed()); + } + Err(e) => { + println!("{}", format!("Invalid enode URL: {}", e).red()); + println!("Expected format: enode://@:"); + } + } + } + } + Ok(()) + } + + /// Execute events commands. + async fn cmd_events(&mut self, cmd: EventsCommand) -> eyre::Result<()> { + let event_stream = &mut self.event_streams[self.active_node]; + match cmd { + EventsCommand::On => { + event_stream.enable(); + println!("{}", "Event stream enabled".green()); + } + EventsCommand::Off => { + event_stream.disable(); + println!("{}", "Event stream disabled".yellow()); + } + EventsCommand::Filter(pattern) => { + event_stream.set_filter(pattern.clone()); + if let Some(p) = pattern { + println!("{}", format!("Event filter set: {}", p).green()); + } else { + println!("{}", "Event filter cleared".yellow()); + } + } + EventsCommand::History(count) => { + let history = event_stream.get_history(count); + if history.is_empty() { + println!("{}", "No events in history".dimmed()); + } else { + println!("{}", format!("Last {} events:", history.len()).bold()); + for (ago, event) in history { + let formatted = event_stream.format_event(event); + println!(" [{:?} ago] {}", ago, formatted); + } + } + } + } + Ok(()) + } + + /// Execute custom actions. + async fn cmd_run(&mut self, cmd: RunCommand) -> eyre::Result<()> { + match cmd { + RunCommand::List => { + println!("{}", "Available Actions:".bold()); + println!(); + + let actions: Vec<_> = self.action_registry.list().collect(); + if actions.is_empty() { + println!("{}", " No actions registered".dimmed()); + println!(); + println!( + "{}", + "To add actions, implement the Action trait and register in ActionRegistry" + .dimmed() + ); + } else { + for action in actions { + println!(" {}", action.name().cyan()); + println!(" {}", action.description()); + if let Some(usage) = action.usage() { + println!(" Usage: {}", usage.dimmed()); + } + println!(); + } + } + } + RunCommand::Execute { name, args } => { + if let Some(action) = self.action_registry.get(&name) { + println!("{}", format!("Running action: {}", action.name()).cyan().bold()); + println!(); + + // Execute the action with mutable access to fixture + action.execute(&mut self.fixture, &args).await?; + } else { + println!("{}", format!("Unknown action: {}", name).red()); + println!("{}", "Use 'run list' to see available actions".dimmed()); + } + } + } + Ok(()) + } + + /// Switch to a different node. + fn cmd_switch_node(&mut self, idx: usize) -> eyre::Result<()> { + if idx >= self.fixture.nodes.len() { + println!( + "{}", + format!("Invalid node index. Valid range: 0-{}", self.fixture.nodes.len() - 1) + .red() + ); + } else { + self.active_node = idx; + let node_type = match self.fixture.nodes[idx].typ { + NodeType::Sequencer => "Sequencer", + NodeType::Follower => "Follower", + }; + println!("{}", format!("Switched to node {} ({})", idx, node_type).green()); + } + Ok(()) + } + + /// List all nodes. + fn cmd_list_nodes(&self) -> eyre::Result<()> { + println!("{}", "Nodes:".bold()); + for (i, node) in self.fixture.nodes.iter().enumerate() { + let node_type = match node.typ { + NodeType::Sequencer => "Sequencer".cyan(), + NodeType::Follower => "Follower".normal(), + }; + let marker = if i == self.active_node { " *" } else { "" }; + println!(" [{}] {}{}", i, node_type, marker.green()); + } + Ok(()) + } + + /// Call any JSON-RPC method against the active node and pretty-print result. + async fn cmd_rpc(&self, method: &str, params: Option<&str>) -> eyre::Result<()> { + let node = &self.fixture.nodes[self.active_node]; + let http_addr = node + .node + .inner + .rpc_server_handle() + .http_local_addr() + .ok_or_else(|| eyre::eyre!("HTTP RPC is not available on active node"))?; + let rpc_url = format!("http://{}", http_addr); + + let provider: alloy_provider::RootProvider = ProviderBuilder::default() + .connect(rpc_url.as_str()) + .await + .map_err(|e| eyre::eyre!("Failed to connect to node RPC {}: {}", rpc_url, e))?; + let pretty = + crate::debug_toolkit::shared::rpc::raw_value(&provider, method, params).await?; + crate::debug_toolkit::shared::output::print_pretty_json(&pretty) + } + + /// Handle admin commands directly through the in-process rollup manager handle. + async fn cmd_admin(&self, cmd: AdminCommand) -> eyre::Result<()> { + let handle = &self.fixture.nodes[self.active_node].rollup_manager_handle; + match cmd { + AdminCommand::EnableSequencing => { + let result = handle.enable_automatic_sequencing().await?; + crate::debug_toolkit::shared::output::print_admin_enable_result(result); + } + AdminCommand::DisableSequencing => { + let result = handle.disable_automatic_sequencing().await?; + crate::debug_toolkit::shared::output::print_admin_disable_result(result); + } + AdminCommand::RevertToL1Block(block_number) => { + crate::debug_toolkit::shared::output::print_admin_revert_start(block_number); + let result = handle.revert_to_l1_block(block_number).await?; + crate::debug_toolkit::shared::output::print_admin_revert_result( + block_number, + result, + ); + } + } + Ok(()) + } + + /// Show database path and access command. + fn cmd_db(&self) -> eyre::Result<()> { + let node = &self.fixture.nodes[self.active_node]; + let db_dir = node.node.inner.config.datadir().db(); + let db_path = db_dir.join("scroll.db"); + + println!("{}", "Database Info:".bold()); + println!(" Path: {}", db_path.display()); + println!(); + println!("{}", "Access from another terminal:".underline()); + println!(" sqlite3 {}", db_path.display()); + println!(); + println!("{}", "Useful queries:".dimmed()); + println!(" .tables -- List all tables"); + println!(" .schema
-- Show table schema"); + println!(" SELECT * FROM metadata; -- View metadata"); + println!(" SELECT * FROM l2_block ORDER BY number DESC LIMIT 10;"); + + Ok(()) + } + + /// Show log file path and tail command. + fn cmd_logs(&self) -> eyre::Result<()> { + crate::debug_toolkit::shared::output::print_log_file(&self.log_path); + Ok(()) + } +} diff --git a/crates/node/src/debug_toolkit/repl/mod.rs b/crates/node/src/debug_toolkit/repl/mod.rs new file mode 100644 index 00000000..aa61a0bf --- /dev/null +++ b/crates/node/src/debug_toolkit/repl/mod.rs @@ -0,0 +1,6 @@ +pub(crate) mod attach; +pub(crate) mod local; +pub(crate) mod terminal; + +pub use attach::AttachRepl; +pub use local::DebugRepl; diff --git a/crates/node/src/debug_toolkit/repl/terminal.rs b/crates/node/src/debug_toolkit/repl/terminal.rs new file mode 100644 index 00000000..ad9caa50 --- /dev/null +++ b/crates/node/src/debug_toolkit/repl/terminal.rs @@ -0,0 +1,81 @@ +//! Shared terminal utilities for the debug REPLs. + +use crossterm::{ + event::{self, Event, KeyCode, KeyModifiers}, + terminal::{disable_raw_mode, enable_raw_mode}, +}; +use std::{io::Write, time::Duration}; + +/// RAII guard: enable raw mode on create, disable on drop. +pub(super) struct RawModeGuard; + +impl RawModeGuard { + pub(super) fn new() -> eyre::Result { + enable_raw_mode()?; + Ok(Self) + } +} + +impl Drop for RawModeGuard { + fn drop(&mut self) { + let _ = disable_raw_mode(); + } +} + +pub(super) enum InputAction { + Command(String), + Quit, + None, +} + +/// Drain pending key events and return the next user action. +pub(super) fn poll_keyboard(input_buffer: &mut String, prompt: &str) -> eyre::Result { + let mut stdout = std::io::stdout(); + + while event::poll(Duration::from_millis(0))? { + if let Event::Key(key_event) = event::read()? { + match key_event.code { + KeyCode::Enter => { + print!("\r\n"); + let _ = stdout.flush(); + let line = input_buffer.trim().to_string(); + input_buffer.clear(); + if !line.is_empty() { + return Ok(InputAction::Command(line)); + } + print!("{}", prompt); + let _ = stdout.flush(); + } + KeyCode::Backspace => { + if !input_buffer.is_empty() { + input_buffer.pop(); + print!("\x08 \x08"); + let _ = stdout.flush(); + } + } + KeyCode::Char(c) => { + if key_event.modifiers.contains(KeyModifiers::CONTROL) && c == 'c' { + print!("\r\nUse 'exit' to quit\r\n{}{}", prompt, input_buffer); + let _ = stdout.flush(); + } else if key_event.modifiers.contains(KeyModifiers::CONTROL) && c == 'd' { + print!("\r\n"); + let _ = stdout.flush(); + return Ok(InputAction::Quit); + } else { + input_buffer.push(c); + print!("{}", c); + let _ = stdout.flush(); + } + } + KeyCode::Esc => { + input_buffer.clear(); + print!("\r\x1b[K{}", prompt); + let _ = stdout.flush(); + } + _ => {} + } + } + } + + Ok(InputAction::None) +} diff --git a/crates/node/src/debug_toolkit/shared/mod.rs b/crates/node/src/debug_toolkit/shared/mod.rs new file mode 100644 index 00000000..3714a96b --- /dev/null +++ b/crates/node/src/debug_toolkit/shared/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod output; +pub(crate) mod rpc; +pub(crate) mod status; diff --git a/crates/node/src/debug_toolkit/shared/output.rs b/crates/node/src/debug_toolkit/shared/output.rs new file mode 100644 index 00000000..2ebfc1b0 --- /dev/null +++ b/crates/node/src/debug_toolkit/shared/output.rs @@ -0,0 +1,71 @@ +//! Shared terminal output helpers for REPL commands. + +use colored::Colorize; +use std::{fmt::Display, path::PathBuf}; + +pub(crate) fn print_admin_enable_result(result: bool) { + if result { + println!("{}", "Automatic sequencing enabled".green()); + } else { + println!("{}", "Enable sequencing returned false".yellow()); + } +} + +pub(crate) fn print_admin_disable_result(result: bool) { + if result { + println!("{}", "Automatic sequencing disabled".yellow()); + } else { + println!("{}", "Disable sequencing returned false".yellow()); + } +} + +pub(crate) fn print_admin_revert_start(block_number: u64) { + println!("{}", format!("Reverting to L1 block {}...", block_number).yellow()); +} + +pub(crate) fn print_admin_revert_result(block_number: u64, result: bool) { + if result { + println!("{}", format!("Reverted to L1 block {}", block_number).green()); + } else { + println!("{}", "Revert returned false".yellow()); + } +} + +pub(crate) fn print_pretty_json(value: &serde_json::Value) -> eyre::Result<()> { + println!("{}", serde_json::to_string_pretty(value)?); + Ok(()) +} + +pub(crate) fn print_log_file(log_path: &Option) { + println!("{}", "Log File:".bold()); + if let Some(path) = log_path { + println!(" Path: {}", path.display()); + println!(); + println!("{}", "View logs in another terminal:".underline()); + println!(" tail -f {}", path.display()); + } else { + println!(" {}", "No log file configured (logs going to stdout)".dimmed()); + } +} + +pub(crate) fn print_tx_sent( + tx_hash: &str, + from: &str, + to: &str, + value_wei: impl Display, + include_build_hint: bool, +) { + println!("{}", "Transaction sent!".green()); + println!(" Hash: {}", tx_hash); + println!(" From: {}", from); + println!(" To: {}", to); + println!(" Value: {} wei", value_wei); + if include_build_hint { + println!("{}", "Note: Run 'build' to include in a block (sequencer mode)".dimmed()); + } +} + +pub(crate) fn print_tx_injected(tx_hash: &str) { + println!("{}", "Transaction injected!".green()); + println!(" Hash: {}", tx_hash); +} diff --git a/crates/node/src/debug_toolkit/shared/rpc.rs b/crates/node/src/debug_toolkit/shared/rpc.rs new file mode 100644 index 00000000..fba00113 --- /dev/null +++ b/crates/node/src/debug_toolkit/shared/rpc.rs @@ -0,0 +1,46 @@ +//! Shared JSON-RPC helpers for debug toolkit REPLs. + +use alloy_provider::{Provider, RootProvider}; +use scroll_alloy_network::Scroll; +use std::borrow::Cow; + +/// Call a typed JSON-RPC method and deserialize into `R`. +pub(crate) async fn raw_typed( + provider: &RootProvider, + method: &'static str, + params: impl serde::Serialize, +) -> eyre::Result { + let raw_params = serde_json::value::to_raw_value(¶ms) + .map_err(|e| eyre::eyre!("Failed to serialize params for {}: {}", method, e))?; + let raw_result = provider + .raw_request_dyn(Cow::Borrowed(method), &raw_params) + .await + .map_err(|e| eyre::eyre!("{}: {}", method, e))?; + serde_json::from_str(raw_result.get()) + .map_err(|e| eyre::eyre!("Failed to deserialize response from {}: {}", method, e)) +} + +/// Call any JSON-RPC method and return the response as a JSON value. +pub(crate) async fn raw_value( + provider: &RootProvider, + method: &str, + params: Option<&str>, +) -> eyre::Result { + let raw_params = match params { + None => serde_json::value::to_raw_value(&())?, + Some(p) => { + let value: serde_json::Value = serde_json::from_str(p) + .unwrap_or_else(|_| serde_json::Value::String(p.to_string())); + let array = + if value.is_array() { value } else { serde_json::Value::Array(vec![value]) }; + serde_json::value::to_raw_value(&array)? + } + }; + + let result = provider + .raw_request_dyn(Cow::Owned(method.to_string()), &raw_params) + .await + .map_err(|e| eyre::eyre!("{}: {}", method, e))?; + serde_json::from_str(result.get()) + .map_err(|e| eyre::eyre!("Failed to deserialize response from {}: {}", method, e)) +} diff --git a/crates/node/src/debug_toolkit/shared/status.rs b/crates/node/src/debug_toolkit/shared/status.rs new file mode 100644 index 00000000..749fff0c --- /dev/null +++ b/crates/node/src/debug_toolkit/shared/status.rs @@ -0,0 +1,102 @@ +//! Shared rendering for rollup node status outputs. + +use colored::Colorize; +use rollup_node_chain_orchestrator::ChainOrchestratorStatus; + +/// Print L2/L1 overview sections used by `status`. +pub(crate) fn print_status_overview(status: &ChainOrchestratorStatus) { + let fcs = &status.l2.fcs; + + println!("{}", "L2:".underline()); + println!( + " Head: #{} ({:.12}...)", + fcs.head_block_info().number.to_string().green(), + format!("{:?}", fcs.head_block_info().hash) + ); + println!( + " Safe: #{} ({:.12}...)", + fcs.safe_block_info().number.to_string().yellow(), + format!("{:?}", fcs.safe_block_info().hash) + ); + println!( + " Finalized: #{} ({:.12}...)", + fcs.finalized_block_info().number.to_string().blue(), + format!("{:?}", fcs.finalized_block_info().hash) + ); + println!( + " Synced: {}", + if status.l2.status.is_synced() { "true".green() } else { "false".red() } + ); + + println!("{}", "L1:".underline()); + println!(" Head: #{}", status.l1.latest.to_string().cyan()); + println!(" Finalized: #{}", status.l1.finalized); + println!(" Processed: #{}", status.l1.processed); + println!( + " Synced: {}", + if status.l1.status.is_synced() { "true".green() } else { "false".red() } + ); +} + +/// Print detailed sync status used by `sync-status`. +pub(crate) fn print_sync_status(status: &ChainOrchestratorStatus) { + println!("{}", "Sync Status:".bold()); + println!(); + println!("{}", "L1 Sync:".underline()); + println!( + " Status: {}", + if status.l1.status.is_synced() { + "SYNCED".green() + } else { + format!("{:?}", status.l1.status).yellow().to_string().into() + } + ); + println!(" Latest: #{}", status.l1.latest.to_string().cyan()); + println!(" Finalized: #{}", status.l1.finalized); + println!(" Processed: #{}", status.l1.processed); + println!(); + + println!("{}", "L2 Sync:".underline()); + println!( + " Status: {}", + if status.l2.status.is_synced() { + "SYNCED".green() + } else { + format!("{:?}", status.l2.status).yellow().to_string().into() + } + ); + println!(); + println!("{}", "Forkchoice:".underline()); + + let fcs = &status.l2.fcs; + println!( + " Head: #{} ({:.12}...)", + fcs.head_block_info().number.to_string().green(), + format!("{:?}", fcs.head_block_info().hash) + ); + println!( + " Safe: #{} ({:.12}...)", + fcs.safe_block_info().number.to_string().yellow(), + format!("{:?}", fcs.safe_block_info().hash) + ); + println!( + " Finalized: #{} ({:.12}...)", + fcs.finalized_block_info().number.to_string().blue(), + format!("{:?}", fcs.finalized_block_info().hash) + ); +} + +/// Print forkchoice section used by `fcs`. +pub(crate) fn print_forkchoice(status: &ChainOrchestratorStatus) { + let fcs = &status.l2.fcs; + println!("{}", "Forkchoice State:".bold()); + println!(" Head:"); + println!(" Number: {}", fcs.head_block_info().number); + println!(" Hash: {:?}", fcs.head_block_info().hash); + println!(" Safe:"); + println!(" Number: {}", fcs.safe_block_info().number); + println!(" Hash: {:?}", fcs.safe_block_info().hash); + println!(" Finalized:"); + println!(" Number: {}", fcs.finalized_block_info().number); + println!(" Hash: {:?}", fcs.finalized_block_info().hash); +} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 8aa24a22..5be56d74 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -10,6 +10,9 @@ pub mod pprof; #[cfg(feature = "test-utils")] pub mod test_utils; +#[cfg(feature = "debug-toolkit")] +pub mod debug_toolkit; + pub use add_ons::*; pub use args::*; pub use builder::network::ScrollNetworkBuilder; diff --git a/crates/node/src/test_utils/fixture.rs b/crates/node/src/test_utils/fixture.rs index f17618ee..e9bf839f 100644 --- a/crates/node/src/test_utils/fixture.rs +++ b/crates/node/src/test_utils/fixture.rs @@ -9,19 +9,24 @@ use crate::{ RollupNodeNetworkArgs, RpcArgs, ScrollRollupNode, ScrollRollupNodeConfig, SequencerArgs, SignerArgs, }; +use alloy_network::Ethereum; +use alloy_provider::Provider; use alloy_eips::BlockNumberOrTag; use alloy_primitives::Address; use alloy_rpc_types_eth::Block; use alloy_signer_local::PrivateKeySigner; use reth_chainspec::EthChainSpec; +use reth_cli::chainspec::ChainSpecParser; use reth_e2e_test_utils::{wallet::Wallet, NodeHelperType, TmpDB}; use reth_eth_wire_types::BasicNetworkPrimitives; use reth_network::NetworkHandle; +use reth_network_peers::TrustedPeer; use reth_node_builder::NodeTypes; use reth_node_types::NodeTypesWithDBAdapter; use reth_provider::providers::BlockchainProvider; -use reth_scroll_chainspec::SCROLL_DEV; +use reth_scroll_chainspec::{ScrollChainSpec, SCROLL_DEV}; +use reth_scroll_cli::ScrollChainSpecParser; use reth_scroll_primitives::ScrollPrimitives; use reth_tasks::TaskManager; use reth_tokio_util::EventStream; @@ -39,8 +44,10 @@ use std::{ }; use tokio::sync::Mutex; +/// L1 provider type for making L1 RPC calls. +pub type L1Provider = Box + Send + Sync>; + /// Main test fixture providing a high-level interface for testing rollup nodes. -#[derive(Debug)] pub struct TestFixture { /// The list of nodes in the test setup. pub nodes: Vec, @@ -48,10 +55,23 @@ pub struct TestFixture { pub wallet: Arc>, /// Chain spec used by the nodes. pub chain_spec: Arc<::ChainSpec>, + /// L1 provider for making L1 RPC calls (if connected to real L1). + pub l1_provider: Option, /// The task manager. Held in order to avoid dropping the node. _tasks: TaskManager, } +impl std::fmt::Debug for TestFixture { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TestFixture") + .field("nodes", &self.nodes) + .field("wallet", &self.wallet) + .field("chain_spec", &self.chain_spec) + .field("l1_provider", &self.l1_provider.as_ref().map(|_| "L1Provider")) + .finish_non_exhaustive() + } +} + /// The network handle to the Scroll network. pub type ScrollNetworkHandle = NetworkHandle>; @@ -156,9 +176,9 @@ impl TestFixture { &mut self, node_index: usize, tx: impl Into, - ) -> eyre::Result<()> { - self.nodes[node_index].node.rpc.inject_tx(tx.into()).await?; - Ok(()) + ) -> eyre::Result { + let tx_hash = self.nodes[node_index].node.rpc.inject_tx(tx.into()).await?; + Ok(tx_hash) } /// Get the current (latest) block from a specific node. @@ -201,13 +221,28 @@ impl TestFixture { } /// Builder for creating test fixtures with a fluent API. -#[derive(Debug)] pub struct TestFixtureBuilder { config: ScrollRollupNodeConfig, num_nodes: usize, chain_spec: Option::ChainSpec>>, is_dev: bool, no_local_transactions_propagation: bool, + bootnodes: Option>, + l1_provider: Option, +} + +impl std::fmt::Debug for TestFixtureBuilder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TestFixtureBuilder") + .field("config", &self.config) + .field("num_nodes", &self.num_nodes) + .field("chain_spec", &self.chain_spec) + .field("is_dev", &self.is_dev) + .field("no_local_transactions_propagation", &self.no_local_transactions_propagation) + .field("bootnodes", &self.bootnodes) + .field("l1_provider", &self.l1_provider.as_ref().map(|_| "L1Provider")) + .finish() + } } impl Default for TestFixtureBuilder { @@ -225,6 +260,8 @@ impl TestFixtureBuilder { chain_spec: None, is_dev: false, no_local_transactions_propagation: false, + bootnodes: None, + l1_provider: None, } } @@ -256,7 +293,7 @@ impl TestFixtureBuilder { } /// Adds a sequencer node to the test with default settings. - pub fn sequencer(mut self) -> Self { + pub const fn sequencer(mut self) -> Self { self.config.sequencer_args.sequencer_enabled = true; self.config.sequencer_args.auto_start = false; self.config.sequencer_args.block_time = 100; @@ -264,12 +301,16 @@ impl TestFixtureBuilder { self.config.sequencer_args.l1_message_inclusion_mode = L1MessageInclusionMode::BlockDepth(0); self.config.sequencer_args.allow_empty_blocks = true; - self.config.database_args.rn_db_path = Some(PathBuf::from("sqlite::memory:")); - self.num_nodes += 1; self } + /// Sets the bootnodes for the test nodes. + pub fn bootnodes(mut self, bootnodes: Vec) -> Self { + self.bootnodes = Some(bootnodes); + self + } + /// Adds `count`s follower nodes to the test. pub const fn followers(mut self, count: usize) -> Self { self.num_nodes += count; @@ -303,6 +344,17 @@ impl TestFixtureBuilder { self } + /// Set the chain by name ("dev", "sepolia", "mainnet") or by file path. + /// + /// This is a convenience method that loads the appropriate chain spec. + /// If the input is a file path (contains '/' or ends with '.json'), it will + /// load the genesis from the file. + pub fn with_chain(mut self, chain: &str) -> eyre::Result { + let chain_spec: Arc = ScrollChainSpecParser::parse(chain)?; + self.chain_spec = Some(chain_spec); + Ok(self) + } + /// Enable dev mode. pub const fn with_dev_mode(mut self, enabled: bool) -> Self { self.is_dev = enabled; @@ -360,9 +412,18 @@ impl TestFixtureBuilder { } /// Use `SystemContract` consensus with the given authorized signer address. - pub const fn with_consensus_system_contract(mut self, authorized_signer: Address) -> Self { + pub const fn with_consensus_system_contract( + mut self, + authorized_signer: Option
, + ) -> Self { self.config.consensus_args.algorithm = ConsensusAlgorithm::SystemContract; - self.config.consensus_args.authorized_signer = Some(authorized_signer); + self.config.consensus_args.authorized_signer = authorized_signer; + self + } + + /// Set the valid signer address for the network. + pub const fn with_network_valid_signer(mut self, address: Option
) -> Self { + self.config.network_args.signer_address = address; self } @@ -419,6 +480,12 @@ impl TestFixtureBuilder { &mut self.config } + /// Set the L1 provider for making L1 RPC calls. + pub fn with_l1_provider(mut self, provider: L1Provider) -> Self { + self.l1_provider = Some(provider); + self + } + /// Build the test fixture. pub async fn build(self) -> eyre::Result { let config = self.config; @@ -430,6 +497,7 @@ impl TestFixtureBuilder { chain_spec.clone(), self.is_dev, self.no_local_transactions_propagation, + self.bootnodes, ) .await?; @@ -470,6 +538,7 @@ impl TestFixtureBuilder { nodes: node_handles, wallet: Arc::new(Mutex::new(wallet)), chain_spec, + l1_provider: self.l1_provider, _tasks, }) } diff --git a/crates/node/src/test_utils/mod.rs b/crates/node/src/test_utils/mod.rs index 62e1cf04..509ea718 100644 --- a/crates/node/src/test_utils/mod.rs +++ b/crates/node/src/test_utils/mod.rs @@ -67,12 +67,14 @@ pub mod l1_helpers; pub mod network_helpers; pub mod tx_helpers; +use alloy_consensus::BlockHeader; // Re-export main types for convenience pub use event_utils::{EventAssertions, EventWaiter}; pub use fixture::{NodeHandle, TestFixture, TestFixtureBuilder}; pub use network_helpers::{ NetworkHelper, NetworkHelperProvider, ReputationChecker, ReputationChecks, }; +use reth_network_peers::TrustedPeer; // Legacy utilities - keep existing functions for backward compatibility use crate::{ @@ -92,12 +94,14 @@ use reth_node_builder::{ NodeHandle as RethNodeHandle, NodeTypes, NodeTypesWithDBAdapter, PayloadAttributesBuilder, PayloadTypes, TreeConfig, }; -use reth_node_core::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs, TxPoolArgs}; +use reth_node_core::args::{ + DiscoveryArgs, NetworkArgs, PayloadBuilderArgs, RpcServerArgs, TxPoolArgs, +}; use reth_provider::providers::BlockchainProvider; use reth_rpc_server_types::RpcModuleSelection; use reth_tasks::TaskManager; use rollup_node_sequencer::L1MessageInclusionMode; -use std::{path::PathBuf, sync::Arc}; +use std::sync::Arc; use tokio::sync::Mutex; use tracing::{span, Level}; @@ -111,6 +115,7 @@ pub async fn setup_engine( chain_spec: Arc<::ChainSpec>, is_dev: bool, no_local_transactions_propagation: bool, + trusted_peers: Option>, ) -> eyre::Result<( Vec< NodeHelperType< @@ -134,6 +139,8 @@ where let network_config = NetworkArgs { discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() }, + trusted_peers: trusted_peers.clone().unwrap_or_default(), + bootnodes: trusted_peers, ..NetworkArgs::default() }; @@ -145,8 +152,13 @@ where if idx != 0 { scroll_node_config.sequencer_args.sequencer_enabled = false; } + let node_config = NodeConfig::new(chain_spec.clone()) .with_network(network_config.clone()) + .with_payload_builder(PayloadBuilderArgs { + gas_limit: Some(chain_spec.genesis_header().gas_limit()), + ..Default::default() + }) .with_unused_ports() .with_rpc( RpcServerArgs::default() @@ -202,7 +214,7 @@ where nodes.push(node); } - Ok((nodes, tasks, Wallet::default().with_chain_id(chain_spec.chain().into()))) + Ok((nodes, tasks, Wallet::new(10).with_chain_id(chain_spec.chain().into()))) } /// Generate a transfer transaction with the given wallet. @@ -256,9 +268,7 @@ pub fn default_sequencer_test_scroll_rollup_node_config() -> ScrollRollupNodeCon ScrollRollupNodeConfig { test: true, network_args: RollupNodeNetworkArgs::default(), - database_args: RollupNodeDatabaseArgs { - rn_db_path: Some(PathBuf::from("sqlite::memory:")), - }, + database_args: RollupNodeDatabaseArgs::default(), l1_provider_args: L1ProviderArgs::default(), engine_driver_args: EngineDriverArgs { sync_at_startup: true }, chain_orchestrator_args: ChainOrchestratorArgs { diff --git a/crates/node/tests/e2e.rs b/crates/node/tests/e2e.rs index 9132598a..6e181895 100644 --- a/crates/node/tests/e2e.rs +++ b/crates/node/tests/e2e.rs @@ -189,7 +189,7 @@ async fn can_penalize_peer_for_invalid_signature() -> eyre::Result<()> { .with_chain_spec(chain_spec) .block_time(0) .allow_empty_blocks(true) - .with_consensus_system_contract(authorized_address) + .with_consensus_system_contract(Some(authorized_address)) .with_signer(authorized_signer.clone()) .payload_building_duration(1000) .build() @@ -296,12 +296,14 @@ async fn can_forward_tx_to_sequencer() -> eyre::Result<()> { // Create the chain spec for scroll mainnet with Euclid v2 activated and a test genesis. let chain_spec = (*SCROLL_DEV).clone(); let (mut sequencer_node, _tasks, _) = - setup_engine(sequencer_node_config, 1, chain_spec.clone(), false, true).await.unwrap(); + setup_engine(sequencer_node_config, 1, chain_spec.clone(), false, true, None) + .await + .unwrap(); let sequencer_url = format!("http://localhost:{}", sequencer_node[0].rpc_url().port().unwrap()); follower_node_config.network_args.sequencer_url = Some(sequencer_url); let (mut follower_node, _tasks, wallet) = - setup_engine(follower_node_config, 1, chain_spec, false, true).await.unwrap(); + setup_engine(follower_node_config, 1, chain_spec, false, true, None).await.unwrap(); let wallet = Arc::new(Mutex::new(wallet)); @@ -463,9 +465,15 @@ async fn can_bridge_blocks() -> eyre::Result<()> { let chain_spec = (*SCROLL_DEV).clone(); // Setup the bridge node and a standard node. - let (mut nodes, tasks, _) = - setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec.clone(), false, false) - .await?; + let (mut nodes, tasks, _) = setup_engine( + default_test_scroll_rollup_node_config(), + 1, + chain_spec.clone(), + false, + false, + None, + ) + .await?; let mut bridge_node = nodes.pop().unwrap(); let bridge_peer_id = bridge_node.network.record().id; let bridge_node_l1_watcher_tx = @@ -564,9 +572,15 @@ async fn shutdown_consolidates_most_recent_batch_on_startup() -> eyre::Result<() let chain_spec = (*SCROLL_MAINNET).clone(); // Launch a node - let (mut nodes, _tasks, _) = - setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec.clone(), false, false) - .await?; + let (mut nodes, _tasks, _) = setup_engine( + default_test_scroll_rollup_node_config(), + 1, + chain_spec.clone(), + false, + false, + None, + ) + .await?; let node = nodes.pop().unwrap(); // Instantiate the rollup node manager. @@ -845,7 +859,7 @@ async fn graceful_shutdown_sets_fcs_to_latest_signed_block_in_db_on_start_up() - // Launch a node let (mut nodes, _tasks, _) = - setup_engine(config.clone(), 1, chain_spec.clone(), false, false).await?; + setup_engine(config.clone(), 1, chain_spec.clone(), false, false, None).await?; let node = nodes.pop().unwrap(); // Instantiate the rollup node manager. @@ -1837,7 +1851,7 @@ async fn signer_rotation() -> eyre::Result<()> { .sequencer() .followers(1) .with_test(false) - .with_consensus_system_contract(signer_1_address) + .with_consensus_system_contract(Some(signer_1_address)) .with_signer(signer_1) .with_sequencer_auto_start(true) .with_eth_scroll_bridge(false) @@ -1848,7 +1862,7 @@ async fn signer_rotation() -> eyre::Result<()> { let mut fixture2 = TestFixture::builder() .sequencer() .with_test(false) - .with_consensus_system_contract(signer_1_address) + .with_consensus_system_contract(Some(signer_1_address)) .with_signer(signer_2) .with_sequencer_auto_start(true) .with_eth_scroll_bridge(false) diff --git a/crates/node/tests/sync.rs b/crates/node/tests/sync.rs index dbd3f87a..a9a54535 100644 --- a/crates/node/tests/sync.rs +++ b/crates/node/tests/sync.rs @@ -81,7 +81,7 @@ async fn test_should_consolidate_to_block_15k() -> eyre::Result<()> { let chain_spec = (*SCROLL_SEPOLIA).clone(); let (mut nodes, _tasks, _) = - setup_engine(node_config, 1, chain_spec.clone(), false, false).await?; + setup_engine(node_config, 1, chain_spec.clone(), false, false, None).await?; let node = nodes.pop().unwrap(); // We perform consolidation up to block 15k. This allows us to capture a batch revert event at @@ -194,6 +194,7 @@ async fn test_should_consolidate_after_optimistic_sync() -> eyre::Result<()> { let mut sequencer = TestFixture::builder() .sequencer() + .with_memory_db() .with_eth_scroll_bridge(true) .with_scroll_wire(true) .auto_start(true) @@ -203,7 +204,7 @@ async fn test_should_consolidate_after_optimistic_sync() -> eyre::Result<()> { .build() .await?; - let mut follower = TestFixture::builder().followers(1).build().await?; + let mut follower = TestFixture::builder().followers(1).with_memory_db().build().await?; // Send a notification to the sequencer node that the L1 watcher is synced. sequencer.l1().sync().await?; @@ -553,7 +554,7 @@ async fn test_chain_orchestrator_l1_reorg() -> eyre::Result<()> { // Create a sequencer node and an unsynced node. let (mut nodes, _tasks, _) = - setup_engine(sequencer_node_config.clone(), 1, chain_spec.clone(), false, false) + setup_engine(sequencer_node_config.clone(), 1, chain_spec.clone(), false, false, None) .await .unwrap(); let mut sequencer = nodes.pop().unwrap(); @@ -563,7 +564,7 @@ async fn test_chain_orchestrator_l1_reorg() -> eyre::Result<()> { sequencer.inner.add_ons_handle.rollup_manager_handle.l1_watcher_mock.clone().unwrap(); let (mut nodes, _tasks, _) = - setup_engine(node_config.clone(), 1, chain_spec.clone(), false, false).await.unwrap(); + setup_engine(node_config.clone(), 1, chain_spec.clone(), false, false, None).await.unwrap(); let mut follower = nodes.pop().unwrap(); let mut follower_events = follower.inner.rollup_manager_handle.get_event_listener().await?; let follower_l1_watcher_tx = diff --git a/crates/sequencer/tests/e2e.rs b/crates/sequencer/tests/e2e.rs index 991d183f..27df2710 100644 --- a/crates/sequencer/tests/e2e.rs +++ b/crates/sequencer/tests/e2e.rs @@ -212,7 +212,7 @@ async fn can_build_blocks_with_delayed_l1_messages() { // setup a test node let (mut nodes, _tasks, wallet) = - setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false) + setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false, None) .await .unwrap(); @@ -337,7 +337,7 @@ async fn can_build_blocks_with_finalized_l1_messages() { let chain_spec = SCROLL_DEV.clone(); // setup a test node let (mut nodes, _tasks, wallet) = - setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false) + setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false, None) .await .unwrap(); let node = nodes.pop().unwrap(); @@ -512,7 +512,7 @@ async fn can_sequence_blocks_with_private_key_file() -> eyre::Result<()> { }; let (nodes, _tasks, wallet) = - setup_engine(rollup_manager_args, 1, chain_spec, false, false).await?; + setup_engine(rollup_manager_args, 1, chain_spec, false, false, None).await?; let wallet = Arc::new(Mutex::new(wallet)); let sequencer_rnm_handle = nodes[0].inner.add_ons_handle.rollup_manager_handle.clone(); @@ -614,7 +614,7 @@ async fn can_sequence_blocks_with_hex_key_file_without_prefix() -> eyre::Result< }; let (nodes, _tasks, wallet) = - setup_engine(rollup_manager_args, 1, chain_spec, false, false).await?; + setup_engine(rollup_manager_args, 1, chain_spec, false, false, None).await?; let wallet = Arc::new(Mutex::new(wallet)); let sequencer_rnm_handle = nodes[0].inner.add_ons_handle.rollup_manager_handle.clone(); @@ -684,6 +684,7 @@ async fn can_build_blocks_and_exit_at_gas_limit() { chain_spec, false, false, + None, ) .await .unwrap(); @@ -770,6 +771,7 @@ async fn can_build_blocks_and_exit_at_time_limit() { chain_spec, false, false, + None, ) .await .unwrap(); @@ -850,7 +852,7 @@ async fn should_limit_l1_message_cumulative_gas() { // setup a test node let chain_spec = SCROLL_DEV.clone(); let (mut nodes, _tasks, wallet) = - setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false) + setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false, None) .await .unwrap(); let node = nodes.pop().unwrap(); @@ -967,7 +969,7 @@ async fn should_not_add_skipped_messages() { // setup a test node let chain_spec = SCROLL_DEV.clone(); let (mut nodes, _tasks, wallet) = - setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false) + setup_engine(default_test_scroll_rollup_node_config(), 1, chain_spec, false, false, None) .await .unwrap(); let node = nodes.pop().unwrap();