Skip to content

Commit 457c4f6

Browse files
committed
feat: add batch-submitter module
1 parent 11ca3ec commit 457c4f6

22 files changed

Lines changed: 1271 additions & 37 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Primary objective in this phase: make sequencer behavior, safety checks, and per
7575
- Storage model is append-oriented; avoid mutable status flags for open/closed entities.
7676
- Open batch/frame are derived by “latest row” convention.
7777
- A frame’s leading direct-input prefix is derivable from `sequenced_l2_txs` plus `frames.safe_block`.
78+
- `direct_inputs` contains only L1 app direct input **bodies**. InputBox payload first byte: **0x00** = direct input (tag stripped, body stored and executed), **0x01** = batch submission (for scheduler, not stored), **others** = discarded (invalid/garbage). The input reader only accepts 0x00-tagged payloads and stores `payload[1..]`.
7879
- Safe cursor/head values should be derived from persisted facts when possible, not duplicated as mutable fields.
7980
- Replay/catch-up must use persisted ordering plus persisted frame fee (`frames.fee`) to mirror inclusion semantics.
8081
- Included user-op identity is constrained by `UNIQUE(sender, nonce)`.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sequencer-core/src/batch.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
use crate::user_op::UserOp;
55
use ssz_derive::{Decode, Encode};
66

7+
/// Tag byte for InputBox payloads that are L1 app direct inputs (e.g. deposits).
8+
/// L1/app must post such inputs as `0x00 || body`. Only these are stored (body only) and executed.
9+
pub const INPUT_TAG_DIRECT_INPUT: u8 = 0x00;
10+
11+
/// Tag byte for InputBox payloads that are batch submissions (for the off-chain scheduler).
12+
/// Batch submitter posts `0x01 || ssz(Batch)`. These are not stored in `direct_inputs`.
13+
pub const INPUT_TAG_BATCH_SUBMISSION: u8 = 0x01;
14+
15+
/// Any other first byte is invalid; the input reader discards such payloads (garbage).
16+
717
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
818
pub struct Batch {
919
pub frames: Vec<Frame>,
@@ -35,3 +45,28 @@ impl WireUserOp {
3545
}
3646
}
3747
}
48+
49+
#[derive(Debug, Clone, PartialEq, Eq)]
50+
pub struct BatchForSubmission {
51+
pub batch_index: u64,
52+
pub created_at_ms: u64,
53+
pub batch: Batch,
54+
}
55+
56+
impl BatchForSubmission {
57+
/// Encode the batch payload for the on-chain scheduler.
58+
///
59+
/// This uses the same SSZ encoding that the canonical scheduler expects when
60+
/// decoding inputs, and prefixes the payload with:
61+
/// - a single tag byte so that batch submissions can be distinguished from other InputBox inputs; and
62+
/// - the batch index as an 8-byte big-endian nonce, so the scheduler can deduplicate.
63+
pub fn encode_for_scheduler(&self) -> Vec<u8> {
64+
let mut out = Vec::new();
65+
// First byte identifies this InputBox payload as a batch submission.
66+
out.push(INPUT_TAG_BATCH_SUBMISSION);
67+
// Next 8 bytes carry the batch index as a big-endian nonce for the scheduler.
68+
out.extend_from_slice(&self.batch_index.to_be_bytes());
69+
out.extend(ssz::Encode::as_ssz_bytes(&self.batch));
70+
out
71+
}
72+
}

sequencer/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ rusqlite = { version = "0.38.0", features = ["bundled"] }
2323
rusqlite_migration = "2.3.0"
2424
alloy-primitives = { version = "1.4.1", features = ["serde", "k256"] }
2525
alloy-sol-types = "1.4.1"
26-
alloy = { version = "1.0", features = ["contract", "network", "reqwest", "rpc-types", "sol-types", "node-bindings"] }
26+
alloy = { version = "1.0", features = ["contract", "network", "reqwest", "rpc-types", "sol-types", "node-bindings", "signer-local", "signers"] }
2727
thiserror = "1"
2828
ssz = { package = "ethereum_ssz", version = "0.10" }
2929
ssz_derive = { package = "ethereum_ssz_derive", version = "0.10" }
3030
clap = { version = "4", features = ["derive", "env"] }
3131
async-recursion = "1"
3232
cartesi-rollups-contracts = "=2.2.0"
33+
async-trait = "0.1"
3334

3435
[dev-dependencies]
3536
futures-util = "0.3"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// (c) Cartesi and individual authors (see AUTHORS)
2+
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)
3+
4+
use std::time::Duration;
5+
6+
use clap::Parser;
7+
8+
/// Batch-submitter-specific options. L1 RPC URL and InputBox address are shared with the
9+
/// input reader and come from the same discovery at startup (see `L1Config` in `config`).
10+
#[derive(Debug, Clone, Parser)]
11+
pub struct BatchSubmitterConfig {
12+
/// Number of blocks to look back from the chain head when reading
13+
/// the latest submitted batch nonce.
14+
#[clap(
15+
long = "seq-batch-submitter-confirmations-depth",
16+
env = "SEQ_BATCH_SUBMITTER_CONFIRMATIONS_DEPTH",
17+
default_value = "5"
18+
)]
19+
pub confirmations_depth: u64,
20+
21+
/// How often the submitter polls storage and L1 for new work.
22+
#[clap(
23+
long = "seq-batch-submitter-idle-poll-interval-ms",
24+
env = "SEQ_BATCH_SUBMITTER_IDLE_POLL_INTERVAL_MS",
25+
default_value = "5000"
26+
)]
27+
pub idle_poll_interval_ms: u64,
28+
29+
/// Maximum number of batches to submit in a single loop iteration.
30+
#[clap(
31+
long = "seq-batch-submitter-max-batches-per-loop",
32+
env = "SEQ_BATCH_SUBMITTER_MAX_BATCHES_PER_LOOP",
33+
default_value = "4"
34+
)]
35+
pub max_batches_per_loop: usize,
36+
}
37+
38+
impl BatchSubmitterConfig {
39+
pub fn idle_poll_interval(&self) -> Duration {
40+
Duration::from_millis(self.idle_poll_interval_ms)
41+
}
42+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// (c) Cartesi and individual authors (see AUTHORS)
2+
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)
3+
4+
//! Batch submitter: posts closed batches to L1 with at-least-once semantics.
5+
//!
6+
//! The batch index is used as the batch nonce (id). The scheduler checks that nonces are
7+
//! strictly increasing and invalidates otherwise, so duplicates are deduplicated at the
8+
//! scheduler level. See `worker` for the wake → read S → compare → submit → sleep loop.
9+
10+
mod config;
11+
mod worker;
12+
13+
pub use config::BatchSubmitterConfig;
14+
pub use worker::BatchSubmitter;

0 commit comments

Comments
 (0)