Skip to content

Safety Gate Task#271

Open
GCdePaula wants to merge 3 commits into
next/3.0from
feature/safety-gate
Open

Safety Gate Task#271
GCdePaula wants to merge 3 commits into
next/3.0from
feature/safety-gate

Conversation

@GCdePaula

Copy link
Copy Markdown
Collaborator

Summary

This PR introduces a minimal task abstraction (ITask / ITaskSpawner) that decouples DaveConsensus from the underlying proof system, and builds on it to add the Safety Gate — a delay-only middleware that gives a permissioned Sentry layer time to react to proof-system or application bugs before an epoch settles, without ever letting it change an outcome. Gated applications are deployed per-app through the existing factory. The reference node changes needed to consume this are described under Downstream impact.

Targets next/3.0. This is a breaking change to the IDaveConsensus / PRT contract surface (see Breaking changes).

Motivation

For early-stage (L2Beat Stage 1) deployments we want to trade a bit of decentralization for the ability to pause and recover from catastrophic bugs. The Safety Gate provides a disagreement buffer that only ever delays settlement — it can never alter the result — buying time for a higher-level authority to intervene. Realizing it cleanly required first separating "the thing that produces a final machine state" (a task) from "the PRT tournament" specifically, so the consensus contract and the application stay agnostic to what settles an epoch.

The ITask abstraction

  • New ITask (result(), cleanup(), ERC-165) and ITaskSpawner (spawn(initial, provider)). A task computes the final machine state for a fixed input range; consumers that only need the settled result depend on ITask rather than on a concrete proof system.
  • ITournament now extends ITask: result() is the projection of arbitrationResult() without the winner commitment, and cleanup() performs bond recovery. arbitrationResult() is unchanged.
  • ITournamentFactory is replaced by ITaskSpawner; MultiLevelTournamentFactory implements spawn.
  • DaveConsensus now spawns and settles tasks via ITask.result(); canSettle() reports the final machine state instead of the winner commitment.

The Safety Gate (middleware)

SafetyGateTask wraps an inner task (a PRT root tournament) and gates its result behind a set of sentries:

  • If all sentries vote and corroborate the inner result -> the gate settles immediately, no delay.
  • Otherwise (missing votes, disagreement, or a claim that contradicts the inner result) anyone may start a permissionless fallback timer; once the disagreement window elapses, the inner result passes through as-is.

It is delay-only by construction: result() only ever surfaces the inner task's state — sentries decide when it becomes visible, never what it is. It requires all sentries to be byzantine to bypass the window; a single disagreement or no-vote is enough to trigger the window. Moreover, if any sentry gets compromised, anyone can still drive settlement via the fallback timer (making the design walkaway-safe). SafetyGateTaskSpawner lets a SENTRY_MANAGER rotate the sentry set for future epochs only. Voting state is exposed as a tri-state sentryStatus() (VOTING / AGREED / DISAGREED); tasks implement ERC-165 so off-chain actors can discover a gate (ISafetyGateTask id 0xf77c3559, pinned by a guard test).

Per-app deployment

Gate governance (sentry manager, sentry set, disagreement window) is app-specific with no sensible deployment-time default, so gates are deployed per app, not as shared infrastructure. DaveAppFactory.newGatedDaveApp(...) atomically deploys an app-specific SafetyGateTaskSpawner (wrapping the factory's bound proof system) and a DaveConsensus wired to it, emitting a distinct GatedDaveAppCreated event. Factory events are the canonical provenance check — they certify the settlement mechanism and distinguish gated from bare apps (this cannot be told from the factory address alone). Both DaveAppCreated and GatedDaveAppCreated now index the app and consensus addresses.

Node changes

  • epoch-manager: detects a gate behind the EpochSealed task via ERC-165, casts a sentry vote for the locally-computed final state when its signer is a sentry, and resolves INNER_TASK() to point the PRT player at the actual tournament. Starting the fallback timer is left as a manual, monitored operation.
  • state-manager: stores the epoch's final machine state alongside the computation hash (since canSettle now reports the final state).
  • blockchain-reader: follows the EpochSealed field rename; the e2e harness deploys a gated app through the factory so the gate stays exercised in the node test pipeline.

Breaking changes & downstream impact

For the reference node (consumes the published contract ABIs). Mostly mechanical after regenerating bindings:

Change Kind
canSettle() 3rd return winnerCommitment (Tree.Node)finalState (Machine.Hash) same ABI/selector, semantic — but the value isn't used in settle logic
getTournamentFactory()getTaskSpawner() selector break (few callers)
EpochSealed.tournamenttask; getCurrentSealedEpoch 4th return ITournamentITask field renames (same topic0 / selector)
ConsensusCreation 3rd param ITournamentFactoryITaskSpawner address, unused by node
DaveAppCreated app/consensus moved to indexed topics decode change
Epoch task may now be a SafetyGateTask wrapper new behavior — unwrap via INNER_TASK() before binding as a tournament
ITournamentFactory.sol deleted → ITaskSpawner.sol no standalone node binding
result / cleanup / supportsInterface added; TournamentNotFinishedYet name kept additive / no-op

Security

The gate's guarantee is delay, never change or freeze. The threat model is documented in prt/docs/safety-gate.md. A max-effort review pass hardened several edges: the sentry set is length-bounded (MAX_SENTRIES = 16) — the sole anti-brick guarantee, enforced at both the spawner and the task, so a careless/compromised manager cannot freeze settlement with an oversized list; the zero address is rejected and duplicates are collapsed; the fallback-timer elapsed check is computed as now - start so a pathological window cannot overflow and brick result(); and the node's ERC-165 probe propagates transport errors instead of silently treating a gated epoch as bare.

Testing

  • prt/contracts: 78 forge tests (gate unit tests across every result() branch, vote reverts, tri-state consensus, timer semantics, sentry-set bounds, the interface-id guard).
  • cartesi-rollups/contracts: 6 forge tests, including an end-to-end suite that settles DaveConsensus → SafetyGateTask → real tournament through all three settlement paths and proves sentries can only delay.
  • Rust workspace: 27 test suites, including blockchain-reader e2e against a devnet deployed through newGatedDaveApp.
  • forge fmt and cargo fmt clean.

Out of scope / follow-ups

  • The full-bytecode gate deployment per epoch (~0.5M gas) could later use clone-with-initializer; deferred and documented.
  • A couple of node-side cleanups (tuple→struct, root_tournamentroot_task rename, per-tick RPC caching) are intentionally deferred, since that node is being rewritten on another branch.
  • Deterministic app addresses remain first-come-first-served (pre-existing shared-CREATE2 behavior); documented, salt derivation unchanged.

Chores (cherry-picked)

Includes two dependency bumps carried over from feature/staging: Foundry 1.4.3 → 1.5.1 (with corrected sha256 pins — the upstream bump left them stale) and Honeypot 3.0.0 → 3.0.1.

guidanoli and others added 3 commits July 9, 2026 14:53
- This commit bumps Foundry on the CI and Dockefile. This is to align
  with the version of Foundry used on rollups-contracts, so that the
  node team can also bump Foundry (given that it uses the devnet image
  built in the dave repo still).
- Honeypot 3.0.1 fixes a bug in the rootfs Dockerfile that would lead to
  its build to fail. The resulting image is the same as 3.0.0 but we can
  now build the custom Honeypot rootfs image with devnet config.
Introduce a minimal task abstraction (ITask/ITaskSpawner) that decouples
DaveConsensus from the proof system:

- ITournament now extends ITask; result() projects arbitrationResult
  without the winner commitment, and cleanup() recovers bonds.
- ITournamentFactory is replaced by ITaskSpawner.
- DaveConsensus spawns tasks and settles on result(); canSettle reports
  the final machine state instead of the winner commitment.

Add the Safety Gate, a delay-only middleware task:

- SafetyGateTask gates an inner task behind unanimous sentry votes, with
  a permissionless fallback timer for liveness. Voting state is exposed
  as a tri-state SentryStatus (VOTING/AGREED/DISAGREED).
- SafetyGateTaskSpawner wraps an inner spawner; the sentry manager can
  rotate the sentry set, effective from the next spawned task.
- Starting the fallback timer is a manual, monitored operation; the node
  only casts sentry votes (see prt/docs/safety-gate.md).

Deploy gates per app, not as shared infrastructure:

- DaveAppFactory.newGatedDaveApp atomically deploys an app-specific
  SafetyGateTaskSpawner (wrapping the factory's bound proof system) and
  a DaveConsensus wired to it. Gate governance (sentry manager,
  sentries, disagreement window) is app-declared, like the template
  hash; deployment scripts carry no app-specific configuration.
- Factory events are the canonical provenance check: DaveAppCreated and
  GatedDaveAppCreated certify the settlement mechanism and distinguish
  gated from bare apps. Both index the app and consensus addresses.

Update the node accordingly:

- epoch-manager detects a safety gate behind the EpochSealed task via
  ERC-165 (pinned ISafetyGateTask id, guarded by a Solidity test), casts
  a sentry vote when the signer is a sentry, and points the PRT player
  at INNER_TASK.
- state-manager stores the epoch's final machine state alongside the
  computation hash, since canSettle now reports the final state.
- the blockchain-reader e2e harness deploys a gated app through the
  factory, keeping the safety gate exercised in the node test pipeline.
@GCdePaula GCdePaula requested review from guidanoli and stephenctw and removed request for stephenctw July 9, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants