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
24 changes: 18 additions & 6 deletions cartesi-rollups/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The dispute engine (formerly the `cartesi-prt-core` crate):
`docs/computation-hash.md` first; this is the arcane part.
- `hero/` - the honest player: per tick it observes, plans, and
dispatches either one dispute action - join, bisect, seal, prove, or
win by timeout - or one bond-freeing cleanup (`gc_planner`).
win by timeout - or one timeout/child cleanup (`gc_planner`).
- `tournament/` - the semantic chain interface: `dispute` owns the recursive,
event-derived tournament tree; `domain` defines wire-independent values;
`observer` performs the narrow pinned point reads; `reader` maintains the
Expand Down Expand Up @@ -56,11 +56,23 @@ Running the node requires an Ethereum JSON-RPC gateway and a funded wallet.
Reads use `--web3-rpc-url`. Raw signed transactions use
`--web3-submit-rpc-url`, which defaults to the read endpoint and may instead
name a private relay with revert protection. The signer must be exclusive to
one node process because the node owns its nonce sequence. Production submits
at most one mutation per tick - a settlement step, a Hero action, one cleanup,
or bond recovery - through the single serial transaction lane. With the
default `GAS_LIMIT=15_000_000`, a pool may require balance for that full limit
at the transaction's max fee, plus any join bond or other call value.
one node process because the node owns its nonce sequence. Each tick batches
the applicable dispute or cleanup action, settlement step, and all available
bond recoveries at consecutive nonces from the latest mined count. The next
tick rebuilds the batch from chain state without waiting for receipts.

The node completes epochs in order: it waits for finalized settlement and its
winning bond recoveries before participating in the next epoch. It resumes the
same unfinished epoch after restart. Other participants may advance meanwhile;
the operating timing assumption allows a modest delay while refunds finish.
The completion cursor is bound to one claimant, so changing signer requires a
fresh state directory. A changed node version or schema also requires a fresh
directory under the node's rebuild policy.

Fund the whole pending batch. With the default `GAS_LIMIT=15_000_000`, a pool
may require each transaction's full gas limit at its max fee, plus its call
value. These requirements accumulate across the batch, and nested tournaments
each require their own join bond.

Here are its arguments:

Expand Down
7 changes: 0 additions & 7 deletions cartesi-rollups/node/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,6 @@ impl NodeConfig {
Ok(access)
}

/// For workers that only read through their own handle (the
/// epoch manager; the dispute Hero opens its own read-write
/// Storage). Fails fast under write pressure instead of stalling.
pub fn storage_read_only(&self) -> Result<Storage, StorageError> {
Storage::open_read_only(&self.state_dir)
}

pub async fn read_provider(&self) -> DynProvider {
create_rpc_provider(&self.ethereum_gateway, self.chain_id).await
}
Expand Down
38 changes: 27 additions & 11 deletions cartesi-rollups/node/src/engine/dispute.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

//! The dispute-facing node source: every merkle node a tournament
//! hero needs, answered by quartet.
//! The dispute-facing computation source: Merkle nodes answered by
//! quartet and transition witnesses checked against both state hashes.
//!
//! A tournament level's commitment tree lives at a [`LevelCoords`]:
//! its root, the node a match contests, bisection children, and
Expand Down Expand Up @@ -231,18 +231,34 @@ impl<F: RulerFactory> DisputeSource<F> {
})
}

pub fn factory(&self) -> &F {
#[cfg(test)]
pub(crate) fn factory(&self) -> &F {
&self.factory
}

/// A ruler positioned at `position`: the machine verb of the
/// facade. This is what proof positioning uses (the disputed
/// leaf's transition witness) and what entering a nested
/// tournament uses to start producing the nested computation
/// hash. Positioning resumes from the boundary store's nearest
/// answer and densifies as it advances.
pub fn machine_at(&mut self, position: U256) -> Result<super::ruler::Ruler<F::S>> {
self.factory.ruler_at(position)
/// A transition witness is usable only if replay reaches the agreed
/// state and proving reaches the claimed post-state. Keep both checks
/// with the positioned machine, before returning any witness bytes.
pub fn prove_transition(
&mut self,
position: U256,
expected_pre_state: Digest,
expected_post_state: Digest,
) -> Result<Vec<u8>> {
let mut ruler = self.factory.ruler_at(position)?;
let pre_state = ruler.state_hash()?;
ensure!(
pre_state == expected_pre_state,
"epoch {} transition {position}: pre-state {pre_state} differs from expected {expected_pre_state}",
self.epoch
);
let (proof, post_state) = ruler.prove_transition()?;
ensure!(
post_state == expected_post_state,
"epoch {} transition {position}: post-state {post_state} differs from expected {expected_post_state}",
self.epoch
);
Ok(proof)
}

/// Frontier coverage: the quartet sits at or above window
Expand Down
100 changes: 47 additions & 53 deletions cartesi-rollups/node/src/engine/machine_stf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

use super::dispute::DisputeSource;
use super::ruler::{Ruler, RulerFactory};
use super::stf::{ProvingStf, Stf};
use super::stf::Stf;
use super::structure::Structure;
use crate::arithmetic::add_and_clamp;
use crate::merkle::Digest;
Expand Down Expand Up @@ -195,7 +195,7 @@ impl MachineStf {
}

fn terminal_fixed(&mut self) -> Result<bool> {
if self.halted()? || self.mcycle_overflow()? {
if self.machine.iflags_h()? || self.mcycle_overflow()? {
return Ok(true);
}
Ok(matches!(
Expand Down Expand Up @@ -235,10 +235,6 @@ impl Stf for MachineStf {
Ok(self.machine.root_hash()?.into())
}

fn halted(&mut self) -> Result<bool> {
Ok(self.machine.iflags_h()?)
}

fn yielded(&mut self) -> Result<bool> {
Ok(self.manual_yield_reason()? == Some(RX_ACCEPTED))
}
Expand Down Expand Up @@ -364,6 +360,51 @@ impl Stf for MachineStf {
self.restore_rejected()?;
Ok(ran)
}

fn log_feed(&mut self, window: u64) -> Result<Vec<u8>> {
// The proving path resolves the payload without touching the
// feed cursor or the checkpoint: the machine is spent after
// the proof.
let payload = match &mut self.feeder {
Feeder::Scratch { inputs, .. } => inputs.get(window as usize).cloned(),
Feeder::Store { storage, epoch, .. } => storage
.input(&InputId {
epoch_number: *epoch,
input_index_in_epoch: window,
})?
.map(|input| input.data),
Feeder::Advance { .. } => {
unreachable!("the advance stf collects forward; proving rides the dispute path")
}
};
match payload {
Some(input) => {
let revert_root = self.machine.root_hash()?;
let cmio_log = self.machine.log_send_cmio_response(
CmioResponseReason::Advance,
&input,
&revert_root,
LogType::default(),
)?;
Ok([Self::encode_da(&input), Self::encode_access_log(&cmio_log)].concat())
}
None => Ok(Self::encode_da(&[])),
}
}

fn log_ustep(&mut self) -> Result<Vec<u8>> {
let log = self.machine.log_step_uarch(LogType::default())?;
self.ucycle += 1;
Ok(Self::encode_access_log(&log))
}

fn log_ureset(&mut self) -> Result<Vec<u8>> {
let log = self.machine.log_reset_uarch(LogType::default())?;
self.ucycle = 0;
let proof = Self::encode_access_log(&log);
self.restore_rejected()?;
Ok(proof)
}
}

// The chain witness encoding, byte-compatible with what the on-chain
Expand Down Expand Up @@ -414,53 +455,6 @@ impl MachineStf {
}
}

impl ProvingStf for MachineStf {
fn log_feed(&mut self, window: u64) -> Result<Vec<u8>> {
// The proving path resolves the payload without touching the
// feed cursor or the checkpoint: the machine is spent after
// the proof.
let payload = match &mut self.feeder {
Feeder::Scratch { inputs, .. } => inputs.get(window as usize).cloned(),
Feeder::Store { storage, epoch, .. } => storage
.input(&InputId {
epoch_number: *epoch,
input_index_in_epoch: window,
})?
.map(|input| input.data),
Feeder::Advance { .. } => {
unreachable!("the advance stf collects forward; proving rides the dispute path")
}
};
match payload {
Some(input) => {
let revert_root = self.machine.root_hash()?;
let cmio_log = self.machine.log_send_cmio_response(
CmioResponseReason::Advance,
&input,
&revert_root,
LogType::default(),
)?;
Ok([Self::encode_da(&input), Self::encode_access_log(&cmio_log)].concat())
}
None => Ok(Self::encode_da(&[])),
}
}

fn log_ustep(&mut self) -> Result<Vec<u8>> {
let log = self.machine.log_step_uarch(LogType::default())?;
self.ucycle += 1;
Ok(Self::encode_access_log(&log))
}

fn log_ureset(&mut self) -> Result<Vec<u8>> {
let log = self.machine.log_reset_uarch(LogType::default())?;
self.ucycle = 0;
let proof = Self::encode_access_log(&log);
self.restore_rejected()?;
Ok(proof)
}
}

/// The engine's positioning residue: a work dir, a spawn counter,
/// and the store handle they serve. Positions rulers by resuming
/// from the boundary store's nearest stored machine and advancing
Expand Down
24 changes: 13 additions & 11 deletions cartesi-rollups/node/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@
//! docs/computation-hash.md.
//!
//! Layering, innermost first:
//! - [`stf::Stf`]: machine verbs (ustep, ureset, feed, revert). Two
//! implementations: the toy (here, for spec tests) and the Cartesi
//! machine.
//! - [`stf::Stf`]: the machine operations the ruler needs. Production
//! uses the Cartesi machine; unit tests use a small scripted machine.
//! - [`ruler::Ruler`]: the geometry engine. Owns every meta-cycle
//! convention (window boundaries, fused feed transition, big-cycle
//! closing ureset, fixed-point padding). Written once, exercised by
//! the toy, reused by the production machine.
//! - [`cache::NodeCache`] and [`cache::get_or_compute`]: the quartet
//! cache with its amortizing fanout.
//! - [`cache::get_or_compute`]: quartet computation and fanout over
//! the node's storage.
//! - [`dispute::DisputeSource`]: the hero-facing face. Tournament
//! coordinates map onto quartets ([`dispute::LevelCoords`]), level 0
//! is served from the persisted regime-1 material (window-root rows
//! plus lazy interior folds), and proofs are sibling descents.
//! plus lazy interior folds). It supplies Merkle proofs by sibling
//! descent and transition witnesses checked against pre/post states.
//!
//! The spec tests in `spec.rs` compare all of this against an
//! independent brute-force oracle; they are the executable form of the
//! leaf-convention specification.
//! The spec tests compare stepping and sampling against a literal
//! leaf sequence. Cache and proof tests also use trees built from those
//! runs; real-machine differentials live in `tests/engine_machine.rs`.

pub mod cache;
pub mod config;
Expand All @@ -41,10 +41,12 @@ pub mod structure;

#[cfg(test)]
pub(crate) mod spec;
#[cfg(test)]
pub(crate) mod toy;

pub use config::EngineConfig;
pub use dispute::{DisputeSource, LevelCoords, fold_runs};
pub use machine_stf::{MachineStf, Positioner};
pub use ruler::{Ruler, RulerFactory, Run, ToyFactory};
pub use stf::{ProvingStf, Stf, ToyInput, ToyOutcome, ToyStf};
pub use ruler::{Ruler, RulerFactory, Run};
pub use stf::Stf;
pub use structure::{InputBoundary, Position, Quartet, Structure};
22 changes: 2 additions & 20 deletions cartesi-rollups/node/src/engine/ruler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
//! start means a broken machine or broken assumptions, and the engine
//! panics rather than inventing a transition shape for it.

use super::stf::{ProvingStf, Stf, ToyInput, ToyStf};
use super::stf::Stf;
use super::structure::Structure;
use crate::merkle::Digest;
use alloy::primitives::U256;
Expand Down Expand Up @@ -379,7 +379,7 @@ impl<S: Stf> Ruler<S> {
}
}

impl<S: ProvingStf> Ruler<S> {
impl<S: Stf> Ruler<S> {
/// Proves the transition at the current position: the chain
/// witness for exactly one of the three shapes the ruler names,
/// plus the post-transition state hash. The caller positions the
Expand Down Expand Up @@ -461,21 +461,3 @@ pub trait RulerFactory {
type S: Stf;
fn ruler_at(&mut self, position: U256) -> Result<Ruler<Self::S>>;
}

/// Toy factory: each scripted input is one epoch input (payloads are
/// irrelevant to the toy).
pub struct ToyFactory {
pub structure: Structure,
pub script: Vec<ToyInput>,
}

impl RulerFactory for ToyFactory {
type S = ToyStf;

fn ruler_at(&mut self, position: U256) -> Result<Ruler<ToyStf>> {
let stf = ToyStf::new(self.structure, self.script.clone());
let mut ruler = Ruler::new(stf, self.structure, self.script.len() as u64);
ruler.advance(position)?;
Ok(ruler)
}
}
Loading
Loading