From 8971e2e3892bc182c6149e6d83745e007ff1fc31 Mon Sep 17 00:00:00 2001 From: Gabriel Coutinho de Paula Date: Tue, 27 Jan 2026 20:08:46 -0300 Subject: [PATCH 1/6] feat(prt): add task interfaces and reorganize tournaments --- .../contracts/src/DaveAppFactory.sol | 12 +- .../contracts/src/DaveConsensus.sol | 66 ++--- .../contracts/src/IDaveConsensus.sol | 33 ++- .../contracts/test/DaveAppFactory.t.sol | 38 +-- .../contracts/test/DaveConsensus.t.sol | 238 +++++------------- prt/contracts/script/Deployment.s.sol | 2 +- prt/contracts/src/ITask.sol | 22 ++ ...TournamentFactory.sol => ITaskSpawner.sol} | 11 +- .../IMultiLevelTournamentFactory.sol | 6 +- .../src/{ => tournament}/ITournament.sol | 3 +- .../MultiLevelTournamentFactory.sol | 18 +- prt/contracts/src/tournament/Tournament.sol | 29 ++- .../src/tournament/libs/Commitment.sol | 2 +- prt/contracts/src/tournament/libs/Match.sol | 2 +- prt/contracts/test/BottomTournament.t.sol | 4 +- prt/contracts/test/MiddleTournament.t.sol | 4 +- prt/contracts/test/TopTournament.t.sol | 4 +- prt/contracts/test/Tournament.t.sol | 4 +- prt/contracts/test/TournamentFactory.t.sol | 4 +- prt/contracts/test/Util.sol | 4 +- 20 files changed, 229 insertions(+), 277 deletions(-) create mode 100644 prt/contracts/src/ITask.sol rename prt/contracts/src/{ITournamentFactory.sol => ITaskSpawner.sol} (52%) rename prt/contracts/src/tournament/{factories => }/IMultiLevelTournamentFactory.sol (79%) rename prt/contracts/src/{ => tournament}/ITournament.sol (99%) rename prt/contracts/src/tournament/{factories => }/MultiLevelTournamentFactory.sol (93%) diff --git a/cartesi-rollups/contracts/src/DaveAppFactory.sol b/cartesi-rollups/contracts/src/DaveAppFactory.sol index 22a96a15..3c5b8fa6 100644 --- a/cartesi-rollups/contracts/src/DaveAppFactory.sol +++ b/cartesi-rollups/contracts/src/DaveAppFactory.sol @@ -13,7 +13,7 @@ import {IApplication} from "cartesi-rollups-contracts-2.1.1/src/dapp/IApplicatio import {IApplicationFactory} from "cartesi-rollups-contracts-2.1.1/src/dapp/IApplicationFactory.sol"; import {IInputBox} from "cartesi-rollups-contracts-2.1.1/src/inputs/IInputBox.sol"; -import {ITournamentFactory} from "prt-contracts/ITournamentFactory.sol"; +import {ITaskSpawner} from "prt-contracts/ITaskSpawner.sol"; import {Machine} from "prt-contracts/types/Machine.sol"; import {DaveConsensus} from "./DaveConsensus.sol"; @@ -23,14 +23,14 @@ import {IDaveConsensus} from "./IDaveConsensus.sol"; contract DaveAppFactory is IDaveAppFactory { IInputBox immutable INPUT_BOX; IApplicationFactory immutable APP_FACTORY; - ITournamentFactory immutable TOURNAMENT_FACTORY; + ITaskSpawner immutable TASK_SPAWNER; IOutputsMerkleRootValidator constant NO_VALIDATOR = IOutputsMerkleRootValidator(address(0)); - constructor(IInputBox inputBox, IApplicationFactory appFactory, ITournamentFactory tournamentFactory) { + constructor(IInputBox inputBox, IApplicationFactory appFactory, ITaskSpawner taskSpawner) { INPUT_BOX = inputBox; APP_FACTORY = appFactory; - TOURNAMENT_FACTORY = tournamentFactory; + TASK_SPAWNER = taskSpawner; } function newDaveApp(bytes32 templateHash, bytes32 salt) @@ -74,7 +74,7 @@ contract DaveAppFactory is IDaveAppFactory { returns (DaveConsensus) { Machine.Hash initialMachineStateHash = Machine.Hash.wrap(templateHash); - return new DaveConsensus{salt: salt}(INPUT_BOX, appContract, TOURNAMENT_FACTORY, initialMachineStateHash); + return new DaveConsensus{salt: salt}(INPUT_BOX, appContract, TASK_SPAWNER, initialMachineStateHash); } /// @notice Calculates the address of an application contract. @@ -95,7 +95,7 @@ contract DaveAppFactory is IDaveAppFactory { keccak256( abi.encodePacked( type(DaveConsensus).creationCode, - abi.encode(INPUT_BOX, appContract, TOURNAMENT_FACTORY, templateHash) + abi.encode(INPUT_BOX, appContract, TASK_SPAWNER, templateHash) ) ) ); diff --git a/cartesi-rollups/contracts/src/DaveConsensus.sol b/cartesi-rollups/contracts/src/DaveConsensus.sol index 5b7a687d..fc6a61a9 100644 --- a/cartesi-rollups/contracts/src/DaveConsensus.sol +++ b/cartesi-rollups/contracts/src/DaveConsensus.sol @@ -13,11 +13,10 @@ import {IInputBox} from "cartesi-rollups-contracts-2.1.1/src/inputs/IInputBox.so import {LibMerkle32} from "cartesi-rollups-contracts-2.1.1/src/library/LibMerkle32.sol"; import {IDataProvider} from "prt-contracts/IDataProvider.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; -import {ITournamentFactory} from "prt-contracts/ITournamentFactory.sol"; +import {ITask} from "prt-contracts/ITask.sol"; +import {ITaskSpawner} from "prt-contracts/ITaskSpawner.sol"; import {Machine} from "prt-contracts/types/Machine.sol"; -import {Tree} from "prt-contracts/types/Tree.sol"; import {EmulatorConstants} from "step/src/EmulatorConstants.sol"; import {Memory} from "step/src/Memory.sol"; @@ -25,7 +24,7 @@ import {Memory} from "step/src/Memory.sol"; import {IDaveConsensus} from "./IDaveConsensus.sol"; import {Merkle} from "./Merkle.sol"; -/// @notice Consensus contract with Dave tournaments. +/// @notice Consensus contract with Dave tasks. /// /// @notice This contract validates only one application, /// which read inputs from the InputBox contract. @@ -57,8 +56,8 @@ contract DaveConsensus is IDaveConsensus, ERC165 { /// @notice The application contract address immutable _APP_CONTRACT; - /// @notice The contract used to instantiate tournaments - ITournamentFactory immutable _TOURNAMENT_FACTORY; + /// @notice The contract used to instantiate tasks + ITaskSpawner immutable _TASK_SPAWNER; /// @notice Deployment block number uint256 immutable _DEPLOYMENT_BLOCK_NUMBER = block.number; @@ -72,8 +71,8 @@ contract DaveConsensus is IDaveConsensus, ERC165 { /// @notice Input index (exclusive) upper bound of the current sealed epoch uint256 _inputIndexUpperBound; - /// @notice Current sealed epoch tournament - ITournament _tournament; + /// @notice Current sealed epoch task + ITask _task; /// @notice Settled output trees' merkle root hash mapping(bytes32 => bool) _outputsMerkleRoots; @@ -81,42 +80,42 @@ contract DaveConsensus is IDaveConsensus, ERC165 { constructor( IInputBox inputBox, address appContract, - ITournamentFactory tournamentFactory, + ITaskSpawner taskSpawner, Machine.Hash initialMachineStateHash ) { // Initialize immutable variables _INPUT_BOX = inputBox; _APP_CONTRACT = appContract; - _TOURNAMENT_FACTORY = tournamentFactory; - emit ConsensusCreation(inputBox, appContract, tournamentFactory); + _TASK_SPAWNER = taskSpawner; + emit ConsensusCreation(inputBox, appContract, taskSpawner); // Initialize first sealed epoch uint256 inputIndexUpperBound = inputBox.getNumberOfInputs(appContract); _inputIndexUpperBound = inputIndexUpperBound; - ITournament tournament = tournamentFactory.instantiate(initialMachineStateHash, this); - _tournament = tournament; - emit EpochSealed(0, 0, inputIndexUpperBound, initialMachineStateHash, bytes32(0), tournament); + ITask task = taskSpawner.spawn(initialMachineStateHash, this); + _task = task; + emit EpochSealed(0, 0, inputIndexUpperBound, initialMachineStateHash, bytes32(0), task); } function canSettle() external view override - returns (bool isFinished, uint256 epochNumber, Tree.Node winnerCommitment) + returns (bool isFinished, uint256 epochNumber, Machine.Hash finalState) { - (isFinished, winnerCommitment,) = _tournament.arbitrationResult(); + (isFinished, finalState) = _task.result(); epochNumber = _epochNumber; } function settle(uint256 epochNumber, bytes32 outputsMerkleRoot, bytes32[] calldata proof) external override { - // Check tournament settlement + // Check task settlement require(epochNumber == _epochNumber, IncorrectEpochNumber(epochNumber, _epochNumber)); - // Check tournament finished - (bool isFinished,, Machine.Hash finalMachineStateHash) = _tournament.arbitrationResult(); + // Check task finished + (bool isFinished, Machine.Hash finalMachineStateHash) = _task.result(); require(isFinished, TournamentNotFinishedYet()); - ITournament oldTournament = _tournament; - _tournament = ITournament(address(0)); + ITask oldTask = _task; + _task = ITask(address(0)); // Check outputs Merkle root _validateOutputTree(finalMachineStateHash, outputsMerkleRoot, proof); @@ -127,8 +126,8 @@ contract DaveConsensus is IDaveConsensus, ERC165 { _inputIndexUpperBound = _INPUT_BOX.getNumberOfInputs(_APP_CONTRACT); _outputsMerkleRoots[outputsMerkleRoot] = true; - // Start new tournament - _tournament = _TOURNAMENT_FACTORY.instantiate(finalMachineStateHash, this); + // Start new task + _task = _TASK_SPAWNER.spawn(finalMachineStateHash, this); emit EpochSealed( _epochNumber, @@ -136,10 +135,11 @@ contract DaveConsensus is IDaveConsensus, ERC165 { _inputIndexUpperBound, finalMachineStateHash, outputsMerkleRoot, - _tournament + _task ); - oldTournament.tryRecoveringBond(); + _tryCleanup(oldTask); + } function getCurrentSealedEpoch() @@ -150,13 +150,13 @@ contract DaveConsensus is IDaveConsensus, ERC165 { uint256 epochNumber, uint256 inputIndexLowerBound, uint256 inputIndexUpperBound, - ITournament tournament + ITask task ) { epochNumber = _epochNumber; inputIndexLowerBound = _inputIndexLowerBound; inputIndexUpperBound = _inputIndexUpperBound; - tournament = _tournament; + task = _task; } function getInputBox() external view override returns (IInputBox) { @@ -167,8 +167,8 @@ contract DaveConsensus is IDaveConsensus, ERC165 { return _APP_CONTRACT; } - function getTournamentFactory() external view override returns (ITournamentFactory) { - return _TOURNAMENT_FACTORY; + function getTaskSpawner() external view override returns (ITaskSpawner) { + return _TASK_SPAWNER; } function provideMerkleRootOfInput(uint256 inputIndexWithinEpoch, bytes calldata input) @@ -227,4 +227,12 @@ contract DaveConsensus is IDaveConsensus, ERC165 { require(machineStateHash == allegedStateHash, InvalidOutputsMerkleRootProof(finalMachineStateHash)); } + + function _tryCleanup(ITask task) internal { + if (address(task) == address(0)) { + return; + } + + try task.cleanup() returns (bool) {} catch {} + } } diff --git a/cartesi-rollups/contracts/src/IDaveConsensus.sol b/cartesi-rollups/contracts/src/IDaveConsensus.sol index d43b2c5a..5b5f1dc1 100644 --- a/cartesi-rollups/contracts/src/IDaveConsensus.sol +++ b/cartesi-rollups/contracts/src/IDaveConsensus.sol @@ -9,13 +9,12 @@ import { import {IInputBox} from "cartesi-rollups-contracts-2.1.1/src/inputs/IInputBox.sol"; import {IDataProvider} from "prt-contracts/IDataProvider.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; -import {ITournamentFactory} from "prt-contracts/ITournamentFactory.sol"; +import {ITask} from "prt-contracts/ITask.sol"; +import {ITaskSpawner} from "prt-contracts/ITaskSpawner.sol"; import {Machine} from "prt-contracts/types/Machine.sol"; -import {Tree} from "prt-contracts/types/Tree.sol"; -/// @notice Consensus contract with Dave tournaments. +/// @notice Consensus contract with Dave tasks. /// /// @notice This contract validates only one application, /// which read inputs from the InputBox contract. @@ -41,8 +40,8 @@ interface IDaveConsensus is IDataProvider, IOutputsMerkleRootValidator { /// @notice Consensus contract was created /// @param inputBox the input box contract /// @param appContract the application contract - /// @param tournamentFactory the tournament factory contract - event ConsensusCreation(IInputBox inputBox, address appContract, ITournamentFactory tournamentFactory); + /// @param taskSpawner the task spawner contract + event ConsensusCreation(IInputBox inputBox, address appContract, ITaskSpawner taskSpawner); /// @notice An epoch was sealed /// @param epochNumber the sealed epoch number @@ -50,14 +49,14 @@ interface IDaveConsensus is IDataProvider, IOutputsMerkleRootValidator { /// @param inputIndexUpperBound the input index (exclusive) upper bound in the sealed epoch /// @param initialMachineStateHash the initial machine state hash /// @param outputsMerkleRoot the Merkle root hash of the outputs tree - /// @param tournament the sealed epoch tournament contract + /// @param task the sealed epoch task contract event EpochSealed( uint256 epochNumber, uint256 inputIndexLowerBound, uint256 inputIndexUpperBound, Machine.Hash initialMachineStateHash, bytes32 outputsMerkleRoot, - ITournament tournament + ITask task ); /// @notice Received epoch number is different from actual @@ -65,7 +64,7 @@ interface IDaveConsensus is IDataProvider, IOutputsMerkleRootValidator { /// @param actual The actual epoch number in storage error IncorrectEpochNumber(uint256 received, uint256 actual); - /// @notice Tournament is not finished yet + /// @notice Task is not finished yet error TournamentNotFinishedYet(); /// @notice Hash of received input blob is different from stored on-chain @@ -95,14 +94,14 @@ interface IDaveConsensus is IDataProvider, IOutputsMerkleRootValidator { /// @notice Get the address of the application contract. function getApplicationContract() external view returns (address); - /// @notice Get the tournament factory contract used to instantiate root tournaments. - function getTournamentFactory() external view returns (ITournamentFactory); + /// @notice Get the task spawner contract used to instantiate root tasks. + function getTaskSpawner() external view returns (ITaskSpawner); - /// @notice Get the current sealed epoch number, boundaries, and tournament. + /// @notice Get the current sealed epoch number, boundaries, and task. /// @param epochNumber The epoch number /// @param inputIndexLowerBound The epoch input index (inclusive) lower bound /// @param inputIndexUpperBound The epoch input index (exclusive) upper bound - /// @param tournament The tournament that will decide the post-epoch state + /// @param task The task that will decide the post-epoch state function getCurrentSealedEpoch() external view @@ -110,14 +109,14 @@ interface IDaveConsensus is IDataProvider, IOutputsMerkleRootValidator { uint256 epochNumber, uint256 inputIndexLowerBound, uint256 inputIndexUpperBound, - ITournament tournament + ITask task ); /// @notice Check whether the current sealed epoch can be settled. - /// @return isFinished Whether the current sealed epoch tournament has finished yet + /// @return isFinished Whether the current sealed epoch task has finished yet /// @return epochNumber The current sealed epoch number - /// @return winnerCommitment If the tournament has finished, the winning commitment - function canSettle() external view returns (bool isFinished, uint256 epochNumber, Tree.Node winnerCommitment); + /// @return finalState If the task has finished, the final machine state + function canSettle() external view returns (bool isFinished, uint256 epochNumber, Machine.Hash finalState); /// @notice Settle the current sealed epoch. /// @param epochNumber The current sealed epoch number (used to avoid race conditions) diff --git a/cartesi-rollups/contracts/test/DaveAppFactory.t.sol b/cartesi-rollups/contracts/test/DaveAppFactory.t.sol index 7913e5b8..a794440f 100644 --- a/cartesi-rollups/contracts/test/DaveAppFactory.t.sol +++ b/cartesi-rollups/contracts/test/DaveAppFactory.t.sol @@ -11,22 +11,22 @@ import {IInputBox} from "cartesi-rollups-contracts-2.1.1/src/inputs/IInputBox.so import {InputBox} from "cartesi-rollups-contracts-2.1.1/src/inputs/InputBox.sol"; import {IDataProvider} from "prt-contracts/IDataProvider.sol"; -import {ITournamentFactory} from "prt-contracts/ITournamentFactory.sol"; -import {ITournament} from "prt-contracts/ITournamentFactory.sol"; +import {ITask} from "prt-contracts/ITask.sol"; +import {ITaskSpawner} from "prt-contracts/ITaskSpawner.sol"; import {Machine} from "prt-contracts/types/Machine.sol"; import {DaveAppFactory} from "src/DaveAppFactory.sol"; import {IDaveAppFactory} from "src/IDaveAppFactory.sol"; import {IDaveConsensus} from "src/IDaveConsensus.sol"; -contract MockTournamentFactory is ITournamentFactory { - address tournamentAddress; +contract MockTaskSpawner is ITaskSpawner { + address taskAddress; function setAddress(address _addr) external { - tournamentAddress = _addr; + taskAddress = _addr; } - function instantiate(Machine.Hash, IDataProvider) external view returns (ITournament) { - return ITournament(tournamentAddress); + function spawn(Machine.Hash, IDataProvider) external view returns (ITask) { + return ITask(taskAddress); } } @@ -34,18 +34,18 @@ contract DaveConsensusFactoryTest is Test { IApplicationFactory _appFactory; IDaveAppFactory _daveAppFactory; IInputBox _inputBox; - MockTournamentFactory _tournamentFactory; + MockTaskSpawner _taskSpawner; Machine.Hash _initialMachineStateHash; function setUp() external { _inputBox = new InputBox(); _appFactory = new ApplicationFactory(); - _tournamentFactory = new MockTournamentFactory(); - _daveAppFactory = new DaveAppFactory(_inputBox, _appFactory, _tournamentFactory); + _taskSpawner = new MockTaskSpawner(); + _daveAppFactory = new DaveAppFactory(_inputBox, _appFactory, _taskSpawner); _initialMachineStateHash = Machine.Hash.wrap(keccak256("foo")); } - function testNewDaveApp(address randomTournamentAddress, bytes32 templateHash, bytes32 salt) external { + function testNewDaveApp(address randomTaskAddress, bytes32 templateHash, bytes32 salt) external { IApplication appContract; IDaveConsensus daveConsensus; @@ -58,7 +58,7 @@ contract DaveConsensusFactoryTest is Test { // Deploy app and Dave consensus addresses vm.recordLogs(); - _tournamentFactory.setAddress(randomTournamentAddress); + _taskSpawner.setAddress(randomTaskAddress); (appContract, daveConsensus) = _daveAppFactory.newDaveApp(templateHash, salt); // Check if addresses match those pre-calculated ones @@ -88,7 +88,7 @@ contract DaveConsensusFactoryTest is Test { } else if ( entry.emitter == address(daveConsensus) && entry.topics[0] == IDaveConsensus.EpochSealed.selector ) { - _checkEpochSealedData(entry.data, templateHash, randomTournamentAddress); + _checkEpochSealedData(entry.data, templateHash, randomTaskAddress); } else if ( entry.emitter == address(_appFactory) && entry.topics[0] == IApplicationFactory.ApplicationCreated.selector @@ -114,17 +114,17 @@ contract DaveConsensusFactoryTest is Test { } // Check current sealed epoch - (uint256 epochNumber, uint256 inputIndexLowerBound, uint256 inputIndexUpperBound, ITournament tournament) = + (uint256 epochNumber, uint256 inputIndexLowerBound, uint256 inputIndexUpperBound, ITask task) = daveConsensus.getCurrentSealedEpoch(); assertEq(epochNumber, 0); assertEq(inputIndexLowerBound, 0); assertEq(inputIndexUpperBound, 0); - assertEq(address(tournament), randomTournamentAddress); + assertEq(address(task), randomTaskAddress); // Check getters assertEq(address(daveConsensus.getInputBox()), address(_inputBox)); assertEq(address(daveConsensus.getApplicationContract()), address(appContract)); - assertEq(address(daveConsensus.getTournamentFactory()), address(_tournamentFactory)); + assertEq(address(daveConsensus.getTaskSpawner()), address(_taskSpawner)); { address appContractAddress; @@ -142,7 +142,7 @@ contract DaveConsensusFactoryTest is Test { _daveAppFactory.newDaveApp(templateHash, salt); } - function _checkEpochSealedData(bytes memory data, bytes32 templateHash, address randomTournamentAddress) + function _checkEpochSealedData(bytes memory data, bytes32 templateHash, address randomTaskAddress) internal pure { @@ -152,7 +152,7 @@ contract DaveConsensusFactoryTest is Test { uint256 inputIndexUpperBound, bytes32 initialMachineStateHash, bytes32 outputTreeHash, - address tournamentAddress + address taskAddress ) = abi.decode(data, (uint256, uint256, uint256, bytes32, bytes32, address)); assertEq(epochNumber, 0); @@ -160,6 +160,6 @@ contract DaveConsensusFactoryTest is Test { assertEq(inputIndexUpperBound, 0); assertEq(initialMachineStateHash, templateHash); assertEq(outputTreeHash, bytes32(0)); - assertEq(tournamentAddress, randomTournamentAddress); + assertEq(taskAddress, randomTaskAddress); } } diff --git a/cartesi-rollups/contracts/test/DaveConsensus.t.sol b/cartesi-rollups/contracts/test/DaveConsensus.t.sol index da91e095..ea19372f 100644 --- a/cartesi-rollups/contracts/test/DaveConsensus.t.sol +++ b/cartesi-rollups/contracts/test/DaveConsensus.t.sol @@ -17,13 +17,9 @@ import {InputBox} from "cartesi-rollups-contracts-2.1.1/src/inputs/InputBox.sol" import {LibMerkle32} from "cartesi-rollups-contracts-2.1.1/src/library/LibMerkle32.sol"; import {IDataProvider} from "prt-contracts/IDataProvider.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; -import {ITournamentFactory} from "prt-contracts/ITournamentFactory.sol"; -import {Clock} from "prt-contracts/tournament/libs/Clock.sol"; -import {Match} from "prt-contracts/tournament/libs/Match.sol"; -import {Time} from "prt-contracts/tournament/libs/Time.sol"; +import {ITask} from "prt-contracts/ITask.sol"; +import {ITaskSpawner} from "prt-contracts/ITaskSpawner.sol"; import {Machine} from "prt-contracts/types/Machine.sol"; -import {Tree} from "prt-contracts/types/Tree.sol"; import {EmulatorConstants} from "step/src/EmulatorConstants.sol"; import {Memory} from "step/src/Memory.sol"; @@ -44,11 +40,10 @@ contract MerkleProxy { } } -contract MockTournament is ITournament { +contract MockTask is ITask { Machine.Hash immutable _INITIAL_STATE; IDataProvider immutable _PROVIDER; bool _finished; - Tree.Node _winnerCommitment; Machine.Hash _finalState; constructor(Machine.Hash initialState, IDataProvider provider) { @@ -56,9 +51,8 @@ contract MockTournament is ITournament { _PROVIDER = provider; } - function finish(Tree.Node winnerCommitment, Machine.Hash finalState) external { + function finish(Machine.Hash finalState) external { _finished = true; - _winnerCommitment = winnerCommitment; _finalState = finalState; } @@ -70,136 +64,35 @@ contract MockTournament is ITournament { return _PROVIDER; } - function arbitrationResult() - external - view - returns (bool finished, Tree.Node winnerCommitment, Machine.Hash finalState) - { + function result() external view returns (bool finished, Machine.Hash finalState) { finished = _finished; - winnerCommitment = _winnerCommitment; finalState = _finalState; } - function tryRecoveringBond() external pure override returns (bool) { - return true; - } - - error NotImplemented(); - - function bondValue() external pure override returns (uint256) { - revert NotImplemented(); - } - - function joinTournament(Machine.Hash, bytes32[] calldata, Tree.Node, Tree.Node) external payable override { - revert NotImplemented(); - } - - function advanceMatch(Match.Id calldata, Tree.Node, Tree.Node, Tree.Node, Tree.Node) external pure override { - revert NotImplemented(); - } - - function winMatchByTimeout(Match.Id calldata, Tree.Node, Tree.Node) external pure override { - revert NotImplemented(); - } - - function eliminateMatchByTimeout(Match.Id calldata) external pure override { - revert NotImplemented(); - } - - function sealInnerMatchAndCreateInnerTournament( - Match.Id calldata, - Tree.Node, - Tree.Node, - Machine.Hash, - bytes32[] calldata - ) external pure override { - revert NotImplemented(); - } - - function winInnerTournament(ITournament, Tree.Node, Tree.Node) external pure override { - revert NotImplemented(); - } - - function eliminateInnerTournament(ITournament) external pure override { - revert NotImplemented(); - } - - function sealLeafMatch(Match.Id calldata, Tree.Node, Tree.Node, Machine.Hash, bytes32[] calldata) - external - pure - override - { - revert NotImplemented(); - } - - function winLeafMatch(Match.Id calldata, Tree.Node, Tree.Node, bytes calldata) external pure override { - revert NotImplemented(); - } - - function canBeEliminated() external pure override returns (bool) { - revert NotImplemented(); - } - - function innerTournamentWinner() external pure override returns (bool, Tree.Node, Tree.Node, Clock.State memory) { - revert NotImplemented(); - } - - function tournamentArguments() external pure override returns (TournamentArguments memory) { - revert NotImplemented(); - } - - function canWinMatchByTimeout(Match.Id calldata) external pure override returns (bool) { - revert NotImplemented(); - } - - function getCommitment(Tree.Node) external pure override returns (Clock.State memory, Machine.Hash) { - revert NotImplemented(); - } - - function getMatch(Match.IdHash) external pure override returns (Match.State memory) { - revert NotImplemented(); - } - - function getMatchCycle(Match.IdHash) external pure override returns (uint256) { - revert NotImplemented(); - } - - function tournamentLevelConstants() external pure override returns (uint64, uint64, uint64, uint64) { - revert NotImplemented(); - } - - function isClosed() external pure override returns (bool) { - revert NotImplemented(); - } - - function isFinished() external view override returns (bool) { + function cleanup() external returns (bool) { return _finished; } - - function timeFinished() external pure override returns (bool, Time.Instant) { - revert NotImplemented(); - } } -contract MockTournamentFactory is ITournamentFactory { - MockTournament[] _mockTournaments; +contract MockTaskSpawner is ITaskSpawner { + MockTask[] _mockTasks; bytes32 _salt; error IndexOutOfBounds(); - function instantiate(Machine.Hash initialState, IDataProvider provider) external returns (ITournament) { - MockTournament mockTournament = new MockTournament{salt: _salt}(initialState, provider); - _mockTournaments.push(mockTournament); - return mockTournament; + function spawn(Machine.Hash initialState, IDataProvider provider) external returns (ITask) { + MockTask mockTask = new MockTask{salt: _salt}(initialState, provider); + _mockTasks.push(mockTask); + return mockTask; } - function calculateTournamentAddress(Machine.Hash initialState, IDataProvider provider) + function calculateTaskAddress(Machine.Hash initialState, IDataProvider provider) external view returns (address) { return Create2.computeAddress( - _salt, keccak256(abi.encodePacked(type(MockTournament).creationCode, abi.encode(initialState, provider))) + _salt, keccak256(abi.encodePacked(type(MockTask).creationCode, abi.encode(initialState, provider))) ); } @@ -207,13 +100,13 @@ contract MockTournamentFactory is ITournamentFactory { _salt = salt; } - function getNumberOfMockTournaments() external view returns (uint256) { - return _mockTournaments.length; + function getNumberOfMockTasks() external view returns (uint256) { + return _mockTasks.length; } - function getMockTournament(uint256 index) external view returns (MockTournament) { - if (index < _mockTournaments.length) { - return _mockTournaments[index]; + function getMockTask(uint256 index) external view returns (MockTask) { + if (index < _mockTasks.length) { + return _mockTasks[index]; } else { revert IndexOutOfBounds(); } @@ -232,22 +125,22 @@ contract LibMerkle32Wrapper { contract DaveConsensusTest is Test { IInputBox _inputBox; - MockTournamentFactory _mockTournamentFactory; + MockTaskSpawner _mockTaskSpawner; MerkleProxy _merkleProxy; function setUp() external { _inputBox = new InputBox(); - _mockTournamentFactory = new MockTournamentFactory(); + _mockTaskSpawner = new MockTaskSpawner(); _merkleProxy = new MerkleProxy(); } - function testMockTournamentFactory() external view { - assertEq(_mockTournamentFactory.getNumberOfMockTournaments(), 0); + function testMockTaskSpawner() external view { + assertEq(_mockTaskSpawner.getNumberOfMockTasks(), 0); } - function testMockTournamentFactory(uint256 index) external { - vm.expectRevert(MockTournamentFactory.IndexOutOfBounds.selector); - _mockTournamentFactory.getMockTournament(index); + function testMockTaskSpawner(uint256 index) external { + vm.expectRevert(MockTaskSpawner.IndexOutOfBounds.selector); + _mockTaskSpawner.getMockTask(index); } function testConstructorAndSettle( @@ -255,7 +148,6 @@ contract DaveConsensusTest is Test { bytes32[3] calldata outputsMerkleRoots, uint256[2] memory inputCounts, bytes32[3] calldata salts, - Tree.Node[2] calldata winnerCommitments, uint256 deploymentBlockNumber ) external { vm.roll(deploymentBlockNumber); @@ -269,32 +161,30 @@ contract DaveConsensusTest is Test { (Machine.Hash state0,,) = _statesAndProofs(outputsMerkleRoots[0]); DaveConsensus daveConsensus; - MockTournament mockTournament; + MockTask mockTask; { address daveConsensusAddress = _calculateNewDaveConsensus(appContract, state0, salts[0]); - _mockTournamentFactory.setSalt(salts[1]); - address mockTournamentAddress = - _mockTournamentFactory.calculateTournamentAddress(state0, IDataProvider(daveConsensusAddress)); + _mockTaskSpawner.setSalt(salts[1]); + address mockTaskAddress = + _mockTaskSpawner.calculateTaskAddress(state0, IDataProvider(daveConsensusAddress)); vm.expectEmit(daveConsensusAddress); - emit IDaveConsensus.ConsensusCreation(_inputBox, appContract, _mockTournamentFactory); + emit IDaveConsensus.ConsensusCreation(_inputBox, appContract, _mockTaskSpawner); vm.expectEmit(daveConsensusAddress); - emit IDaveConsensus.EpochSealed( - 0, 0, inputCounts[0], state0, bytes32(0), ITournament(mockTournamentAddress) - ); + emit IDaveConsensus.EpochSealed(0, 0, inputCounts[0], state0, bytes32(0), ITask(mockTaskAddress)); daveConsensus = _newDaveConsensus(appContract, state0, salts[0]); assertEq(address(daveConsensus), daveConsensusAddress); assertEq(address(daveConsensus.getInputBox()), address(_inputBox)); assertEq(daveConsensus.getApplicationContract(), appContract); - assertEq(address(daveConsensus.getTournamentFactory()), address(_mockTournamentFactory)); + assertEq(address(daveConsensus.getTaskSpawner()), address(_mockTaskSpawner)); assertEq(daveConsensus.getDeploymentBlockNumber(), deploymentBlockNumber); - mockTournament = MockTournament(mockTournamentAddress); + mockTask = MockTask(mockTaskAddress); } { @@ -311,41 +201,39 @@ contract DaveConsensusTest is Test { uint256 epochNumber; uint256 inputIndexLowerBound; uint256 inputIndexUpperBound; - ITournament tournament; + ITask task; - (epochNumber, inputIndexLowerBound, inputIndexUpperBound, tournament) = + (epochNumber, inputIndexLowerBound, inputIndexUpperBound, task) = daveConsensus.getCurrentSealedEpoch(); assertEq(epochNumber, 0); assertEq(inputIndexLowerBound, 0); assertEq(inputIndexUpperBound, inputCounts[0]); - assertEq(address(tournament), address(mockTournament)); + assertEq(address(task), address(mockTask)); } - assertEq(_mockTournamentFactory.getNumberOfMockTournaments(), 1); - assertEq(address(_mockTournamentFactory.getMockTournament(0)), address(mockTournament)); + assertEq(_mockTaskSpawner.getNumberOfMockTasks(), 1); + assertEq(address(_mockTaskSpawner.getMockTask(0)), address(mockTask)); - assertEq(Machine.Hash.unwrap(mockTournament.getInitialState()), Machine.Hash.unwrap(state0)); - assertEq(address(mockTournament.getProvider()), address(daveConsensus)); + assertEq(Machine.Hash.unwrap(mockTask.getInitialState()), Machine.Hash.unwrap(state0)); + assertEq(address(mockTask.getProvider()), address(daveConsensus)); { - (bool isFinished,,) = mockTournament.arbitrationResult(); + (bool isFinished,) = mockTask.result(); assertFalse(isFinished); } (Machine.Hash state1,,) = _statesAndProofs(outputsMerkleRoots[1]); - mockTournament.finish(winnerCommitments[0], state1); + mockTask.finish(state1); { bool isFinished; - Tree.Node winnerCommitmentTmp; Machine.Hash finalStateTmp; - (isFinished, winnerCommitmentTmp, finalStateTmp) = mockTournament.arbitrationResult(); + (isFinished, finalStateTmp) = mockTask.result(); assertTrue(isFinished); - assertEq(Tree.Node.unwrap(winnerCommitmentTmp), Tree.Node.unwrap(winnerCommitments[0])); assertEq(Machine.Hash.unwrap(finalStateTmp), Machine.Hash.unwrap(state1)); } @@ -364,23 +252,23 @@ contract DaveConsensusTest is Test { _addInputs(appContract, inputCounts[1]); { - _mockTournamentFactory.setSalt(salts[2]); - address mockTournamentAddress = _mockTournamentFactory.calculateTournamentAddress(state1, daveConsensus); + _mockTaskSpawner.setSalt(salts[2]); + address mockTaskAddress = _mockTaskSpawner.calculateTaskAddress(state1, daveConsensus); (, bytes32[] memory proof1, bytes32 leaf1) = _statesAndProofs(outputsMerkleRoots[1]); vm.expectEmit(address(daveConsensus)); emit IDaveConsensus.EpochSealed( - 1, inputCounts[0], inputCounts[0] + inputCounts[1], state1, leaf1, ITournament(mockTournamentAddress) + 1, inputCounts[0], inputCounts[0] + inputCounts[1], state1, leaf1, ITask(mockTaskAddress) ); daveConsensus.settle(0, leaf1, proof1); - assertEq(_mockTournamentFactory.getNumberOfMockTournaments(), 2); + assertEq(_mockTaskSpawner.getNumberOfMockTasks(), 2); - mockTournament = _mockTournamentFactory.getMockTournament(1); + mockTask = _mockTaskSpawner.getMockTask(1); - assertEq(address(mockTournament), mockTournamentAddress); + assertEq(address(mockTask), mockTaskAddress); } { @@ -397,22 +285,22 @@ contract DaveConsensusTest is Test { uint256 epochNumber; uint256 inputIndexLowerBound; uint256 inputIndexUpperBound; - ITournament tournament; + ITask task; - (epochNumber, inputIndexLowerBound, inputIndexUpperBound, tournament) = + (epochNumber, inputIndexLowerBound, inputIndexUpperBound, task) = daveConsensus.getCurrentSealedEpoch(); assertEq(epochNumber, 1); assertEq(inputIndexLowerBound, inputCounts[0]); assertEq(inputIndexUpperBound, inputCounts[0] + inputCounts[1]); - assertEq(address(tournament), address(mockTournament)); + assertEq(address(task), address(mockTask)); } - assertEq(Machine.Hash.unwrap(mockTournament.getInitialState()), Machine.Hash.unwrap(state1)); - assertEq(address(mockTournament.getProvider()), address(daveConsensus)); + assertEq(Machine.Hash.unwrap(mockTask.getInitialState()), Machine.Hash.unwrap(state1)); + assertEq(address(mockTask.getProvider()), address(daveConsensus)); { - (bool isFinished,,) = mockTournament.arbitrationResult(); + (bool isFinished,) = mockTask.result(); assertFalse(isFinished); } @@ -420,17 +308,15 @@ contract DaveConsensusTest is Test { assertTrue(daveConsensus.isOutputsMerkleRootValid(appContract, outputsMerkleRoots[1])); (Machine.Hash state2,,) = _statesAndProofs(outputsMerkleRoots[2]); - mockTournament.finish(winnerCommitments[1], state2); + mockTask.finish(state2); { bool isFinished; - Tree.Node winnerCommitmentTmp; Machine.Hash finalStateTmp; - (isFinished, winnerCommitmentTmp, finalStateTmp) = mockTournament.arbitrationResult(); + (isFinished, finalStateTmp) = mockTask.result(); assertTrue(isFinished); - assertEq(Tree.Node.unwrap(winnerCommitmentTmp), Tree.Node.unwrap(winnerCommitments[1])); assertEq(Machine.Hash.unwrap(finalStateTmp), Machine.Hash.unwrap(state2)); } } @@ -450,7 +336,7 @@ contract DaveConsensusTest is Test { _addInputs(appContract, inputCounts[0]); - _mockTournamentFactory.setSalt(salts[0]); + _mockTaskSpawner.setSalt(salts[0]); DaveConsensus daveConsensus = _newDaveConsensus(appContract, states[0], salts[1]); @@ -473,7 +359,7 @@ contract DaveConsensusTest is Test { ) external { bytes[] memory inputs = _addInputs(appContract, payloads); - _mockTournamentFactory.setSalt(salts[0]); + _mockTaskSpawner.setSalt(salts[0]); DaveConsensus daveConsensus = _newDaveConsensus(appContract, initialState, salts[1]); @@ -575,7 +461,7 @@ contract DaveConsensusTest is Test { keccak256( abi.encodePacked( type(DaveConsensus).creationCode, - abi.encode(_inputBox, appContract, _mockTournamentFactory, initialState) + abi.encode(_inputBox, appContract, _mockTaskSpawner, initialState) ) ) ); @@ -585,7 +471,7 @@ contract DaveConsensusTest is Test { internal returns (DaveConsensus) { - return new DaveConsensus{salt: salt}(_inputBox, appContract, _mockTournamentFactory, initialState); + return new DaveConsensus{salt: salt}(_inputBox, appContract, _mockTaskSpawner, initialState); } function _statesAndProofs(bytes32 outputsMerkleRoot) private returns (Machine.Hash, bytes32[] memory, bytes32) { diff --git a/prt/contracts/script/Deployment.s.sol b/prt/contracts/script/Deployment.s.sol index df9c0f6a..fd362dd9 100644 --- a/prt/contracts/script/Deployment.s.sol +++ b/prt/contracts/script/Deployment.s.sol @@ -26,7 +26,7 @@ import { import {Tournament} from "src/tournament/Tournament.sol"; import { MultiLevelTournamentFactory -} from "src/tournament/factories/MultiLevelTournamentFactory.sol"; +} from "src/tournament/MultiLevelTournamentFactory.sol"; import {Time} from "src/tournament/libs/Time.sol"; type Milliseconds is uint64; diff --git a/prt/contracts/src/ITask.sol b/prt/contracts/src/ITask.sol new file mode 100644 index 00000000..cbdbc10c --- /dev/null +++ b/prt/contracts/src/ITask.sol @@ -0,0 +1,22 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import {Machine} from "prt-contracts/types/Machine.sol"; + +/// @notice Task interface for asynchronous proof systems. +interface ITask { + /// @notice Get the task result. + /// @return finished Whether the task has finished + /// @return finalState The finalized machine state (if finished) + function result() + external + view + returns (bool finished, Machine.Hash finalState); + + /// @notice Best-effort cleanup hook for post-settlement actions. + /// @dev Should be safe to call multiple times and return false if not applicable. + /// @return cleaned Whether any cleanup action succeeded. + function cleanup() external returns (bool cleaned); +} diff --git a/prt/contracts/src/ITournamentFactory.sol b/prt/contracts/src/ITaskSpawner.sol similarity index 52% rename from prt/contracts/src/ITournamentFactory.sol rename to prt/contracts/src/ITaskSpawner.sol index c835bca5..5cb39eac 100644 --- a/prt/contracts/src/ITournamentFactory.sol +++ b/prt/contracts/src/ITaskSpawner.sol @@ -4,13 +4,12 @@ pragma solidity ^0.8.17; import {IDataProvider} from "prt-contracts/IDataProvider.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; +import {ITask} from "prt-contracts/ITask.sol"; import {Machine} from "prt-contracts/types/Machine.sol"; -interface ITournamentFactory { - event TournamentCreated(ITournament tournament); - - function instantiate(Machine.Hash initialState, IDataProvider provider) +/// @notice Spawner interface for tasks/proof systems. +interface ITaskSpawner { + function spawn(Machine.Hash initial, IDataProvider provider) external - returns (ITournament); + returns (ITask); } diff --git a/prt/contracts/src/tournament/factories/IMultiLevelTournamentFactory.sol b/prt/contracts/src/tournament/IMultiLevelTournamentFactory.sol similarity index 79% rename from prt/contracts/src/tournament/factories/IMultiLevelTournamentFactory.sol rename to prt/contracts/src/tournament/IMultiLevelTournamentFactory.sol index 2962c8a1..abd57add 100644 --- a/prt/contracts/src/tournament/factories/IMultiLevelTournamentFactory.sol +++ b/prt/contracts/src/tournament/IMultiLevelTournamentFactory.sol @@ -4,13 +4,13 @@ pragma solidity ^0.8.17; import {IDataProvider} from "prt-contracts/IDataProvider.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; -import {ITournamentFactory} from "prt-contracts/ITournamentFactory.sol"; +import {ITaskSpawner} from "prt-contracts/ITaskSpawner.sol"; +import {ITournament} from "prt-contracts/tournament/ITournament.sol"; import {Time} from "prt-contracts/tournament/libs/Time.sol"; import {Machine} from "prt-contracts/types/Machine.sol"; import {Tree} from "prt-contracts/types/Tree.sol"; -interface IMultiLevelTournamentFactory is ITournamentFactory { +interface IMultiLevelTournamentFactory is ITaskSpawner { function instantiateInner( Machine.Hash _initialHash, Tree.Node _contestedCommitmentOne, diff --git a/prt/contracts/src/ITournament.sol b/prt/contracts/src/tournament/ITournament.sol similarity index 99% rename from prt/contracts/src/ITournament.sol rename to prt/contracts/src/tournament/ITournament.sol index 33c71ba5..3da353a5 100644 --- a/prt/contracts/src/ITournament.sol +++ b/prt/contracts/src/tournament/ITournament.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.17; import {IDataProvider} from "prt-contracts/IDataProvider.sol"; +import {ITask} from "prt-contracts/ITask.sol"; import {IStateTransition} from "prt-contracts/IStateTransition.sol"; import {Clock} from "prt-contracts/tournament/libs/Clock.sol"; import {Commitment} from "prt-contracts/tournament/libs/Commitment.sol"; @@ -13,7 +14,7 @@ import {Machine} from "prt-contracts/types/Machine.sol"; import {Tree} from "prt-contracts/types/Tree.sol"; /// @notice Tournament interface -interface ITournament { +interface ITournament is ITask { // // Types // diff --git a/prt/contracts/src/tournament/factories/MultiLevelTournamentFactory.sol b/prt/contracts/src/tournament/MultiLevelTournamentFactory.sol similarity index 93% rename from prt/contracts/src/tournament/factories/MultiLevelTournamentFactory.sol rename to prt/contracts/src/tournament/MultiLevelTournamentFactory.sol index 975c8d03..6887f529 100644 --- a/prt/contracts/src/tournament/factories/MultiLevelTournamentFactory.sol +++ b/prt/contracts/src/tournament/MultiLevelTournamentFactory.sol @@ -5,10 +5,13 @@ pragma solidity ^0.8.17; import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; -import {IMultiLevelTournamentFactory} from "./IMultiLevelTournamentFactory.sol"; +import { + IMultiLevelTournamentFactory +} from "prt-contracts/tournament/IMultiLevelTournamentFactory.sol"; import {IDataProvider} from "prt-contracts/IDataProvider.sol"; +import {ITask} from "prt-contracts/ITask.sol"; import {IStateTransition} from "prt-contracts/IStateTransition.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; +import {ITournament} from "prt-contracts/tournament/ITournament.sol"; import { ITournamentParametersProvider } from "prt-contracts/arbitration-config/ITournamentParametersProvider.sol"; @@ -24,10 +27,20 @@ import {Tree} from "prt-contracts/types/Tree.sol"; contract MultiLevelTournamentFactory is IMultiLevelTournamentFactory { using Clones for address; + event TournamentCreated(ITournament tournament); + Tournament immutable IMPL; ITournamentParametersProvider immutable TOURNAMENT_PARAMETERS_PROVIDER; IStateTransition immutable STATE_TRANSITION; + function spawn(Machine.Hash _initialHash, IDataProvider _provider) + external + override + returns (ITask) + { + return this.instantiate(_initialHash, _provider); + } + constructor( Tournament _impl, ITournamentParametersProvider _tournamentParametersProvider, @@ -40,7 +53,6 @@ contract MultiLevelTournamentFactory is IMultiLevelTournamentFactory { function instantiate(Machine.Hash _initialHash, IDataProvider _provider) external - override returns (ITournament) { ITournament _tournament = instantiateTop(_initialHash, _provider); diff --git a/prt/contracts/src/tournament/Tournament.sol b/prt/contracts/src/tournament/Tournament.sol index fa0e4bb5..0d438037 100644 --- a/prt/contracts/src/tournament/Tournament.sol +++ b/prt/contracts/src/tournament/Tournament.sol @@ -7,10 +7,11 @@ import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; import {Math} from "@openzeppelin-contracts-5.5.0/utils/math/Math.sol"; import {IStateTransition} from "prt-contracts/IStateTransition.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; +import {ITask} from "prt-contracts/ITask.sol"; +import {ITournament} from "prt-contracts/tournament/ITournament.sol"; import { IMultiLevelTournamentFactory -} from "prt-contracts/tournament/factories/IMultiLevelTournamentFactory.sol"; +} from "prt-contracts/tournament/IMultiLevelTournamentFactory.sol"; import {Clock} from "prt-contracts/tournament/libs/Clock.sol"; import {Commitment} from "prt-contracts/tournament/libs/Commitment.sol"; import {Gas} from "prt-contracts/tournament/libs/Gas.sol"; @@ -814,6 +815,30 @@ contract Tournament is ITournament { return (true, _danglingCommitment, _finalState); } + /// @inheritdoc ITask + function result() + external + view + override + returns (bool finished, Machine.Hash finalState) + { + (finished,, finalState) = this.arbitrationResult(); + } + + /// @inheritdoc ITask + /// @dev Best-effort bond recovery for finished tournaments. + function cleanup() external override returns (bool cleaned) { + if (!isFinished()) { + return false; + } + + try this.tryRecoveringBond() returns (bool ok) { + return ok; + } catch { + return false; + } + } + // // Internal functions // diff --git a/prt/contracts/src/tournament/libs/Commitment.sol b/prt/contracts/src/tournament/libs/Commitment.sol index bcb5e24f..602b9221 100644 --- a/prt/contracts/src/tournament/libs/Commitment.sol +++ b/prt/contracts/src/tournament/libs/Commitment.sol @@ -7,7 +7,7 @@ import { Hashes } from "@openzeppelin-contracts-5.5.0/utils/cryptography/Hashes.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; +import {ITournament} from "prt-contracts/tournament/ITournament.sol"; import {Machine} from "prt-contracts/types/Machine.sol"; import {Tree} from "prt-contracts/types/Tree.sol"; diff --git a/prt/contracts/src/tournament/libs/Match.sol b/prt/contracts/src/tournament/libs/Match.sol index 6a4defc4..397cc810 100644 --- a/prt/contracts/src/tournament/libs/Match.sol +++ b/prt/contracts/src/tournament/libs/Match.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.17; -import {ITournament} from "prt-contracts/ITournament.sol"; +import {ITournament} from "prt-contracts/tournament/ITournament.sol"; import {Commitment} from "prt-contracts/tournament/libs/Commitment.sol"; import {Machine} from "prt-contracts/types/Machine.sol"; import {Tree} from "prt-contracts/types/Tree.sol"; diff --git a/prt/contracts/test/BottomTournament.t.sol b/prt/contracts/test/BottomTournament.t.sol index 000f1f44..97e11747 100644 --- a/prt/contracts/test/BottomTournament.t.sol +++ b/prt/contracts/test/BottomTournament.t.sol @@ -15,7 +15,7 @@ pragma solidity ^0.8.0; import {Vm} from "forge-std-1.9.6/src/Vm.sol"; import {IStateTransition} from "src/IStateTransition.sol"; -import {ITournament} from "src/ITournament.sol"; +import {ITournament} from "src/tournament/ITournament.sol"; import { ArbitrationConstants } from "src/arbitration-config/ArbitrationConstants.sol"; @@ -24,7 +24,7 @@ import { } from "src/state-transition/CartesiStateTransition.sol"; import { MultiLevelTournamentFactory -} from "src/tournament/factories/MultiLevelTournamentFactory.sol"; +} from "src/tournament/MultiLevelTournamentFactory.sol"; import {Clock} from "src/tournament/libs/Clock.sol"; import {Match} from "src/tournament/libs/Match.sol"; import {Time} from "src/tournament/libs/Time.sol"; diff --git a/prt/contracts/test/MiddleTournament.t.sol b/prt/contracts/test/MiddleTournament.t.sol index 7c53bf8f..d3eb1063 100644 --- a/prt/contracts/test/MiddleTournament.t.sol +++ b/prt/contracts/test/MiddleTournament.t.sol @@ -14,13 +14,13 @@ pragma solidity ^0.8.0; import {Vm} from "forge-std-1.9.6/src/Vm.sol"; -import {ITournament} from "src/ITournament.sol"; +import {ITournament} from "src/tournament/ITournament.sol"; import { ArbitrationConstants } from "src/arbitration-config/ArbitrationConstants.sol"; import { MultiLevelTournamentFactory -} from "src/tournament/factories/MultiLevelTournamentFactory.sol"; +} from "src/tournament/MultiLevelTournamentFactory.sol"; import {Clock} from "src/tournament/libs/Clock.sol"; import {Match} from "src/tournament/libs/Match.sol"; import {Time} from "src/tournament/libs/Time.sol"; diff --git a/prt/contracts/test/TopTournament.t.sol b/prt/contracts/test/TopTournament.t.sol index d88d5936..9d8fd715 100644 --- a/prt/contracts/test/TopTournament.t.sol +++ b/prt/contracts/test/TopTournament.t.sol @@ -12,13 +12,13 @@ pragma solidity ^0.8.0; -import {ITournament} from "src/ITournament.sol"; +import {ITournament} from "src/tournament/ITournament.sol"; import { ArbitrationConstants } from "src/arbitration-config/ArbitrationConstants.sol"; import { MultiLevelTournamentFactory -} from "src/tournament/factories/MultiLevelTournamentFactory.sol"; +} from "src/tournament/MultiLevelTournamentFactory.sol"; import {Match} from "src/tournament/libs/Match.sol"; import {Time} from "src/tournament/libs/Time.sol"; import {Machine} from "src/types/Machine.sol"; diff --git a/prt/contracts/test/Tournament.t.sol b/prt/contracts/test/Tournament.t.sol index ee49aec0..27f9050a 100644 --- a/prt/contracts/test/Tournament.t.sol +++ b/prt/contracts/test/Tournament.t.sol @@ -12,13 +12,13 @@ pragma solidity ^0.8.0; -import {ITournament} from "src/ITournament.sol"; +import {ITournament} from "src/tournament/ITournament.sol"; import { ArbitrationConstants } from "src/arbitration-config/ArbitrationConstants.sol"; import { MultiLevelTournamentFactory -} from "src/tournament/factories/MultiLevelTournamentFactory.sol"; +} from "src/tournament/MultiLevelTournamentFactory.sol"; import {Clock} from "src/tournament/libs/Clock.sol"; import {Match} from "src/tournament/libs/Match.sol"; import {Time} from "src/tournament/libs/Time.sol"; diff --git a/prt/contracts/test/TournamentFactory.t.sol b/prt/contracts/test/TournamentFactory.t.sol index bb448413..c01a25b2 100644 --- a/prt/contracts/test/TournamentFactory.t.sol +++ b/prt/contracts/test/TournamentFactory.t.sol @@ -13,13 +13,13 @@ pragma solidity ^0.8.0; import {IDataProvider} from "src/IDataProvider.sol"; -import {ITournament} from "src/ITournament.sol"; +import {ITournament} from "src/tournament/ITournament.sol"; import { ArbitrationConstants } from "src/arbitration-config/ArbitrationConstants.sol"; import { MultiLevelTournamentFactory -} from "src/tournament/factories/MultiLevelTournamentFactory.sol"; +} from "src/tournament/MultiLevelTournamentFactory.sol"; import {Util} from "./Util.sol"; diff --git a/prt/contracts/test/Util.sol b/prt/contracts/test/Util.sol index de6d17bf..c3a77d69 100644 --- a/prt/contracts/test/Util.sol +++ b/prt/contracts/test/Util.sol @@ -15,7 +15,7 @@ pragma solidity ^0.8.0; import {Test} from "forge-std-1.9.6/src/Test.sol"; import {IDataProvider} from "src/IDataProvider.sol"; -import {ITournament} from "src/ITournament.sol"; +import {ITournament} from "src/tournament/ITournament.sol"; import { ArbitrationConstants } from "src/arbitration-config/ArbitrationConstants.sol"; @@ -37,7 +37,7 @@ import { import {Tournament} from "src/tournament/Tournament.sol"; import { MultiLevelTournamentFactory -} from "src/tournament/factories/MultiLevelTournamentFactory.sol"; +} from "src/tournament/MultiLevelTournamentFactory.sol"; import {Match} from "src/tournament/libs/Match.sol"; import {Time} from "src/tournament/libs/Time.sol"; import {Machine} from "src/types/Machine.sol"; From 49189b349eb97cf8a140762f6f18413afc5af3c8 Mon Sep 17 00:00:00 2001 From: Gabriel Coutinho de Paula Date: Wed, 28 Jan 2026 06:07:00 -0300 Subject: [PATCH 2/6] feat(prt): add safety-gate task and spawner with tests --- prt/contracts/script/Deployment.s.sol | 2 +- .../src/safety-gate-task/SafetyGateTask.sol | 190 +++++++++ .../SafetyGateTaskSpawner.sol | 108 ++++++ prt/contracts/src/tournament/ITournament.sol | 2 +- .../MultiLevelTournamentFactory.sol | 10 +- prt/contracts/src/tournament/Tournament.sol | 2 +- prt/contracts/src/tournament/libs/Match.sol | 5 +- prt/contracts/test/BottomTournament.t.sol | 2 +- prt/contracts/test/MiddleTournament.t.sol | 2 +- prt/contracts/test/SafetyGateTask.t.sol | 362 ++++++++++++++++++ prt/contracts/test/TopTournament.t.sol | 2 +- prt/contracts/test/Tournament.t.sol | 2 +- prt/contracts/test/TournamentFactory.t.sol | 2 +- prt/contracts/test/Util.sol | 4 +- 14 files changed, 676 insertions(+), 19 deletions(-) create mode 100644 prt/contracts/src/safety-gate-task/SafetyGateTask.sol create mode 100644 prt/contracts/src/safety-gate-task/SafetyGateTaskSpawner.sol create mode 100644 prt/contracts/test/SafetyGateTask.t.sol diff --git a/prt/contracts/script/Deployment.s.sol b/prt/contracts/script/Deployment.s.sol index fd362dd9..bc2fb3cd 100644 --- a/prt/contracts/script/Deployment.s.sol +++ b/prt/contracts/script/Deployment.s.sol @@ -23,10 +23,10 @@ import { import { RiscVStateTransition } from "src/state-transition/RiscVStateTransition.sol"; -import {Tournament} from "src/tournament/Tournament.sol"; import { MultiLevelTournamentFactory } from "src/tournament/MultiLevelTournamentFactory.sol"; +import {Tournament} from "src/tournament/Tournament.sol"; import {Time} from "src/tournament/libs/Time.sol"; type Milliseconds is uint64; diff --git a/prt/contracts/src/safety-gate-task/SafetyGateTask.sol b/prt/contracts/src/safety-gate-task/SafetyGateTask.sol new file mode 100644 index 00000000..06cf8ffb --- /dev/null +++ b/prt/contracts/src/safety-gate-task/SafetyGateTask.sol @@ -0,0 +1,190 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import {ITask} from "prt-contracts/ITask.sol"; +import {Time} from "prt-contracts/tournament/libs/Time.sol"; +import {Machine} from "prt-contracts/types/Machine.sol"; + +/// @title SafetyGateTask +/// @notice Middleware that gates an inner task result behind N sentry votes. +/// @dev Semantics: +/// - All sentries must vote and agree on a non-zero claim to form consensus. +/// - If sentries disagree or fail to vote, anyone may start a fallback timer; +/// after it elapses, the inner task result is accepted. +/// - This contract does not auto-start the fallback timer; an offchain actor +/// must call `startFallbackTimer`. This is a deliberate liveness assumption. +contract SafetyGateTask is ITask { + using Machine for Machine.Hash; + using Time for Time.Instant; + using Time for Time.Duration; + + /// @notice Inner task that provides the primary result (e.g., PRT/Dave). + ITask public immutable INNER_TASK; + + /// @notice Delay window before falling back to the inner task result. + Time.Duration public immutable DISAGREEMENT_WINDOW; + + /// @notice Total number of sentries configured at task creation. + uint256 public sentryCount; + + /// @notice Total number of sentry votes submitted for this task. + uint256 public sentryTotalVotes; + + /// @notice Current sentry claim (ZERO_STATE means disagreement or no votes). + Machine.Hash public currentSentryClaim; + + /// @notice Whether an address is a sentry for this task (configured list). + mapping(address => bool) public isSentry; + + /// @notice Whether a given sentry has already voted. + mapping(address => bool) public hasVoted; + + /// @notice Start of the fallback timer; zero means not started. + Time.Instant public disagreementStart; + + /// @notice Emitted when a sentry casts a vote. + event SentryVoted(address indexed sentry, Machine.Hash vote); + + /// @notice Emitted when the fallback timer is started. + event DisagreementWindowStarted(Time.Instant start); + + error NotSentry(); + error AlreadyVoted(); + error InvalidSentryVote(); + + /// @dev Restricts to sentries configured at construction time. + modifier onlySentry() { + require(isSentry[msg.sender], NotSentry()); + _; + } + + /// @notice Create a safety-gated task around an inner task. + /// @param innerTask The inner task whose result is gated. + /// @param disagreementWindow The delay window before falling back to inner task. + /// @param initialSentries Immutable list of sentries for this task instance. + constructor( + ITask innerTask, + Time.Duration disagreementWindow, + address[] memory initialSentries + ) { + INNER_TASK = innerTask; + DISAGREEMENT_WINDOW = disagreementWindow; + + for (uint256 i = 0; i < initialSentries.length; i++) { + address sentry = initialSentries[i]; + isSentry[sentry] = true; + sentryCount++; + } + } + + /// @notice Submit a sentry vote for the expected final state. + /// @dev + /// - Each sentry can vote once. + /// - A zero vote is invalid. + /// - If any vote differs from the first, the claim becomes ZERO_STATE, + /// signaling disagreement. + function sentryVote(Machine.Hash vote) external onlySentry { + require(!hasVoted[msg.sender], AlreadyVoted()); + require(!vote.eq(Machine.ZERO_STATE), InvalidSentryVote()); + + if (sentryTotalVotes == 0) { + currentSentryClaim = vote; + } else if (!currentSentryClaim.eq(vote)) { + currentSentryClaim = Machine.ZERO_STATE; + } + + hasVoted[msg.sender] = true; + sentryTotalVotes++; + emit SentryVoted(msg.sender, vote); + } + + /// @notice Returns whether all sentries voted and agreed on a claim. + /// @return ok True if all sentries voted and agree on a non-zero claim. + /// @return claim The agreed claim if ok, otherwise ZERO_STATE. + function sentryVotingConsensus() public view returns (bool, Machine.Hash) { + bool sentriesAgree = !currentSentryClaim.eq(Machine.ZERO_STATE); + + if (sentryCount == sentryTotalVotes && sentriesAgree) { + return (true, currentSentryClaim); + } else { + return (false, Machine.ZERO_STATE); + } + } + + /// @notice Start the fallback timer if sentries disagree or are missing. + /// @dev Anyone can call this; required for liveness in disagreement cases. + /// This does not resolve immediately; `result()` returns the inner + /// outcome only after the timer elapses. + /// @return started True if the timer was started in this call. + function startFallbackTimer() external returns (bool) { + if (!canStartFallbackTimer()) { + return false; + } + + disagreementStart = Time.currentTime(); + emit DisagreementWindowStarted(disagreementStart); + return true; + } + + /// @notice Returns whether the fallback timer can be started now. + /// @dev True only if inner task finished AND sentry consensus is missing/mismatched. + function canStartFallbackTimer() public view returns (bool) { + if (!disagreementStart.isZero()) { + return false; + } + + (bool innerFinished, Machine.Hash innerState) = INNER_TASK.result(); + if (!innerFinished) { + return false; + } + + (bool sentriesAgree, Machine.Hash sentryClaim) = sentryVotingConsensus(); + return !sentriesAgree || !sentryClaim.eq(innerState); + } + + /// @inheritdoc ITask + /// @dev Resolution policy: + /// - If inner task is unfinished: return (false, 0). + /// - If all sentries agree and match inner result: return (true, inner result). + /// - Else: return (false, 0) until the fallback timer elapses, then return inner result. + function result() + external + view + override + returns (bool finished, Machine.Hash finalState) + { + (bool innerFinished, Machine.Hash innerState) = INNER_TASK.result(); + if (!innerFinished) { + return (false, Machine.ZERO_STATE); + } + + (bool sentriesAgree, Machine.Hash sentryClaim) = sentryVotingConsensus(); + + if (sentriesAgree && sentryClaim.eq(innerState)) { + return (true, innerState); + } else if (disagreementStart.isZero()) { + return (false, Machine.ZERO_STATE); + } else if (disagreementStart.timeoutElapsed(DISAGREEMENT_WINDOW)) { + return (true, innerState); + } else { + return (false, Machine.ZERO_STATE); + } + } + + /// @inheritdoc ITask + /// @dev Best-effort passthrough to the inner task cleanup. + function cleanup() external override returns (bool cleaned) { + (bool innerFinished,) = INNER_TASK.result(); + if (!innerFinished) { + return false; + } + + try INNER_TASK.cleanup() returns (bool ok) { + return ok; + } catch { + return false; + } + } +} diff --git a/prt/contracts/src/safety-gate-task/SafetyGateTaskSpawner.sol b/prt/contracts/src/safety-gate-task/SafetyGateTaskSpawner.sol new file mode 100644 index 00000000..29510b71 --- /dev/null +++ b/prt/contracts/src/safety-gate-task/SafetyGateTaskSpawner.sol @@ -0,0 +1,108 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import {IDataProvider} from "prt-contracts/IDataProvider.sol"; +import {ITask} from "prt-contracts/ITask.sol"; +import {ITaskSpawner} from "prt-contracts/ITaskSpawner.sol"; +import { + SafetyGateTask +} from "prt-contracts/safety-gate-task/SafetyGateTask.sol"; +import {Time} from "prt-contracts/tournament/libs/Time.sol"; +import {Machine} from "prt-contracts/types/Machine.sol"; + +/// @title SafetyGateTaskSpawner +/// @notice Spawns safety-gated tasks around an inner task spawner. +/// @dev The sentry list is mutable here, but immutable per spawned task. +contract SafetyGateTaskSpawner is ITaskSpawner { + /// @notice Security council address that manages the sentry set. + address public immutable SECURITY_COUNCIL; + /// @notice Inner task spawner (e.g., Dave/PRT factory). + ITaskSpawner public immutable INNER_SPAWNER; + /// @notice Delay window before falling back to the inner task result. + Time.Duration public immutable DISAGREEMENT_WINDOW; + + /// @notice Current sentry list used for future tasks. + /// @dev Tooling can read the full list from this public array getter. + address[] public sentries; + /// @notice Emitted when a safety-gated task is spawned. + event SafetyGateTaskSpawned( + SafetyGateTask indexed task, ITask indexed innerTask + ); + /// @notice Emitted when the sentry list is replaced. + event SentriesUpdated(address[] sentries); + + error NotSecurityCouncil(); + + /// @dev Restricts to the security council. + modifier onlySecurityCouncil() { + require(msg.sender == SECURITY_COUNCIL, NotSecurityCouncil()); + _; + } + + /// @notice Create a safety-gate task spawner. + /// @param securityCouncil The security council address. + /// @param innerSpawner The inner task spawner to wrap. + /// @param disagreementWindow Delay window before fallback to inner result. + /// @param initialSentries Initial sentry list for future tasks. + constructor( + address securityCouncil, + ITaskSpawner innerSpawner, + Time.Duration disagreementWindow, + address[] memory initialSentries + ) { + SECURITY_COUNCIL = securityCouncil; + INNER_SPAWNER = innerSpawner; + DISAGREEMENT_WINDOW = disagreementWindow; + + _overrideSentries(initialSentries); + } + + /// @inheritdoc ITaskSpawner + /// @dev Uses a snapshot of the current sentry list; later changes do not + /// affect already-spawned tasks. + function spawn(Machine.Hash initial, IDataProvider provider) + external + override + returns (ITask) + { + ITask innerTask = INNER_SPAWNER.spawn(initial, provider); + SafetyGateTask task = + new SafetyGateTask(innerTask, DISAGREEMENT_WINDOW, sentries); + emit SafetyGateTaskSpawned(task, innerTask); + return ITask(address(task)); + } + + /// @notice Replace the full sentry list (affects future tasks only). + /// @dev This does not validate the list; governance is responsible for correctness. + function setSentries(address[] calldata newSentries) + external + onlySecurityCouncil + { + _overrideSentries(newSentries); + } + + /// @notice Returns whether an address is a sentry in the spawner list. + /// @dev If duplicates are present, this still returns true on the first match. + function isSentry(address sentry) external view returns (bool) { + address[] memory current = sentries; + for (uint256 i = 0; i < current.length; i++) { + if (current[i] == sentry) { + return true; + } + } + return false; + } + + /// @dev No validation: list is stored verbatim. + function _overrideSentries(address[] memory newSentries) private { + delete sentries; + + for (uint256 i = 0; i < newSentries.length; i++) { + sentries.push(newSentries[i]); + } + + emit SentriesUpdated(sentries); + } +} diff --git a/prt/contracts/src/tournament/ITournament.sol b/prt/contracts/src/tournament/ITournament.sol index 3da353a5..989c84d4 100644 --- a/prt/contracts/src/tournament/ITournament.sol +++ b/prt/contracts/src/tournament/ITournament.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.17; import {IDataProvider} from "prt-contracts/IDataProvider.sol"; -import {ITask} from "prt-contracts/ITask.sol"; import {IStateTransition} from "prt-contracts/IStateTransition.sol"; +import {ITask} from "prt-contracts/ITask.sol"; import {Clock} from "prt-contracts/tournament/libs/Clock.sol"; import {Commitment} from "prt-contracts/tournament/libs/Commitment.sol"; import {Match} from "prt-contracts/tournament/libs/Match.sol"; diff --git a/prt/contracts/src/tournament/MultiLevelTournamentFactory.sol b/prt/contracts/src/tournament/MultiLevelTournamentFactory.sol index 6887f529..567580af 100644 --- a/prt/contracts/src/tournament/MultiLevelTournamentFactory.sol +++ b/prt/contracts/src/tournament/MultiLevelTournamentFactory.sol @@ -5,16 +5,16 @@ pragma solidity ^0.8.17; import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; -import { - IMultiLevelTournamentFactory -} from "prt-contracts/tournament/IMultiLevelTournamentFactory.sol"; import {IDataProvider} from "prt-contracts/IDataProvider.sol"; -import {ITask} from "prt-contracts/ITask.sol"; import {IStateTransition} from "prt-contracts/IStateTransition.sol"; -import {ITournament} from "prt-contracts/tournament/ITournament.sol"; +import {ITask} from "prt-contracts/ITask.sol"; import { ITournamentParametersProvider } from "prt-contracts/arbitration-config/ITournamentParametersProvider.sol"; +import { + IMultiLevelTournamentFactory +} from "prt-contracts/tournament/IMultiLevelTournamentFactory.sol"; +import {ITournament} from "prt-contracts/tournament/ITournament.sol"; import {Tournament} from "prt-contracts/tournament/Tournament.sol"; import {Commitment} from "prt-contracts/tournament/libs/Commitment.sol"; import {Time} from "prt-contracts/tournament/libs/Time.sol"; diff --git a/prt/contracts/src/tournament/Tournament.sol b/prt/contracts/src/tournament/Tournament.sol index 0d438037..72c2420e 100644 --- a/prt/contracts/src/tournament/Tournament.sol +++ b/prt/contracts/src/tournament/Tournament.sol @@ -8,10 +8,10 @@ import {Math} from "@openzeppelin-contracts-5.5.0/utils/math/Math.sol"; import {IStateTransition} from "prt-contracts/IStateTransition.sol"; import {ITask} from "prt-contracts/ITask.sol"; -import {ITournament} from "prt-contracts/tournament/ITournament.sol"; import { IMultiLevelTournamentFactory } from "prt-contracts/tournament/IMultiLevelTournamentFactory.sol"; +import {ITournament} from "prt-contracts/tournament/ITournament.sol"; import {Clock} from "prt-contracts/tournament/libs/Clock.sol"; import {Commitment} from "prt-contracts/tournament/libs/Commitment.sol"; import {Gas} from "prt-contracts/tournament/libs/Gas.sol"; diff --git a/prt/contracts/src/tournament/libs/Match.sol b/prt/contracts/src/tournament/libs/Match.sol index 397cc810..ed738614 100644 --- a/prt/contracts/src/tournament/libs/Match.sol +++ b/prt/contracts/src/tournament/libs/Match.sol @@ -204,10 +204,7 @@ library Match { return args.toCycle(state.runningLeafPosition); } - function getDivergence( - State memory state, - Commitment.Arguments memory args - ) + function getDivergence(State memory state, Commitment.Arguments memory args) internal pure returns ( diff --git a/prt/contracts/test/BottomTournament.t.sol b/prt/contracts/test/BottomTournament.t.sol index 97e11747..b6a138fc 100644 --- a/prt/contracts/test/BottomTournament.t.sol +++ b/prt/contracts/test/BottomTournament.t.sol @@ -15,13 +15,13 @@ pragma solidity ^0.8.0; import {Vm} from "forge-std-1.9.6/src/Vm.sol"; import {IStateTransition} from "src/IStateTransition.sol"; -import {ITournament} from "src/tournament/ITournament.sol"; import { ArbitrationConstants } from "src/arbitration-config/ArbitrationConstants.sol"; import { CartesiStateTransition } from "src/state-transition/CartesiStateTransition.sol"; +import {ITournament} from "src/tournament/ITournament.sol"; import { MultiLevelTournamentFactory } from "src/tournament/MultiLevelTournamentFactory.sol"; diff --git a/prt/contracts/test/MiddleTournament.t.sol b/prt/contracts/test/MiddleTournament.t.sol index d3eb1063..fe8f1a66 100644 --- a/prt/contracts/test/MiddleTournament.t.sol +++ b/prt/contracts/test/MiddleTournament.t.sol @@ -14,10 +14,10 @@ pragma solidity ^0.8.0; import {Vm} from "forge-std-1.9.6/src/Vm.sol"; -import {ITournament} from "src/tournament/ITournament.sol"; import { ArbitrationConstants } from "src/arbitration-config/ArbitrationConstants.sol"; +import {ITournament} from "src/tournament/ITournament.sol"; import { MultiLevelTournamentFactory } from "src/tournament/MultiLevelTournamentFactory.sol"; diff --git a/prt/contracts/test/SafetyGateTask.t.sol b/prt/contracts/test/SafetyGateTask.t.sol new file mode 100644 index 00000000..9197c554 --- /dev/null +++ b/prt/contracts/test/SafetyGateTask.t.sol @@ -0,0 +1,362 @@ +// Copyright 2023 Cartesi Pte. Ltd. + +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +pragma solidity ^0.8.0; + +import {Test} from "forge-std-1.9.6/src/Test.sol"; + +import {IDataProvider} from "src/IDataProvider.sol"; +import {ITask} from "src/ITask.sol"; +import {ITaskSpawner} from "src/ITaskSpawner.sol"; +import {SafetyGateTask} from "src/safety-gate-task/SafetyGateTask.sol"; +import { + SafetyGateTaskSpawner +} from "src/safety-gate-task/SafetyGateTaskSpawner.sol"; +import {Time} from "src/tournament/libs/Time.sol"; +import {Machine} from "src/types/Machine.sol"; + +contract MockTask is ITask { + bool private _finished; + Machine.Hash private _state; + + function setResult(bool finished, Machine.Hash state) external { + _finished = finished; + _state = state; + } + + function result() external view returns (bool, Machine.Hash) { + return (_finished, _state); + } + + function cleanup() external returns (bool) { + return _finished; + } +} + +contract MockSpawner is ITaskSpawner { + MockTask public lastTask; + Machine.Hash public lastInitial; + IDataProvider public lastProvider; + bool public nextFinished; + Machine.Hash public nextState; + + function setNextResult(bool finished, Machine.Hash state) external { + nextFinished = finished; + nextState = state; + } + + function spawn(Machine.Hash initial, IDataProvider provider) + external + returns (ITask) + { + lastInitial = initial; + lastProvider = provider; + lastTask = new MockTask(); + lastTask.setResult(nextFinished, nextState); + return ITask(address(lastTask)); + } +} + +contract SafetyGateTaskTest is Test { + using Machine for Machine.Hash; + using Time for Time.Instant; + + Machine.Hash constant STATE_ONE = Machine.Hash.wrap(bytes32(uint256(1))); + Machine.Hash constant STATE_TWO = Machine.Hash.wrap(bytes32(uint256(2))); + Time.Duration constant WINDOW = Time.Duration.wrap(10); + + address constant SENTRY_ONE = address(0x1001); + address constant SENTRY_TWO = address(0x1002); + address constant OTHER = address(0x2001); + address constant SECURITY_COUNCIL = address(0x3001); + + function _newTask(address[] memory sentries) + internal + returns (SafetyGateTask task, MockTask inner) + { + inner = new MockTask(); + task = new SafetyGateTask(inner, WINDOW, sentries); + } + + function testConstructorSetsSentriesAndCount() public { + address[] memory sentries = new address[](2); + sentries[0] = SENTRY_ONE; + sentries[1] = SENTRY_TWO; + + (SafetyGateTask task,) = _newTask(sentries); + + assertEq(task.sentryCount(), 2); + assertTrue(task.isSentry(SENTRY_ONE)); + assertTrue(task.isSentry(SENTRY_TWO)); + (bool ok, Machine.Hash claim) = task.sentryVotingConsensus(); + assertFalse(ok); + assertTrue(claim.eq(Machine.ZERO_STATE)); + } + + function testSentryVoteRequiresSentry() public { + address[] memory sentries = new address[](1); + sentries[0] = SENTRY_ONE; + + (SafetyGateTask task,) = _newTask(sentries); + + vm.expectRevert( + abi.encodeWithSelector(SafetyGateTask.NotSentry.selector) + ); + vm.prank(OTHER); + task.sentryVote(STATE_ONE); + } + + function testSentryVoteRejectsZero() public { + address[] memory sentries = new address[](1); + sentries[0] = SENTRY_ONE; + + (SafetyGateTask task,) = _newTask(sentries); + + vm.expectRevert( + abi.encodeWithSelector(SafetyGateTask.InvalidSentryVote.selector) + ); + vm.prank(SENTRY_ONE); + task.sentryVote(Machine.ZERO_STATE); + } + + function testSentryVoteOnlyOnce() public { + address[] memory sentries = new address[](1); + sentries[0] = SENTRY_ONE; + + (SafetyGateTask task,) = _newTask(sentries); + + vm.prank(SENTRY_ONE); + task.sentryVote(STATE_ONE); + + vm.expectRevert( + abi.encodeWithSelector(SafetyGateTask.AlreadyVoted.selector) + ); + vm.prank(SENTRY_ONE); + task.sentryVote(STATE_ONE); + } + + function testConsensusAfterAllVotesAgree() public { + address[] memory sentries = new address[](2); + sentries[0] = SENTRY_ONE; + sentries[1] = SENTRY_TWO; + + (SafetyGateTask task,) = _newTask(sentries); + + vm.prank(SENTRY_ONE); + task.sentryVote(STATE_ONE); + vm.prank(SENTRY_TWO); + task.sentryVote(STATE_ONE); + + (bool ok, Machine.Hash claim) = task.sentryVotingConsensus(); + assertTrue(ok); + assertTrue(claim.eq(STATE_ONE)); + } + + function testDisagreementResetsClaim() public { + address[] memory sentries = new address[](2); + sentries[0] = SENTRY_ONE; + sentries[1] = SENTRY_TWO; + + (SafetyGateTask task,) = _newTask(sentries); + + vm.prank(SENTRY_ONE); + task.sentryVote(STATE_ONE); + vm.prank(SENTRY_TWO); + task.sentryVote(STATE_TWO); + + assertTrue(task.currentSentryClaim().eq(Machine.ZERO_STATE)); + (bool ok, Machine.Hash claim) = task.sentryVotingConsensus(); + assertFalse(ok); + assertTrue(claim.eq(Machine.ZERO_STATE)); + } + + function testResultWhenInnerNotFinished() public { + address[] memory sentries = new address[](1); + sentries[0] = SENTRY_ONE; + + (SafetyGateTask task, MockTask inner) = _newTask(sentries); + inner.setResult(false, STATE_ONE); + + (bool finished, Machine.Hash finalState) = task.result(); + assertFalse(finished); + assertTrue(finalState.eq(Machine.ZERO_STATE)); + } + + function testResultWhenSentriesAgreeAndMatchInner() public { + address[] memory sentries = new address[](2); + sentries[0] = SENTRY_ONE; + sentries[1] = SENTRY_TWO; + + (SafetyGateTask task, MockTask inner) = _newTask(sentries); + inner.setResult(true, STATE_ONE); + + vm.prank(SENTRY_ONE); + task.sentryVote(STATE_ONE); + vm.prank(SENTRY_TWO); + task.sentryVote(STATE_ONE); + + (bool finished, Machine.Hash finalState) = task.result(); + assertTrue(finished); + assertTrue(finalState.eq(STATE_ONE)); + } + + function testResultMismatchRequiresFallbackTimer() public { + address[] memory sentries = new address[](2); + sentries[0] = SENTRY_ONE; + sentries[1] = SENTRY_TWO; + + (SafetyGateTask task, MockTask inner) = _newTask(sentries); + inner.setResult(true, STATE_ONE); + + vm.prank(SENTRY_ONE); + task.sentryVote(STATE_TWO); + vm.prank(SENTRY_TWO); + task.sentryVote(STATE_TWO); + + (bool finished, Machine.Hash finalState) = task.result(); + assertFalse(finished); + assertTrue(finalState.eq(Machine.ZERO_STATE)); + + assertTrue(task.canStartFallbackTimer()); + + assertTrue(task.startFallbackTimer()); + assertFalse(task.canStartFallbackTimer()); + uint256 startBlock = Time.Instant.unwrap(task.disagreementStart()); + assertGt(startBlock, 0); + + vm.roll(startBlock + Time.Duration.unwrap(WINDOW) - 1); + (finished, finalState) = task.result(); + assertFalse(finished); + assertTrue(finalState.eq(Machine.ZERO_STATE)); + + vm.roll(startBlock + Time.Duration.unwrap(WINDOW)); + (finished, finalState) = task.result(); + assertTrue(finished); + assertTrue(finalState.eq(STATE_ONE)); + } + + function testResultMissingVotesRequiresFallbackTimer() public { + address[] memory sentries = new address[](2); + sentries[0] = SENTRY_ONE; + sentries[1] = SENTRY_TWO; + + (SafetyGateTask task, MockTask inner) = _newTask(sentries); + inner.setResult(true, STATE_ONE); + + vm.prank(SENTRY_ONE); + task.sentryVote(STATE_ONE); + + assertTrue(task.canStartFallbackTimer()); + assertTrue(task.startFallbackTimer()); + + uint256 startBlock = Time.Instant.unwrap(task.disagreementStart()); + vm.roll(startBlock + Time.Duration.unwrap(WINDOW)); + (bool finished, Machine.Hash finalState) = task.result(); + assertTrue(finished); + assertTrue(finalState.eq(STATE_ONE)); + } + + function testStartFallbackTimerRequiresInnerFinished() public { + address[] memory sentries = new address[](1); + sentries[0] = SENTRY_ONE; + + (SafetyGateTask task, MockTask inner) = _newTask(sentries); + inner.setResult(false, STATE_ONE); + + assertFalse(task.canStartFallbackTimer()); + assertFalse(task.startFallbackTimer()); + assertTrue(task.disagreementStart().isZero()); + } + + function testStartFallbackTimerIdempotent() public { + address[] memory sentries = new address[](1); + sentries[0] = SENTRY_ONE; + + (SafetyGateTask task, MockTask inner) = _newTask(sentries); + inner.setResult(true, STATE_ONE); + + assertTrue(task.startFallbackTimer()); + uint256 startBlock = Time.Instant.unwrap(task.disagreementStart()); + assertGt(startBlock, 0); + + vm.roll(startBlock + 1); + assertFalse(task.startFallbackTimer()); + assertEq(Time.Instant.unwrap(task.disagreementStart()), startBlock); + } + + function testSpawnerOnlySecurityCouncilCanSetSentries() public { + address[] memory sentries = new address[](1); + sentries[0] = SENTRY_ONE; + + MockSpawner innerSpawner = new MockSpawner(); + SafetyGateTaskSpawner spawner = new SafetyGateTaskSpawner( + SECURITY_COUNCIL, innerSpawner, WINDOW, sentries + ); + + address[] memory newSentries = new address[](1); + newSentries[0] = SENTRY_TWO; + + vm.expectRevert( + abi.encodeWithSelector( + SafetyGateTaskSpawner.NotSecurityCouncil.selector + ) + ); + vm.prank(OTHER); + spawner.setSentries(newSentries); + } + + function testSpawnerSpawnUsesSnapshot() public { + address[] memory sentries = new address[](1); + sentries[0] = SENTRY_ONE; + + MockSpawner innerSpawner = new MockSpawner(); + innerSpawner.setNextResult(true, STATE_ONE); + SafetyGateTaskSpawner spawner = new SafetyGateTaskSpawner( + SECURITY_COUNCIL, innerSpawner, WINDOW, sentries + ); + + SafetyGateTask taskOne = SafetyGateTask( + address(spawner.spawn(STATE_ONE, IDataProvider(address(0)))) + ); + assertTrue(taskOne.isSentry(SENTRY_ONE)); + assertFalse(taskOne.isSentry(SENTRY_TWO)); + + address[] memory nextSentries = new address[](1); + nextSentries[0] = SENTRY_TWO; + vm.prank(SECURITY_COUNCIL); + spawner.setSentries(nextSentries); + + SafetyGateTask taskTwo = SafetyGateTask( + address(spawner.spawn(STATE_TWO, IDataProvider(address(0)))) + ); + assertFalse(taskTwo.isSentry(SENTRY_ONE)); + assertTrue(taskTwo.isSentry(SENTRY_TWO)); + } + + function testSpawnerStoresDuplicatesVerbatim() public { + address[] memory sentries = new address[](2); + sentries[0] = SENTRY_ONE; + sentries[1] = SENTRY_ONE; + + MockSpawner innerSpawner = new MockSpawner(); + innerSpawner.setNextResult(true, STATE_ONE); + SafetyGateTaskSpawner spawner = new SafetyGateTaskSpawner( + SECURITY_COUNCIL, innerSpawner, WINDOW, sentries + ); + + SafetyGateTask task = SafetyGateTask( + address(spawner.spawn(STATE_ONE, IDataProvider(address(0)))) + ); + assertEq(task.sentryCount(), 2); + assertTrue(task.isSentry(SENTRY_ONE)); + } +} diff --git a/prt/contracts/test/TopTournament.t.sol b/prt/contracts/test/TopTournament.t.sol index 9d8fd715..a334cfe7 100644 --- a/prt/contracts/test/TopTournament.t.sol +++ b/prt/contracts/test/TopTournament.t.sol @@ -12,10 +12,10 @@ pragma solidity ^0.8.0; -import {ITournament} from "src/tournament/ITournament.sol"; import { ArbitrationConstants } from "src/arbitration-config/ArbitrationConstants.sol"; +import {ITournament} from "src/tournament/ITournament.sol"; import { MultiLevelTournamentFactory } from "src/tournament/MultiLevelTournamentFactory.sol"; diff --git a/prt/contracts/test/Tournament.t.sol b/prt/contracts/test/Tournament.t.sol index 27f9050a..0ec96725 100644 --- a/prt/contracts/test/Tournament.t.sol +++ b/prt/contracts/test/Tournament.t.sol @@ -12,10 +12,10 @@ pragma solidity ^0.8.0; -import {ITournament} from "src/tournament/ITournament.sol"; import { ArbitrationConstants } from "src/arbitration-config/ArbitrationConstants.sol"; +import {ITournament} from "src/tournament/ITournament.sol"; import { MultiLevelTournamentFactory } from "src/tournament/MultiLevelTournamentFactory.sol"; diff --git a/prt/contracts/test/TournamentFactory.t.sol b/prt/contracts/test/TournamentFactory.t.sol index c01a25b2..b9046e4f 100644 --- a/prt/contracts/test/TournamentFactory.t.sol +++ b/prt/contracts/test/TournamentFactory.t.sol @@ -13,10 +13,10 @@ pragma solidity ^0.8.0; import {IDataProvider} from "src/IDataProvider.sol"; -import {ITournament} from "src/tournament/ITournament.sol"; import { ArbitrationConstants } from "src/arbitration-config/ArbitrationConstants.sol"; +import {ITournament} from "src/tournament/ITournament.sol"; import { MultiLevelTournamentFactory } from "src/tournament/MultiLevelTournamentFactory.sol"; diff --git a/prt/contracts/test/Util.sol b/prt/contracts/test/Util.sol index c3a77d69..21742ac1 100644 --- a/prt/contracts/test/Util.sol +++ b/prt/contracts/test/Util.sol @@ -15,7 +15,6 @@ pragma solidity ^0.8.0; import {Test} from "forge-std-1.9.6/src/Test.sol"; import {IDataProvider} from "src/IDataProvider.sol"; -import {ITournament} from "src/tournament/ITournament.sol"; import { ArbitrationConstants } from "src/arbitration-config/ArbitrationConstants.sol"; @@ -34,10 +33,11 @@ import { import { RiscVStateTransition } from "src/state-transition/RiscVStateTransition.sol"; -import {Tournament} from "src/tournament/Tournament.sol"; +import {ITournament} from "src/tournament/ITournament.sol"; import { MultiLevelTournamentFactory } from "src/tournament/MultiLevelTournamentFactory.sol"; +import {Tournament} from "src/tournament/Tournament.sol"; import {Match} from "src/tournament/libs/Match.sol"; import {Time} from "src/tournament/libs/Time.sol"; import {Machine} from "src/types/Machine.sol"; From 4ca6496da195e2aa2ad06b9d57d3b72400f53cea Mon Sep 17 00:00:00 2001 From: Gabriel Coutinho de Paula Date: Wed, 28 Jan 2026 08:40:44 -0300 Subject: [PATCH 3/6] feat(rollups): add pause and upgrade primitives to DaveConsensus --- .../contracts/src/DaveAppFactory.sol | 11 ++- .../contracts/src/DaveConsensus.sol | 71 ++++++++++++---- .../contracts/src/IDaveConsensus.sol | 46 ++++++++-- .../contracts/test/DaveAppFactory.t.sol | 10 +-- .../contracts/test/DaveConsensus.t.sol | 83 ++++++++++++++++--- cartesi-rollups/contracts/test/Merkle.t.sol | 2 +- 6 files changed, 178 insertions(+), 45 deletions(-) diff --git a/cartesi-rollups/contracts/src/DaveAppFactory.sol b/cartesi-rollups/contracts/src/DaveAppFactory.sol index 3c5b8fa6..7d8cd62d 100644 --- a/cartesi-rollups/contracts/src/DaveAppFactory.sol +++ b/cartesi-rollups/contracts/src/DaveAppFactory.sol @@ -24,13 +24,15 @@ contract DaveAppFactory is IDaveAppFactory { IInputBox immutable INPUT_BOX; IApplicationFactory immutable APP_FACTORY; ITaskSpawner immutable TASK_SPAWNER; + address immutable SECURITY_COUNCIL; IOutputsMerkleRootValidator constant NO_VALIDATOR = IOutputsMerkleRootValidator(address(0)); - constructor(IInputBox inputBox, IApplicationFactory appFactory, ITaskSpawner taskSpawner) { + constructor(IInputBox inputBox, IApplicationFactory appFactory, ITaskSpawner taskSpawner, address securityCouncil) { INPUT_BOX = inputBox; APP_FACTORY = appFactory; TASK_SPAWNER = taskSpawner; + SECURITY_COUNCIL = securityCouncil; } function newDaveApp(bytes32 templateHash, bytes32 salt) @@ -74,7 +76,10 @@ contract DaveAppFactory is IDaveAppFactory { returns (DaveConsensus) { Machine.Hash initialMachineStateHash = Machine.Hash.wrap(templateHash); - return new DaveConsensus{salt: salt}(INPUT_BOX, appContract, TASK_SPAWNER, initialMachineStateHash); + return + new DaveConsensus{salt: salt}( + INPUT_BOX, appContract, TASK_SPAWNER, SECURITY_COUNCIL, initialMachineStateHash + ); } /// @notice Calculates the address of an application contract. @@ -95,7 +100,7 @@ contract DaveAppFactory is IDaveAppFactory { keccak256( abi.encodePacked( type(DaveConsensus).creationCode, - abi.encode(INPUT_BOX, appContract, TASK_SPAWNER, templateHash) + abi.encode(INPUT_BOX, appContract, TASK_SPAWNER, SECURITY_COUNCIL, templateHash) ) ) ); diff --git a/cartesi-rollups/contracts/src/DaveConsensus.sol b/cartesi-rollups/contracts/src/DaveConsensus.sol index fc6a61a9..b195b6a4 100644 --- a/cartesi-rollups/contracts/src/DaveConsensus.sol +++ b/cartesi-rollups/contracts/src/DaveConsensus.sol @@ -56,8 +56,11 @@ contract DaveConsensus is IDaveConsensus, ERC165 { /// @notice The application contract address immutable _APP_CONTRACT; + /// @notice Security council address + address immutable _SECURITY_COUNCIL; + /// @notice The contract used to instantiate tasks - ITaskSpawner immutable _TASK_SPAWNER; + ITaskSpawner _taskSpawner; /// @notice Deployment block number uint256 immutable _DEPLOYMENT_BLOCK_NUMBER = block.number; @@ -77,16 +80,21 @@ contract DaveConsensus is IDaveConsensus, ERC165 { /// @notice Settled output trees' merkle root hash mapping(bytes32 => bool) _outputsMerkleRoots; + /// @notice Whether settlement is paused + bool _paused; + constructor( IInputBox inputBox, address appContract, ITaskSpawner taskSpawner, + address securityCouncil, Machine.Hash initialMachineStateHash ) { // Initialize immutable variables _INPUT_BOX = inputBox; _APP_CONTRACT = appContract; - _TASK_SPAWNER = taskSpawner; + _SECURITY_COUNCIL = securityCouncil; + _taskSpawner = taskSpawner; emit ConsensusCreation(inputBox, appContract, taskSpawner); // Initialize first sealed epoch @@ -97,6 +105,15 @@ contract DaveConsensus is IDaveConsensus, ERC165 { emit EpochSealed(0, 0, inputIndexUpperBound, initialMachineStateHash, bytes32(0), task); } + function _onlySecurityCouncil() internal view { + require(msg.sender == _SECURITY_COUNCIL, NotSecurityCouncil()); + } + + modifier onlySecurityCouncil() { + _onlySecurityCouncil(); + _; + } + function canSettle() external view @@ -108,6 +125,8 @@ contract DaveConsensus is IDaveConsensus, ERC165 { } function settle(uint256 epochNumber, bytes32 outputsMerkleRoot, bytes32[] calldata proof) external override { + require(!_paused, PausedError()); + // Check task settlement require(epochNumber == _epochNumber, IncorrectEpochNumber(epochNumber, _epochNumber)); @@ -127,31 +146,20 @@ contract DaveConsensus is IDaveConsensus, ERC165 { _outputsMerkleRoots[outputsMerkleRoot] = true; // Start new task - _task = _TASK_SPAWNER.spawn(finalMachineStateHash, this); + _task = _taskSpawner.spawn(finalMachineStateHash, this); emit EpochSealed( - _epochNumber, - _inputIndexLowerBound, - _inputIndexUpperBound, - finalMachineStateHash, - outputsMerkleRoot, - _task + _epochNumber, _inputIndexLowerBound, _inputIndexUpperBound, finalMachineStateHash, outputsMerkleRoot, _task ); _tryCleanup(oldTask); - } function getCurrentSealedEpoch() external view override - returns ( - uint256 epochNumber, - uint256 inputIndexLowerBound, - uint256 inputIndexUpperBound, - ITask task - ) + returns (uint256 epochNumber, uint256 inputIndexLowerBound, uint256 inputIndexUpperBound, ITask task) { epochNumber = _epochNumber; inputIndexLowerBound = _inputIndexLowerBound; @@ -168,7 +176,36 @@ contract DaveConsensus is IDaveConsensus, ERC165 { } function getTaskSpawner() external view override returns (ITaskSpawner) { - return _TASK_SPAWNER; + return _taskSpawner; + } + + function getSecurityCouncil() external view override returns (address) { + return _SECURITY_COUNCIL; + } + + function isPaused() external view override returns (bool) { + return _paused; + } + + function upgrade(Machine.Hash newInitialState, ITaskSpawner newTaskSpawner) external override onlySecurityCouncil { + _taskSpawner = newTaskSpawner; + _task = newTaskSpawner.spawn(newInitialState, this); + + emit TaskUpgraded(_epochNumber, newInitialState, newTaskSpawner, _task); + } + + function pause() external override onlySecurityCouncil { + if (!_paused) { + _paused = true; + emit Paused(msg.sender); + } + } + + function unpause() external override onlySecurityCouncil { + if (_paused) { + _paused = false; + emit Unpaused(msg.sender); + } } function provideMerkleRootOfInput(uint256 inputIndexWithinEpoch, bytes calldata input) diff --git a/cartesi-rollups/contracts/src/IDaveConsensus.sol b/cartesi-rollups/contracts/src/IDaveConsensus.sol index 5b5f1dc1..d5870159 100644 --- a/cartesi-rollups/contracts/src/IDaveConsensus.sol +++ b/cartesi-rollups/contracts/src/IDaveConsensus.sol @@ -59,6 +59,23 @@ interface IDaveConsensus is IDataProvider, IOutputsMerkleRootValidator { ITask task ); + /// @notice Task was upgraded for the current epoch. + /// @param epochNumber The current sealed epoch number + /// @param newInitialState The new initial state used to respawn the task + /// @param newTaskSpawner The new task spawner used for future tasks + /// @param task The new task instance + event TaskUpgraded( + uint256 indexed epochNumber, Machine.Hash newInitialState, ITaskSpawner newTaskSpawner, ITask task + ); + + /// @notice Emitted when the contract is paused. + /// @param account The caller that triggered the pause + event Paused(address indexed account); + + /// @notice Emitted when the contract is unpaused. + /// @param account The caller that triggered the unpause + event Unpaused(address indexed account); + /// @notice Received epoch number is different from actual /// @param received The epoch number received as argument /// @param actual The actual epoch number in storage @@ -66,6 +83,10 @@ interface IDaveConsensus is IDataProvider, IOutputsMerkleRootValidator { /// @notice Task is not finished yet error TournamentNotFinishedYet(); + /// @notice Caller is not the security council + error NotSecurityCouncil(); + /// @notice Contract is paused + error PausedError(); /// @notice Hash of received input blob is different from stored on-chain /// @param fromReceivedInput Hash of received input blob @@ -97,6 +118,12 @@ interface IDaveConsensus is IDataProvider, IOutputsMerkleRootValidator { /// @notice Get the task spawner contract used to instantiate root tasks. function getTaskSpawner() external view returns (ITaskSpawner); + /// @notice Get the security council address. + function getSecurityCouncil() external view returns (address); + + /// @notice Returns whether the contract is paused. + function isPaused() external view returns (bool); + /// @notice Get the current sealed epoch number, boundaries, and task. /// @param epochNumber The epoch number /// @param inputIndexLowerBound The epoch input index (inclusive) lower bound @@ -105,12 +132,7 @@ interface IDaveConsensus is IDataProvider, IOutputsMerkleRootValidator { function getCurrentSealedEpoch() external view - returns ( - uint256 epochNumber, - uint256 inputIndexLowerBound, - uint256 inputIndexUpperBound, - ITask task - ); + returns (uint256 epochNumber, uint256 inputIndexLowerBound, uint256 inputIndexUpperBound, ITask task); /// @notice Check whether the current sealed epoch can be settled. /// @return isFinished Whether the current sealed epoch task has finished yet @@ -124,4 +146,16 @@ interface IDaveConsensus is IDataProvider, IOutputsMerkleRootValidator { /// @param proof The bottom-up Merkle proof of the outputs Merkle root in the final machine state /// @dev On success, emits an `EpochSealed` event. function settle(uint256 epochNumber, bytes32 outputsMerkleRoot, bytes32[] calldata proof) external; + + /// @notice Pause settlement. Only callable by the security council. + function pause() external; + + /// @notice Unpause settlement. Only callable by the security council. + function unpause() external; + + /// @notice Upgrade the current task and task spawner. + /// @param newInitialState The new initial machine state hash + /// @param newTaskSpawner The new task spawner + /// @dev Emits `TaskUpgraded` on success. + function upgrade(Machine.Hash newInitialState, ITaskSpawner newTaskSpawner) external; } diff --git a/cartesi-rollups/contracts/test/DaveAppFactory.t.sol b/cartesi-rollups/contracts/test/DaveAppFactory.t.sol index a794440f..f1213ea4 100644 --- a/cartesi-rollups/contracts/test/DaveAppFactory.t.sol +++ b/cartesi-rollups/contracts/test/DaveAppFactory.t.sol @@ -36,12 +36,14 @@ contract DaveConsensusFactoryTest is Test { IInputBox _inputBox; MockTaskSpawner _taskSpawner; Machine.Hash _initialMachineStateHash; + address _securityCouncil; function setUp() external { _inputBox = new InputBox(); _appFactory = new ApplicationFactory(); _taskSpawner = new MockTaskSpawner(); - _daveAppFactory = new DaveAppFactory(_inputBox, _appFactory, _taskSpawner); + _securityCouncil = address(0xBEEF); + _daveAppFactory = new DaveAppFactory(_inputBox, _appFactory, _taskSpawner, _securityCouncil); _initialMachineStateHash = Machine.Hash.wrap(keccak256("foo")); } @@ -125,6 +127,7 @@ contract DaveConsensusFactoryTest is Test { assertEq(address(daveConsensus.getInputBox()), address(_inputBox)); assertEq(address(daveConsensus.getApplicationContract()), address(appContract)); assertEq(address(daveConsensus.getTaskSpawner()), address(_taskSpawner)); + assertEq(daveConsensus.getSecurityCouncil(), _securityCouncil); { address appContractAddress; @@ -142,10 +145,7 @@ contract DaveConsensusFactoryTest is Test { _daveAppFactory.newDaveApp(templateHash, salt); } - function _checkEpochSealedData(bytes memory data, bytes32 templateHash, address randomTaskAddress) - internal - pure - { + function _checkEpochSealedData(bytes memory data, bytes32 templateHash, address randomTaskAddress) internal pure { ( uint256 epochNumber, uint256 inputIndexLowerBound, diff --git a/cartesi-rollups/contracts/test/DaveConsensus.t.sol b/cartesi-rollups/contracts/test/DaveConsensus.t.sol index ea19372f..bef32636 100644 --- a/cartesi-rollups/contracts/test/DaveConsensus.t.sol +++ b/cartesi-rollups/contracts/test/DaveConsensus.t.sol @@ -86,11 +86,7 @@ contract MockTaskSpawner is ITaskSpawner { return mockTask; } - function calculateTaskAddress(Machine.Hash initialState, IDataProvider provider) - external - view - returns (address) - { + function calculateTaskAddress(Machine.Hash initialState, IDataProvider provider) external view returns (address) { return Create2.computeAddress( _salt, keccak256(abi.encodePacked(type(MockTask).creationCode, abi.encode(initialState, provider))) ); @@ -127,11 +123,13 @@ contract DaveConsensusTest is Test { IInputBox _inputBox; MockTaskSpawner _mockTaskSpawner; MerkleProxy _merkleProxy; + address _securityCouncil; function setUp() external { _inputBox = new InputBox(); _mockTaskSpawner = new MockTaskSpawner(); _merkleProxy = new MerkleProxy(); + _securityCouncil = address(0xBEEF); } function testMockTaskSpawner() external view { @@ -167,8 +165,7 @@ contract DaveConsensusTest is Test { address daveConsensusAddress = _calculateNewDaveConsensus(appContract, state0, salts[0]); _mockTaskSpawner.setSalt(salts[1]); - address mockTaskAddress = - _mockTaskSpawner.calculateTaskAddress(state0, IDataProvider(daveConsensusAddress)); + address mockTaskAddress = _mockTaskSpawner.calculateTaskAddress(state0, IDataProvider(daveConsensusAddress)); vm.expectEmit(daveConsensusAddress); emit IDaveConsensus.ConsensusCreation(_inputBox, appContract, _mockTaskSpawner); @@ -182,6 +179,7 @@ contract DaveConsensusTest is Test { assertEq(address(daveConsensus.getInputBox()), address(_inputBox)); assertEq(daveConsensus.getApplicationContract(), appContract); assertEq(address(daveConsensus.getTaskSpawner()), address(_mockTaskSpawner)); + assertEq(daveConsensus.getSecurityCouncil(), _securityCouncil); assertEq(daveConsensus.getDeploymentBlockNumber(), deploymentBlockNumber); mockTask = MockTask(mockTaskAddress); @@ -203,8 +201,7 @@ contract DaveConsensusTest is Test { uint256 inputIndexUpperBound; ITask task; - (epochNumber, inputIndexLowerBound, inputIndexUpperBound, task) = - daveConsensus.getCurrentSealedEpoch(); + (epochNumber, inputIndexLowerBound, inputIndexUpperBound, task) = daveConsensus.getCurrentSealedEpoch(); assertEq(epochNumber, 0); assertEq(inputIndexLowerBound, 0); @@ -287,8 +284,7 @@ contract DaveConsensusTest is Test { uint256 inputIndexUpperBound; ITask task; - (epochNumber, inputIndexLowerBound, inputIndexUpperBound, task) = - daveConsensus.getCurrentSealedEpoch(); + (epochNumber, inputIndexLowerBound, inputIndexUpperBound, task) = daveConsensus.getCurrentSealedEpoch(); assertEq(epochNumber, 1); assertEq(inputIndexLowerBound, inputCounts[0]); @@ -349,6 +345,67 @@ contract DaveConsensusTest is Test { daveConsensus.settle(0, bytes32(0), new bytes32[](0)); } + function testPauseBlocksSettle( + address appContract, + Machine.Hash initialState, + bytes32 outputsMerkleRoot, + bytes32 salt + ) external { + _mockTaskSpawner.setSalt(salt); + DaveConsensus daveConsensus = _newDaveConsensus(appContract, initialState, salt); + + MockTask task = _mockTaskSpawner.getMockTask(0); + (Machine.Hash finalState, bytes32[] memory proof, bytes32 outputRoot) = _statesAndProofs(outputsMerkleRoot); + task.finish(finalState); + + vm.prank(_securityCouncil); + daveConsensus.pause(); + + vm.expectRevert(IDaveConsensus.PausedError.selector); + daveConsensus.settle(0, outputRoot, proof); + + vm.prank(_securityCouncil); + daveConsensus.unpause(); + + daveConsensus.settle(0, outputRoot, proof); + } + + function testUpgradeOnlySecurityCouncil(address appContract, Machine.Hash initialState, bytes32 salt) external { + DaveConsensus daveConsensus = _newDaveConsensus(appContract, initialState, salt); + + MockTaskSpawner newSpawner = new MockTaskSpawner(); + vm.expectRevert(IDaveConsensus.NotSecurityCouncil.selector); + daveConsensus.upgrade(initialState, newSpawner); + } + + function testUpgradeSpawnsNewTask(address appContract, Machine.Hash initialState, bytes32 salt, bytes32 newSalt) + external + { + DaveConsensus daveConsensus = _newDaveConsensus(appContract, initialState, salt); + + MockTaskSpawner newSpawner = new MockTaskSpawner(); + newSpawner.setSalt(newSalt); + + Machine.Hash newState = Machine.Hash.wrap(keccak256("upgrade-state")); + address expectedTask = newSpawner.calculateTaskAddress(newState, IDataProvider(address(daveConsensus))); + + vm.expectEmit(address(daveConsensus)); + emit IDaveConsensus.TaskUpgraded(0, newState, newSpawner, ITask(expectedTask)); + + vm.prank(_securityCouncil); + daveConsensus.upgrade(newState, newSpawner); + + assertEq(address(daveConsensus.getTaskSpawner()), address(newSpawner)); + + (uint256 epochNumber,,, ITask task) = daveConsensus.getCurrentSealedEpoch(); + assertEq(epochNumber, 0); + assertEq(address(task), expectedTask); + + MockTask spawned = MockTask(expectedTask); + assertEq(Machine.Hash.unwrap(spawned.getInitialState()), Machine.Hash.unwrap(newState)); + assertEq(address(spawned.getProvider()), address(daveConsensus)); + } + function testProvideMerkleRootOfInput( address appContract, bytes[] calldata payloads, @@ -461,7 +518,7 @@ contract DaveConsensusTest is Test { keccak256( abi.encodePacked( type(DaveConsensus).creationCode, - abi.encode(_inputBox, appContract, _mockTaskSpawner, initialState) + abi.encode(_inputBox, appContract, _mockTaskSpawner, _securityCouncil, initialState) ) ) ); @@ -471,7 +528,7 @@ contract DaveConsensusTest is Test { internal returns (DaveConsensus) { - return new DaveConsensus{salt: salt}(_inputBox, appContract, _mockTaskSpawner, initialState); + return new DaveConsensus{salt: salt}(_inputBox, appContract, _mockTaskSpawner, _securityCouncil, initialState); } function _statesAndProofs(bytes32 outputsMerkleRoot) private returns (Machine.Hash, bytes32[] memory, bytes32) { diff --git a/cartesi-rollups/contracts/test/Merkle.t.sol b/cartesi-rollups/contracts/test/Merkle.t.sol index dc890480..fd738ed2 100644 --- a/cartesi-rollups/contracts/test/Merkle.t.sol +++ b/cartesi-rollups/contracts/test/Merkle.t.sol @@ -364,7 +364,7 @@ contract MerkleTest is Test { } function testGetHashOfLeafAtIndex(bytes calldata data, uint256 index) external pure { - index = bound(index, _getNumOfLeaves(data.length), type(uint256).max); + index = bound(index, _getNumOfLeaves(data.length), type(uint256).max >> MerkleConstants.LOG2_LEAF_SIZE); bytes32 leafHash = PristineMerkleTree.getNodeAtHeight(0); assertEq(MerkleWrapper.getHashOfLeafAtIndex(data, index), leafHash); } From 2d22d92af1f9f270f629ea2b165ab55a1c47797e Mon Sep 17 00:00:00 2001 From: Gabriel Coutinho de Paula Date: Wed, 28 Jan 2026 10:24:03 -0300 Subject: [PATCH 4/6] docs: document pause and upgrade primitives, and security gate task --- {dave => docs/dave}/README.md | 8 +- {dave/docs => docs/dave}/dave.pdf | Bin {prt/docs => docs/prt}/prt.pdf | Bin docs/stage-1/architecture.md | 200 ++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+), 6 deletions(-) rename {dave => docs/dave}/README.md (81%) rename {dave/docs => docs/dave}/dave.pdf (100%) rename {prt/docs => docs/prt}/prt.pdf (100%) create mode 100644 docs/stage-1/architecture.md diff --git a/dave/README.md b/docs/dave/README.md similarity index 81% rename from dave/README.md rename to docs/dave/README.md index 2c7ec65a..a7652265 100644 --- a/dave/README.md +++ b/docs/dave/README.md @@ -1,8 +1,4 @@ -# Dave - -The implementation of the Dave fraud-proof algorithm. - -## The Dave fraud-proof algorithm — triumphing over Sybils with a laptop and a small collateral +# The Dave fraud-proof algorithm — triumphing over Sybils with a laptop and a small collateral The Dave fraud-proof algorithm offers an unprecedented combination of decentralization, security, and liveness. The resources that must be mobilized by an honest participant to defeat an adversary grow only logarithmically with what the adversary ultimately loses. @@ -11,4 +7,4 @@ This makes the system very inclusive and frees participants from having to pool Finally, the maximum delay to finalization also grows only logarithmically with total adversarial expenditure, with the smallest multiplicative factor to date. In summary: the entire dispute completes in 2–5 challenge periods, the only way to break consensus is to censor the honest party for more than one challenge period, and the costs of engaging in the dispute are minimal. -We've published our initial research [here](https://arxiv.org/abs/2411.05463), and committed it [here](docs/dave.pdf) for convenience. +We've published our initial research [here](https://arxiv.org/abs/2411.05463), then peer-reviewed [here](https://doi.org/10.1145/3734698) and committed it [here](docs/dave/dave.pdf) for convenience. diff --git a/dave/docs/dave.pdf b/docs/dave/dave.pdf similarity index 100% rename from dave/docs/dave.pdf rename to docs/dave/dave.pdf diff --git a/prt/docs/prt.pdf b/docs/prt/prt.pdf similarity index 100% rename from prt/docs/prt.pdf rename to docs/prt/prt.pdf diff --git a/docs/stage-1/architecture.md b/docs/stage-1/architecture.md new file mode 100644 index 00000000..e07d6146 --- /dev/null +++ b/docs/stage-1/architecture.md @@ -0,0 +1,200 @@ +# Stage 1 Architecture for Cartesi Rollups + +This document describes the Stage 1 design implemented in this repository, alongside the proof system itself that can be deployed as Stage 2. +It is written to be audit-friendly for external reviewers and to align with the L2Beat Stages Framework. + +The design intentionally trades some decentralization for safety and agility during early-stage deployments. +It introduces: +* a Security Council with pause and upgrade powers; and +* a permissioned Sentry layer that adds a safety delay without giving it the ability to change the outcome of the proof system. + +## Goals + +- Reduce catastrophic risk from application bugs or proof system bugs. +- Preserve liveness without requiring continuous Security Council action (walkaway test). +- Allow upgrades by the Security Council. + +## Components + +### 1) DaveConsensus (this repo) + +- Tracks epoch boundaries and validates outputs against finalized machine state. +- Spawns a task for each epoch, exposes `canSettle`, and finalizes via `settle`. +- Implements the **Upgrade Primitive** (`upgrade(newInitialState, newTaskSpawner)`), only callable + by the Security Council. + +### 2) Task Interface (`ITask` / `ITaskSpawner`) + +- Minimal interface that lets the application stay agnostic to which proof system is used. +- Enables swapping proof systems or wrapping them with middleware. + +### 3) Proof System Task (Dave / PRT tournaments) + +- Permissionless dispute system that resolves to a final machine hash. +- Configured with a challenge period, state-transition function and on-chain rules for resolving disagreements. + +### 4) Safety Gate Task (optional middleware) + +- A wrapper task that requires sentry agreement before returning results. +- Implements a *disagreement buffer* that only delays settlement. +- Multiple sentries are supported; all must agree on the same non-zero claim. + +### 5) Security Council + +- High-threshold multisig (e.g., 6-of-8) of cold keys. +- Recommended: >=8 signers with >=75% threshold, and membership diversity across entities and jurisdictions. +- Holds "break-glass" powers: upgrade, pause, and output-tree excision (app-specific). + +### 6) Sentries + +- Hot EOAs or low-threshold multisigs that can attest to the correct final state. +- Designed to be fast and replaceable. +- Designed to have minimal powers (i.e. delay only). + +## Protocol Flow (Normal Operation) + +1. DaveConsensus spawns a task for the current epoch using the current task spawner. +2. The task resolves to a final machine hash. +3. DaveConsensus validates the outputs Merkle root against that final hash. +4. The epoch is sealed and the next epoch task is spawned. + +If the Safety Gate is used, `result()` is mediated by sentries and a disagreement window. + +## Pause Primitive (DaveConsensus) + +Function (DaveConsensus): + +- `pause()` / `unpause()` by Security Council only. + +Behavior: + +- While paused, `settle()` reverts and no new epoch can be sealed. +- The current task continues to run off-chain; the pause only blocks finalization on L1. + +Rationale: + +- Buys time to diagnose issues and coordinate an upgrade during live incidents. + + +## Upgrade Primitive + +Function (DaveConsensus): + +- `upgrade(newInitialState, newTaskSpawner)` by Security Council only. + +Behavior (in-flight task swap): + +- The task spawner is swapped immediately. +- A new task is spawned for the *current epoch bounds* (a "replay") using the new initial state. +- The old task is ghosted (a "zombie"); its result is ignored. + +Rationale: + +- Allows recovery from proof system bugs or application bugs. +- Keeps the application logic minimal and agnostic to the proof system implementation. + +Operational note: + +- Validators must have the new upgraded machine snapshot. + + +## Safety Gate Task + +Decision logic: + +- All sentries vote and agree on a claim. +- If any sentry (1-of-N) disagrees or fails to vote, anyone can start a fallback timer once the inner task + is finished. +- After the disagreement window elapses, the inner task result is accepted. + +Properties: + +- Sentries can delay settlement, but cannot change the result. +- One byzantine or fail-stop sentry can force delay; this is intentional to prioritize safety. +- Sentry set is immutable per task; Security Council can update it for future tasks. + +## Stage 1 Alignment (L2Beat) + +Our design fulfills the Stage 1 requirements in the following ways: + +- **Permissionless Proof System**: +- **Challenge period**: The dispute window is configured to be >=7 days. +- **Single point of emergency authority**: Only the Security Council can upgrade the task spawner and change the rollup trajectory. All other roles can only delay. +- **Upgrade window**: There are no non-SC upgrade paths in this design. +- **Walkaway test**: If the Security Council becomes inactive, the system still resolves by falling back to the permissionless proof system. Moreover, the Sentry cannot halt the chain. + +The Security Council configuration above is intended to satisfy the robustness guidelines of the +L2Beat framework. + +## Threat Scenarios and Outcomes + +This section lists key failure modes and why funds remain safe (or not). + +### Sentry failures + +- **Single sentry byzantine**: + - Effect: Forces disagreement path and delays settlement by the disagreement window. + - Outcome: No safety loss. Security Council can rotate sentries. + +- **Single sentry fail-stop**: + - Effect: Missing votes trigger fallback timer, adding delay. + - Outcome: Liveness degradation only. + +- **All sentries fail-stop**: + - Effect: Same as above; anyone can start fallback after inner task finishes. + - Outcome: Liveness degradation; no safety loss. + +- **All sentries byzantine**: + - Effect: They can delay (by disagreeing) or agree with a bad result. + - Outcome: If they agree with a bad result, the safety gate offers no protection. We rely + on the proof system (or Security Council). + +### Proof system failures (Dave/PRT bug) + +- **PRT bug + honest sentries**: + - Effect: Mismatch between sentry claim and inner result triggers buffer. + - Outcome: Security Council can pause/upgrade before settlement. + +- **PRT bug + all sentries compromised to match bad result**: + - Effect: No buffer; bad result proceeds. + - Outcome: Funds are safe only if the Security Council intervenes in time (pause and upgrade). + +### Security Council failures + +- **Security Council fail-stop**: + - Effect: No upgrades or emergency actions. + - Outcome: Chain still resolves via proof system; liveness preserved (walkaway test). + +- **Security Council byzantine (>= threshold)**: + - Effect: Can upgrade to arbitrary state, pause, or censor withdrawals. + - Outcome: This is the explicit Stage 1 trust assumption. + +## What Must Go Wrong for Funds to Be Stolen + +At least one of the following must occur: + +1. A Security Council threshold is compromised (>=75% of SC signers), or +2. A proof-system bug exists *and* all sentries fail to raise a discrepancy *and* the Security Council fails to intervene before outputs are executed, or + +In other words, safety is compromised only if a bug aligns with a governance failure, or the Security Council itself is compromised. + +## Operational Practices + +- Maintain at least one independent, always-on validator per ecosystem stakeholder. +- Monitor disagreements and ensure the fallback timer is started when needed. +- Run periodic key-rotation exercises for sentries. +- Require multi-party signing ceremonies for upgrades. +- Maintain a tested playbook for pause and upgrade procedures. + +## Current Implementation Scope + +Implemented in this repo: + +- Task interface (`ITask` / `ITaskSpawner`). +- DaveConsensus with Security Council upgrade primitive. +- Safety Gate Task with multi-sentry disagreement buffer. + +## References + +- https://forum.l2beat.com/t/the-stages-framework/291 +- https://forum.l2beat.com/t/stages-update-a-high-level-guiding-principle-for-stage-1/338 From 0473e533699484089dd99ca4f1d32b54bf708c73 Mon Sep 17 00:00:00 2001 From: Gabriel Coutinho de Paula Date: Tue, 3 Feb 2026 22:52:06 -0300 Subject: [PATCH 5/6] feat(rollups): add ERC165 support to tasks --- .../contracts/src/DaveConsensus.sol | 7 +- .../contracts/test/DaveConsensus.t.sol | 4 ++ prt/contracts/src/ITask.sol | 3 +- .../src/safety-gate-task/ISafetyGateTask.sol | 68 +++++++++++++++++++ .../src/safety-gate-task/SafetyGateTask.sol | 39 +++++------ prt/contracts/src/tournament/Tournament.sol | 11 +++ prt/contracts/test/SafetyGateTask.t.sol | 4 ++ 7 files changed, 112 insertions(+), 24 deletions(-) create mode 100644 prt/contracts/src/safety-gate-task/ISafetyGateTask.sol diff --git a/cartesi-rollups/contracts/src/DaveConsensus.sol b/cartesi-rollups/contracts/src/DaveConsensus.sol index b195b6a4..3c136b80 100644 --- a/cartesi-rollups/contracts/src/DaveConsensus.sol +++ b/cartesi-rollups/contracts/src/DaveConsensus.sol @@ -120,7 +120,12 @@ contract DaveConsensus is IDaveConsensus, ERC165 { override returns (bool isFinished, uint256 epochNumber, Machine.Hash finalState) { - (isFinished, finalState) = _task.result(); + if (_paused) { + (isFinished, finalState) = (false, Machine.ZERO_STATE); + } else { + (isFinished, finalState) = _task.result(); + } + epochNumber = _epochNumber; } diff --git a/cartesi-rollups/contracts/test/DaveConsensus.t.sol b/cartesi-rollups/contracts/test/DaveConsensus.t.sol index bef32636..5f6ecf21 100644 --- a/cartesi-rollups/contracts/test/DaveConsensus.t.sol +++ b/cartesi-rollups/contracts/test/DaveConsensus.t.sol @@ -72,6 +72,10 @@ contract MockTask is ITask { function cleanup() external returns (bool) { return _finished; } + + function supportsInterface(bytes4 interfaceId) external pure returns (bool) { + return interfaceId == type(ITask).interfaceId; + } } contract MockTaskSpawner is ITaskSpawner { diff --git a/prt/contracts/src/ITask.sol b/prt/contracts/src/ITask.sol index cbdbc10c..b1ee8f2a 100644 --- a/prt/contracts/src/ITask.sol +++ b/prt/contracts/src/ITask.sol @@ -3,10 +3,11 @@ pragma solidity ^0.8.17; +import {IERC165} from "@openzeppelin-contracts-5.5.0/utils/introspection/IERC165.sol"; import {Machine} from "prt-contracts/types/Machine.sol"; /// @notice Task interface for asynchronous proof systems. -interface ITask { +interface ITask is IERC165 { /// @notice Get the task result. /// @return finished Whether the task has finished /// @return finalState The finalized machine state (if finished) diff --git a/prt/contracts/src/safety-gate-task/ISafetyGateTask.sol b/prt/contracts/src/safety-gate-task/ISafetyGateTask.sol new file mode 100644 index 00000000..c53021f1 --- /dev/null +++ b/prt/contracts/src/safety-gate-task/ISafetyGateTask.sol @@ -0,0 +1,68 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.17; + +import {ITask} from "prt-contracts/ITask.sol"; +import {Machine} from "prt-contracts/types/Machine.sol"; +import {Time} from "prt-contracts/tournament/libs/Time.sol"; + +/// @title ISafetyGateTask +/// @notice Interface for a safety-gated task wrapper. +/// @dev Semantics: +/// - All sentries must vote and agree on a non-zero claim to form consensus. +/// - If sentries disagree or fail to vote, anyone may start a fallback timer; +/// after it elapses, the inner task result is accepted. +/// - This interface does not prescribe auto-start of the fallback timer; +/// an offchain actor must call `startFallbackTimer` for liveness. +interface ISafetyGateTask is ITask { + /// @notice Inner task that provides the primary result. + function INNER_TASK() external view returns (ITask); + + /// @notice Delay window before falling back to the inner task result. + function DISAGREEMENT_WINDOW() external view returns (Time.Duration); + + /// @notice Total number of sentries configured at task creation. + function sentryCount() external view returns (uint256); + + /// @notice Total number of sentry votes submitted for this task. + function sentryTotalVotes() external view returns (uint256); + + /// @notice Current sentry claim (ZERO_STATE means disagreement or no votes). + function currentSentryClaim() external view returns (Machine.Hash); + + /// @notice Whether an address is a sentry for this task (configured list). + function isSentry(address) external view returns (bool); + + /// @notice Whether a given sentry has already voted. + function hasVoted(address) external view returns (bool); + + /// @notice Start of the fallback timer; zero means not started. + function disagreementStart() external view returns (Time.Instant); + + /// @notice Submit a sentry vote for the expected final state. + /// @dev + /// - Each sentry can vote once. + /// - A zero vote is invalid. + /// - If any vote differs from the first, the claim becomes ZERO_STATE. + function sentryVote(Machine.Hash vote) external; + + /// @notice Returns whether all sentries voted and agreed on a claim. + /// @return ok True if all sentries voted and agree on a non-zero claim. + /// @return claim The agreed claim if ok, otherwise ZERO_STATE. + function sentryVotingConsensus() + external + view + returns (bool ok, Machine.Hash claim); + + /// @notice Start the fallback timer if sentries disagree or are missing. + /// @dev Anyone can call this; required for liveness in disagreement cases. + /// This does not resolve immediately; `result()` returns the inner + /// outcome only after the timer elapses. + /// @return started True if the timer was started in this call. + function startFallbackTimer() external returns (bool started); + + /// @notice Returns whether the fallback timer can be started now. + /// @dev True only if inner task finished AND sentry consensus is missing/mismatched. + function canStartFallbackTimer() external view returns (bool); +} diff --git a/prt/contracts/src/safety-gate-task/SafetyGateTask.sol b/prt/contracts/src/safety-gate-task/SafetyGateTask.sol index 06cf8ffb..0acd4c6c 100644 --- a/prt/contracts/src/safety-gate-task/SafetyGateTask.sol +++ b/prt/contracts/src/safety-gate-task/SafetyGateTask.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.17; import {ITask} from "prt-contracts/ITask.sol"; +import {ISafetyGateTask} from "prt-contracts/safety-gate-task/ISafetyGateTask.sol"; +import {IERC165} from "@openzeppelin-contracts-5.5.0/utils/introspection/IERC165.sol"; import {Time} from "prt-contracts/tournament/libs/Time.sol"; import {Machine} from "prt-contracts/types/Machine.sol"; @@ -15,7 +17,7 @@ import {Machine} from "prt-contracts/types/Machine.sol"; /// after it elapses, the inner task result is accepted. /// - This contract does not auto-start the fallback timer; an offchain actor /// must call `startFallbackTimer`. This is a deliberate liveness assumption. -contract SafetyGateTask is ITask { +contract SafetyGateTask is ISafetyGateTask { using Machine for Machine.Hash; using Time for Time.Instant; using Time for Time.Duration; @@ -79,12 +81,7 @@ contract SafetyGateTask is ITask { } } - /// @notice Submit a sentry vote for the expected final state. - /// @dev - /// - Each sentry can vote once. - /// - A zero vote is invalid. - /// - If any vote differs from the first, the claim becomes ZERO_STATE, - /// signaling disagreement. + /// @inheritdoc ISafetyGateTask function sentryVote(Machine.Hash vote) external onlySentry { require(!hasVoted[msg.sender], AlreadyVoted()); require(!vote.eq(Machine.ZERO_STATE), InvalidSentryVote()); @@ -100,9 +97,7 @@ contract SafetyGateTask is ITask { emit SentryVoted(msg.sender, vote); } - /// @notice Returns whether all sentries voted and agreed on a claim. - /// @return ok True if all sentries voted and agree on a non-zero claim. - /// @return claim The agreed claim if ok, otherwise ZERO_STATE. + /// @inheritdoc ISafetyGateTask function sentryVotingConsensus() public view returns (bool, Machine.Hash) { bool sentriesAgree = !currentSentryClaim.eq(Machine.ZERO_STATE); @@ -113,11 +108,7 @@ contract SafetyGateTask is ITask { } } - /// @notice Start the fallback timer if sentries disagree or are missing. - /// @dev Anyone can call this; required for liveness in disagreement cases. - /// This does not resolve immediately; `result()` returns the inner - /// outcome only after the timer elapses. - /// @return started True if the timer was started in this call. + /// @inheritdoc ISafetyGateTask function startFallbackTimer() external returns (bool) { if (!canStartFallbackTimer()) { return false; @@ -128,8 +119,7 @@ contract SafetyGateTask is ITask { return true; } - /// @notice Returns whether the fallback timer can be started now. - /// @dev True only if inner task finished AND sentry consensus is missing/mismatched. + /// @inheritdoc ISafetyGateTask function canStartFallbackTimer() public view returns (bool) { if (!disagreementStart.isZero()) { return false; @@ -145,10 +135,6 @@ contract SafetyGateTask is ITask { } /// @inheritdoc ITask - /// @dev Resolution policy: - /// - If inner task is unfinished: return (false, 0). - /// - If all sentries agree and match inner result: return (true, inner result). - /// - Else: return (false, 0) until the fallback timer elapses, then return inner result. function result() external view @@ -174,7 +160,6 @@ contract SafetyGateTask is ITask { } /// @inheritdoc ITask - /// @dev Best-effort passthrough to the inner task cleanup. function cleanup() external override returns (bool cleaned) { (bool innerFinished,) = INNER_TASK.result(); if (!innerFinished) { @@ -187,4 +172,14 @@ contract SafetyGateTask is ITask { return false; } } + + function supportsInterface(bytes4 interfaceId) + external + view + returns (bool) + { + return interfaceId == type(IERC165).interfaceId + || interfaceId == type(ITask).interfaceId + || interfaceId == type(ISafetyGateTask).interfaceId; + } } diff --git a/prt/contracts/src/tournament/Tournament.sol b/prt/contracts/src/tournament/Tournament.sol index 72c2420e..231a0d0a 100644 --- a/prt/contracts/src/tournament/Tournament.sol +++ b/prt/contracts/src/tournament/Tournament.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.17; import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; import {Math} from "@openzeppelin-contracts-5.5.0/utils/math/Math.sol"; +import {IERC165} from "@openzeppelin-contracts-5.5.0/utils/introspection/IERC165.sol"; import {IStateTransition} from "prt-contracts/IStateTransition.sol"; import {ITask} from "prt-contracts/ITask.sol"; @@ -839,6 +840,16 @@ contract Tournament is ITournament { } } + function supportsInterface(bytes4 interfaceId) + external + view + returns (bool) + { + return interfaceId == type(IERC165).interfaceId + || interfaceId == type(ITask).interfaceId + || interfaceId == type(ITournament).interfaceId; + } + // // Internal functions // diff --git a/prt/contracts/test/SafetyGateTask.t.sol b/prt/contracts/test/SafetyGateTask.t.sol index 9197c554..9e691d40 100644 --- a/prt/contracts/test/SafetyGateTask.t.sol +++ b/prt/contracts/test/SafetyGateTask.t.sol @@ -40,6 +40,10 @@ contract MockTask is ITask { function cleanup() external returns (bool) { return _finished; } + + function supportsInterface(bytes4 interfaceId) external pure returns (bool) { + return interfaceId == type(ITask).interfaceId; + } } contract MockSpawner is ITaskSpawner { From 4e78bfd032d6f7c8936c00db7e1276f7f90c9c2c Mon Sep 17 00:00:00 2001 From: Gabriel Coutinho de Paula Date: Wed, 4 Feb 2026 00:19:30 -0300 Subject: [PATCH 6/6] feat: update Solidity deployments, update node --- Cargo.lock | 1 + .../contracts/script/Deployment.s.sol | 15 +- .../node/blockchain-reader/src/lib.rs | 2 +- .../node/cartesi-rollups-prt-node/src/lib.rs | 1 + cartesi-rollups/node/epoch-manager/Cargo.toml | 1 + cartesi-rollups/node/epoch-manager/src/lib.rs | 141 ++++++++++++++++-- cartesi-rollups/node/state-manager/src/lib.rs | 1 + .../src/persistent_state_access.rs | 17 ++- .../node/state-manager/src/sql/migrations.sql | 1 + .../node/state-manager/src/sql/rollup_data.rs | 22 ++- .../cartesi-machine-sys/build.rs | 2 +- prt/client-rs/core/src/tournament/sender.rs | 9 +- prt/contracts/justfile | 2 +- prt/contracts/script/Deployment.s.sol | 16 +- 14 files changed, 195 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b75aa03a..cb83ba29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4409,6 +4409,7 @@ dependencies = [ "alloy", "anyhow", "cartesi-dave-contracts", + "cartesi-prt-contracts", "cartesi-prt-core", "log", "num-traits", diff --git a/cartesi-rollups/contracts/script/Deployment.s.sol b/cartesi-rollups/contracts/script/Deployment.s.sol index dcfa4091..d5cfa0dd 100644 --- a/cartesi-rollups/contracts/script/Deployment.s.sol +++ b/cartesi-rollups/contracts/script/Deployment.s.sol @@ -18,12 +18,25 @@ contract DeploymentScript is BaseDeploymentScript { address inputBox = _loadDeployment(".", "InputBox"); address appFactory = _loadDeployment(".", "ApplicationFactory"); address tournamentFactory = _loadDeployment(".", "MultiLevelTournamentFactory"); + address safetyGateTaskSpawner = _loadDeployment("../../prt/contracts", "SafetyGateTaskSpawner"); + address securityCouncil = tx.origin; vmSafe.startBroadcast(); _storeDeployment( type(DaveAppFactory).name, - _create2(type(DaveAppFactory).creationCode, abi.encode(inputBox, appFactory, tournamentFactory)) + _create2( + type(DaveAppFactory).creationCode, + abi.encode(inputBox, appFactory, tournamentFactory, securityCouncil) + ) + ); + + _storeDeployment( + "DaveAppFactorySafetyGate", + _create2( + type(DaveAppFactory).creationCode, + abi.encode(inputBox, appFactory, safetyGateTaskSpawner, securityCouncil) + ) ); if (block.chainid == 31337) { diff --git a/cartesi-rollups/node/blockchain-reader/src/lib.rs b/cartesi-rollups/node/blockchain-reader/src/lib.rs index 23973a57..2d733920 100644 --- a/cartesi-rollups/node/blockchain-reader/src/lib.rs +++ b/cartesi-rollups/node/blockchain-reader/src/lib.rs @@ -257,7 +257,7 @@ impl BlockchainReader { .inputIndexUpperBound .to_u64() .expect("fail to convert epoch boundary"), - root_tournament: e.tournament, + root_tournament: e.task, block_created_number: meta.block_number.expect("block number should exist"), }; info!( diff --git a/cartesi-rollups/node/cartesi-rollups-prt-node/src/lib.rs b/cartesi-rollups/node/cartesi-rollups-prt-node/src/lib.rs index 5d9589bf..c8f5997b 100644 --- a/cartesi-rollups/node/cartesi-rollups-prt-node/src/lib.rs +++ b/cartesi-rollups/node/cartesi-rollups-prt-node/src/lib.rs @@ -95,6 +95,7 @@ pub fn create_epoch_manager_task(watch: Watch, parameters: &PRTConfig) -> thread let epoch_manager = EpochManager::new( Arc::new(Mutex::new(arena_sender)), params.address_book.consensus, + params.signer_address, state_manager, params.sleep_duration, ); diff --git a/cartesi-rollups/node/epoch-manager/Cargo.toml b/cartesi-rollups/node/epoch-manager/Cargo.toml index 22591a1e..f5134664 100644 --- a/cartesi-rollups/node/epoch-manager/Cargo.toml +++ b/cartesi-rollups/node/epoch-manager/Cargo.toml @@ -11,6 +11,7 @@ repository.workspace = true [dependencies] cartesi-dave-contracts = { workspace = true } +cartesi-prt-contracts = { workspace = true } cartesi-prt-core = { workspace = true } rollups-state-manager = { workspace = true } diff --git a/cartesi-rollups/node/epoch-manager/src/lib.rs b/cartesi-rollups/node/epoch-manager/src/lib.rs index cc07239a..14ce53e8 100644 --- a/cartesi-rollups/node/epoch-manager/src/lib.rs +++ b/cartesi-rollups/node/epoch-manager/src/lib.rs @@ -3,10 +3,9 @@ mod error; -use alloy::{ - primitives::{Address, B256}, - providers::{DynProvider, Provider}, -}; +use alloy::primitives::{Address, B256, FixedBytes}; +use alloy::providers::{DynProvider, Provider}; +use alloy::sol_types::SolInterface; use error::Result; use log::{debug, info, trace}; use num_traits::cast::ToPrimitive; @@ -14,34 +13,38 @@ use std::{ops::ControlFlow, sync::Arc, time::Duration}; use tokio::sync::Mutex; use cartesi_dave_contracts::dave_consensus::DaveConsensus; +use cartesi_prt_contracts::safety_gate_task; use cartesi_prt_core::{ db::dispute_state_access::{Input, Leaf}, strategy::player::Player, tournament::{ArenaSender, allow_revert_rethrow_others}, }; -use rollups_state_manager::{Epoch, Proof, StateManager, sync::Watch}; +use rollups_state_manager::{Epoch, Proof, Settlement, StateManager, sync::Watch}; pub struct EpochManager { arena_sender: Arc>, consensus: Address, + signer_address: Address, sleep_duration: Duration, state_manager: SM, - last_react_epoch: (Option>, u64), + last_react_epoch: (Option>, u64, Address), } impl EpochManager { pub fn new( arena_sender: Arc>, consensus_address: Address, + signer_address: Address, state_manager: SM, sleep_duration: Duration, ) -> Self { Self { arena_sender, consensus: consensus_address, + signer_address, sleep_duration, state_manager, - last_react_epoch: (None, 0), + last_react_epoch: (None, 0, Address::ZERO), } } @@ -80,10 +83,10 @@ impl EpochManager { )? { Some(settlement) => { assert_eq!( - settlement.computation_hash.data(), - can_settle.winnerCommitment, - "Winner commitment mismatch, notify all users!" + settlement.final_state, can_settle.finalState, + "Winner state mismatch, notify all users!" ); + info!( "settle epoch {} with claim {}", can_settle.epochNumber, @@ -116,12 +119,20 @@ impl EpochManager { .state_manager .settlement_info(last_sealed_epoch.epoch_number)? { - Some(_) => { + Some(settlement) => { trace!( "dispute tournaments for epoch {}", last_sealed_epoch.epoch_number ); - self.react_dispute(provider, &last_sealed_epoch).await? + let tournament_address = self + .resolve_tournament_address( + provider.clone(), + last_sealed_epoch.root_tournament, + &settlement, + ) + .await?; + self.react_dispute(provider, &last_sealed_epoch, tournament_address) + .await? } None => { debug!( @@ -138,8 +149,9 @@ impl EpochManager { &mut self, provider: DynProvider, last_sealed_epoch: &Epoch, + tournament_address: Address, ) -> Result<()> { - self.get_latest_player(last_sealed_epoch, provider)?; + self.get_latest_player(last_sealed_epoch, provider, tournament_address)?; self.last_react_epoch .0 .as_mut() @@ -154,6 +166,7 @@ impl EpochManager { &mut self, last_sealed_epoch: &Epoch, provider: DynProvider, + tournament_address: Address, ) -> Result<()> { let snapshot = self .state_manager @@ -164,6 +177,7 @@ impl EpochManager { // we need to instantiate new epoch player with appropriate data if self.last_react_epoch.0.is_none() || self.last_react_epoch.1 != last_sealed_epoch.epoch_number + || self.last_react_epoch.2 != tournament_address { let inputs = self .state_manager @@ -188,18 +202,77 @@ impl EpochManager { leafs, provider.erased(), snapshot.to_string_lossy().to_string(), - last_sealed_epoch.root_tournament, + tournament_address, last_sealed_epoch.block_created_number, self.state_manager .epoch_directory(last_sealed_epoch.epoch_number)?, ) .expect("fail to initialize prt player"); - self.last_react_epoch = (Some(player), last_sealed_epoch.epoch_number); + self.last_react_epoch = ( + Some(player), + last_sealed_epoch.epoch_number, + tournament_address, + ); } Ok(()) } + + async fn resolve_tournament_address( + &self, + provider: DynProvider, + task_address: Address, + settlement: &Settlement, + ) -> Result
{ + if let Some(inner_task) = self + .try_safety_gate(provider, task_address, settlement) + .await? + { + Ok(inner_task) + } else { + Ok(task_address) + } + } + + async fn try_safety_gate( + &self, + provider: DynProvider, + task_address: Address, + settlement: &Settlement, + ) -> Result> { + if !supports_interface(provider.clone(), task_address, safety_gate_interface_id()).await { + return Ok(None); + } + + let safety_gate = safety_gate_task::SafetyGateTask::new(task_address, provider.clone()); + + self.try_sentry_vote(&safety_gate, settlement).await?; + + let inner_task = safety_gate.INNER_TASK().call().await?; + Ok(Some(inner_task)) + } + + async fn try_sentry_vote( + &self, + safety_gate: &safety_gate_task::SafetyGateTask::SafetyGateTaskInstance, + settlement: &Settlement, + ) -> Result<()> { + let is_sentry = safety_gate.isSentry(self.signer_address).call().await?; + if !is_sentry { + return Ok(()); + } + + let has_voted = safety_gate.hasVoted(self.signer_address).call().await?; + if has_voted { + return Ok(()); + } + + let vote = B256::from(settlement.final_state); + let tx_result = safety_gate.sentryVote(vote).send().await; + allow_revert_rethrow_others("sentryVote", tx_result).await?; + Ok(()) + } } fn to_bytes_32_vec(proof: Proof) -> Vec { @@ -209,3 +282,41 @@ fn to_bytes_32_vec(proof: Proof) -> Vec { fn vec_u8_to_bytes_32(hash: Vec) -> B256 { B256::from_slice(&hash) } + +fn safety_gate_interface_id() -> FixedBytes<4> { + interface_id_from_selectors(safety_gate_task::SafetyGateTask::SafetyGateTaskCalls::selectors()) +} + +fn interface_id_from_selectors(selectors: I) -> FixedBytes<4> +where + I: IntoIterator, +{ + let mut id = [0u8; 4]; + for selector in selectors { + id[0] ^= selector[0]; + id[1] ^= selector[1]; + id[2] ^= selector[2]; + id[3] ^= selector[3]; + } + FixedBytes::from(id) +} + +async fn supports_interface( + provider: DynProvider, + contract: Address, + interface_id: FixedBytes<4>, +) -> bool { + let erc165 = safety_gate_task::SafetyGateTask::new(contract, provider); + match erc165.supportsInterface(interface_id).call().await { + Ok(value) => value, + Err(err) => { + let message = err.to_string(); + if message.contains("execution reverted") { + trace!("supportsInterface reverted: {}", message); + } else { + debug!("supportsInterface call failed: {}", message); + } + false + } + } +} diff --git a/cartesi-rollups/node/state-manager/src/lib.rs b/cartesi-rollups/node/state-manager/src/lib.rs index 442afc82..bb83317d 100644 --- a/cartesi-rollups/node/state-manager/src/lib.rs +++ b/cartesi-rollups/node/state-manager/src/lib.rs @@ -65,6 +65,7 @@ impl Proof { #[derive(Clone, Debug, PartialEq, Eq)] pub struct Settlement { pub computation_hash: Digest, + pub final_state: Hash, pub output_merkle: Hash, pub output_proof: Proof, } diff --git a/cartesi-rollups/node/state-manager/src/persistent_state_access.rs b/cartesi-rollups/node/state-manager/src/persistent_state_access.rs index b11d772f..d8a618d6 100644 --- a/cartesi-rollups/node/state-manager/src/persistent_state_access.rs +++ b/cartesi-rollups/node/state-manager/src/persistent_state_access.rs @@ -12,6 +12,7 @@ use crate::{ use alloy::primitives::U256; use cartesi_dave_merkle::{Digest, MerkleBuilder}; +use cartesi_machine::types::Hash; use rusqlite::Connection; #[derive(Debug)] @@ -212,7 +213,7 @@ impl StateManager for PersistentStateAccess { let settlement = { let leafs = rollup_data::get_all_commitments(&self.connection, previous_epoch_number)?; - let computation_hash = if !leafs.is_empty() { + let (computation_hash, final_state) = if !leafs.is_empty() { build_commitment_from_hashes(&leafs) } else { assert_eq!(machine.next_input_index_in_epoch(), 0); @@ -226,6 +227,7 @@ impl StateManager for PersistentStateAccess { Settlement { computation_hash, + final_state, output_merkle, output_proof, } @@ -286,7 +288,7 @@ impl StateManager for PersistentStateAccess { } } -fn build_commitment_from_hashes(state_hashes: &[CommitmentLeaf]) -> Digest { +fn build_commitment_from_hashes(state_hashes: &[CommitmentLeaf]) -> (Digest, Hash) { let mut builder = MerkleBuilder::default(); assert!(!state_hashes.is_empty()); @@ -306,7 +308,7 @@ fn build_commitment_from_hashes(state_hashes: &[CommitmentLeaf]) -> Digest { ); let tree = builder.build(); - tree.root_hash() + (tree.root_hash(), last.hash) } #[cfg(test)] @@ -523,13 +525,14 @@ mod tests { access.roll_epoch()?; assert_eq!(access.latest_snapshot()?.epoch(), 1); + let (computation_hash, final_state) = + build_commitment_from_hashes(&[commitment_leaf_1.clone(), commitment_leaf_2.clone()]); + assert_eq!( access.settlement_info(0)?.unwrap(), Settlement { - computation_hash: build_commitment_from_hashes(&[ - commitment_leaf_1.clone(), - commitment_leaf_2.clone() - ]), + computation_hash, + final_state, output_merkle, output_proof }, diff --git a/cartesi-rollups/node/state-manager/src/sql/migrations.sql b/cartesi-rollups/node/state-manager/src/sql/migrations.sql index e14e5fa9..9006e653 100644 --- a/cartesi-rollups/node/state-manager/src/sql/migrations.sql +++ b/cartesi-rollups/node/state-manager/src/sql/migrations.sql @@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS settlement_info ( epoch_number INTEGER NOT NULL PRIMARY KEY CHECK (epoch_number >= 0), computation_hash BLOB NOT NULL, + final_state BLOB NOT NULL, output_merkle BLOB NOT NULL, output_proof BLOB NOT NULL ); diff --git a/cartesi-rollups/node/state-manager/src/sql/rollup_data.rs b/cartesi-rollups/node/state-manager/src/sql/rollup_data.rs index 0b9be5d2..25463648 100644 --- a/cartesi-rollups/node/state-manager/src/sql/rollup_data.rs +++ b/cartesi-rollups/node/state-manager/src/sql/rollup_data.rs @@ -29,16 +29,21 @@ fn convert_row_to_settlement(row: &rusqlite::Row) -> rusqlite::Result [u8;32] + let fs_blob: Vec = row.get(1)?; + let final_state: Hash = fs_blob.try_into().expect("final_state must be 32 bytes"); + // output_merkle blob -> [u8;32] - let om_blob: Vec = row.get(1)?; + let om_blob: Vec = row.get(2)?; let output_merkle: Hash = om_blob.try_into().expect("output_merkle must be 32 bytes"); // output_proof blob -> Proof - let proof_blob: Vec = row.get(2)?; + let proof_blob: Vec = row.get(3)?; let output_proof = Proof::from_flattened(proof_blob); Ok(Settlement { computation_hash, + final_state, output_merkle, output_proof, }) @@ -108,7 +113,7 @@ pub fn settlement_info(conn: &Connection, epoch_number: u64) -> Result B256 { (*h).into() }) .collect(); trace!( - "final state for tournament {} at position {}", - proof.node, proof.position + "join tournament {:?} with final_state {} at position {}, left {}, right {}, proof_len {}", + tournament, + proof.node, + proof.position, + left_child, + right_child, + proof.siblings.len() ); let tx_result = tournament .joinTournament( diff --git a/prt/contracts/justfile b/prt/contracts/justfile index adfdacbb..3e1f942a 100644 --- a/prt/contracts/justfile +++ b/prt/contracts/justfile @@ -1,7 +1,7 @@ BINDINGS_DIR := "./bindings-rs/src/contract" DEPLOYMENTS_DIR := "./deployments" SRC_DIR := "." -BINDINGS_FILTER := "^[^I].+TournamentFactory|^Tournament$" +BINDINGS_FILTER := "^[^I].+TournamentFactory|^Tournament$|^SafetyGateTask$" default: build diff --git a/prt/contracts/script/Deployment.s.sol b/prt/contracts/script/Deployment.s.sol index bc2fb3cd..40876431 100644 --- a/prt/contracts/script/Deployment.s.sol +++ b/prt/contracts/script/Deployment.s.sol @@ -26,6 +26,9 @@ import { import { MultiLevelTournamentFactory } from "src/tournament/MultiLevelTournamentFactory.sol"; +import { + SafetyGateTaskSpawner +} from "src/safety-gate-task/SafetyGateTaskSpawner.sol"; import {Tournament} from "src/tournament/Tournament.sol"; import {Time} from "src/tournament/libs/Time.sol"; @@ -190,7 +193,7 @@ contract DeploymentScript is BaseDeploymentScript { ) ); - _storeDeployment( + address multiLevelTournamentFactory = _storeDeployment( type(MultiLevelTournamentFactory).name, _create2( type(MultiLevelTournamentFactory).creationCode, @@ -202,6 +205,17 @@ contract DeploymentScript is BaseDeploymentScript { ) ); + address[] memory sentries = new address[](1); + sentries[0] = tx.origin; + + _storeDeployment( + type(SafetyGateTaskSpawner).name, + _create2( + type(SafetyGateTaskSpawner).creationCode, + abi.encode(tx.origin, multiLevelTournamentFactory, maxAllowance, sentries) + ) + ); + vmSafe.stopBroadcast(); }