From 9fd27af0fb8b9a5e93be504e7299c8787e1b5f81 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 21 Jul 2026 17:24:08 +0800 Subject: [PATCH 1/2] feat(settlement): make the time-based settlement trigger a first-class setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the settlement runtime's `idle-flush-secs` knob to `settle-interval-secs` to match what it actually does: a batch settles when it reaches `batch-size` blocks or when this many seconds have elapsed since its first pending block — whichever comes first. The old name (and its doc comment, "settle after this many seconds without a new block") suggested an inactivity timer, but the deadline was never reset by blocks arriving mid-window, so it has always been a max-interval cadence: even a single block produced in the window settles once the interval elapses. The old `idle-flush-secs` key still parses via a serde alias, so existing chain configs keep working. New paused-clock tests drive the worker's run loop end to end and pin the guarantee: a lone pending block settles once the interval elapses, and blocks arriving mid-window do not push the deadline back. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 1 + crates/chain-spec/src/settlement.rs | 11 ++- crates/cli/src/chain_config.rs | 36 ++++++- crates/settlement/Cargo.toml | 2 + crates/settlement/src/config.rs | 8 +- crates/settlement/src/service.rs | 144 ++++++++++++++++++++++++---- docs/tee-deployment.md | 9 +- tests/saya-tee/src/nodes.rs | 4 +- 8 files changed, 181 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 378ce4408..f2e001ce0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8368,6 +8368,7 @@ dependencies = [ "garaga_rs", "hex", "katana-chain-spec", + "katana-db", "katana-genesis", "katana-metrics", "katana-primitives", diff --git a/crates/chain-spec/src/settlement.rs b/crates/chain-spec/src/settlement.rs index 4545b581a..c3a9a825d 100644 --- a/crates/chain-spec/src/settlement.rs +++ b/crates/chain-spec/src/settlement.rs @@ -67,16 +67,19 @@ pub struct SettlementRuntime { #[serde(default = "default_settlement_batch_size")] pub batch_size: usize, - /// Settle a partial batch after this many seconds without a new block. - #[serde(default = "default_settlement_idle_flush_secs")] - pub idle_flush_secs: u64, + /// Maximum seconds between settlements while blocks are pending. + /// + /// A batch is settled when it reaches `batch_size` blocks or when this many + /// seconds have elapsed since its first pending block — whichever comes first. + #[serde(default = "default_settle_interval_secs", alias = "idle-flush-secs")] + pub settle_interval_secs: u64, } fn default_settlement_batch_size() -> usize { 10 } -fn default_settlement_idle_flush_secs() -> u64 { +fn default_settle_interval_secs() -> u64 { 120 } diff --git a/crates/cli/src/chain_config.rs b/crates/cli/src/chain_config.rs index d2ba49887..4f7ce8e63 100644 --- a/crates/cli/src/chain_config.rs +++ b/crates/cli/src/chain_config.rs @@ -448,7 +448,7 @@ mod tests { tee_registry: ContractAddress::from(felt!("0x789")), prover_key: Some("sp1_dummy".to_string()), batch_size: 3, - idle_flush_secs: 7, + settle_interval_secs: 7, }), }; @@ -458,6 +458,40 @@ mod tests { assert_eq!(Some(settlement), read_settlement); } + /// Configs written before the rename still parse: the old `idle-flush-secs` key + /// deserializes into `settle_interval_secs` via its serde alias. + #[test] + fn settlement_interval_old_key_still_parses() { + let config = r#" +kind = "rollup" + +[id] +Id = "0x4b4154414e41" + +[fee-contract] +strk = "0x0" + +[settlement.layer.starknet] +rpc_url = "http://localhost:5050/" +core_contract = "0x0" +block = 0 +proof_kind = "tee" + +[settlement.layer.starknet.id] +Id = "0x4b4154414e41" + +[settlement.runtime] +account-address = "0x123" +account-private-key = "0x456" +tee-registry = "0x789" +idle-flush-secs = 7 +"#; + + let parsed: super::ChainConfigFile = toml::from_str(config).expect("old key parses"); + let runtime = parsed.settlement.expect("settlement present").runtime.expect("runtime"); + assert_eq!(runtime.settle_interval_secs, 7); + } + /// A settlement section with no `[settlement.runtime]` parses with `runtime: None`; a config /// with no `[settlement]` parses to `None`. #[test] diff --git a/crates/settlement/Cargo.toml b/crates/settlement/Cargo.toml index 51b1be54a..55f39d7b5 100644 --- a/crates/settlement/Cargo.toml +++ b/crates/settlement/Cargo.toml @@ -34,6 +34,8 @@ tracing.workspace = true url.workspace = true [dev-dependencies] +# direct table writes to advance the chain head in the settle-interval tests +katana-db.workspace = true katana-genesis.workspace = true # test-util: paused-clock tests of the submission-retry loop (`start_paused`). tokio = { workspace = true, features = [ "test-util" ] } diff --git a/crates/settlement/src/config.rs b/crates/settlement/src/config.rs index e753a34c4..d41084b16 100644 --- a/crates/settlement/src/config.rs +++ b/crates/settlement/src/config.rs @@ -30,8 +30,10 @@ pub struct SettlementConfig { /// Number of blocks settled per `update_state` transaction. pub batch_size: usize, - /// Settle a partial batch after this long without a new block. - pub idle_flush_interval: Duration, + /// Maximum time between settlements while blocks are pending: a batch is settled when it + /// reaches `batch_size` blocks or when this much time has elapsed since its first pending + /// block, whichever comes first. + pub settle_interval: Duration, } /// Proving-system-specific settlement configuration. @@ -81,7 +83,7 @@ impl SettlementConfig { account_address: runtime.account_address, account_private_key: runtime.account_private_key, batch_size: runtime.batch_size, - idle_flush_interval: Duration::from_secs(runtime.idle_flush_secs), + settle_interval: Duration::from_secs(runtime.settle_interval_secs), prover: ProverConfig::Tee { tee_registry: runtime.tee_registry, prover_key: runtime.prover_key.clone(), diff --git a/crates/settlement/src/service.rs b/crates/settlement/src/service.rs index 63998fe83..7178e1eb7 100644 --- a/crates/settlement/src/service.rs +++ b/crates/settlement/src/service.rs @@ -94,7 +94,7 @@ where backend: self.backend.clone(), provider: self.provider.clone(), batch_size: self.config.batch_size.max(1) as u64, - idle_flush_interval: self.config.idle_flush_interval, + settle_interval: self.config.settle_interval, metrics: SettlementMetrics::default(), proof_metrics: SettlementProofMetrics::new_with_labels(&[( "proof_type", @@ -154,7 +154,7 @@ struct Worker

{ backend: Arc, piltover: PiltoverClient, batch_size: u64, - idle_flush_interval: tokio::time::Duration, + settle_interval: tokio::time::Duration, /// Last settled block, from Piltover's `get_state()`. `None` = nothing settled yet. cursor: Option, metrics: SettlementMetrics, @@ -273,7 +273,7 @@ where enum Action { /// Settle this inclusive block range now. Settle { first: BlockNumber, last: BlockNumber }, - /// Blocks are pending but the batch is partial — wait for more blocks or the idle deadline. + /// Blocks are pending but the batch is partial — wait for more blocks or the settle deadline. WaitForBatch, /// Fully caught up — wait for a new block. Idle, @@ -286,7 +286,7 @@ fn next_action( cursor: Option, head: BlockNumber, batch_size: u64, - idle_elapsed: bool, + interval_elapsed: bool, ) -> Action { let next = cursor.map(|c| c + 1).unwrap_or(0); @@ -295,7 +295,7 @@ fn next_action( } let pending = head - next + 1; - if pending >= batch_size || idle_elapsed { + if pending >= batch_size || interval_elapsed { Action::Settle { first: next, last: head.min(next + batch_size - 1) } } else { Action::WaitForBatch @@ -317,7 +317,7 @@ where // shutdown-responsive mid-retry. mut shutdown_rx: oneshot::Receiver<()>, ) { - let mut idle_deadline = Instant::now() + self.idle_flush_interval; + let mut settle_deadline = Instant::now() + self.settle_interval; let mut backoff = RETRY_BACKOFF_MIN; let mut consecutive_failures: u32 = 0; @@ -331,9 +331,9 @@ where } }; - let idle_elapsed = Instant::now() >= idle_deadline; + let interval_elapsed = Instant::now() >= settle_deadline; - match next_action(self.cursor, head, self.batch_size, idle_elapsed) { + match next_action(self.cursor, head, self.batch_size, interval_elapsed) { Action::Settle { first, last } => { let batch_start = Instant::now(); match self.settle_batch(first, last, &mut shutdown_rx).await { @@ -364,7 +364,7 @@ where if let Some(proof) = proof { persist_block_proofs(&self.provider, first, last, proof); } - idle_deadline = Instant::now() + self.idle_flush_interval; + settle_deadline = Instant::now() + self.settle_interval; backoff = RETRY_BACKOFF_MIN; consecutive_failures = 0; // Loop again immediately: drain any remaining backlog. @@ -398,7 +398,7 @@ where Action::WaitForBatch => { tokio::select! { _ = &mut shutdown_rx => break, - _ = tokio::time::sleep_until(idle_deadline) => {} + _ = tokio::time::sleep_until(settle_deadline) => {} r = notify_rx.recv() => match r { // New block mined — re-evaluate. The payload is irrelevant; the // provider is re-read on the next iteration. @@ -422,8 +422,10 @@ where _ = &mut shutdown_rx => break, r = notify_rx.recv() => match r { Ok(_) => { - // First block of a fresh batch window: arm the idle flush timer. - idle_deadline = Instant::now() + self.idle_flush_interval; + // First block of a fresh batch window: the window settles no + // later than `settle_interval` from now, even if the batch + // never fills. Later blocks do not push the deadline back. + settle_deadline = Instant::now() + self.settle_interval; } Err(broadcast::error::RecvError::Lagged(_)) => {} Err(broadcast::error::RecvError::Closed) => { @@ -641,9 +643,9 @@ mod tests { fn nothing_settled() { // Only the genesis block present, batch of 1 → settle block 0 immediately. assert_eq!(next_action(None, 0, 1, false), Action::Settle { first: 0, last: 0 }); - // Only the genesis block, larger batch → wait for more blocks (or the idle flush). + // Only the genesis block, larger batch → wait for more blocks (or the settle deadline). assert_eq!(next_action(None, 0, 10, false), Action::WaitForBatch); - // A few blocks present, batch not yet full → wait unless idle. + // A few blocks present, batch not yet full → wait unless the interval elapsed. assert_eq!(next_action(None, 2, 10, false), Action::WaitForBatch); assert_eq!(next_action(None, 2, 10, true), Action::Settle { first: 0, last: 2 }); } @@ -668,7 +670,7 @@ mod tests { } #[test] - fn idle_elapsed_flushes_partial_batch() { + fn interval_elapsed_settles_partial_batch() { assert_eq!(next_action(Some(2), 4, 10, true), Action::Settle { first: 3, last: 4 }); } @@ -712,15 +714,15 @@ mod tests { /// Counts `prove`/`recover` calls; `prove` fails when `fail` is set. The proof id /// encodes the prove-call count so tests can tell which round produced a payload. - struct CountingBackend { - calls: AtomicUsize, + pub(super) struct CountingBackend { + pub(super) calls: AtomicUsize, recover_calls: AtomicUsize, fail: bool, recover: RecoverBehavior, } impl CountingBackend { - fn new(fail: bool) -> Self { + pub(super) fn new(fail: bool) -> Self { Self { calls: AtomicUsize::new(0), recover_calls: AtomicUsize::new(0), @@ -797,7 +799,7 @@ mod tests { }) } - fn test_worker( + pub(super) fn test_worker( backend: Arc, provider: DbProviderFactory, ) -> Worker { @@ -816,7 +818,7 @@ mod tests { backend: backend.clone(), provider, batch_size: 10, - idle_flush_interval: Duration::from_secs(120), + settle_interval: Duration::from_secs(120), cursor: None, metrics: SettlementMetrics::default(), proof_metrics: SettlementProofMetrics::new_with_labels(&[("proof_type", "mock")]), @@ -936,4 +938,106 @@ mod tests { assert_eq!(backend.calls.load(Ordering::SeqCst), 2); } } + + /// Exercises the run loop's time-based trigger under a paused tokio clock: a partial + /// batch settles once `settle_interval` elapses, measured from when the batch window + /// opens — blocks arriving mid-window must not push the deadline back. + /// + /// The Piltover endpoint is unreachable, so "settled" is observed at the proving layer + /// (a `CountingBackend` prove call) and via the persisted pending-proof range, not a + /// landed transaction. + mod settle_interval { + use std::sync::atomic::Ordering; + use std::sync::Arc; + + use katana_db::abstraction::{Database, DbTx, DbTxMut}; + use katana_db::tables; + use katana_primitives::block::BlockNumber; + use katana_primitives::Felt; + use katana_provider::DbProviderFactory; + use tokio::sync::{broadcast, oneshot}; + use tokio::time::{Duration, Instant}; + + use super::proof_reuse::{test_worker, CountingBackend}; + use crate::service::read_pending_batch_proof; + + /// Advances the local chain head as the block producer would, by recording the + /// block's hash — `latest_number` reads the last `BlockHashes` entry. + fn insert_block(provider: &DbProviderFactory, number: BlockNumber) { + let tx = provider.db().tx_mut().unwrap(); + tx.put::(number, Felt::from(number)).unwrap(); + tx.commit().unwrap(); + } + + /// Polls (in virtual time) until the backend has proven at least `count` batches. + async fn wait_for_prove(backend: &CountingBackend, count: usize) { + for _ in 0..600 { + if backend.calls.load(Ordering::SeqCst) >= count { + return; + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + panic!("prove was never attempted"); + } + + #[tokio::test(start_paused = true)] + async fn partial_batch_settles_once_interval_elapses() { + let provider = DbProviderFactory::new_in_memory(); + insert_block(&provider, 0); + + let backend = Arc::new(CountingBackend::new(false)); + let mut worker = test_worker(backend.clone(), provider.clone()); + worker.settle_interval = Duration::from_secs(60); + + let (notify_tx, notify_rx) = broadcast::channel::<()>(8); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let handle = tokio::spawn(worker.run(notify_rx, shutdown_rx)); + + // One pending block out of a batch of 10: just short of the deadline, + // nothing settles. + tokio::time::sleep(Duration::from_secs(59)).await; + assert_eq!(backend.calls.load(Ordering::SeqCst), 0); + + // Once the interval elapses, the lone pending block is settled. + wait_for_prove(&backend, 1).await; + let pending = read_pending_batch_proof(&provider).unwrap(); + assert_eq!((pending.first, pending.last), (0, 0)); + + drop(notify_tx); + let _ = shutdown_tx.send(()); + handle.await.unwrap(); + } + + #[tokio::test(start_paused = true)] + async fn mid_window_blocks_do_not_postpone_settlement() { + let provider = DbProviderFactory::new_in_memory(); + insert_block(&provider, 0); + + let backend = Arc::new(CountingBackend::new(false)); + let mut worker = test_worker(backend.clone(), provider.clone()); + worker.settle_interval = Duration::from_secs(60); + + let (notify_tx, notify_rx) = broadcast::channel::<()>(8); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let start = Instant::now(); + let handle = tokio::spawn(worker.run(notify_rx, shutdown_rx)); + + // A second block lands 50s into the 60s window. + tokio::time::sleep(Duration::from_secs(50)).await; + assert_eq!(backend.calls.load(Ordering::SeqCst), 0); + insert_block(&provider, 1); + notify_tx.send(()).unwrap(); + + // Both blocks settle at the original deadline (~60s from the window opening), + // not a fresh interval from the second block (110s). + wait_for_prove(&backend, 1).await; + assert!(start.elapsed() < Duration::from_secs(70), "deadline was pushed back"); + let pending = read_pending_batch_proof(&provider).unwrap(); + assert_eq!((pending.first, pending.last), (0, 1)); + + drop(notify_tx); + let _ = shutdown_tx.send(()); + handle.await.unwrap(); + } + } } diff --git a/docs/tee-deployment.md b/docs/tee-deployment.md index 6bcbe2729..1b26b8bb8 100644 --- a/docs/tee-deployment.md +++ b/docs/tee-deployment.md @@ -129,8 +129,9 @@ operator-local half you add by hand: account-address = "" account-private-key = "" tee-registry = "" -batch-size = 1 # blocks per settlement tx; raise for prod -idle-flush-secs = 30 # settle a partial batch after this many idle seconds +batch-size = 1 # blocks per settlement tx; raise for prod +settle-interval-secs = 30 # settle pending blocks at most this many seconds apart + # (batch-size or this, whichever comes first) # prover-key is omitted: with a mock attester no SP1 proving happens. ``` @@ -237,8 +238,8 @@ account-address = "" account-private-key = "" tee-registry = "" prover-key = "" -batch-size = 32 # amortize settlement gas; tune to throughput -idle-flush-secs = 60 +batch-size = 32 # amortize settlement gas; tune to throughput +settle-interval-secs = 60 # max seconds between settlements while blocks are pending ``` Commit `chain-config/` (minus the `[settlement-runtime]` secrets, if you diff --git a/tests/saya-tee/src/nodes.rs b/tests/saya-tee/src/nodes.rs index 730646685..d7343d3e4 100644 --- a/tests/saya-tee/src/nodes.rs +++ b/tests/saya-tee/src/nodes.rs @@ -156,14 +156,14 @@ pub async fn spawn_l3(l2: &L2InProcess, bootstrap: &BootstrapResult) -> L3InProc // Drives the embedded settlement service. `batch_size: 1` settles every block // immediately, so the per-iteration `wait_for_settlement` assertions don't sit - // out the idle-flush window. + // out the settle-interval window. let settlement_runtime = SettlementRuntime { account_address: bootstrap.account_address.into(), account_private_key: bootstrap.account_private_key, tee_registry: bootstrap.tee_registry_address.into(), prover_key: None, batch_size: 1, - idle_flush_secs: 120, + settle_interval_secs: 120, }; let l3_chain = rollup::ChainSpec { From 2a8b558ce4c429c447a4c45b9a34042b2292df5e Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 21 Jul 2026 17:59:17 +0800 Subject: [PATCH 2/2] revert the idle-flush-secs rename; keep the corrected docs and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the existing `idle-flush-secs` / `idle_flush_interval` names. The doc comments still get the fix — the knob is a max time between settlements while blocks are pending (the deadline is not reset by mid-window blocks), not an inactivity timer — and the new paused-clock run-loop tests that pin that behavior stay. Co-Authored-By: Claude Fable 5 --- crates/chain-spec/src/settlement.rs | 6 ++-- crates/cli/src/chain_config.rs | 36 +----------------------- crates/settlement/Cargo.toml | 2 +- crates/settlement/src/config.rs | 4 +-- crates/settlement/src/service.rs | 43 +++++++++++++++-------------- docs/tee-deployment.md | 10 +++---- tests/saya-tee/src/nodes.rs | 4 +-- 7 files changed, 36 insertions(+), 69 deletions(-) diff --git a/crates/chain-spec/src/settlement.rs b/crates/chain-spec/src/settlement.rs index c3a9a825d..95743bb47 100644 --- a/crates/chain-spec/src/settlement.rs +++ b/crates/chain-spec/src/settlement.rs @@ -71,15 +71,15 @@ pub struct SettlementRuntime { /// /// A batch is settled when it reaches `batch_size` blocks or when this many /// seconds have elapsed since its first pending block — whichever comes first. - #[serde(default = "default_settle_interval_secs", alias = "idle-flush-secs")] - pub settle_interval_secs: u64, + #[serde(default = "default_settlement_idle_flush_secs")] + pub idle_flush_secs: u64, } fn default_settlement_batch_size() -> usize { 10 } -fn default_settle_interval_secs() -> u64 { +fn default_settlement_idle_flush_secs() -> u64 { 120 } diff --git a/crates/cli/src/chain_config.rs b/crates/cli/src/chain_config.rs index 4f7ce8e63..d2ba49887 100644 --- a/crates/cli/src/chain_config.rs +++ b/crates/cli/src/chain_config.rs @@ -448,7 +448,7 @@ mod tests { tee_registry: ContractAddress::from(felt!("0x789")), prover_key: Some("sp1_dummy".to_string()), batch_size: 3, - settle_interval_secs: 7, + idle_flush_secs: 7, }), }; @@ -458,40 +458,6 @@ mod tests { assert_eq!(Some(settlement), read_settlement); } - /// Configs written before the rename still parse: the old `idle-flush-secs` key - /// deserializes into `settle_interval_secs` via its serde alias. - #[test] - fn settlement_interval_old_key_still_parses() { - let config = r#" -kind = "rollup" - -[id] -Id = "0x4b4154414e41" - -[fee-contract] -strk = "0x0" - -[settlement.layer.starknet] -rpc_url = "http://localhost:5050/" -core_contract = "0x0" -block = 0 -proof_kind = "tee" - -[settlement.layer.starknet.id] -Id = "0x4b4154414e41" - -[settlement.runtime] -account-address = "0x123" -account-private-key = "0x456" -tee-registry = "0x789" -idle-flush-secs = 7 -"#; - - let parsed: super::ChainConfigFile = toml::from_str(config).expect("old key parses"); - let runtime = parsed.settlement.expect("settlement present").runtime.expect("runtime"); - assert_eq!(runtime.settle_interval_secs, 7); - } - /// A settlement section with no `[settlement.runtime]` parses with `runtime: None`; a config /// with no `[settlement]` parses to `None`. #[test] diff --git a/crates/settlement/Cargo.toml b/crates/settlement/Cargo.toml index 55f39d7b5..5b09e0b39 100644 --- a/crates/settlement/Cargo.toml +++ b/crates/settlement/Cargo.toml @@ -34,7 +34,7 @@ tracing.workspace = true url.workspace = true [dev-dependencies] -# direct table writes to advance the chain head in the settle-interval tests +# direct table writes to advance the chain head in the idle-flush tests katana-db.workspace = true katana-genesis.workspace = true # test-util: paused-clock tests of the submission-retry loop (`start_paused`). diff --git a/crates/settlement/src/config.rs b/crates/settlement/src/config.rs index d41084b16..c3536e334 100644 --- a/crates/settlement/src/config.rs +++ b/crates/settlement/src/config.rs @@ -33,7 +33,7 @@ pub struct SettlementConfig { /// Maximum time between settlements while blocks are pending: a batch is settled when it /// reaches `batch_size` blocks or when this much time has elapsed since its first pending /// block, whichever comes first. - pub settle_interval: Duration, + pub idle_flush_interval: Duration, } /// Proving-system-specific settlement configuration. @@ -83,7 +83,7 @@ impl SettlementConfig { account_address: runtime.account_address, account_private_key: runtime.account_private_key, batch_size: runtime.batch_size, - settle_interval: Duration::from_secs(runtime.settle_interval_secs), + idle_flush_interval: Duration::from_secs(runtime.idle_flush_secs), prover: ProverConfig::Tee { tee_registry: runtime.tee_registry, prover_key: runtime.prover_key.clone(), diff --git a/crates/settlement/src/service.rs b/crates/settlement/src/service.rs index 7178e1eb7..fc748592b 100644 --- a/crates/settlement/src/service.rs +++ b/crates/settlement/src/service.rs @@ -94,7 +94,7 @@ where backend: self.backend.clone(), provider: self.provider.clone(), batch_size: self.config.batch_size.max(1) as u64, - settle_interval: self.config.settle_interval, + idle_flush_interval: self.config.idle_flush_interval, metrics: SettlementMetrics::default(), proof_metrics: SettlementProofMetrics::new_with_labels(&[( "proof_type", @@ -154,7 +154,7 @@ struct Worker

{ backend: Arc, piltover: PiltoverClient, batch_size: u64, - settle_interval: tokio::time::Duration, + idle_flush_interval: tokio::time::Duration, /// Last settled block, from Piltover's `get_state()`. `None` = nothing settled yet. cursor: Option, metrics: SettlementMetrics, @@ -273,7 +273,7 @@ where enum Action { /// Settle this inclusive block range now. Settle { first: BlockNumber, last: BlockNumber }, - /// Blocks are pending but the batch is partial — wait for more blocks or the settle deadline. + /// Blocks are pending but the batch is partial — wait for more blocks or the idle deadline. WaitForBatch, /// Fully caught up — wait for a new block. Idle, @@ -286,7 +286,7 @@ fn next_action( cursor: Option, head: BlockNumber, batch_size: u64, - interval_elapsed: bool, + idle_elapsed: bool, ) -> Action { let next = cursor.map(|c| c + 1).unwrap_or(0); @@ -295,7 +295,7 @@ fn next_action( } let pending = head - next + 1; - if pending >= batch_size || interval_elapsed { + if pending >= batch_size || idle_elapsed { Action::Settle { first: next, last: head.min(next + batch_size - 1) } } else { Action::WaitForBatch @@ -317,7 +317,7 @@ where // shutdown-responsive mid-retry. mut shutdown_rx: oneshot::Receiver<()>, ) { - let mut settle_deadline = Instant::now() + self.settle_interval; + let mut idle_deadline = Instant::now() + self.idle_flush_interval; let mut backoff = RETRY_BACKOFF_MIN; let mut consecutive_failures: u32 = 0; @@ -331,9 +331,9 @@ where } }; - let interval_elapsed = Instant::now() >= settle_deadline; + let idle_elapsed = Instant::now() >= idle_deadline; - match next_action(self.cursor, head, self.batch_size, interval_elapsed) { + match next_action(self.cursor, head, self.batch_size, idle_elapsed) { Action::Settle { first, last } => { let batch_start = Instant::now(); match self.settle_batch(first, last, &mut shutdown_rx).await { @@ -364,7 +364,7 @@ where if let Some(proof) = proof { persist_block_proofs(&self.provider, first, last, proof); } - settle_deadline = Instant::now() + self.settle_interval; + idle_deadline = Instant::now() + self.idle_flush_interval; backoff = RETRY_BACKOFF_MIN; consecutive_failures = 0; // Loop again immediately: drain any remaining backlog. @@ -398,7 +398,7 @@ where Action::WaitForBatch => { tokio::select! { _ = &mut shutdown_rx => break, - _ = tokio::time::sleep_until(settle_deadline) => {} + _ = tokio::time::sleep_until(idle_deadline) => {} r = notify_rx.recv() => match r { // New block mined — re-evaluate. The payload is irrelevant; the // provider is re-read on the next iteration. @@ -423,9 +423,9 @@ where r = notify_rx.recv() => match r { Ok(_) => { // First block of a fresh batch window: the window settles no - // later than `settle_interval` from now, even if the batch + // later than `idle_flush_interval` from now, even if the batch // never fills. Later blocks do not push the deadline back. - settle_deadline = Instant::now() + self.settle_interval; + idle_deadline = Instant::now() + self.idle_flush_interval; } Err(broadcast::error::RecvError::Lagged(_)) => {} Err(broadcast::error::RecvError::Closed) => { @@ -643,9 +643,9 @@ mod tests { fn nothing_settled() { // Only the genesis block present, batch of 1 → settle block 0 immediately. assert_eq!(next_action(None, 0, 1, false), Action::Settle { first: 0, last: 0 }); - // Only the genesis block, larger batch → wait for more blocks (or the settle deadline). + // Only the genesis block, larger batch → wait for more blocks (or the idle flush). assert_eq!(next_action(None, 0, 10, false), Action::WaitForBatch); - // A few blocks present, batch not yet full → wait unless the interval elapsed. + // A few blocks present, batch not yet full → wait unless the idle deadline elapsed. assert_eq!(next_action(None, 2, 10, false), Action::WaitForBatch); assert_eq!(next_action(None, 2, 10, true), Action::Settle { first: 0, last: 2 }); } @@ -670,7 +670,7 @@ mod tests { } #[test] - fn interval_elapsed_settles_partial_batch() { + fn idle_elapsed_flushes_partial_batch() { assert_eq!(next_action(Some(2), 4, 10, true), Action::Settle { first: 3, last: 4 }); } @@ -818,7 +818,7 @@ mod tests { backend: backend.clone(), provider, batch_size: 10, - settle_interval: Duration::from_secs(120), + idle_flush_interval: Duration::from_secs(120), cursor: None, metrics: SettlementMetrics::default(), proof_metrics: SettlementProofMetrics::new_with_labels(&[("proof_type", "mock")]), @@ -940,13 +940,14 @@ mod tests { } /// Exercises the run loop's time-based trigger under a paused tokio clock: a partial - /// batch settles once `settle_interval` elapses, measured from when the batch window - /// opens — blocks arriving mid-window must not push the deadline back. + /// batch settles once `idle_flush_interval` elapses, measured from when the batch window + /// opens — blocks arriving mid-window must not push the deadline back, so the interval + /// is the maximum time between settlements while blocks are pending. /// /// The Piltover endpoint is unreachable, so "settled" is observed at the proving layer /// (a `CountingBackend` prove call) and via the persisted pending-proof range, not a /// landed transaction. - mod settle_interval { + mod idle_flush { use std::sync::atomic::Ordering; use std::sync::Arc; @@ -987,7 +988,7 @@ mod tests { let backend = Arc::new(CountingBackend::new(false)); let mut worker = test_worker(backend.clone(), provider.clone()); - worker.settle_interval = Duration::from_secs(60); + worker.idle_flush_interval = Duration::from_secs(60); let (notify_tx, notify_rx) = broadcast::channel::<()>(8); let (shutdown_tx, shutdown_rx) = oneshot::channel(); @@ -1015,7 +1016,7 @@ mod tests { let backend = Arc::new(CountingBackend::new(false)); let mut worker = test_worker(backend.clone(), provider.clone()); - worker.settle_interval = Duration::from_secs(60); + worker.idle_flush_interval = Duration::from_secs(60); let (notify_tx, notify_rx) = broadcast::channel::<()>(8); let (shutdown_tx, shutdown_rx) = oneshot::channel(); diff --git a/docs/tee-deployment.md b/docs/tee-deployment.md index 1b26b8bb8..932005ba7 100644 --- a/docs/tee-deployment.md +++ b/docs/tee-deployment.md @@ -129,9 +129,9 @@ operator-local half you add by hand: account-address = "" account-private-key = "" tee-registry = "" -batch-size = 1 # blocks per settlement tx; raise for prod -settle-interval-secs = 30 # settle pending blocks at most this many seconds apart - # (batch-size or this, whichever comes first) +batch-size = 1 # blocks per settlement tx; raise for prod +idle-flush-secs = 30 # settle pending blocks at most this many seconds apart + # (batch-size or this, whichever comes first) # prover-key is omitted: with a mock attester no SP1 proving happens. ``` @@ -238,8 +238,8 @@ account-address = "" account-private-key = "" tee-registry = "" prover-key = "" -batch-size = 32 # amortize settlement gas; tune to throughput -settle-interval-secs = 60 # max seconds between settlements while blocks are pending +batch-size = 32 # amortize settlement gas; tune to throughput +idle-flush-secs = 60 # max seconds between settlements while blocks are pending ``` Commit `chain-config/` (minus the `[settlement-runtime]` secrets, if you diff --git a/tests/saya-tee/src/nodes.rs b/tests/saya-tee/src/nodes.rs index d7343d3e4..730646685 100644 --- a/tests/saya-tee/src/nodes.rs +++ b/tests/saya-tee/src/nodes.rs @@ -156,14 +156,14 @@ pub async fn spawn_l3(l2: &L2InProcess, bootstrap: &BootstrapResult) -> L3InProc // Drives the embedded settlement service. `batch_size: 1` settles every block // immediately, so the per-iteration `wait_for_settlement` assertions don't sit - // out the settle-interval window. + // out the idle-flush window. let settlement_runtime = SettlementRuntime { account_address: bootstrap.account_address.into(), account_private_key: bootstrap.account_private_key, tee_registry: bootstrap.tee_registry_address.into(), prover_key: None, batch_size: 1, - settle_interval_secs: 120, + idle_flush_secs: 120, }; let l3_chain = rollup::ChainSpec {