Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

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

5 changes: 4 additions & 1 deletion crates/chain-spec/src/settlement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ 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.
/// 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_settlement_idle_flush_secs")]
pub idle_flush_secs: u64,
}
Expand Down
2 changes: 2 additions & 0 deletions crates/settlement/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ tracing.workspace = true
url.workspace = true

[dev-dependencies]
# 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`).
tokio = { workspace = true, features = [ "test-util" ] }
4 changes: 3 additions & 1 deletion crates/settlement/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ 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.
/// 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 idle_flush_interval: Duration,
}

Expand Down
117 changes: 111 additions & 6 deletions crates/settlement/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,9 @@ where
_ = &mut shutdown_rx => break,
r = notify_rx.recv() => match r {
Ok(_) => {
// First block of a fresh batch window: arm the idle flush timer.
// First block of a fresh batch window: the window settles no
// later than `idle_flush_interval` from now, even if the batch
// never fills. Later blocks do not push the deadline back.
idle_deadline = Instant::now() + self.idle_flush_interval;
}
Err(broadcast::error::RecvError::Lagged(_)) => {}
Expand Down Expand Up @@ -643,7 +645,7 @@ mod tests {
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).
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 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 });
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -797,7 +799,7 @@ mod tests {
})
}

fn test_worker(
pub(super) fn test_worker(
backend: Arc<dyn ProvingBackend>,
provider: DbProviderFactory,
) -> Worker<DbProviderFactory> {
Expand Down Expand Up @@ -936,4 +938,107 @@ 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 `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 idle_flush {
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::<tables::BlockHashes>(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.idle_flush_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.idle_flush_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();
}
}
}
9 changes: 5 additions & 4 deletions docs/tee-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@ operator-local half you add by hand:
account-address = "<DEPLOYER_ADDRESS>"
account-private-key = "<SEPOLIA_DEPLOYER_PRIVATE_KEY>"
tee-registry = "<TEE_REGISTRY_ADDRESS>"
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
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.
```

Expand Down Expand Up @@ -237,8 +238,8 @@ account-address = "<DEPLOYER_ADDRESS>"
account-private-key = "<SEPOLIA_DEPLOYER_PRIVATE_KEY>"
tee-registry = "<TEE_REGISTRY_ADDRESS>"
prover-key = "<SP1_PROVER_NETWORK_KEY>"
batch-size = 32 # amortize settlement gas; tune to throughput
idle-flush-secs = 60
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
Expand Down
Loading