Skip to content

Commit 63d155e

Browse files
committed
feat!: extract ITask from ITournament and add the Safety Gate
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.
1 parent fd9af66 commit 63d155e

35 files changed

Lines changed: 2323 additions & 186 deletions

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.

cartesi-rollups/contracts/script/Deployment.s.sol

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ contract DeploymentScript is BaseDeploymentScript {
1414

1515
address inputBox = _loadDeployment(".", "InputBox");
1616
address appFactory = _loadDeployment(".", "ApplicationFactory");
17-
address tournamentFactory = _loadDeployment(".", "MultiLevelTournamentFactory");
17+
18+
// The factory's bound proof system; apps that want a safety gate
19+
// deploy one at app-creation time via `newGatedDaveApp` (see
20+
// prt/docs/safety-gate.md).
21+
address taskSpawner = _loadDeployment(".", "MultiLevelTournamentFactory");
1822

1923
vmSafe.startBroadcast();
2024

2125
_storeDeployment(
2226
type(DaveAppFactory).name,
23-
_create2(type(DaveAppFactory).creationCode, abi.encode(inputBox, appFactory, tournamentFactory))
27+
_create2(type(DaveAppFactory).creationCode, abi.encode(inputBox, appFactory, taskSpawner))
2428
);
2529

2630
vmSafe.stopBroadcast();

cartesi-rollups/contracts/src/DaveAppFactory.sol

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import {IApplication} from "cartesi-rollups-contracts-3.0.0/src/dapp/IApplicatio
1414
import {IApplicationFactory} from "cartesi-rollups-contracts-3.0.0/src/dapp/IApplicationFactory.sol";
1515
import {IInputBox} from "cartesi-rollups-contracts-3.0.0/src/inputs/IInputBox.sol";
1616

17-
import {ITournamentFactory} from "prt-contracts/ITournamentFactory.sol";
17+
import {ITaskSpawner} from "prt-contracts/ITaskSpawner.sol";
18+
import {SafetyGateTaskSpawner} from "prt-contracts/safety-gate-task/SafetyGateTaskSpawner.sol";
19+
import {Time} from "prt-contracts/tournament/libs/Time.sol";
1820
import {Machine} from "prt-contracts/types/Machine.sol";
1921

2022
import {DaveConsensus} from "./DaveConsensus.sol";
@@ -24,14 +26,14 @@ import {IDaveConsensus} from "./IDaveConsensus.sol";
2426
contract DaveAppFactory is IDaveAppFactory {
2527
IInputBox immutable INPUT_BOX;
2628
IApplicationFactory immutable APP_FACTORY;
27-
ITournamentFactory immutable TOURNAMENT_FACTORY;
29+
ITaskSpawner immutable TASK_SPAWNER;
2830

2931
IOutputsMerkleRootValidator constant NO_VALIDATOR = IOutputsMerkleRootValidator(address(0));
3032

31-
constructor(IInputBox inputBox, IApplicationFactory appFactory, ITournamentFactory tournamentFactory) {
33+
constructor(IInputBox inputBox, IApplicationFactory appFactory, ITaskSpawner taskSpawner) {
3234
INPUT_BOX = inputBox;
3335
APP_FACTORY = appFactory;
34-
TOURNAMENT_FACTORY = tournamentFactory;
36+
TASK_SPAWNER = taskSpawner;
3537
}
3638

3739
function newDaveApp(bytes32 templateHash, WithdrawalConfig calldata withdrawalConfig, bytes32 salt)
@@ -40,9 +42,8 @@ contract DaveAppFactory is IDaveAppFactory {
4042
returns (IApplication appContract, IDaveConsensus daveConsensus)
4143
{
4244
appContract = _newApplication(templateHash, withdrawalConfig, salt);
43-
daveConsensus = _newDaveConsensus(address(appContract), templateHash, salt);
44-
appContract.migrateToOutputsMerkleRootValidator(daveConsensus);
45-
appContract.renounceOwnership();
45+
daveConsensus = _newDaveConsensus(address(appContract), templateHash, TASK_SPAWNER, salt);
46+
_wireApp(appContract, daveConsensus);
4647
emit DaveAppCreated(appContract, daveConsensus);
4748
}
4849

@@ -53,14 +54,59 @@ contract DaveAppFactory is IDaveAppFactory {
5354
returns (address appContractAddress, address daveConsensusAddress)
5455
{
5556
appContractAddress = _calculateApplicationAddress(templateHash, withdrawalConfig, salt);
56-
daveConsensusAddress = _calculateDaveConsensusAddress(appContractAddress, templateHash, salt);
57+
daveConsensusAddress = _calculateDaveConsensusAddress(appContractAddress, templateHash, TASK_SPAWNER, salt);
58+
}
59+
60+
function newGatedDaveApp(
61+
bytes32 templateHash,
62+
WithdrawalConfig calldata withdrawalConfig,
63+
address sentryManager,
64+
Time.Duration disagreementWindow,
65+
address[] calldata sentries,
66+
bytes32 salt
67+
)
68+
external
69+
override
70+
returns (IApplication appContract, IDaveConsensus daveConsensus, SafetyGateTaskSpawner gateSpawner)
71+
{
72+
appContract = _newApplication(templateHash, withdrawalConfig, salt);
73+
gateSpawner = new SafetyGateTaskSpawner{salt: salt}(sentryManager, TASK_SPAWNER, disagreementWindow, sentries);
74+
daveConsensus = _newDaveConsensus(address(appContract), templateHash, gateSpawner, salt);
75+
_wireApp(appContract, daveConsensus);
76+
emit GatedDaveAppCreated(appContract, daveConsensus, gateSpawner);
77+
}
78+
79+
function calculateGatedDaveAppAddress(
80+
bytes32 templateHash,
81+
WithdrawalConfig calldata withdrawalConfig,
82+
address sentryManager,
83+
Time.Duration disagreementWindow,
84+
address[] calldata sentries,
85+
bytes32 salt
86+
)
87+
external
88+
view
89+
override
90+
returns (address appContractAddress, address daveConsensusAddress, address gateSpawnerAddress)
91+
{
92+
appContractAddress = _calculateApplicationAddress(templateHash, withdrawalConfig, salt);
93+
gateSpawnerAddress = _calculateGateSpawnerAddress(sentryManager, disagreementWindow, sentries, salt);
94+
daveConsensusAddress =
95+
_calculateDaveConsensusAddress(appContractAddress, templateHash, ITaskSpawner(gateSpawnerAddress), salt);
5796
}
5897

5998
/// @notice Encode the data availability blob for applications that only use the input box as DA.
6099
function _encodeInputBoxDataAvailability() internal view returns (bytes memory) {
61100
return abi.encodeCall(DataAvailability.InputBox, (INPUT_BOX));
62101
}
63102

103+
/// @notice Hand the application over to its consensus: set the outputs
104+
/// Merkle root validator and renounce the factory's temporary ownership.
105+
function _wireApp(IApplication appContract, IDaveConsensus daveConsensus) internal {
106+
appContract.migrateToOutputsMerkleRootValidator(daveConsensus);
107+
appContract.renounceOwnership();
108+
}
109+
64110
/// @notice Instantiate a new application contract owned by the current contract,
65111
/// with no outputs Merkle root validator (the zero address), and with the input box
66112
/// as the only data availability source.
@@ -76,12 +122,12 @@ contract DaveAppFactory is IDaveAppFactory {
76122
}
77123

78124
/// @notice Instantiate a new `DaveConsensus` contract.
79-
function _newDaveConsensus(address appContract, bytes32 templateHash, bytes32 salt)
125+
function _newDaveConsensus(address appContract, bytes32 templateHash, ITaskSpawner taskSpawner, bytes32 salt)
80126
internal
81127
returns (DaveConsensus)
82128
{
83129
Machine.Hash initialMachineStateHash = Machine.Hash.wrap(templateHash);
84-
return new DaveConsensus{salt: salt}(INPUT_BOX, appContract, TOURNAMENT_FACTORY, initialMachineStateHash);
130+
return new DaveConsensus{salt: salt}(INPUT_BOX, appContract, taskSpawner, initialMachineStateHash);
85131
}
86132

87133
/// @notice Calculates the address of an application contract.
@@ -97,19 +143,38 @@ contract DaveAppFactory is IDaveAppFactory {
97143
}
98144

99145
/// @notice Calculates the address of a `DaveConsensus` contract.
100-
function _calculateDaveConsensusAddress(address appContract, bytes32 templateHash, bytes32 salt)
146+
function _calculateDaveConsensusAddress(
147+
address appContract,
148+
bytes32 templateHash,
149+
ITaskSpawner taskSpawner,
150+
bytes32 salt
151+
) internal view returns (address) {
152+
return _calculateCreate2Address(
153+
type(DaveConsensus).creationCode, abi.encode(INPUT_BOX, appContract, taskSpawner, templateHash), salt
154+
);
155+
}
156+
157+
/// @notice Calculates the address of a `SafetyGateTaskSpawner` contract.
158+
function _calculateGateSpawnerAddress(
159+
address sentryManager,
160+
Time.Duration disagreementWindow,
161+
address[] calldata sentries,
162+
bytes32 salt
163+
) internal view returns (address) {
164+
return _calculateCreate2Address(
165+
type(SafetyGateTaskSpawner).creationCode,
166+
abi.encode(sentryManager, TASK_SPAWNER, disagreementWindow, sentries),
167+
salt
168+
);
169+
}
170+
171+
/// @notice Address of a contract this factory would CREATE2-deploy from
172+
/// the given creation code and constructor arguments under `salt`.
173+
function _calculateCreate2Address(bytes memory creationCode, bytes memory args, bytes32 salt)
101174
internal
102175
view
103176
returns (address)
104177
{
105-
return Create2.computeAddress(
106-
salt,
107-
keccak256(
108-
abi.encodePacked(
109-
type(DaveConsensus).creationCode,
110-
abi.encode(INPUT_BOX, appContract, TOURNAMENT_FACTORY, templateHash)
111-
)
112-
)
113-
);
178+
return Create2.computeAddress(salt, keccak256(abi.encodePacked(creationCode, args)));
114179
}
115180
}

cartesi-rollups/contracts/src/DaveConsensus.sol

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,17 @@ import {LibKeccak256} from "cartesi-rollups-contracts-3.0.0/src/library/LibKecca
1616
import {LibMath} from "cartesi-rollups-contracts-3.0.0/src/library/LibMath.sol";
1717

1818
import {IDataProvider} from "prt-contracts/IDataProvider.sol";
19-
import {ITournament} from "prt-contracts/ITournament.sol";
20-
import {ITournamentFactory} from "prt-contracts/ITournamentFactory.sol";
19+
import {ITask} from "prt-contracts/ITask.sol";
20+
import {ITaskSpawner} from "prt-contracts/ITaskSpawner.sol";
2121

2222
import {Machine} from "prt-contracts/types/Machine.sol";
23-
import {Tree} from "prt-contracts/types/Tree.sol";
2423

2524
import {EmulatorConstants} from "step/src/EmulatorConstants.sol";
2625
import {Memory} from "step/src/Memory.sol";
2726

2827
import {IDaveConsensus} from "./IDaveConsensus.sol";
2928

30-
/// @notice Consensus contract with Dave tournaments.
29+
/// @notice Consensus contract with Dave tasks.
3130
///
3231
/// @notice This contract validates only one application,
3332
/// which read inputs from the InputBox contract.
@@ -60,8 +59,8 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
6059
/// @notice The application contract
6160
address immutable _APP_CONTRACT;
6261

63-
/// @notice The contract used to instantiate tournaments
64-
ITournamentFactory immutable _TOURNAMENT_FACTORY;
62+
/// @notice The contract used to instantiate tasks
63+
ITaskSpawner immutable _TASK_SPAWNER;
6564

6665
/// @notice Deployment block number
6766
uint256 immutable _DEPLOYMENT_BLOCK_NUMBER = block.number;
@@ -75,8 +74,8 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
7574
/// @notice Input index (exclusive) upper bound of the current sealed epoch
7675
uint256 _inputIndexUpperBound;
7776

78-
/// @notice Current sealed epoch tournament
79-
ITournament _tournament;
77+
/// @notice Current sealed epoch task
78+
ITask _task;
8079

8180
/// @notice Settled output trees' merkle root hash
8281
mapping(bytes32 => bool) _outputsMerkleRoots;
@@ -87,30 +86,30 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
8786
constructor(
8887
IInputBox inputBox,
8988
address appContract,
90-
ITournamentFactory tournamentFactory,
89+
ITaskSpawner taskSpawner,
9190
Machine.Hash initialMachineStateHash
9291
) {
9392
// Initialize immutable variables
9493
_INPUT_BOX = inputBox;
9594
_APP_CONTRACT = appContract;
96-
_TOURNAMENT_FACTORY = tournamentFactory;
97-
emit ConsensusCreation(inputBox, appContract, tournamentFactory);
95+
_TASK_SPAWNER = taskSpawner;
96+
emit ConsensusCreation(inputBox, appContract, taskSpawner);
9897

9998
// Initialize first sealed epoch
10099
uint256 inputIndexUpperBound = inputBox.getNumberOfInputs(appContract);
101100
_inputIndexUpperBound = inputIndexUpperBound;
102-
ITournament tournament = tournamentFactory.instantiate(initialMachineStateHash, this);
103-
_tournament = tournament;
104-
emit EpochSealed(0, 0, inputIndexUpperBound, initialMachineStateHash, bytes32(0), tournament);
101+
ITask task = taskSpawner.spawn(initialMachineStateHash, this);
102+
_task = task;
103+
emit EpochSealed(0, 0, inputIndexUpperBound, initialMachineStateHash, bytes32(0), task);
105104
}
106105

107106
function canSettle()
108107
external
109108
view
110109
override
111-
returns (bool isFinished, uint256 epochNumber, Tree.Node winnerCommitment)
110+
returns (bool isFinished, uint256 epochNumber, Machine.Hash finalState)
112111
{
113-
(isFinished, winnerCommitment,) = _tournament.arbitrationResult();
112+
(isFinished, finalState) = _task.result();
114113
epochNumber = _epochNumber;
115114
}
116115

@@ -119,14 +118,14 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
119118
override
120119
notForeclosed(_APP_CONTRACT)
121120
{
122-
// Check tournament settlement
121+
// Check task settlement
123122
require(epochNumber == _epochNumber, IncorrectEpochNumber(epochNumber, _epochNumber));
124123

125-
// Check tournament finished
126-
(bool isFinished,, Machine.Hash finalMachineStateHash) = _tournament.arbitrationResult();
124+
// Check task finished
125+
(bool isFinished, Machine.Hash finalMachineStateHash) = _task.result();
127126
require(isFinished, TournamentNotFinishedYet());
128-
ITournament oldTournament = _tournament;
129-
_tournament = ITournament(address(0));
127+
ITask oldTask = _task;
128+
_task = ITask(address(0));
130129

131130
// Check outputs Merkle root
132131
_validateOutputTree(finalMachineStateHash, outputsMerkleRoot, proof);
@@ -138,36 +137,26 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
138137
_outputsMerkleRoots[outputsMerkleRoot] = true;
139138
_lastFinalizedMachineStateHash = finalMachineStateHash;
140139

141-
// Start new tournament
142-
_tournament = _TOURNAMENT_FACTORY.instantiate(finalMachineStateHash, this);
140+
// Start new task
141+
_task = _TASK_SPAWNER.spawn(finalMachineStateHash, this);
143142

144143
emit EpochSealed(
145-
_epochNumber,
146-
_inputIndexLowerBound,
147-
_inputIndexUpperBound,
148-
finalMachineStateHash,
149-
outputsMerkleRoot,
150-
_tournament
144+
_epochNumber, _inputIndexLowerBound, _inputIndexUpperBound, finalMachineStateHash, outputsMerkleRoot, _task
151145
);
152146

153-
oldTournament.tryRecoveringBond();
147+
_tryCleanup(oldTask);
154148
}
155149

156150
function getCurrentSealedEpoch()
157151
external
158152
view
159153
override
160-
returns (
161-
uint256 epochNumber,
162-
uint256 inputIndexLowerBound,
163-
uint256 inputIndexUpperBound,
164-
ITournament tournament
165-
)
154+
returns (uint256 epochNumber, uint256 inputIndexLowerBound, uint256 inputIndexUpperBound, ITask task)
166155
{
167156
epochNumber = _epochNumber;
168157
inputIndexLowerBound = _inputIndexLowerBound;
169158
inputIndexUpperBound = _inputIndexUpperBound;
170-
tournament = _tournament;
159+
task = _task;
171160
}
172161

173162
function getInputBox() external view override returns (IInputBox) {
@@ -178,8 +167,8 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
178167
return _APP_CONTRACT;
179168
}
180169

181-
function getTournamentFactory() external view override returns (ITournamentFactory) {
182-
return _TOURNAMENT_FACTORY;
170+
function getTaskSpawner() external view override returns (ITaskSpawner) {
171+
return _TASK_SPAWNER;
183172
}
184173

185174
function provideMerkleRootOfInput(uint256 inputIndexWithinEpoch, bytes calldata input)
@@ -250,6 +239,12 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
250239
require(machineStateHash == allegedStateHash, InvalidOutputsMerkleRootProof(finalMachineStateHash));
251240
}
252241

242+
/// @dev Best-effort: settlement must never be blocked by a task whose
243+
/// cleanup reverts.
244+
function _tryCleanup(ITask task) internal {
245+
try task.cleanup() returns (bool) {} catch {}
246+
}
247+
253248
modifier onlyValidAppContract(address appContract) {
254249
_ensureAppContractIsValid(appContract);
255250
_;

0 commit comments

Comments
 (0)