From 3a1701c945b69e7d353651b9a650b6211f2ef706 Mon Sep 17 00:00:00 2001 From: Stephen Chen <20940639+stephenctw@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:41:11 +0800 Subject: [PATCH 1/2] refactor(prt-contracts): merge (non)leafTournament into Tournament --- .../contracts/test/DaveConsensus.t.sol | 6 +- prt/client-rs/core/src/tournament/reader.rs | 5 +- prt/client-rs/core/src/tournament/sender.rs | 14 +- .../core/src/tournament/tournament.rs | 18 - prt/contracts/justfile | 2 +- prt/contracts/script/Deployment.s.sol | 57 +- prt/contracts/src/ITournament.sol | 38 +- prt/contracts/src/tournament/Tournament.sol | 1052 +++++++++++++++++ .../tournament/abstracts/LeafTournament.sol | 155 --- .../abstracts/NonLeafTournament.sol | 243 ---- .../src/tournament/abstracts/Tournament.sol | 754 ------------ .../tournament/concretes/BottomTournament.sol | 53 - .../tournament/concretes/MiddleTournament.sol | 55 - .../concretes/SingleLevelTournament.sol | 47 - .../tournament/concretes/TopTournament.sol | 45 - .../IMultiLevelTournamentFactory.sol | 18 +- .../factories/MultiLevelTournamentFactory.sol | 150 +-- .../SingleLevelTournamentFactory.sol | 85 -- .../multilevel/BottomTournamentFactory.sol | 73 -- .../multilevel/MiddleTournamentFactory.sol | 74 -- .../multilevel/TopTournamentFactory.sol | 62 - prt/contracts/test/TournamentFactory.t.sol | 7 +- prt/contracts/test/Util.sol | 99 +- 23 files changed, 1236 insertions(+), 1876 deletions(-) create mode 100644 prt/contracts/src/tournament/Tournament.sol delete mode 100644 prt/contracts/src/tournament/abstracts/LeafTournament.sol delete mode 100644 prt/contracts/src/tournament/abstracts/NonLeafTournament.sol delete mode 100644 prt/contracts/src/tournament/abstracts/Tournament.sol delete mode 100644 prt/contracts/src/tournament/concretes/BottomTournament.sol delete mode 100644 prt/contracts/src/tournament/concretes/MiddleTournament.sol delete mode 100644 prt/contracts/src/tournament/concretes/SingleLevelTournament.sol delete mode 100644 prt/contracts/src/tournament/concretes/TopTournament.sol delete mode 100644 prt/contracts/src/tournament/factories/SingleLevelTournamentFactory.sol delete mode 100644 prt/contracts/src/tournament/factories/multilevel/BottomTournamentFactory.sol delete mode 100644 prt/contracts/src/tournament/factories/multilevel/MiddleTournamentFactory.sol delete mode 100644 prt/contracts/src/tournament/factories/multilevel/TopTournamentFactory.sol diff --git a/cartesi-rollups/contracts/test/DaveConsensus.t.sol b/cartesi-rollups/contracts/test/DaveConsensus.t.sol index 2c201377..da91e095 100644 --- a/cartesi-rollups/contracts/test/DaveConsensus.t.sol +++ b/cartesi-rollups/contracts/test/DaveConsensus.t.sol @@ -84,6 +84,8 @@ contract MockTournament is ITournament { return true; } + error NotImplemented(); + function bondValue() external pure override returns (uint256) { revert NotImplemented(); } @@ -146,10 +148,6 @@ contract MockTournament is ITournament { revert NotImplemented(); } - function nonRootTournamentArgs() external pure override returns (NonRootArguments memory) { - revert NotImplemented(); - } - function canWinMatchByTimeout(Match.Id calldata) external pure override returns (bool) { revert NotImplemented(); } diff --git a/prt/client-rs/core/src/tournament/reader.rs b/prt/client-rs/core/src/tournament/reader.rs index 596ba2bb..307edf7a 100644 --- a/prt/client-rs/core/src/tournament/reader.rs +++ b/prt/client-rs/core/src/tournament/reader.rs @@ -16,7 +16,7 @@ use crate::tournament::{ TournamentWinner, }; use cartesi_dave_merkle::Digest; -use cartesi_prt_contracts::{non_leaf_tournament, tournament}; +use cartesi_prt_contracts::tournament; #[derive(Clone)] pub struct StateReader { @@ -37,8 +37,7 @@ impl StateReader { tournament_address: Address, match_id: MatchID, ) -> Result> { - let tournament = - non_leaf_tournament::NonLeafTournament::new(tournament_address, &self.client); + let tournament = tournament::Tournament::new(tournament_address, &self.client); let events = tournament .NewInnerTournament_filter() .address(tournament_address) diff --git a/prt/client-rs/core/src/tournament/sender.rs b/prt/client-rs/core/src/tournament/sender.rs index f9de3e71..4e240d7b 100644 --- a/prt/client-rs/core/src/tournament/sender.rs +++ b/prt/client-rs/core/src/tournament/sender.rs @@ -14,7 +14,7 @@ use ruint::aliases::U256; use crate::{machine::MachineProof, tournament::MatchID}; use cartesi_dave_merkle::{Digest, MerkleProof}; -use cartesi_prt_contracts::{leaf_tournament, non_leaf_tournament, tournament}; +use cartesi_prt_contracts::tournament; #[derive(Clone, Debug)] pub struct EthArenaSender { @@ -167,7 +167,7 @@ impl ArenaSender for EthArenaSender { right_leaf: Digest, initial_hash_proof: &MerkleProof, ) -> Result<()> { - let tournament = non_leaf_tournament::NonLeafTournament::new(tournament, &self.provider); + let tournament = tournament::Tournament::new(tournament, &self.provider); let initial_hash_siblings = initial_hash_proof .siblings .iter() @@ -193,7 +193,7 @@ impl ArenaSender for EthArenaSender { left_node: Digest, right_node: Digest, ) -> Result<()> { - let tournament = non_leaf_tournament::NonLeafTournament::new(tournament, &self.provider); + let tournament = tournament::Tournament::new(tournament, &self.provider); let tx_result = tournament .winInnerTournament(child_tournament, left_node.into(), right_node.into()) .send() @@ -208,7 +208,7 @@ impl ArenaSender for EthArenaSender { left_node: Digest, right_node: Digest, ) -> Result<()> { - let tournament = non_leaf_tournament::NonLeafTournament::new(tournament, &self.provider); + let tournament = tournament::Tournament::new(tournament, &self.provider); let tx_result = tournament .winMatchByTimeout(match_id.into(), left_node.into(), right_node.into()) .send() @@ -224,7 +224,7 @@ impl ArenaSender for EthArenaSender { right_leaf: Digest, initial_hash_proof: &MerkleProof, ) -> Result<()> { - let tournament = leaf_tournament::LeafTournament::new(tournament, &self.provider); + let tournament = tournament::Tournament::new(tournament, &self.provider); let initial_hash_siblings = initial_hash_proof .siblings .iter() @@ -251,7 +251,7 @@ impl ArenaSender for EthArenaSender { right_node: Digest, proofs: MachineProof, ) -> Result<()> { - let tournament = leaf_tournament::LeafTournament::new(tournament, &self.provider); + let tournament = tournament::Tournament::new(tournament, &self.provider); let tx_result = tournament .winLeafMatch( match_id.into(), @@ -278,7 +278,7 @@ impl ArenaSender for EthArenaSender { tournament: Address, inner_tournament: Address, ) -> Result<()> { - let tournament = non_leaf_tournament::NonLeafTournament::new(tournament, &self.provider); + let tournament = tournament::Tournament::new(tournament, &self.provider); let tx_result = tournament .eliminateInnerTournament(inner_tournament) .send() diff --git a/prt/client-rs/core/src/tournament/tournament.rs b/prt/client-rs/core/src/tournament/tournament.rs index 48d1a8c5..b1c1d650 100644 --- a/prt/client-rs/core/src/tournament/tournament.rs +++ b/prt/client-rs/core/src/tournament/tournament.rs @@ -34,24 +34,6 @@ impl From for cartesi_prt_contracts::tournament::Match::Id { } } -impl From for cartesi_prt_contracts::non_leaf_tournament::Match::Id { - fn from(match_id: MatchID) -> Self { - cartesi_prt_contracts::non_leaf_tournament::Match::Id { - commitmentOne: match_id.commitment_one.into(), - commitmentTwo: match_id.commitment_two.into(), - } - } -} - -impl From for cartesi_prt_contracts::leaf_tournament::Match::Id { - fn from(match_id: MatchID) -> Self { - cartesi_prt_contracts::leaf_tournament::Match::Id { - commitmentOne: match_id.commitment_one.into(), - commitmentTwo: match_id.commitment_two.into(), - } - } -} - /// Struct used to communicate the state of a commitment. #[derive(Clone, Copy, Debug)] pub struct CommitmentState { diff --git a/prt/contracts/justfile b/prt/contracts/justfile index ba68abf3..adfdacbb 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|LeafTournament|^Tournament$" +BINDINGS_FILTER := "^[^I].+TournamentFactory|^Tournament$" default: build diff --git a/prt/contracts/script/Deployment.s.sol b/prt/contracts/script/Deployment.s.sol index 69063758..df9c0f6a 100644 --- a/prt/contracts/script/Deployment.s.sol +++ b/prt/contracts/script/Deployment.s.sol @@ -23,21 +23,10 @@ import { import { RiscVStateTransition } from "src/state-transition/RiscVStateTransition.sol"; -import {BottomTournament} from "src/tournament/concretes/BottomTournament.sol"; -import {MiddleTournament} from "src/tournament/concretes/MiddleTournament.sol"; -import {TopTournament} from "src/tournament/concretes/TopTournament.sol"; +import {Tournament} from "src/tournament/Tournament.sol"; import { MultiLevelTournamentFactory } from "src/tournament/factories/MultiLevelTournamentFactory.sol"; -import { - BottomTournamentFactory -} from "src/tournament/factories/multilevel/BottomTournamentFactory.sol"; -import { - MiddleTournamentFactory -} from "src/tournament/factories/multilevel/MiddleTournamentFactory.sol"; -import { - TopTournamentFactory -} from "src/tournament/factories/multilevel/TopTournamentFactory.sol"; import {Time} from "src/tournament/libs/Time.sol"; type Milliseconds is uint64; @@ -188,43 +177,9 @@ contract DeploymentScript is BaseDeploymentScript { ) ); - address topTournament = _storeDeployment( - type(TopTournament).name, - _create2(type(TopTournament).creationCode, abi.encode()) - ); - - address middleTournament = _storeDeployment( - type(MiddleTournament).name, - _create2(type(MiddleTournament).creationCode, abi.encode()) - ); - - address bottomTournament = _storeDeployment( - type(BottomTournament).name, - _create2(type(BottomTournament).creationCode, abi.encode()) - ); - - address topTournamentFactory = _storeDeployment( - type(TopTournamentFactory).name, - _create2( - type(TopTournamentFactory).creationCode, - abi.encode(topTournament) - ) - ); - - address middleTournamentFactory = _storeDeployment( - type(MiddleTournamentFactory).name, - _create2( - type(MiddleTournamentFactory).creationCode, - abi.encode(middleTournament) - ) - ); - - address bottomTournamentFactory = _storeDeployment( - type(BottomTournamentFactory).name, - _create2( - type(BottomTournamentFactory).creationCode, - abi.encode(bottomTournament) - ) + address tournamentImpl = _storeDeployment( + type(Tournament).name, + _create2(type(Tournament).creationCode, abi.encode()) ); address canonicalTournamentParametersProvider = _storeDeployment( @@ -240,9 +195,7 @@ contract DeploymentScript is BaseDeploymentScript { _create2( type(MultiLevelTournamentFactory).creationCode, abi.encode( - topTournamentFactory, - middleTournamentFactory, - bottomTournamentFactory, + tournamentImpl, canonicalTournamentParametersProvider, cartesiStateTransition ) diff --git a/prt/contracts/src/ITournament.sol b/prt/contracts/src/ITournament.sol index cbd9964a..bf2dfdcb 100644 --- a/prt/contracts/src/ITournament.sol +++ b/prt/contracts/src/ITournament.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.17; import {IDataProvider} from "prt-contracts/IDataProvider.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"; import {Match} from "prt-contracts/tournament/libs/Match.sol"; @@ -17,6 +18,17 @@ interface ITournament { // Types // + /// @notice Dispute information from a parent match. + /// @dev For non-root tournaments (level > 0), contains the two contested commitments + /// and final states from the parent match that created this tournament. + /// For root tournaments (level == 0), all fields are zero. + struct NestedDispute { + Tree.Node contestedCommitmentOne; + Machine.Hash contestedFinalStateOne; + Tree.Node contestedCommitmentTwo; + Machine.Hash contestedFinalStateTwo; + } + /// @notice Tournament arguments /// @param commitmentArgs The commitment arguments /// @param level The tournament level @@ -26,10 +38,14 @@ interface ITournament { /// @param maxAllowance The maximum time of a player clock /// @param matchEffort The worst-case time to compute a commitment /// @param provider The contract that provides input Merkle roots + /// @param nestedDispute Dispute information from parent match (zero for root tournaments) + /// @param stateTransition State transition contract, used by leaf-level operations + /// @param tournamentFactory Multi-level factory address (cast to IMultiLevelTournamentFactory when needed), used by non-leaf operations when instantiating inner tournaments /// @dev A root tournament is at level 0. /// A single-level tournament has 1 level. /// A multi-level tournament has 2 or more levels. /// Time is measured in base-layer blocks. + /// For root tournaments (level == 0), nestedDispute fields are zero. struct TournamentArguments { Commitment.Arguments commitmentArgs; uint64 level; @@ -39,17 +55,9 @@ interface ITournament { Time.Duration maxAllowance; Time.Duration matchEffort; IDataProvider provider; - } - - /// @notice Arguments for non-root tournaments (level > 0) - /// @dev Non-root tournaments are inner tournaments created by parent tournaments. - /// They need to track which two final states are being contested. - /// Root tournaments (level == 0) don't need these arguments. - struct NonRootArguments { - Tree.Node contestedCommitmentOne; - Machine.Hash contestedFinalStateOne; - Tree.Node contestedCommitmentTwo; - Machine.Hash contestedFinalStateTwo; + NestedDispute nestedDispute; + IStateTransition stateTransition; + address tournamentFactory; // Cast to IMultiLevelTournamentFactory when needed to avoid circular dependency } /// @notice Match deletion reason @@ -214,7 +222,9 @@ interface ITournament { uint256 commitment, Machine.Hash computed, Machine.Hash claimed ); error WrongNodesForStep(); - error NotImplemented(); + error RequireLeafTournament(); + error RequireNonLeafTournament(); + error RequireNonRootTournament(); // // Functions @@ -432,10 +442,6 @@ interface ITournament { returns (TournamentArguments memory); /// @notice Returns non-root tournament arguments - function nonRootTournamentArgs() - external - view - returns (NonRootArguments memory); /// @notice Check whether a match can be won by timeout. /// @param matchId The match ID diff --git a/prt/contracts/src/tournament/Tournament.sol b/prt/contracts/src/tournament/Tournament.sol new file mode 100644 index 00000000..fa0e4bb5 --- /dev/null +++ b/prt/contracts/src/tournament/Tournament.sol @@ -0,0 +1,1052 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +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 {IStateTransition} from "prt-contracts/IStateTransition.sol"; +import {ITournament} from "prt-contracts/ITournament.sol"; +import { + IMultiLevelTournamentFactory +} from "prt-contracts/tournament/factories/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"; +import {Match} from "prt-contracts/tournament/libs/Match.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"; + +/// @title Tournament — Asynchronous PRT-style dispute resolution +/// @notice Core, permissionless tournament that resolves disputes among +/// N parties in O(log N) depth under chess-clock timing. Pairing is asynchronous: +/// claims are matched as they arrive (or when winners re-enter), without a +/// prebuilt bracket. +/// +/// @dev +/// HIGH-LEVEL ROLE SPLIT (BY LEVEL) +/// - Root tournaments (level == 0, arbitrary levels >= 1): +/// * Entry point via `joinTournament`. +/// * Never have a parent match or contested final states. +/// * Cannot be eliminated (`canBeEliminated` reverts with `RequireNonRootTournament`). +/// * Winner is obtained via `arbitrationResult`. +/// +/// - Inner, non-root tournaments (level > 0, arbitrary levels >= 2): +/// * Always created by a parent tournament via +/// `sealInnerMatchAndCreateInnerTournament`. +/// * Have exactly two contested final states, stored in `NestedDispute`. +/// * Can be eliminated by the parent once the inner winner's allowance +/// window expires. +/// * Winner is obtained via `innerTournamentWinner`. +/// +/// - Leaf vs. non-leaf tournaments (by `level` vs `levels`): +/// * Leaf tournaments (level == levels - 1): +/// - Use `sealLeafMatch` and `winLeafMatch` (on-chain state transition). +/// - Do NOT create further inner tournaments. +/// * Non-leaf tournaments (level < levels - 1): +/// - Use `sealInnerMatchAndCreateInnerTournament` and `winInnerTournament`. +/// - Can recursively create new inner tournaments via `instantiateInner`. +contract Tournament is ITournament { + using Clones for address; + using Machine for Machine.Hash; + using Tree for Tree.Node; + using Commitment for Tree.Node; + using Commitment for Commitment.Arguments; + + using Time for Time.Instant; + using Time for Time.Duration; + + using Clock for Clock.State; + + using Match for Match.Id; + using Match for Match.IdHash; + using Match for Match.State; + + using Math for uint256; + + // + // Storage + // + Tree.Node danglingCommitment; + uint256 matchCount; + Time.Instant lastMatchDeleted; + + uint256 constant MAX_GAS_PRICE = 50 gwei; + uint256 constant MESSAGE_SENDER_PROFIT = 10 gwei; + bool transient locked; + + mapping(Tree.Node => Clock.State) clocks; + mapping(Tree.Node => Machine.Hash) finalStates; + mapping(Tree.Node => address) claimers; + + // matches existing in current tournament + mapping(Match.IdHash => Match.State) matches; + + /// @notice Mapping from inner tournament to its originating match id + /// @dev Used by nested (non-leaf) tournaments + mapping(ITournament => Match.Id) matchIdFromInnerTournaments; + + // + // Modifiers + // + + modifier tournamentNotFinished() { + _ensureTournamentIsNotFinished(); + _; + } + + modifier tournamentOpen() { + _ensureTournamentIsOpen(); + _; + } + + /// @notice Refunds the message sender with the amount + /// of Ether wasted on gas on this function call plus + /// a profit, capped by the current contract balance + /// and a fraction of the bond value. + /// @param gasEstimate A worst-case gas estimate for the modified function + /// forge-lint: disable-next-line(unwrapped-modifier-logic) + modifier refundable(uint256 gasEstimate) { + uint256 gasBefore = _refundableBefore(); + _; + _refundableAfter(gasBefore, gasEstimate); + } + + // + // Internal helpers and virtual-like methods + // + + /// @notice Get tournament arguments for this tournament instance + /// @dev Decodes immutable arguments passed during clone creation + function _tournamentArgs() + internal + view + returns (TournamentArguments memory) + { + return abi.decode(address(this).fetchCloneArgs(), (TournamentArguments)); + } + + /// @inheritdoc ITournament + function tournamentArguments() + public + view + override + returns (TournamentArguments memory) + { + return _tournamentArgs(); + } + + /// @notice Check if this tournament is a leaf tournament (level == levels - 1) + function _isLeafTournament(TournamentArguments memory _args) + internal + pure + returns (bool) + { + return _args.level == _args.levels - 1; + } + + /// @notice Check if this tournament is a root tournament (level == 0) + function _isRootTournament(TournamentArguments memory _args) + internal + pure + returns (bool) + { + return _args.level == 0; + } + + /// @notice Check if a final state is allowed to join the tournament. + function validContestedFinalState(Machine.Hash _finalState) + internal + view + returns (bool, Machine.Hash, Machine.Hash) + { + TournamentArguments memory args = tournamentArguments(); + + // ROOT CASE: level == 0 + // - Root tournaments are open to all participants, so any final state is valid. + // - There is no concept of "contested final states" at level 0. + if (args.level == 0) { + return (true, Machine.ZERO_STATE, Machine.ZERO_STATE); + } + + // NON-ROOT CASE: level > 0 + // - Inner tournaments only accept commitments that match one of the two + // contested final states from the parent match that created them. + NestedDispute memory nestedDispute = args.nestedDispute; + return ( + nestedDispute.contestedFinalStateOne.eq(_finalState) + || nestedDispute.contestedFinalStateTwo.eq(_finalState), + nestedDispute.contestedFinalStateOne, + nestedDispute.contestedFinalStateTwo + ); + } + + /// @notice Total gas estimate used to size the tournament bond. + /// @dev Includes: + /// - ADVANCE_MATCH across tree height (common to all levels), + /// - Leaf operations (seal + win), + /// - Inner operations (seal + win). + /// The extra term is chosen as max(leaf, inner) to keep a single, + /// safe bond size across root / inner / leaf tournaments. + function _totalGasEstimate() internal view returns (uint256) { + TournamentArguments memory args = tournamentArguments(); + uint256 base = Gas.ADVANCE_MATCH * args.commitmentArgs.height; + uint256 leafPart = Gas.SEAL_LEAF_MATCH + Gas.WIN_LEAF_MATCH; + uint256 innerPart = Gas.SEAL_INNER_MATCH_AND_CREATE_INNER_TOURNAMENT + + Gas.WIN_INNER_TOURNAMENT; + uint256 extra = leafPart > innerPart ? leafPart : innerPart; + return base + extra; + } + + // + // Methods + // + + function bondValue() public view override returns (uint256) { + return _totalGasEstimate() * MAX_GAS_PRICE; + } + + /// @notice Join a tournament (root or inner) with a commitment. + /// @dev + /// - ROOT (level == 0): + /// * Open to all final states, contested fields in TournamentArguments are zero. + /// - NON-ROOT (level > 0): + /// * Final state must match one of the two contested final states. + function joinTournament( + Machine.Hash _finalState, + bytes32[] calldata _proof, + Tree.Node _leftNode, + Tree.Node _rightNode + ) external payable override tournamentOpen { + require(msg.value >= bondValue(), InsufficientBond()); + + Tree.Node _commitmentRoot = _leftNode.join(_rightNode); + + TournamentArguments memory args = tournamentArguments(); + + _commitmentRoot.requireFinalState( + args.commitmentArgs.height, _finalState, _proof + ); + + requireValidContestedFinalState(_finalState); + finalStates[_commitmentRoot] = _finalState; + + Clock.State storage _clock = clocks[_commitmentRoot]; + _clock.requireNotInitialized(); + _clock.setNewPaused(args.startInstant, args.allowance); + + pairCommitment(_commitmentRoot, _clock, _leftNode, _rightNode); + claimers[_commitmentRoot] = msg.sender; + emit CommitmentJoined(_commitmentRoot, _finalState, msg.sender); + } + + /// @inheritdoc ITournament + function advanceMatch( + Match.Id calldata _matchId, + Tree.Node _leftNode, + Tree.Node _rightNode, + Tree.Node _newLeftNode, + Tree.Node _newRightNode + ) external override refundable(Gas.ADVANCE_MATCH) tournamentNotFinished { + Match.State storage _matchState = matches[_matchId.hashFromId()]; + _matchState.requireExist(); + _matchState.requireCanBeAdvanced(); + + _matchState.advanceMatch( + _matchId, _leftNode, _rightNode, _newLeftNode, _newRightNode + ); + + clocks[_matchId.commitmentOne].advanceClock(); + clocks[_matchId.commitmentTwo].advanceClock(); + } + + /// @notice Win a match by timeout at any level (root or inner). + /// @dev + /// - Behavior is identical for root and inner tournaments; level only affects + /// how the winner is later interpreted by parent tournaments. + function winMatchByTimeout( + Match.Id calldata _matchId, + Tree.Node _leftNode, + Tree.Node _rightNode + ) + external + override + refundable(Gas.WIN_MATCH_BY_TIMEOUT) + tournamentNotFinished + { + matches[_matchId.hashFromId()].requireExist(); + Clock.State storage _clockOne = clocks[_matchId.commitmentOne]; + Clock.State storage _clockTwo = clocks[_matchId.commitmentTwo]; + + _clockOne.requireInitialized(); + _clockTwo.requireInitialized(); + + if (_clockOne.hasTimeLeft() && !_clockTwo.hasTimeLeft()) { + require( + _matchId.commitmentOne.verify(_leftNode, _rightNode), + WrongChildren(1, _matchId.commitmentOne, _leftNode, _rightNode) + ); + + _clockOne.deducted(_clockTwo.timeSinceTimeout()); + pairCommitment( + _matchId.commitmentOne, _clockOne, _leftNode, _rightNode + ); + + deleteMatch( + _matchId, MatchDeletionReason.TIMEOUT, WinnerCommitment.ONE + ); + } else if (!_clockOne.hasTimeLeft() && _clockTwo.hasTimeLeft()) { + require( + _matchId.commitmentTwo.verify(_leftNode, _rightNode), + WrongChildren(2, _matchId.commitmentTwo, _leftNode, _rightNode) + ); + + _clockTwo.deducted(_clockOne.timeSinceTimeout()); + pairCommitment( + _matchId.commitmentTwo, _clockTwo, _leftNode, _rightNode + ); + + deleteMatch( + _matchId, MatchDeletionReason.TIMEOUT, WinnerCommitment.TWO + ); + } else { + revert ClockNotTimedOut(); + } + } + + function eliminateMatchByTimeout(Match.Id calldata _matchId) + external + override + refundable(Gas.ELIMINATE_MATCH_BY_TIMEOUT) + tournamentNotFinished + { + matches[_matchId.hashFromId()].requireExist(); + Clock.State storage _clockOne = clocks[_matchId.commitmentOne]; + Clock.State storage _clockTwo = clocks[_matchId.commitmentTwo]; + + _clockOne.requireInitialized(); + _clockTwo.requireInitialized(); + + if ( + (!_clockOne.hasTimeLeft() + && !_clockTwo.timeLeft().gt(_clockOne.timeSinceTimeout())) + || (!_clockTwo.hasTimeLeft() + && !_clockOne.timeLeft().gt(_clockTwo.timeSinceTimeout())) + ) { + deleteMatch( + _matchId, MatchDeletionReason.TIMEOUT, WinnerCommitment.NONE + ); + } else { + revert BothClocksHaveNotTimedOut(); + } + } + + /// @notice Try to recover the bond of the winning commitment submitter. + /// @dev + /// - ROOT: + /// * Winner is the root tournament winner. + /// - NON-ROOT: + /// * Winner is the inner winner that will be used by the parent tournament. + function tryRecoveringBond() public override returns (bool) { + require(isFinished(), TournamentNotFinished()); + + (bool hasDangling, Tree.Node winningCommitment) = + hasDanglingCommitment(); + require(hasDangling, NoWinner()); + + address winner = claimers[winningCommitment]; + assert(winner != address(0)); + + uint256 contractBalance = address(this).balance; + (bool success,) = winner.call{value: contractBalance}(""); + + if (success) { + deleteClaimer(winningCommitment); + } + + return success; + } + + // + // Leaf tournament operations + // + + /// @inheritdoc ITournament + /// @dev + /// - LEAF ONLY (level == levels - 1): + /// * Seals a leaf-level match using the on-chain state commitment tree. + /// - NON-LEAF (level < levels - 1): + /// * Not implemented; will revert with `RequireLeafTournament`. + function sealLeafMatch( + Match.Id calldata _matchId, + Tree.Node _leftLeaf, + Tree.Node _rightLeaf, + Machine.Hash _agreeHash, + bytes32[] calldata _agreeHashProof + ) external override refundable(Gas.SEAL_LEAF_MATCH) tournamentNotFinished { + TournamentArguments memory args = tournamentArguments(); + if (!_isLeafTournament(args)) { + revert RequireLeafTournament(); + } + + Match.State storage _matchState = matches[_matchId.hashFromId()]; + _matchState.requireExist(); + _matchState.requireCanBeFinalized(); + + { + Clock.State storage _clock1 = clocks[_matchId.commitmentOne]; + Clock.State storage _clock2 = clocks[_matchId.commitmentTwo]; + _clock1.setPaused(); + _clock1.advanceClock(); + _clock2.setPaused(); + _clock2.advanceClock(); + } + + _matchState.sealMatch( + args.commitmentArgs, + _matchId, + _leftLeaf, + _rightLeaf, + _agreeHash, + _agreeHashProof + ); + } + + /// @inheritdoc ITournament + function winLeafMatch( + Match.Id calldata _matchId, + Tree.Node _leftNode, + Tree.Node _rightNode, + bytes calldata proofs + ) external override refundable(Gas.WIN_LEAF_MATCH) tournamentNotFinished { + TournamentArguments memory args = tournamentArguments(); + if (!_isLeafTournament(args)) { + revert RequireLeafTournament(); + } + + Clock.State storage _clockOne = clocks[_matchId.commitmentOne]; + Clock.State storage _clockTwo = clocks[_matchId.commitmentTwo]; + _clockOne.requireInitialized(); + _clockTwo.requireInitialized(); + + Match.State storage _matchState = matches[_matchId.hashFromId()]; + _matchState.requireExist(); + _matchState.requireIsFinished(); + + ( + Machine.Hash _agreeHash, + uint256 _agreeCycle, + Machine.Hash _finalStateOne, + Machine.Hash _finalStateTwo + ) = _matchState.getDivergence(args.commitmentArgs); + + IStateTransition stateTransition = _tournamentArgs().stateTransition; + Machine.Hash _finalState = Machine.Hash + .wrap( + stateTransition.transitionState( + Machine.Hash.unwrap(_agreeHash), + _agreeCycle, + proofs, + args.provider + ) + ); + + if (_leftNode.join(_rightNode).eq(_matchId.commitmentOne)) { + require( + _finalState.eq(_finalStateOne), + WrongFinalState(1, _finalState, _finalStateOne) + ); + + _clockOne.setPaused(); + pairCommitment( + _matchId.commitmentOne, _clockOne, _leftNode, _rightNode + ); + + deleteMatch( + _matchId, MatchDeletionReason.STEP, WinnerCommitment.ONE + ); + } else if (_leftNode.join(_rightNode).eq(_matchId.commitmentTwo)) { + require( + _finalState.eq(_finalStateTwo), + WrongFinalState(2, _finalState, _finalStateTwo) + ); + + _clockTwo.setPaused(); + pairCommitment( + _matchId.commitmentTwo, _clockTwo, _leftNode, _rightNode + ); + + deleteMatch( + _matchId, MatchDeletionReason.STEP, WinnerCommitment.TWO + ); + } else { + revert WrongNodesForStep(); + } + } + + // + // Inner (non-leaf) tournament operations + // + + /// @inheritdoc ITournament + /// @dev + /// - NON-LEAF ONLY (level < levels - 1): + /// * Seals an inner match and spawns an inner tournament at `level + 1`. + /// - LEAF (level == levels - 1): + /// * Not implemented; will revert with `RequireNonLeafTournament`. + function sealInnerMatchAndCreateInnerTournament( + Match.Id calldata _matchId, + Tree.Node _leftLeaf, + Tree.Node _rightLeaf, + Machine.Hash _agreeHash, + bytes32[] calldata _agreeHashProof + ) + external + override + refundable(Gas.SEAL_INNER_MATCH_AND_CREATE_INNER_TOURNAMENT) + tournamentNotFinished + { + TournamentArguments memory args = tournamentArguments(); + if (_isLeafTournament(args)) { + revert RequireNonLeafTournament(); + } + + Match.State storage _matchState = matches[_matchId.hashFromId()]; + _matchState.requireCanBeFinalized(); + + Time.Duration _maxDuration; + { + Clock.State storage _clock1 = clocks[_matchId.commitmentOne]; + Clock.State storage _clock2 = clocks[_matchId.commitmentTwo]; + _clock1.setPaused(); + _clock2.setPaused(); + _maxDuration = Clock.max(_clock1, _clock2); + } + + (Machine.Hash _finalStateOne, Machine.Hash _finalStateTwo) = _matchState.sealMatch( + args.commitmentArgs, + _matchId, + _leftLeaf, + _rightLeaf, + _agreeHash, + _agreeHashProof + ); + + ITournament _inner = instantiateInner( + _agreeHash, + _matchId.commitmentOne, + _finalStateOne, + _matchId.commitmentTwo, + _finalStateTwo, + _maxDuration, + _matchState.toCycle(args.commitmentArgs), + args.level + 1 + ); + matchIdFromInnerTournaments[_inner] = _matchId; + + emit NewInnerTournament(_matchId.hashFromId(), _inner); + } + + /// @inheritdoc ITournament + function winInnerTournament( + ITournament _childTournament, + Tree.Node _leftNode, + Tree.Node _rightNode + ) + external + override + refundable(Gas.WIN_INNER_TOURNAMENT) + tournamentNotFinished + { + TournamentArguments memory args = tournamentArguments(); + if (_isLeafTournament(args)) { + revert RequireNonLeafTournament(); + } + + Match.Id memory _matchId = matchIdFromInnerTournaments[_childTournament]; + Match.IdHash _matchIdHash = _matchId.hashFromId(); + _matchIdHash.requireExist(); + + Match.State storage _matchState = matches[_matchIdHash]; + _matchState.requireExist(); + _matchState.requireIsFinished(); + + require( + !_childTournament.canBeEliminated(), + ChildTournamentMustBeEliminated() + ); + + (bool finished, Tree.Node _winner,, Clock.State memory _innerClock) = + _childTournament.innerTournamentWinner(); + require(finished, ChildTournamentNotFinished()); + _winner.requireExist(); + + Tree.Node _commitmentRoot = _leftNode.join(_rightNode); + require( + _commitmentRoot.eq(_winner), + WrongTournamentWinner(_commitmentRoot, _winner) + ); + + Clock.State storage _clock = clocks[_commitmentRoot]; + _clock.requireInitialized(); + _clock.reInitialized(_innerClock); + + pairCommitment(_commitmentRoot, _clock, _leftNode, _rightNode); + + WinnerCommitment _winnerCommitment; + + if (_winner.eq(_matchId.commitmentOne)) { + _winnerCommitment = WinnerCommitment.ONE; + } else if (_winner.eq(_matchId.commitmentTwo)) { + _winnerCommitment = WinnerCommitment.TWO; + } else { + revert InvalidTournamentWinner(_winner); + } + + deleteMatch( + _matchId, MatchDeletionReason.CHILD_TOURNAMENT, _winnerCommitment + ); + delete matchIdFromInnerTournaments[_childTournament]; + + _childTournament.tryRecoveringBond(); + } + + /// @inheritdoc ITournament + function eliminateInnerTournament(ITournament _childTournament) + external + override + refundable(Gas.ELIMINATE_INNER_TOURNAMENT) + tournamentNotFinished + { + TournamentArguments memory args = tournamentArguments(); + if (_isLeafTournament(args)) { + revert RequireNonLeafTournament(); + } + + Match.Id memory _matchId = matchIdFromInnerTournaments[_childTournament]; + Match.IdHash _matchIdHash = _matchId.hashFromId(); + _matchIdHash.requireExist(); + + Match.State storage _matchState = matches[_matchIdHash]; + _matchState.requireExist(); + _matchState.requireIsFinished(); + + require( + _childTournament.canBeEliminated(), + ChildTournamentCannotBeEliminated() + ); + + deleteMatch( + _matchId, + MatchDeletionReason.CHILD_TOURNAMENT, + WinnerCommitment.NONE + ); + delete matchIdFromInnerTournaments[_childTournament]; + } + + /// @notice Instantiate an inner tournament using the configured factory. + /// @dev + /// - Called only on NON-LEAF tournaments. + /// - The factory determines leaf vs non-leaf configuration based on `_level`. + function instantiateInner( + Machine.Hash _initialHash, + Tree.Node _contestedCommitmentOne, + Machine.Hash _contestedFinalStateOne, + Tree.Node _contestedCommitmentTwo, + Machine.Hash _contestedFinalStateTwo, + Time.Duration _allowance, + uint256 _startCycle, + uint64 _level + ) private returns (ITournament) { + TournamentArguments memory args = tournamentArguments(); + + IMultiLevelTournamentFactory tournamentFactory = + IMultiLevelTournamentFactory(_tournamentArgs().tournamentFactory); + return tournamentFactory.instantiateInner( + _initialHash, + _contestedCommitmentOne, + _contestedFinalStateOne, + _contestedCommitmentTwo, + _contestedFinalStateTwo, + _allowance, + _startCycle, + _level, + args.provider + ); + } + + // + // View methods + // + + function canWinMatchByTimeout(Match.Id calldata _matchId) + external + view + override + returns (bool) + { + Clock.State memory _clockOne = clocks[_matchId.commitmentOne]; + Clock.State memory _clockTwo = clocks[_matchId.commitmentTwo]; + + return !_clockOne.hasTimeLeft() || !_clockTwo.hasTimeLeft(); + } + + function getCommitment(Tree.Node _commitmentRoot) + public + view + override + returns (Clock.State memory, Machine.Hash) + { + return (clocks[_commitmentRoot], finalStates[_commitmentRoot]); + } + + function getMatch(Match.IdHash _matchIdHash) + public + view + override + returns (Match.State memory) + { + return matches[_matchIdHash]; + } + + function getMatchCycle(Match.IdHash _matchIdHash) + external + view + override + returns (uint256) + { + Match.State memory _m = getMatch(_matchIdHash); + Commitment.Arguments memory args = tournamentArguments().commitmentArgs; + + return args.toCycle(_m.runningLeafPosition); + } + + /// @notice Return core tournament parameters derived from `TournamentArguments`. + /// @dev + /// - `maxLevel` (levels): total number of levels in the hierarchy. + /// - `level`: this tournament's level. + /// - `log2step` / `height`: leaf spacing and tree height for commitments. + function tournamentLevelConstants() + external + view + override + returns ( + uint64 _maxLevel, + uint64 _level, + uint64 _log2step, + uint64 _height + ) + { + TournamentArguments memory args; + args = tournamentArguments(); + _maxLevel = args.levels; + _level = args.level; + _log2step = args.commitmentArgs.log2step; + _height = args.commitmentArgs.height; + } + + // + // Time view methods + // + + /// @notice Returns true iff the tournament's global allowance has elapsed. + /// @dev + /// - ROOT and NON-ROOT: + /// * Same behavior: closed if `now >= startInstant + allowance`. + function isClosed() public view override returns (bool) { + TournamentArguments memory args = tournamentArguments(); + return args.startInstant.timeoutElapsed(args.allowance); + } + + /// @notice Returns true iff the tournament is closed and has no active matches. + /// @dev + /// - ROOT: + /// * Finished when there are no more matches and the global timeout elapsed. + /// - NON-ROOT: + /// * Same condition; used both for elimination and inner-winner computation. + function isFinished() public view override returns (bool) { + return isClosed() && matchCount == 0; + } + + /// @notice Returns the time at which this tournament became "safe to decide". + /// @dev + /// - ROOT: + /// * Used to measure bond recovery and elimination windows when acting + /// as an inner tournament of a hypothetical higher level. + /// - NON-ROOT: + /// * Used by `canBeEliminated` and `innerTournamentWinner`. + function timeFinished() public view override returns (bool, Time.Instant) { + if (!isFinished()) { + return (false, Time.ZERO_INSTANT); + } + + TournamentArguments memory args = tournamentArguments(); + + Time.Instant tournamentClosed = args.startInstant.add(args.allowance); + Time.Instant winnerCouldWin = tournamentClosed.max(lastMatchDeleted); + + return (true, winnerCouldWin); + } + + /// @notice Get the root tournament's final result. + /// @dev + /// - ROOT ONLY (level == 0): + /// * Returns the winner commitment and its final state once finished. + /// - NON-ROOT: + /// * Not used; parents call `innerTournamentWinner` instead. + function arbitrationResult() + external + view + override + returns (bool, Tree.Node, Machine.Hash) + { + if (!isFinished()) { + return (false, Tree.ZERO_NODE, Machine.ZERO_STATE); + } + + (bool _hasDanglingCommitment, Tree.Node _danglingCommitment) = + hasDanglingCommitment(); + require(_hasDanglingCommitment, TournamentFailedNoWinner()); + + Machine.Hash _finalState = finalStates[_danglingCommitment]; + return (true, _danglingCommitment, _finalState); + } + + // + // Internal functions + // + + function setDanglingCommitment(Tree.Node _node) internal { + danglingCommitment = _node; + } + + function clearDanglingCommitment() internal { + danglingCommitment = Tree.ZERO_NODE; + } + + function hasDanglingCommitment() + internal + view + returns (bool _h, Tree.Node _node) + { + _node = danglingCommitment; + + if (!_node.isZero()) { + _h = true; + } + } + + /// @notice Pair a new commitment into the tournament, creating a match if an + /// existing dangling commitment is available. + /// @dev If there's a dangling commitment, creates a match between it and the + /// new commitment. Otherwise, stores the new commitment as dangling. + function pairCommitment( + Tree.Node _rootHash, + Clock.State storage _newClock, + Tree.Node _leftNode, + Tree.Node _rightNode + ) internal { + assert(_leftNode.join(_rightNode).eq(_rootHash)); + (bool _hasDanglingCommitment, Tree.Node _danglingCommitment) = + hasDanglingCommitment(); + + if (_hasDanglingCommitment) { + TournamentArguments memory args = tournamentArguments(); + (Match.IdHash _matchId, Match.State memory _matchState) = Match.createMatch( + args.commitmentArgs, + _danglingCommitment, + _rootHash, + _leftNode, + _rightNode + ); + + matches[_matchId] = _matchState; + + Clock.State storage _firstClock = clocks[_danglingCommitment]; + + _firstClock.addMatchEffort(args.matchEffort, args.maxAllowance); + _newClock.addMatchEffort(args.matchEffort, args.maxAllowance); + + _firstClock.advanceClock(); + + clearDanglingCommitment(); + matchCount++; + + emit MatchCreated( + _matchId, _danglingCommitment, _rootHash, _leftNode + ); + } else { + setDanglingCommitment(_rootHash); + } + } + + function deleteMatch( + Match.Id memory _matchId, + MatchDeletionReason _reason, + WinnerCommitment _winnerCommitment + ) internal { + matchCount--; + lastMatchDeleted = Time.currentTime(); + if (_winnerCommitment == WinnerCommitment.NONE) { + deleteClaimer(_matchId.commitmentOne); + deleteClaimer(_matchId.commitmentTwo); + } else if (_winnerCommitment == WinnerCommitment.ONE) { + deleteClaimer(_matchId.commitmentTwo); + } else if (_winnerCommitment == WinnerCommitment.TWO) { + deleteClaimer(_matchId.commitmentOne); + } else { + revert InvalidWinnerCommitment(_winnerCommitment); + } + Match.IdHash _matchIdHash = _matchId.hashFromId(); + delete matches[_matchIdHash]; + emit MatchDeleted( + _matchIdHash, + _matchId.commitmentOne, + _matchId.commitmentTwo, + _reason, + _winnerCommitment + ); + } + + function deleteClaimer(Tree.Node commitment) internal { + delete claimers[commitment]; + } + + function requireValidContestedFinalState(Machine.Hash _finalState) + internal + view + { + ( + bool valid, + Machine.Hash contestedFinalStateOne, + Machine.Hash contestedFinalStateTwo + ) = validContestedFinalState(_finalState); + require( + valid, + InvalidContestedFinalState( + contestedFinalStateOne, contestedFinalStateTwo, _finalState + ) + ); + } + + function _min(uint256 a, uint256 b, uint256 c) + internal + pure + returns (uint256) + { + return a.min(b).min(c); + } + + function _refundableBefore() private returns (uint256 gasBefore) { + require(!locked, ReentrancyDetected()); + locked = true; + gasBefore = gasleft(); + } + + function _refundableAfter(uint256 gasBefore, uint256 gasEstimate) private { + uint256 gasAfter = gasleft(); + + uint256 refundValue = _min( + address(this).balance, + bondValue() * gasEstimate / _totalGasEstimate(), + (Gas.TX + gasBefore - gasAfter) + * (tx.gasprice + MESSAGE_SENDER_PROFIT) + ); + + (bool status, bytes memory ret) = + msg.sender.call{value: refundValue}(""); + emit PartialBondRefund(msg.sender, refundValue, status, ret); + + locked = false; + } + + /// @inheritdoc ITournament + /// @dev + /// - ROOT: + /// * Reverts with `RequireNonRootTournament` — root tournaments are never eliminated. + /// - NON-ROOT: + /// * Returns true iff: + /// 1. Tournament finished and has no winner, OR + /// 2. Tournament finished and enough time elapsed after the winning + /// commitment could have won (winner's allowance window). + function canBeEliminated() external view override returns (bool) { + TournamentArguments memory args = tournamentArguments(); + + if (_isRootTournament(args)) { + revert RequireNonRootTournament(); + } + + (bool finished, Time.Instant winnerCouldHaveWon) = timeFinished(); + + if (!finished) { + return false; + } + + (bool _hasDanglingCommitment, Tree.Node _danglingCommitment) = + hasDanglingCommitment(); + + if (!_hasDanglingCommitment) { + return true; + } + + (Clock.State memory clock,) = getCommitment(_danglingCommitment); + return winnerCouldHaveWon.timeoutElapsed(clock.allowance); + } + + /// @inheritdoc ITournament + /// @dev + /// - ROOT: + /// * Reverts with `RequireNonRootTournament` — use `arbitrationResult` instead. + /// - NON-ROOT: + /// * Returns: + /// - contested parent commitment (from the parent match), + /// - winning inner commitment (dangling commitment), + /// - adjusted clock of the winner. + function innerTournamentWinner() + external + view + override + returns (bool, Tree.Node, Tree.Node, Clock.State memory) + { + TournamentArguments memory args = tournamentArguments(); + + if (_isRootTournament(args)) { + revert RequireNonRootTournament(); + } + + if (!isFinished() || this.canBeEliminated()) { + Clock.State memory zeroClock; + return (false, Tree.ZERO_NODE, Tree.ZERO_NODE, zeroClock); + } + + (bool _hasDanglingCommitment, Tree.Node _winner) = + hasDanglingCommitment(); + assert(_hasDanglingCommitment); + + (bool finished, Time.Instant finishedTime) = timeFinished(); + assert(finished); + + Clock.State memory _clock = clocks[_winner]; + _clock = _clock.deduct(Time.currentTime().timeSpan(finishedTime)); + + NestedDispute memory nestedDispute = args.nestedDispute; + Machine.Hash _finalState = finalStates[_winner]; + + if (_finalState.eq(nestedDispute.contestedFinalStateOne)) { + return (true, nestedDispute.contestedCommitmentOne, _winner, _clock); + } else { + assert(_finalState.eq(nestedDispute.contestedFinalStateTwo)); + return (true, nestedDispute.contestedCommitmentTwo, _winner, _clock); + } + } + + function _ensureTournamentIsNotFinished() private view { + require(!isFinished(), TournamentIsFinished()); + } + + function _ensureTournamentIsOpen() private view { + require(!isClosed(), TournamentIsClosed()); + } +} diff --git a/prt/contracts/src/tournament/abstracts/LeafTournament.sol b/prt/contracts/src/tournament/abstracts/LeafTournament.sol deleted file mode 100644 index 2a2d36f3..00000000 --- a/prt/contracts/src/tournament/abstracts/LeafTournament.sol +++ /dev/null @@ -1,155 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.17; - -import {Tournament} from "./Tournament.sol"; -import {IStateTransition} from "prt-contracts/IStateTransition.sol"; -import {ITournament} from "prt-contracts/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"; -import {Match} from "prt-contracts/tournament/libs/Match.sol"; -import {Machine} from "prt-contracts/types/Machine.sol"; -import {Tree} from "prt-contracts/types/Tree.sol"; - -/// @notice Leaf tournament is the one that seals leaf match -abstract contract LeafTournament is Tournament { - using Machine for Machine.Hash; - using Commitment for Tree.Node; - using Tree for Tree.Node; - using Clock for Clock.State; - using Match for Match.Id; - using Match for Match.State; - - function sealLeafMatch( - Match.Id calldata _matchId, - Tree.Node _leftLeaf, - Tree.Node _rightLeaf, - Machine.Hash _agreeHash, - bytes32[] calldata _agreeHashProof - ) external override refundable(Gas.SEAL_LEAF_MATCH) tournamentNotFinished { - Match.State storage _matchState = matches[_matchId.hashFromId()]; - _matchState.requireExist(); - _matchState.requireCanBeFinalized(); - - // At the final step (leaf sealing), both sides may know how to prove - // the state transition. We intentionally run BOTH clocks to incentivize - // rapid completion by either party. This departs from the single-active - // clock used during bisection steps. - { - Clock.State storage _clock1 = clocks[_matchId.commitmentOne]; - Clock.State storage _clock2 = clocks[_matchId.commitmentTwo]; - _clock1.setPaused(); - _clock1.advanceClock(); - _clock2.setPaused(); - _clock2.advanceClock(); - } - - _matchState.sealMatch( - tournamentArguments().commitmentArgs, - _matchId, - _leftLeaf, - _rightLeaf, - _agreeHash, - _agreeHashProof - ); - } - - function winLeafMatch( - Match.Id calldata _matchId, - Tree.Node _leftNode, - Tree.Node _rightNode, - bytes calldata proofs - ) external override refundable(Gas.WIN_LEAF_MATCH) tournamentNotFinished { - Clock.State storage _clockOne = clocks[_matchId.commitmentOne]; - Clock.State storage _clockTwo = clocks[_matchId.commitmentTwo]; - _clockOne.requireInitialized(); - _clockTwo.requireInitialized(); - - Match.State storage _matchState = matches[_matchId.hashFromId()]; - _matchState.requireExist(); - _matchState.requireIsFinished(); - - TournamentArguments memory args = tournamentArguments(); - - ( - Machine.Hash _agreeHash, - uint256 _agreeCycle, - Machine.Hash _finalStateOne, - Machine.Hash _finalStateTwo - ) = _matchState.getDivergence(args.commitmentArgs); - - IStateTransition stateTransition = _stateTransition(); - Machine.Hash _finalState = Machine.Hash - .wrap( - stateTransition.transitionState( - Machine.Hash.unwrap(_agreeHash), - _agreeCycle, - proofs, - args.provider - ) - ); - - if (_leftNode.join(_rightNode).eq(_matchId.commitmentOne)) { - require( - _finalState.eq(_finalStateOne), - WrongFinalState(1, _finalState, _finalStateOne) - ); - - _clockOne.setPaused(); - pairCommitment( - _matchId.commitmentOne, _clockOne, _leftNode, _rightNode - ); - - deleteMatch( - _matchId, MatchDeletionReason.STEP, WinnerCommitment.ONE - ); - } else if (_leftNode.join(_rightNode).eq(_matchId.commitmentTwo)) { - require( - _finalState.eq(_finalStateTwo), - WrongFinalState(2, _finalState, _finalStateTwo) - ); - - _clockTwo.setPaused(); - pairCommitment( - _matchId.commitmentTwo, _clockTwo, _leftNode, _rightNode - ); - - deleteMatch( - _matchId, MatchDeletionReason.STEP, WinnerCommitment.TWO - ); - } else { - revert WrongNodesForStep(); - } - } - - function eliminateInnerTournament(ITournament) 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 _totalGasEstimate() internal view override returns (uint256) { - return Gas.ADVANCE_MATCH * tournamentArguments().commitmentArgs.height - + Gas.SEAL_LEAF_MATCH + Gas.WIN_LEAF_MATCH; - } - - function _stateTransition() internal view virtual returns (IStateTransition); -} diff --git a/prt/contracts/src/tournament/abstracts/NonLeafTournament.sol b/prt/contracts/src/tournament/abstracts/NonLeafTournament.sol deleted file mode 100644 index 79d56fe3..00000000 --- a/prt/contracts/src/tournament/abstracts/NonLeafTournament.sol +++ /dev/null @@ -1,243 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.28; - -import {Tournament} from "./Tournament.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; -import { - IMultiLevelTournamentFactory -} from "prt-contracts/tournament/factories/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"; -import {Match} from "prt-contracts/tournament/libs/Match.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"; - -/// @notice Non-leaf tournament can create inner tournaments and matches -abstract contract NonLeafTournament is Tournament { - using Clock for Clock.State; - using Commitment for Tree.Node; - using Machine for Machine.Hash; - using Tree for Tree.Node; - using Time for Time.Instant; - using Match for Match.State; - using Match for Match.Id; - using Match for Match.IdHash; - - // - // Storage - // - mapping(ITournament => Match.Id) matchIdFromInnerTournaments; - - function sealInnerMatchAndCreateInnerTournament( - Match.Id calldata _matchId, - Tree.Node _leftLeaf, - Tree.Node _rightLeaf, - Machine.Hash _agreeHash, - bytes32[] calldata _agreeHashProof - ) - external - override - refundable(Gas.SEAL_INNER_MATCH_AND_CREATE_INNER_TOURNAMENT) - tournamentNotFinished - { - Match.State storage _matchState = matches[_matchId.hashFromId()]; - _matchState.requireCanBeFinalized(); - // Pause clocks - Time.Duration _maxDuration; - { - Clock.State storage _clock1 = clocks[_matchId.commitmentOne]; - Clock.State storage _clock2 = clocks[_matchId.commitmentTwo]; - _clock1.setPaused(); - _clock2.setPaused(); - _maxDuration = Clock.max(_clock1, _clock2); - } - TournamentArguments memory args = tournamentArguments(); - - (Machine.Hash _finalStateOne, Machine.Hash _finalStateTwo) = _matchState.sealMatch( - args.commitmentArgs, - _matchId, - _leftLeaf, - _rightLeaf, - _agreeHash, - _agreeHashProof - ); - - ITournament _inner = instantiateInner( - _agreeHash, - _matchId.commitmentOne, - _finalStateOne, - _matchId.commitmentTwo, - _finalStateTwo, - _maxDuration, - _matchState.toCycle(args.commitmentArgs), - args.level + 1 - ); - matchIdFromInnerTournaments[_inner] = _matchId; - - emit NewInnerTournament(_matchId.hashFromId(), _inner); - } - - function winInnerTournament( - ITournament _childTournament, - Tree.Node _leftNode, - Tree.Node _rightNode - ) - external - override - refundable(Gas.WIN_INNER_TOURNAMENT) - tournamentNotFinished - { - Match.Id memory _matchId = matchIdFromInnerTournaments[_childTournament]; - Match.IdHash _matchIdHash = _matchId.hashFromId(); - _matchIdHash.requireExist(); - - Match.State storage _matchState = matches[_matchIdHash]; - _matchState.requireExist(); - _matchState.requireIsFinished(); - - require( - !_childTournament.canBeEliminated(), - ChildTournamentMustBeEliminated() - ); - - (bool finished, Tree.Node _winner,, Clock.State memory _innerClock) = - _childTournament.innerTournamentWinner(); - require(finished, ChildTournamentNotFinished()); - _winner.requireExist(); - - Tree.Node _commitmentRoot = _leftNode.join(_rightNode); - require( - _commitmentRoot.eq(_winner), - WrongTournamentWinner(_commitmentRoot, _winner) - ); - - Clock.State storage _clock = clocks[_commitmentRoot]; - _clock.requireInitialized(); - _clock.reInitialized(_innerClock); - - pairCommitment(_commitmentRoot, _clock, _leftNode, _rightNode); - - WinnerCommitment _winnerCommitment; - - if (_winner.eq(_matchId.commitmentOne)) { - _winnerCommitment = WinnerCommitment.ONE; - } else if (_winner.eq(_matchId.commitmentTwo)) { - _winnerCommitment = WinnerCommitment.TWO; - } else { - revert InvalidTournamentWinner(_winner); - } - - deleteMatch( - _matchId, MatchDeletionReason.CHILD_TOURNAMENT, _winnerCommitment - ); - delete matchIdFromInnerTournaments[_childTournament]; - - _childTournament.tryRecoveringBond(); - } - - function eliminateInnerTournament(ITournament _childTournament) - external - override - refundable(Gas.ELIMINATE_INNER_TOURNAMENT) - tournamentNotFinished - { - Match.Id memory _matchId = matchIdFromInnerTournaments[_childTournament]; - Match.IdHash _matchIdHash = _matchId.hashFromId(); - _matchIdHash.requireExist(); - - Match.State storage _matchState = matches[_matchIdHash]; - _matchState.requireExist(); - _matchState.requireIsFinished(); - - require( - _childTournament.canBeEliminated(), - ChildTournamentCannotBeEliminated() - ); - - deleteMatch( - _matchId, - MatchDeletionReason.CHILD_TOURNAMENT, - WinnerCommitment.NONE - ); - delete matchIdFromInnerTournaments[_childTournament]; - } - - function instantiateInner( - Machine.Hash _initialHash, - Tree.Node _contestedCommitmentOne, - Machine.Hash _contestedFinalStateOne, - Tree.Node _contestedCommitmentTwo, - Machine.Hash _contestedFinalStateTwo, - Time.Duration _allowance, - uint256 _startCycle, - uint64 _level - ) private returns (ITournament) { - // the inner tournament is bottom tournament at last level - // else instantiate middle tournament - TournamentArguments memory args = tournamentArguments(); - ITournament _tournament; - IMultiLevelTournamentFactory tournamentFactory = _tournamentFactory(); - if (_level == args.levels - 1) { - _tournament = tournamentFactory.instantiateBottom( - _initialHash, - _contestedCommitmentOne, - _contestedFinalStateOne, - _contestedCommitmentTwo, - _contestedFinalStateTwo, - _allowance, - _startCycle, - _level, - args.provider - ); - } else { - _tournament = tournamentFactory.instantiateMiddle( - _initialHash, - _contestedCommitmentOne, - _contestedFinalStateOne, - _contestedCommitmentTwo, - _contestedFinalStateTwo, - _allowance, - _startCycle, - _level, - args.provider - ); - } - - return _tournament; - } - - 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 _totalGasEstimate() internal view override returns (uint256) { - return Gas.ADVANCE_MATCH * tournamentArguments().commitmentArgs.height - + Gas.SEAL_INNER_MATCH_AND_CREATE_INNER_TOURNAMENT - + Gas.WIN_INNER_TOURNAMENT; - } - - function _tournamentFactory() - internal - view - virtual - returns (IMultiLevelTournamentFactory); -} diff --git a/prt/contracts/src/tournament/abstracts/Tournament.sol b/prt/contracts/src/tournament/abstracts/Tournament.sol deleted file mode 100644 index 068022e8..00000000 --- a/prt/contracts/src/tournament/abstracts/Tournament.sol +++ /dev/null @@ -1,754 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.17; - -import {Math} from "@openzeppelin-contracts-5.5.0/utils/math/Math.sol"; - -import {ITournament} from "prt-contracts/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"; -import {Match} from "prt-contracts/tournament/libs/Match.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"; - -/// @title Tournament (Abstract) — Asynchronous PRT-style dispute resolution -/// @notice Core, permissionless tournament skeleton that resolves disputes among -/// N parties in O(log N) depth under chess-clock timing. Pairing is asynchronous: -/// claims are matched as they arrive (or when winners re-enter), without a -/// prebuilt bracket. -/// -/// @dev -/// MODEL -/// - Structure: Tournaments and matches alternate recursively. Subclasses choose -/// how divergence is resolved (step or nested tournament). -/// - Pairing: Maintains at most one “dangling” (unmatched) commitment. When a -/// second commitment appears (new joiner or last match’s winner), a match is -/// created immediately and the dangling pointer is cleared. If none exists, -/// the newcomer becomes the dangling commitment. -/// - Clocks: Exactly one side’s clock ticks inside any active match; dangling -/// claims are paused. On match creation both sides receive bounded effort -/// allowance (e.g., `matchEffort`, capped by `maxAllowance`). "Late joiners" -/// inherit less remaining allowance (join-time deduction). -/// - Multi-level Tournaments: For computationally viable computation hash, -/// multi-level tournaments allows sparse commitments, but liveness grows to -/// O((log N)^L), where L is the number of levels. -/// -/// LIVENESS & COMPLEXITY -/// - At least half the clocks tick at any time, ensuring progress even if arrival -/// order is adversarial. Winners re-enter pairing immediately, preserving -/// logarithmic tournament depth without requiring a balanced bracket. -abstract contract Tournament is ITournament { - using Machine for Machine.Hash; - using Tree for Tree.Node; - using Commitment for Tree.Node; - using Commitment for Commitment.Arguments; - - using Time for Time.Instant; - using Time for Time.Duration; - - using Clock for Clock.State; - - using Match for Match.Id; - using Match for Match.IdHash; - using Match for Match.State; - - using Math for uint256; - - // - // Storage - // - Tree.Node danglingCommitment; - uint256 matchCount; - Time.Instant lastMatchDeleted; - - uint256 constant MAX_GAS_PRICE = 50 gwei; - uint256 constant MESSAGE_SENDER_PROFIT = 10 gwei; - bool transient locked; - - mapping(Tree.Node => Clock.State) clocks; - mapping(Tree.Node => Machine.Hash) finalStates; - mapping(Tree.Node => address) claimers; - - // matches existing in current tournament - mapping(Match.IdHash => Match.State) matches; - - // - // Modifiers - // - - modifier tournamentNotFinished() { - _ensureTournamentIsNotFinished(); - _; - } - - modifier tournamentOpen() { - _ensureTournamentIsOpen(); - _; - } - - /// @notice Refunds the message sender with the amount - /// of Ether wasted on gas on this function call plus - /// a profit, capped by the current contract balance - /// and a fraction of the bond value. - /// @param gasEstimate A worst-case gas estimate for the modified function - /// forge-lint: disable-next-line(unwrapped-modifier-logic) - modifier refundable(uint256 gasEstimate) { - uint256 gasBefore = _refundableBefore(); - _; - _refundableAfter(gasBefore, gasEstimate); - } - - // - // Virtual Methods - // - - /// @notice Returns non-root tournament arguments - /// @dev - /// ROOT TOURNAMENT (level == 0): - /// - Uses default implementation that returns zero values - /// - This function is never actually called for root tournaments since - /// validContestedFinalState() returns early when level == 0 - /// NON-ROOT TOURNAMENT (level > 0): - /// - Must override this function to return the actual contested final states - /// - These are stored in the tournament's argument struct - function nonRootTournamentArgs() - public - view - virtual - returns (NonRootArguments memory) - { - // Default implementation for root tournaments (level == 0) - // Non-root tournaments (level > 0) must override this - return NonRootArguments({ - contestedCommitmentOne: Tree.ZERO_NODE, - contestedFinalStateOne: Machine.ZERO_STATE, - contestedCommitmentTwo: Tree.ZERO_NODE, - contestedFinalStateTwo: Machine.ZERO_STATE - }); - } - - /// @notice Check if a final state is allowed to join the tournament - /// @param _finalState The final state hash to validate - /// @return bool Whether the final state is valid - /// @return Machine.Hash The first contested final state (for error reporting) - /// @return Machine.Hash The second contested final state (for error reporting) - /// @dev - /// ROOT TOURNAMENT (level == 0): - /// - Always accepts any final state (open to all participants) - /// - Returns (true, ZERO_STATE, ZERO_STATE) - /// NON-ROOT TOURNAMENT (level > 0): - /// - Only accepts final states that match one of the two contested final states - /// - These are the two final states from the parent match that created this tournament - /// - Returns (true/false, contestedFinalStateOne, contestedFinalStateTwo) - function validContestedFinalState(Machine.Hash _finalState) - internal - view - returns (bool, Machine.Hash, Machine.Hash) - { - TournamentArguments memory args = tournamentArguments(); - - // ROOT CASE: level == 0 - // Root tournaments are open to all participants, so any final state is valid - if (args.level == 0) { - return (true, Machine.ZERO_STATE, Machine.ZERO_STATE); - } - - // NON-ROOT CASE: level > 0 - // Non-root tournaments only accept commitments that match one of the two - // contested final states from the parent match - NonRootArguments memory nonRootArgs = nonRootTournamentArgs(); - return ( - nonRootArgs.contestedFinalStateOne.eq(_finalState) - || nonRootArgs.contestedFinalStateTwo.eq(_finalState), - nonRootArgs.contestedFinalStateOne, - nonRootArgs.contestedFinalStateTwo - ); - } - - function _totalGasEstimate() internal view virtual returns (uint256); - - // - // Methods - // - - function bondValue() public view override returns (uint256) { - return _totalGasEstimate() * MAX_GAS_PRICE; - } - - function joinTournament( - Machine.Hash _finalState, - bytes32[] calldata _proof, - Tree.Node _leftNode, - Tree.Node _rightNode - ) external payable override tournamentOpen { - require(msg.value >= bondValue(), InsufficientBond()); - - Tree.Node _commitmentRoot = _leftNode.join(_rightNode); - - TournamentArguments memory args = tournamentArguments(); - - // Prove final state is in commitmentRoot - _commitmentRoot.requireFinalState( - args.commitmentArgs.height, _finalState, _proof - ); - - // Verify whether finalState is one of the two allowed of tournament if nested - requireValidContestedFinalState(_finalState); - finalStates[_commitmentRoot] = _finalState; - - Clock.State storage _clock = clocks[_commitmentRoot]; - _clock.requireNotInitialized(); // reverts if commitment is duplicate - _clock.setNewPaused(args.startInstant, args.allowance); - - pairCommitment(_commitmentRoot, _clock, _leftNode, _rightNode); - claimers[_commitmentRoot] = msg.sender; - emit CommitmentJoined(_commitmentRoot, _finalState, msg.sender); - } - - function advanceMatch( - Match.Id calldata _matchId, - Tree.Node _leftNode, - Tree.Node _rightNode, - Tree.Node _newLeftNode, - Tree.Node _newRightNode - ) external override refundable(Gas.ADVANCE_MATCH) tournamentNotFinished { - Match.State storage _matchState = matches[_matchId.hashFromId()]; - _matchState.requireExist(); - _matchState.requireCanBeAdvanced(); - - _matchState.advanceMatch( - _matchId, _leftNode, _rightNode, _newLeftNode, _newRightNode - ); - - // advance clocks - clocks[_matchId.commitmentOne].advanceClock(); - clocks[_matchId.commitmentTwo].advanceClock(); - } - - function winMatchByTimeout( - Match.Id calldata _matchId, - Tree.Node _leftNode, - Tree.Node _rightNode - ) - external - override - refundable(Gas.WIN_MATCH_BY_TIMEOUT) - tournamentNotFinished - { - matches[_matchId.hashFromId()].requireExist(); - Clock.State storage _clockOne = clocks[_matchId.commitmentOne]; - Clock.State storage _clockTwo = clocks[_matchId.commitmentTwo]; - - _clockOne.requireInitialized(); - _clockTwo.requireInitialized(); - - if (_clockOne.hasTimeLeft() && !_clockTwo.hasTimeLeft()) { - require( - _matchId.commitmentOne.verify(_leftNode, _rightNode), - WrongChildren(1, _matchId.commitmentOne, _leftNode, _rightNode) - ); - - _clockOne.deducted(_clockTwo.timeSinceTimeout()); - pairCommitment( - _matchId.commitmentOne, _clockOne, _leftNode, _rightNode - ); - - deleteMatch( - _matchId, MatchDeletionReason.TIMEOUT, WinnerCommitment.ONE - ); - } else if (!_clockOne.hasTimeLeft() && _clockTwo.hasTimeLeft()) { - require( - _matchId.commitmentTwo.verify(_leftNode, _rightNode), - WrongChildren(2, _matchId.commitmentTwo, _leftNode, _rightNode) - ); - - _clockTwo.deducted(_clockOne.timeSinceTimeout()); - pairCommitment( - _matchId.commitmentTwo, _clockTwo, _leftNode, _rightNode - ); - - deleteMatch( - _matchId, MatchDeletionReason.TIMEOUT, WinnerCommitment.TWO - ); - } else { - revert ClockNotTimedOut(); - } - } - - function eliminateMatchByTimeout(Match.Id calldata _matchId) - external - override - refundable(Gas.ELIMINATE_MATCH_BY_TIMEOUT) - tournamentNotFinished - { - matches[_matchId.hashFromId()].requireExist(); - Clock.State storage _clockOne = clocks[_matchId.commitmentOne]; - Clock.State storage _clockTwo = clocks[_matchId.commitmentTwo]; - - _clockOne.requireInitialized(); - _clockTwo.requireInitialized(); - - // check if both clocks are out of time - if ( - (!_clockOne.hasTimeLeft() - && !_clockTwo.timeLeft().gt(_clockOne.timeSinceTimeout())) - || (!_clockTwo.hasTimeLeft() - && !_clockOne.timeLeft().gt(_clockTwo.timeSinceTimeout())) - ) { - deleteMatch( - _matchId, MatchDeletionReason.TIMEOUT, WinnerCommitment.NONE - ); - } else { - revert BothClocksHaveNotTimedOut(); - } - } - - function tryRecoveringBond() public override returns (bool) { - require(isFinished(), TournamentNotFinished()); - - // Ensure there is a winner - (bool hasDangling, Tree.Node winningCommitment) = - hasDanglingCommitment(); - require(hasDangling, NoWinner()); - - // Get the address associated with the winning claim - address winner = claimers[winningCommitment]; - assert(winner != address(0)); - - // Refund the entire contract balance to the winner - uint256 contractBalance = address(this).balance; - (bool success,) = winner.call{value: contractBalance}(""); - - // clear the claimer for the winning commitment if successfully recovered bond - if (success) { - deleteClaimer(winningCommitment); - } - - return success; - } - - // - // View methods - // - - function tournamentArguments() - public - view - virtual - returns (TournamentArguments memory); - - function canWinMatchByTimeout(Match.Id calldata _matchId) - external - view - override - returns (bool) - { - Clock.State memory _clockOne = clocks[_matchId.commitmentOne]; - Clock.State memory _clockTwo = clocks[_matchId.commitmentTwo]; - - return !_clockOne.hasTimeLeft() || !_clockTwo.hasTimeLeft(); - } - - function getCommitment(Tree.Node _commitmentRoot) - public - view - override - returns (Clock.State memory, Machine.Hash) - { - return (clocks[_commitmentRoot], finalStates[_commitmentRoot]); - } - - function getMatch(Match.IdHash _matchIdHash) - public - view - override - returns (Match.State memory) - { - return matches[_matchIdHash]; - } - - function getMatchCycle(Match.IdHash _matchIdHash) - external - view - override - returns (uint256) - { - Match.State memory _m = getMatch(_matchIdHash); - Commitment.Arguments memory args = tournamentArguments().commitmentArgs; - - return args.toCycle(_m.runningLeafPosition); - } - - function tournamentLevelConstants() - external - view - override - returns ( - uint64 _maxLevel, - uint64 _level, - uint64 _log2step, - uint64 _height - ) - { - TournamentArguments memory args; - args = tournamentArguments(); - _maxLevel = args.levels; - _level = args.level; - _log2step = args.commitmentArgs.log2step; - _height = args.commitmentArgs.height; - } - - // - // Time view methods - // - - function isClosed() public view override returns (bool) { - TournamentArguments memory args = tournamentArguments(); - return args.startInstant.timeoutElapsed(args.allowance); - } - - function isFinished() public view override returns (bool) { - return isClosed() && matchCount == 0; - } - - function timeFinished() public view override returns (bool, Time.Instant) { - if (!isFinished()) { - return (false, Time.ZERO_INSTANT); - } - - TournamentArguments memory args = tournamentArguments(); - - // Here, we know that `lastMatchDeleted` holds the Instant when `matchCount` became zero. - // However, we still must consider when the tournament was closed, in case it - // happens after `lastMatchDeleted`. - // Note that `lastMatchDeleted` could be zero if there are no matches eliminated. - // In this case, we'd only care about `tournamentClosed`. - Time.Instant tournamentClosed = args.startInstant.add(args.allowance); - Time.Instant winnerCouldWin = tournamentClosed.max(lastMatchDeleted); - - return (true, winnerCouldWin); - } - - function arbitrationResult() - external - view - override - returns (bool, Tree.Node, Machine.Hash) - { - if (!isFinished()) { - return (false, Tree.ZERO_NODE, Machine.ZERO_STATE); - } - - (bool _hasDanglingCommitment, Tree.Node _danglingCommitment) = - hasDanglingCommitment(); - require(_hasDanglingCommitment, TournamentFailedNoWinner()); - - Machine.Hash _finalState = finalStates[_danglingCommitment]; - return (true, _danglingCommitment, _finalState); - } - - // - // Internal functions - // - - function setDanglingCommitment(Tree.Node _node) internal { - danglingCommitment = _node; - } - - function clearDanglingCommitment() internal { - danglingCommitment = Tree.ZERO_NODE; - } - - function hasDanglingCommitment() - internal - view - returns (bool _h, Tree.Node _node) - { - _node = danglingCommitment; - - if (!_node.isZero()) { - _h = true; - } - } - - /// @dev Pair a new commitment into the tournament, creating a match if an - /// unmatched opponent is waiting, or queuing this commitment otherwise. - /// Guarantees continuous progress by (a) keeping at most one dangling - /// commitment and (b) enforcing chess-clock style timing on matches. - /// @param _rootHash The commitment (Merkle root) for the new/advancing claim. - /// @param _newClock Storage reference to the clock state tied to `_rootHash`. - /// @param _leftNode Left child; must join with right to form `_rootHash`. - /// @param _rightNode Right child; must join with left to form `_rootHash`. - /// - /// Invariants / Effects: - /// - Verifies `_leftNode.join(_rightNode) == _rootHash`. - /// - If a dangling opponent exists: - /// * Creates a Match between dangling and `_rootHash`. - /// * Credits bounded match effort to both clocks (anti-starvation). - /// * Toggles the previously dangling clock to start its turn. - /// * Clears the dangling pointer; increments match counter; emits `matchCreated`. - /// - Otherwise: stores `_rootHash` as the dangling commitment and pauses its clock. - /// - /// Rationale: - /// - Asynchronous matchmaking avoids fixed brackets and preserves O(log N) progress - /// under Dave-style chess clocks by ensuring at least half the clocks tick at all times. - /// - Late arrivals are deducted their delay and as such do not gain extra time. - function pairCommitment( - Tree.Node _rootHash, - Clock.State storage _newClock, - Tree.Node _leftNode, - Tree.Node _rightNode - ) internal { - assert(_leftNode.join(_rightNode).eq(_rootHash)); - (bool _hasDanglingCommitment, Tree.Node _danglingCommitment) = - hasDanglingCommitment(); - - if (_hasDanglingCommitment) { - TournamentArguments memory args = tournamentArguments(); - (Match.IdHash _matchId, Match.State memory _matchState) = Match.createMatch( - args.commitmentArgs, - _danglingCommitment, - _rootHash, - _leftNode, - _rightNode - ); - - matches[_matchId] = _matchState; - - Clock.State storage _firstClock = clocks[_danglingCommitment]; - - // grant extra match effort for both clocks - _firstClock.addMatchEffort(args.matchEffort, args.maxAllowance); - _newClock.addMatchEffort(args.matchEffort, args.maxAllowance); - - // toggle clock of first claim - _firstClock.advanceClock(); - - clearDanglingCommitment(); - matchCount++; - - emit MatchCreated( - _matchId, _danglingCommitment, _rootHash, _leftNode - ); - } else { - setDanglingCommitment(_rootHash); - } - } - - function deleteMatch( - Match.Id memory _matchId, - MatchDeletionReason _reason, - WinnerCommitment _winnerCommitment - ) internal { - matchCount--; - lastMatchDeleted = Time.currentTime(); - if (_winnerCommitment == WinnerCommitment.NONE) { - deleteClaimer(_matchId.commitmentOne); - deleteClaimer(_matchId.commitmentTwo); - } else if (_winnerCommitment == WinnerCommitment.ONE) { - deleteClaimer(_matchId.commitmentTwo); - } else if (_winnerCommitment == WinnerCommitment.TWO) { - deleteClaimer(_matchId.commitmentOne); - } else { - revert InvalidWinnerCommitment(_winnerCommitment); - } - Match.IdHash _matchIdHash = _matchId.hashFromId(); - delete matches[_matchIdHash]; - emit MatchDeleted( - _matchIdHash, - _matchId.commitmentOne, - _matchId.commitmentTwo, - _reason, - _winnerCommitment - ); - } - - function deleteClaimer(Tree.Node commitment) internal { - delete claimers[commitment]; - } - - function requireValidContestedFinalState(Machine.Hash _finalState) - internal - view - { - ( - bool valid, - Machine.Hash contestedFinalStateOne, - Machine.Hash contestedFinalStateTwo - ) = validContestedFinalState(_finalState); - require( - valid, - InvalidContestedFinalState( - contestedFinalStateOne, contestedFinalStateTwo, _finalState - ) - ); - } - - /// @notice Returns the minimum of three values - /// @param a First value - /// @param b Second value - /// @param c Third value - /// @return The minimum value - function _min(uint256 a, uint256 b, uint256 c) - internal - pure - returns (uint256) - { - return a.min(b).min(c); - } - - /// @notice This function is run at the start of every refundable function. - /// @return gasBefore The available gas amount before running the function - /// @dev Ensures the lock is not taken, and takes the lock. - function _refundableBefore() private returns (uint256 gasBefore) { - require(!locked, ReentrancyDetected()); - locked = true; - gasBefore = gasleft(); - } - - /// @notice This function is run at the end of every refundable function. - /// @param gasBefore The available gas amount before running the function - /// @param gasEstimate A worst-case gas estimate for the modified function - /// @dev Releases the lock and tries to refund the sender for the wasted gas. - /// @dev Emits a PartialBondRefund event even if the refund fails. - /// @dev The refund is capped by the contract balance and weighted fraction - /// of the bond value (where the weight is the expected gas of the function call). - function _refundableAfter(uint256 gasBefore, uint256 gasEstimate) private { - uint256 gasAfter = gasleft(); - - uint256 refundValue = _min( - address(this).balance, - bondValue() * gasEstimate / _totalGasEstimate(), - (Gas.TX + gasBefore - gasAfter) - * (tx.gasprice + MESSAGE_SENDER_PROFIT) - ); - - (bool status, bytes memory ret) = - msg.sender.call{value: refundValue}(""); - emit PartialBondRefund(msg.sender, refundValue, status, ret); - - locked = false; - } - - /// @notice Check if this inner tournament can be safely eliminated - /// @return bool Whether the tournament can be eliminated - /// @dev - /// ROOT TOURNAMENT (level == 0): - /// - Root tournaments cannot be eliminated - /// - Reverts with NotImplemented() error - /// NON-ROOT TOURNAMENT (level > 0): - /// - Can be eliminated if: - /// 1. Tournament is finished AND has no winners, OR - /// 2. Tournament is finished AND the winner's clock allowance has elapsed - /// since the tournament finished - /// - This allows parent tournaments to clean up stalled inner tournaments - function canBeEliminated() external view override returns (bool) { - TournamentArguments memory args = tournamentArguments(); - - // ROOT CASE: level == 0 - // Root tournaments cannot be eliminated - they are the top-level tournament - if (args.level == 0) { - revert NotImplemented(); - } - - // NON-ROOT CASE: level > 0 - // Inner tournaments can be eliminated under certain conditions - (bool finished, Time.Instant winnerCouldHaveWon) = timeFinished(); - - if (!finished) { - return false; - } - - (bool _hasDanglingCommitment, Tree.Node _danglingCommitment) = - hasDanglingCommitment(); - - // If the tournament is finished but has no winners, - // inner tournament can be eliminated - if (!_hasDanglingCommitment) { - return true; - } - - // We know that, after `winnerCouldHaveWon` plus winner's clock.allowance has elapsed, - // it is safe to eliminate the tournament. - (Clock.State memory clock,) = getCommitment(_danglingCommitment); - return winnerCouldHaveWon.timeoutElapsed(clock.allowance); - } - - /// @notice Get the winner of this inner tournament - /// @return bool Whether the tournament has finished and has a winner - /// @return Tree.Node The contested parent commitment (from the parent match) - /// @return Tree.Node The winning inner commitment (the dangling commitment) - /// @return Clock.State The paused clock state of the winning commitment - /// @dev - /// ROOT TOURNAMENT (level == 0): - /// - Root tournaments don't have "inner tournament winners" - /// - Use arbitrationResult() instead to get the root tournament winner - /// - Reverts with NotImplemented() error - /// NON-ROOT TOURNAMENT (level > 0): - /// - Returns the winner of this inner tournament - /// - Maps the winning final state to one of the two contested final states - /// - Returns the corresponding contested commitment from the parent match - /// - The clock is adjusted to account for time elapsed since tournament finished - function innerTournamentWinner() - external - view - override - returns (bool, Tree.Node, Tree.Node, Clock.State memory) - { - TournamentArguments memory args = tournamentArguments(); - - // ROOT CASE: level == 0 - // Root tournaments don't have inner tournament winners - they are the root - // Use arbitrationResult() to get the root tournament winner instead - if (args.level == 0) { - revert NotImplemented(); - } - - // NON-ROOT CASE: level > 0 - // Return the winner of this inner tournament, mapping it back to the parent match - if (!isFinished() || this.canBeEliminated()) { - Clock.State memory zeroClock; - return (false, Tree.ZERO_NODE, Tree.ZERO_NODE, zeroClock); - } - - (bool _hasDanglingCommitment, Tree.Node _winner) = - hasDanglingCommitment(); - assert(_hasDanglingCommitment); - - (bool finished, Time.Instant finishedTime) = timeFinished(); - assert(finished); - - // Adjust the clock to account for time elapsed since tournament finished - Clock.State memory _clock = clocks[_winner]; - _clock = _clock.deduct(Time.currentTime().timeSpan(finishedTime)); - - // Map the winning final state to one of the two contested final states - // from the parent match - NonRootArguments memory nonRootArgs = nonRootTournamentArgs(); - Machine.Hash _finalState = finalStates[_winner]; - - if (_finalState.eq(nonRootArgs.contestedFinalStateOne)) { - // Winner matches the first contested final state - return (true, nonRootArgs.contestedCommitmentOne, _winner, _clock); - } else { - // Winner matches the second contested final state - assert(_finalState.eq(nonRootArgs.contestedFinalStateTwo)); - return (true, nonRootArgs.contestedCommitmentTwo, _winner, _clock); - } - } - - /// @notice Ensure the tournament is not finished. - /// @dev Raises a `TournamentIsFinished` error otherwise. - function _ensureTournamentIsNotFinished() private view { - require(!isFinished(), TournamentIsFinished()); - } - - /// @notice Ensure the tournament is open (not closed). - /// @dev Raises a `TournamentIsClosed` error otherwise. - function _ensureTournamentIsOpen() private view { - require(!isClosed(), TournamentIsClosed()); - } -} diff --git a/prt/contracts/src/tournament/concretes/BottomTournament.sol b/prt/contracts/src/tournament/concretes/BottomTournament.sol deleted file mode 100644 index e983ac54..00000000 --- a/prt/contracts/src/tournament/concretes/BottomTournament.sol +++ /dev/null @@ -1,53 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.17; - -import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; - -import {IStateTransition} from "prt-contracts/IStateTransition.sol"; -import { - LeafTournament -} from "prt-contracts/tournament/abstracts/LeafTournament.sol"; - -/// @notice Bottom tournament of a multi-level instance -contract BottomTournament is LeafTournament { - using Clones for address; - - struct BottomArguments { - TournamentArguments tournamentArgs; - NonRootArguments nonRootTournamentArgs; - IStateTransition stateTransition; - } - - function _bottomArgs() internal view returns (BottomArguments memory) { - return abi.decode(address(this).fetchCloneArgs(), (BottomArguments)); - } - - function tournamentArguments() - public - view - override - returns (TournamentArguments memory) - { - return _bottomArgs().tournamentArgs; - } - - function nonRootTournamentArgs() - public - view - override - returns (NonRootArguments memory) - { - return _bottomArgs().nonRootTournamentArgs; - } - - function _stateTransition() - internal - view - override - returns (IStateTransition) - { - return _bottomArgs().stateTransition; - } -} diff --git a/prt/contracts/src/tournament/concretes/MiddleTournament.sol b/prt/contracts/src/tournament/concretes/MiddleTournament.sol deleted file mode 100644 index ad3af786..00000000 --- a/prt/contracts/src/tournament/concretes/MiddleTournament.sol +++ /dev/null @@ -1,55 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.17; - -import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; - -import { - NonLeafTournament -} from "prt-contracts/tournament/abstracts/NonLeafTournament.sol"; -import { - IMultiLevelTournamentFactory -} from "prt-contracts/tournament/factories/IMultiLevelTournamentFactory.sol"; - -/// @notice Middle tournament is non-top, non-bottom of a multi-level instance -contract MiddleTournament is NonLeafTournament { - using Clones for address; - - struct MiddleArguments { - TournamentArguments tournamentArgs; - NonRootArguments nonRootTournamentArgs; - IMultiLevelTournamentFactory tournamentFactory; - } - - function _middleArgs() internal view returns (MiddleArguments memory) { - return abi.decode(address(this).fetchCloneArgs(), (MiddleArguments)); - } - - function tournamentArguments() - public - view - override - returns (TournamentArguments memory) - { - return _middleArgs().tournamentArgs; - } - - function nonRootTournamentArgs() - public - view - override - returns (NonRootArguments memory) - { - return _middleArgs().nonRootTournamentArgs; - } - - function _tournamentFactory() - internal - view - override - returns (IMultiLevelTournamentFactory) - { - return _middleArgs().tournamentFactory; - } -} diff --git a/prt/contracts/src/tournament/concretes/SingleLevelTournament.sol b/prt/contracts/src/tournament/concretes/SingleLevelTournament.sol deleted file mode 100644 index 7f825e18..00000000 --- a/prt/contracts/src/tournament/concretes/SingleLevelTournament.sol +++ /dev/null @@ -1,47 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.17; - -import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; - -import {IStateTransition} from "prt-contracts/IStateTransition.sol"; -import { - LeafTournament -} from "prt-contracts/tournament/abstracts/LeafTournament.sol"; - -contract SingleLevelTournament is LeafTournament { - using Clones for address; - - struct SingleLevelArguments { - TournamentArguments tournamentArgs; - IStateTransition stateTransition; - } - - function _singleLevelArgs() - internal - view - returns (SingleLevelArguments memory) - { - return - abi.decode(address(this).fetchCloneArgs(), (SingleLevelArguments)); - } - - function tournamentArguments() - public - view - override - returns (TournamentArguments memory) - { - return _singleLevelArgs().tournamentArgs; - } - - function _stateTransition() - internal - view - override - returns (IStateTransition) - { - return _singleLevelArgs().stateTransition; - } -} diff --git a/prt/contracts/src/tournament/concretes/TopTournament.sol b/prt/contracts/src/tournament/concretes/TopTournament.sol deleted file mode 100644 index 63dea66f..00000000 --- a/prt/contracts/src/tournament/concretes/TopTournament.sol +++ /dev/null @@ -1,45 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.17; - -import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; - -import { - NonLeafTournament -} from "prt-contracts/tournament/abstracts/NonLeafTournament.sol"; -import { - IMultiLevelTournamentFactory -} from "prt-contracts/tournament/factories/IMultiLevelTournamentFactory.sol"; - -/// @notice Top tournament of a multi-level instance -contract TopTournament is NonLeafTournament { - using Clones for address; - - struct TopArguments { - TournamentArguments tournamentArgs; - IMultiLevelTournamentFactory tournamentFactory; - } - - function _topArgs() internal view returns (TopArguments memory) { - return abi.decode(address(this).fetchCloneArgs(), (TopArguments)); - } - - function tournamentArguments() - public - view - override - returns (TournamentArguments memory) - { - return _topArgs().tournamentArgs; - } - - function _tournamentFactory() - internal - view - override - returns (IMultiLevelTournamentFactory) - { - return _topArgs().tournamentFactory; - } -} diff --git a/prt/contracts/src/tournament/factories/IMultiLevelTournamentFactory.sol b/prt/contracts/src/tournament/factories/IMultiLevelTournamentFactory.sol index 23069e5e..2962c8a1 100644 --- a/prt/contracts/src/tournament/factories/IMultiLevelTournamentFactory.sol +++ b/prt/contracts/src/tournament/factories/IMultiLevelTournamentFactory.sol @@ -11,23 +11,7 @@ import {Machine} from "prt-contracts/types/Machine.sol"; import {Tree} from "prt-contracts/types/Tree.sol"; interface IMultiLevelTournamentFactory is ITournamentFactory { - function instantiateTop(Machine.Hash _initialHash, IDataProvider _provider) - external - returns (ITournament); - - function instantiateMiddle( - Machine.Hash _initialHash, - Tree.Node _contestedCommitmentOne, - Machine.Hash _contestedFinalStateOne, - Tree.Node _contestedCommitmentTwo, - Machine.Hash _contestedFinalStateTwo, - Time.Duration _allowance, - uint256 _startCycle, - uint64 _level, - IDataProvider _provider - ) external returns (ITournament); - - function instantiateBottom( + function instantiateInner( Machine.Hash _initialHash, Tree.Node _contestedCommitmentOne, Machine.Hash _contestedFinalStateOne, diff --git a/prt/contracts/src/tournament/factories/MultiLevelTournamentFactory.sol b/prt/contracts/src/tournament/factories/MultiLevelTournamentFactory.sol index 5ace77c6..975c8d03 100644 --- a/prt/contracts/src/tournament/factories/MultiLevelTournamentFactory.sol +++ b/prt/contracts/src/tournament/factories/MultiLevelTournamentFactory.sol @@ -3,20 +3,17 @@ pragma solidity ^0.8.17; +import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; + import {IMultiLevelTournamentFactory} from "./IMultiLevelTournamentFactory.sol"; -import { - BottomTournamentFactory -} from "./multilevel/BottomTournamentFactory.sol"; -import { - MiddleTournamentFactory -} from "./multilevel/MiddleTournamentFactory.sol"; -import {TopTournamentFactory} from "./multilevel/TopTournamentFactory.sol"; import {IDataProvider} from "prt-contracts/IDataProvider.sol"; import {IStateTransition} from "prt-contracts/IStateTransition.sol"; import {ITournament} from "prt-contracts/ITournament.sol"; import { ITournamentParametersProvider } from "prt-contracts/arbitration-config/ITournamentParametersProvider.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"; import {Machine} from "prt-contracts/types/Machine.sol"; import { @@ -25,22 +22,18 @@ import { import {Tree} from "prt-contracts/types/Tree.sol"; contract MultiLevelTournamentFactory is IMultiLevelTournamentFactory { - TopTournamentFactory immutable TOP_FACTORY; - MiddleTournamentFactory immutable MIDDLE_FACTORY; - BottomTournamentFactory immutable BOTTOM_FACTORY; + using Clones for address; + + Tournament immutable IMPL; ITournamentParametersProvider immutable TOURNAMENT_PARAMETERS_PROVIDER; IStateTransition immutable STATE_TRANSITION; constructor( - TopTournamentFactory _topFactory, - MiddleTournamentFactory _middleFactory, - BottomTournamentFactory _bottomFactory, + Tournament _impl, ITournamentParametersProvider _tournamentParametersProvider, IStateTransition _stateTransition ) { - TOP_FACTORY = _topFactory; - MIDDLE_FACTORY = _middleFactory; - BOTTOM_FACTORY = _bottomFactory; + IMPL = _impl; TOURNAMENT_PARAMETERS_PROVIDER = _tournamentParametersProvider; STATE_TRANSITION = _stateTransition; } @@ -55,43 +48,58 @@ contract MultiLevelTournamentFactory is IMultiLevelTournamentFactory { return _tournament; } + /// @notice Instantiate a top-level tournament (root tournament at level 0). + /// @dev + /// - Always passes STATE_TRANSITION and tournamentFactory (address(this)). + /// - Uses `address(this)` instead of `this` to avoid circular dependency: + /// ITournament imports IMultiLevelTournamentFactory, and IMultiLevelTournamentFactory imports ITournament. + /// Storing as `address` breaks the cycle; it's cast back to IMultiLevelTournamentFactory when needed. + /// - For single-level tournaments (levels == 1): factory is set but unused (leaf tournaments don't create inner tournaments). + /// - For multi-level tournaments (levels > 1): factory is used to create inner tournaments. function instantiateTop(Machine.Hash _initialHash, IDataProvider _provider) - public - override + private returns (ITournament) { - return TOP_FACTORY.instantiate( - _initialHash, _getTopTournamentParameters(), _provider, this - ); - } + TournamentParameters memory params = _getTournamentParameters(0); - function instantiateMiddle( - Machine.Hash _initialHash, - Tree.Node _contestedCommitmentOne, - Machine.Hash _contestedFinalStateOne, - Tree.Node _contestedCommitmentTwo, - Machine.Hash _contestedFinalStateTwo, - Time.Duration _allowance, - uint256 _startCycle, - uint64 _level, - IDataProvider _provider - ) external override returns (ITournament) { - return MIDDLE_FACTORY.instantiate( - _initialHash, - _contestedCommitmentOne, - _contestedFinalStateOne, - _contestedCommitmentTwo, - _contestedFinalStateTwo, - _allowance, - _startCycle, - _level, - _getTournamentParameters(_level), - _provider, - this - ); + ITournament.TournamentArguments memory args = + ITournament.TournamentArguments({ + commitmentArgs: Commitment.Arguments({ + initialHash: _initialHash, + startCycle: 0, + log2step: params.log2step, + height: params.height + }), + level: 0, + levels: params.levels, + startInstant: Time.currentTime(), + allowance: params.maxAllowance, + maxAllowance: params.maxAllowance, + matchEffort: params.matchEffort, + provider: _provider, + nestedDispute: ITournament.NestedDispute({ + contestedCommitmentOne: Tree.ZERO_NODE, + contestedFinalStateOne: Machine.ZERO_STATE, + contestedCommitmentTwo: Tree.ZERO_NODE, + contestedFinalStateTwo: Machine.ZERO_STATE + }), + stateTransition: STATE_TRANSITION, + tournamentFactory: address(this) + }); + + address clone = address(IMPL).cloneWithImmutableArgs(abi.encode(args)); + return ITournament(clone); } - function instantiateBottom( + /// @notice Instantiate an inner tournament (middle or bottom level). + /// @dev + /// - Always passes STATE_TRANSITION and tournamentFactory (address(this)). + /// - Uses `address(this)` instead of `this` to avoid circular dependency: + /// ITournament imports IMultiLevelTournamentFactory, and IMultiLevelTournamentFactory imports ITournament. + /// Storing as `address` breaks the cycle; it's cast back to IMultiLevelTournamentFactory when needed. + /// - For leaf tournaments (`_level == params.levels - 1`): factory is set but unused (can't create deeper tournaments). + /// - For non-leaf tournaments (`_level < params.levels - 1`): factory is used to create deeper tournaments. + function instantiateInner( Machine.Hash _initialHash, Tree.Node _contestedCommitmentOne, Machine.Hash _contestedFinalStateOne, @@ -102,27 +110,35 @@ contract MultiLevelTournamentFactory is IMultiLevelTournamentFactory { uint64 _level, IDataProvider _provider ) external override returns (ITournament) { - return BOTTOM_FACTORY.instantiate( - _initialHash, - _contestedCommitmentOne, - _contestedFinalStateOne, - _contestedCommitmentTwo, - _contestedFinalStateTwo, - _allowance, - _startCycle, - _level, - _getTournamentParameters(_level), - _provider, - STATE_TRANSITION - ); - } + TournamentParameters memory params = _getTournamentParameters(_level); - function _getTopTournamentParameters() - internal - view - returns (TournamentParameters memory) - { - return _getTournamentParameters(0); + ITournament.TournamentArguments memory args = + ITournament.TournamentArguments({ + commitmentArgs: Commitment.Arguments({ + initialHash: _initialHash, + startCycle: _startCycle, + log2step: params.log2step, + height: params.height + }), + level: _level, + levels: params.levels, + startInstant: Time.currentTime(), + allowance: _allowance, + maxAllowance: params.maxAllowance, + matchEffort: params.matchEffort, + provider: _provider, + nestedDispute: ITournament.NestedDispute({ + contestedCommitmentOne: _contestedCommitmentOne, + contestedFinalStateOne: _contestedFinalStateOne, + contestedCommitmentTwo: _contestedCommitmentTwo, + contestedFinalStateTwo: _contestedFinalStateTwo + }), + stateTransition: STATE_TRANSITION, + tournamentFactory: address(this) + }); + + address clone = address(IMPL).cloneWithImmutableArgs(abi.encode(args)); + return ITournament(clone); } function _getTournamentParameters(uint64 _level) diff --git a/prt/contracts/src/tournament/factories/SingleLevelTournamentFactory.sol b/prt/contracts/src/tournament/factories/SingleLevelTournamentFactory.sol deleted file mode 100644 index 535a4085..00000000 --- a/prt/contracts/src/tournament/factories/SingleLevelTournamentFactory.sol +++ /dev/null @@ -1,85 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.17; - -import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; - -import {IDataProvider} from "prt-contracts/IDataProvider.sol"; -import {IStateTransition} from "prt-contracts/IStateTransition.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; -import {ITournamentFactory} from "prt-contracts/ITournamentFactory.sol"; -import { - SingleLevelTournament -} from "prt-contracts/tournament/concretes/SingleLevelTournament.sol"; -import {Commitment} from "prt-contracts/tournament/libs/Commitment.sol"; -import {Time} from "prt-contracts/tournament/libs/Time.sol"; -import {Machine} from "prt-contracts/types/Machine.sol"; - -contract SingleLevelTournamentFactory is ITournamentFactory { - using Clones for address; - - uint64 constant START_CYCLE = 0; - uint64 constant LEVEL = 0; - uint64 constant LEVELS = 1; - - SingleLevelTournament immutable IMPL; - IStateTransition immutable STATE_TRANSITION; - uint64 immutable LOG2_STEP; - uint64 immutable HEIGHT; - Time.Duration immutable MAX_ALLOWANCE; - Time.Duration immutable MATCH_EFFORT; - - constructor( - SingleLevelTournament impl, - IStateTransition stateTransition, - uint64 log2step, - uint64 height, - Time.Duration maxAllowance, - Time.Duration matchEffort - ) { - IMPL = impl; - STATE_TRANSITION = stateTransition; - LOG2_STEP = log2step; - HEIGHT = height; - MAX_ALLOWANCE = maxAllowance; - MATCH_EFFORT = matchEffort; - } - - function instantiateSingleLevel( - Machine.Hash initialHash, - IDataProvider provider - ) public returns (SingleLevelTournament) { - SingleLevelTournament.SingleLevelArguments memory - args = - SingleLevelTournament.SingleLevelArguments({ - tournamentArgs: ITournament.TournamentArguments({ - commitmentArgs: Commitment.Arguments({ - initialHash: initialHash, - startCycle: START_CYCLE, - log2step: LOG2_STEP, - height: HEIGHT - }), - level: LEVEL, - levels: LEVELS, - startInstant: Time.currentTime(), - allowance: MAX_ALLOWANCE, - maxAllowance: MAX_ALLOWANCE, - matchEffort: MATCH_EFFORT, - provider: provider - }), - stateTransition: STATE_TRANSITION - }); - address clone = address(IMPL).cloneWithImmutableArgs(abi.encode(args)); - SingleLevelTournament tournament = SingleLevelTournament(clone); - emit TournamentCreated(tournament); - return tournament; - } - - function instantiate(Machine.Hash initialHash, IDataProvider provider) - external - returns (ITournament) - { - return instantiateSingleLevel(initialHash, provider); - } -} diff --git a/prt/contracts/src/tournament/factories/multilevel/BottomTournamentFactory.sol b/prt/contracts/src/tournament/factories/multilevel/BottomTournamentFactory.sol deleted file mode 100644 index 08cbe4e6..00000000 --- a/prt/contracts/src/tournament/factories/multilevel/BottomTournamentFactory.sol +++ /dev/null @@ -1,73 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.17; - -import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; - -import {IDataProvider} from "prt-contracts/IDataProvider.sol"; -import {IStateTransition} from "prt-contracts/IStateTransition.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; -import { - BottomTournament -} from "prt-contracts/tournament/concretes/BottomTournament.sol"; -import {Commitment} from "prt-contracts/tournament/libs/Commitment.sol"; -import {Time} from "prt-contracts/tournament/libs/Time.sol"; -import {Machine} from "prt-contracts/types/Machine.sol"; -import { - TournamentParameters -} from "prt-contracts/types/TournamentParameters.sol"; -import {Tree} from "prt-contracts/types/Tree.sol"; - -contract BottomTournamentFactory { - using Clones for address; - - BottomTournament immutable IMPL; - - constructor(BottomTournament impl) { - IMPL = impl; - } - - function instantiate( - Machine.Hash initialHash, - Tree.Node contestedCommitmentOne, - Machine.Hash contestedFinalStateOne, - Tree.Node contestedCommitmentTwo, - Machine.Hash contestedFinalStateTwo, - Time.Duration allowance, - uint256 startCycle, - uint64 level, - TournamentParameters calldata tournamentParameters, - IDataProvider provider, - IStateTransition stateTransition - ) external returns (BottomTournament) { - BottomTournament.BottomArguments memory - args = - BottomTournament.BottomArguments({ - tournamentArgs: ITournament.TournamentArguments({ - commitmentArgs: Commitment.Arguments({ - initialHash: initialHash, - startCycle: startCycle, - log2step: tournamentParameters.log2step, - height: tournamentParameters.height - }), - level: level, - levels: tournamentParameters.levels, - startInstant: Time.currentTime(), - allowance: allowance, - maxAllowance: tournamentParameters.maxAllowance, - matchEffort: tournamentParameters.matchEffort, - provider: provider - }), - nonRootTournamentArgs: ITournament.NonRootArguments({ - contestedCommitmentOne: contestedCommitmentOne, - contestedFinalStateOne: contestedFinalStateOne, - contestedCommitmentTwo: contestedCommitmentTwo, - contestedFinalStateTwo: contestedFinalStateTwo - }), - stateTransition: stateTransition - }); - address clone = address(IMPL).cloneWithImmutableArgs(abi.encode(args)); - return BottomTournament(clone); - } -} diff --git a/prt/contracts/src/tournament/factories/multilevel/MiddleTournamentFactory.sol b/prt/contracts/src/tournament/factories/multilevel/MiddleTournamentFactory.sol deleted file mode 100644 index 15fd8f3e..00000000 --- a/prt/contracts/src/tournament/factories/multilevel/MiddleTournamentFactory.sol +++ /dev/null @@ -1,74 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.17; - -import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; - -import {IDataProvider} from "prt-contracts/IDataProvider.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; -import { - MiddleTournament -} from "prt-contracts/tournament/concretes/MiddleTournament.sol"; -import { - IMultiLevelTournamentFactory -} from "prt-contracts/tournament/factories/IMultiLevelTournamentFactory.sol"; -import {Commitment} from "prt-contracts/tournament/libs/Commitment.sol"; -import {Time} from "prt-contracts/tournament/libs/Time.sol"; -import {Machine} from "prt-contracts/types/Machine.sol"; -import { - TournamentParameters -} from "prt-contracts/types/TournamentParameters.sol"; -import {Tree} from "prt-contracts/types/Tree.sol"; - -contract MiddleTournamentFactory { - using Clones for address; - - MiddleTournament immutable IMPL; - - constructor(MiddleTournament impl) { - IMPL = impl; - } - - function instantiate( - Machine.Hash initialHash, - Tree.Node contestedCommitmentOne, - Machine.Hash contestedFinalStateOne, - Tree.Node contestedCommitmentTwo, - Machine.Hash contestedFinalStateTwo, - Time.Duration allowance, - uint256 startCycle, - uint64 level, - TournamentParameters memory tournamentParameters, - IDataProvider provider, - IMultiLevelTournamentFactory tournamentFactory - ) external returns (MiddleTournament) { - MiddleTournament.MiddleArguments memory args = - MiddleTournament.MiddleArguments({ - tournamentArgs: ITournament.TournamentArguments({ - commitmentArgs: Commitment.Arguments({ - initialHash: initialHash, - startCycle: startCycle, - log2step: tournamentParameters.log2step, - height: tournamentParameters.height - }), - level: level, - levels: tournamentParameters.levels, - startInstant: Time.currentTime(), - allowance: allowance, - maxAllowance: tournamentParameters.maxAllowance, - matchEffort: tournamentParameters.matchEffort, - provider: provider - }), - nonRootTournamentArgs: ITournament.NonRootArguments({ - contestedCommitmentOne: contestedCommitmentOne, - contestedFinalStateOne: contestedFinalStateOne, - contestedCommitmentTwo: contestedCommitmentTwo, - contestedFinalStateTwo: contestedFinalStateTwo - }), - tournamentFactory: tournamentFactory - }); - address clone = address(IMPL).cloneWithImmutableArgs(abi.encode(args)); - return MiddleTournament(clone); - } -} diff --git a/prt/contracts/src/tournament/factories/multilevel/TopTournamentFactory.sol b/prt/contracts/src/tournament/factories/multilevel/TopTournamentFactory.sol deleted file mode 100644 index 88afe446..00000000 --- a/prt/contracts/src/tournament/factories/multilevel/TopTournamentFactory.sol +++ /dev/null @@ -1,62 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.17; - -import {Clones} from "@openzeppelin-contracts-5.5.0/proxy/Clones.sol"; - -import {IDataProvider} from "prt-contracts/IDataProvider.sol"; -import {ITournament} from "prt-contracts/ITournament.sol"; -import { - TopTournament -} from "prt-contracts/tournament/concretes/TopTournament.sol"; -import { - IMultiLevelTournamentFactory -} from "prt-contracts/tournament/factories/IMultiLevelTournamentFactory.sol"; -import {Commitment} from "prt-contracts/tournament/libs/Commitment.sol"; -import {Time} from "prt-contracts/tournament/libs/Time.sol"; -import {Machine} from "prt-contracts/types/Machine.sol"; -import { - TournamentParameters -} from "prt-contracts/types/TournamentParameters.sol"; - -contract TopTournamentFactory { - using Clones for address; - - uint64 constant START_CYCLE = 0; - uint64 constant LEVEL = 0; - - TopTournament immutable IMPL; - - constructor(TopTournament impl) { - IMPL = impl; - } - - function instantiate( - Machine.Hash initialHash, - TournamentParameters memory tournamentParameters, - IDataProvider provider, - IMultiLevelTournamentFactory tournamentFactory - ) external returns (TopTournament) { - TopTournament.TopArguments memory args = TopTournament.TopArguments({ - tournamentArgs: ITournament.TournamentArguments({ - commitmentArgs: Commitment.Arguments({ - initialHash: initialHash, - startCycle: START_CYCLE, - log2step: tournamentParameters.log2step, - height: tournamentParameters.height - }), - level: LEVEL, - levels: tournamentParameters.levels, - startInstant: Time.currentTime(), - allowance: tournamentParameters.maxAllowance, - maxAllowance: tournamentParameters.maxAllowance, - matchEffort: tournamentParameters.matchEffort, - provider: provider - }), - tournamentFactory: tournamentFactory - }); - address clone = address(IMPL).cloneWithImmutableArgs(abi.encode(args)); - return TopTournament(clone); - } -} diff --git a/prt/contracts/test/TournamentFactory.t.sol b/prt/contracts/test/TournamentFactory.t.sol index 3e45f2ff..bb448413 100644 --- a/prt/contracts/test/TournamentFactory.t.sol +++ b/prt/contracts/test/TournamentFactory.t.sol @@ -20,14 +20,11 @@ import { import { MultiLevelTournamentFactory } from "src/tournament/factories/MultiLevelTournamentFactory.sol"; -import { - SingleLevelTournamentFactory -} from "src/tournament/factories/SingleLevelTournamentFactory.sol"; import {Util} from "./Util.sol"; contract TournamentFactoryTest is Util { - SingleLevelTournamentFactory singleLevelfactory; + MultiLevelTournamentFactory singleLevelfactory; MultiLevelTournamentFactory multiLevelfactory; function setUp() public { @@ -53,7 +50,7 @@ contract TournamentFactoryTest is Util { _height, ArbitrationConstants.height(_level), "height should match" ); - rootTournament = multiLevelfactory.instantiateTop( + rootTournament = multiLevelfactory.instantiate( Util.ONE_STATE, IDataProvider(address(0)) ); diff --git a/prt/contracts/test/Util.sol b/prt/contracts/test/Util.sol index 7c7f5e4e..de6d17bf 100644 --- a/prt/contracts/test/Util.sol +++ b/prt/contracts/test/Util.sol @@ -22,6 +22,9 @@ import { import { CanonicalTournamentParametersProvider } from "src/arbitration-config/CanonicalTournamentParametersProvider.sol"; +import { + ITournamentParametersProvider +} from "src/arbitration-config/ITournamentParametersProvider.sol"; import { CartesiStateTransition } from "src/state-transition/CartesiStateTransition.sol"; @@ -31,32 +34,53 @@ import { import { RiscVStateTransition } from "src/state-transition/RiscVStateTransition.sol"; -import {BottomTournament} from "src/tournament/concretes/BottomTournament.sol"; -import {MiddleTournament} from "src/tournament/concretes/MiddleTournament.sol"; -import { - SingleLevelTournament -} from "src/tournament/concretes/SingleLevelTournament.sol"; -import {TopTournament} from "src/tournament/concretes/TopTournament.sol"; +import {Tournament} from "src/tournament/Tournament.sol"; import { MultiLevelTournamentFactory } from "src/tournament/factories/MultiLevelTournamentFactory.sol"; -import { - SingleLevelTournamentFactory -} from "src/tournament/factories/SingleLevelTournamentFactory.sol"; -import { - BottomTournamentFactory -} from "src/tournament/factories/multilevel/BottomTournamentFactory.sol"; -import { - MiddleTournamentFactory -} from "src/tournament/factories/multilevel/MiddleTournamentFactory.sol"; -import { - TopTournamentFactory -} from "src/tournament/factories/multilevel/TopTournamentFactory.sol"; import {Match} from "src/tournament/libs/Match.sol"; import {Time} from "src/tournament/libs/Time.sol"; import {Machine} from "src/types/Machine.sol"; +import {TournamentParameters} from "src/types/TournamentParameters.sol"; import {Tree} from "src/types/Tree.sol"; +// Simple parameters provider for single-level tournaments (levels = 1) +contract SingleLevelTournamentParametersProvider is + ITournamentParametersProvider +{ + Time.Duration immutable MATCH_EFFORT; + Time.Duration immutable MAX_ALLOWANCE; + uint64 immutable LOG2_STEP; + uint64 immutable HEIGHT; + + constructor( + uint64 log2step, + uint64 height, + Time.Duration matchEffort, + Time.Duration maxAllowance + ) { + LOG2_STEP = log2step; + HEIGHT = height; + MATCH_EFFORT = matchEffort; + MAX_ALLOWANCE = maxAllowance; + } + + function tournamentParameters(uint64) + external + view + override + returns (TournamentParameters memory) + { + return TournamentParameters({ + levels: 1, // Single-level tournament + log2step: LOG2_STEP, + height: HEIGHT, + matchEffort: MATCH_EFFORT, + maxAllowance: MAX_ALLOWANCE + }); + } +} + contract Util is Test { using Tree for Tree.Node; using Machine for Machine.Hash; @@ -190,11 +214,10 @@ contract Util is Test { // create new _topTournament and player 0 joins it function initializePlayer0Tournament(MultiLevelTournamentFactory _factory) internal - returns (TopTournament _topTournament) + returns (ITournament _topTournament) { - _topTournament = TopTournament( - address(_factory.instantiate(ONE_STATE, IDataProvider(address(0)))) - ); + _topTournament = + _factory.instantiate(ONE_STATE, IDataProvider(address(0))); // player 0 joins tournament joinTournament(_topTournament, 0); } @@ -202,14 +225,10 @@ contract Util is Test { // create new _topTournament and player 0 joins it function initializePlayer0RollupsTournament(MultiLevelTournamentFactory _factory) internal - returns (TopTournament _topTournament) + returns (ITournament _topTournament) { - _topTournament = TopTournament( - address( - _factory.instantiate( - ONE_STATE, IDataProvider(address(0x12345678901234567890)) - ) - ) + _topTournament = _factory.instantiate( + ONE_STATE, IDataProvider(address(0x12345678901234567890)) ); // player 0 joins tournament joinTournament(_topTournament, 0); @@ -295,17 +314,19 @@ contract Util is Test { // instantiates all sub-factories and TournamentFactory function instantiateSingleLevelTournamentFactory() internal - returns (SingleLevelTournamentFactory) + returns (MultiLevelTournamentFactory) { (CartesiStateTransition stateTransition,,) = instantiateStateTransition(); - SingleLevelTournamentFactory singleLevelFactory = new SingleLevelTournamentFactory( - new SingleLevelTournament(), - stateTransition, - ArbitrationConstants.log2step(0), - ArbitrationConstants.height(0), - MAX_ALLOWANCE, - MATCH_EFFORT + MultiLevelTournamentFactory singleLevelFactory = new MultiLevelTournamentFactory( + new Tournament(), + new SingleLevelTournamentParametersProvider( + ArbitrationConstants.log2step(0), + ArbitrationConstants.height(0), + MATCH_EFFORT, + MAX_ALLOWANCE + ), + stateTransition ); return singleLevelFactory; @@ -320,9 +341,7 @@ contract Util is Test { instantiateStateTransition(); return ( new MultiLevelTournamentFactory( - new TopTournamentFactory(new TopTournament()), - new MiddleTournamentFactory(new MiddleTournament()), - new BottomTournamentFactory(new BottomTournament()), + new Tournament(), new CanonicalTournamentParametersProvider( MATCH_EFFORT, MAX_ALLOWANCE ), From fac561d1b7762a578f2ccde775c21f1fbacedb02 Mon Sep 17 00:00:00 2001 From: Stephen Chen <20940639+stephenctw@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:13:27 +0800 Subject: [PATCH 2/2] ci: fix build install cartesi machine --- .github/workflows/build.yml | 2 ++ Cargo.toml | 2 +- justfile | 20 +++++++++++++------ .../cartesi-machine-sys/build.rs | 4 ++-- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1c8e23f..a2f9b4e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,6 +138,8 @@ jobs: working-directory: ./machine/emulator run: | make bundle-boost + wget https://github.com/cartesi/machine-emulator/releases/download/v0.19.0/add-generated-files.diff + git apply add-generated-files.diff make sudo make install diff --git a/Cargo.toml b/Cargo.toml index f041f24a..133a6ce7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ repository = "https://github.com/cartesi/dave" # machine bindings cartesi-machine = { path = "machine/rust-bindings/cartesi-machine", features = [ - "build_uarch", + "download_uarch", ] } # solidity bindings diff --git a/justfile b/justfile index 2e732642..5918c637 100644 --- a/justfile +++ b/justfile @@ -1,14 +1,22 @@ update-submodules: git submodule update --recursive --init +apply-generated-files-diff VERSION="v0.19.0": + cd machine/emulator && \ + wget https://github.com/cartesi/machine-emulator/releases/download/{{VERSION}}/add-generated-files.diff && \ + git apply add-generated-files.diff + +bundle-boost: + make -C machine/emulator bundle-boost + clean-emulator: make -C machine/emulator clean depclean distclean clean-contracts: clean-consensus-contracts clean-prt-contracts clean-bindings clean-deployments make -C machine/emulator clean depclean distclean -setup: update-submodules clean-emulator clean-contracts - make -C machine/emulator uarch-with-toolchain # Requires docker, necessary for machine bindings +setup: update-submodules clean-emulator clean-contracts bundle-boost apply-generated-files-diff + make -C machine/emulator # Requires docker, necessary for machine bindings # Run this once after cloning, if using a docker environment setup-docker: setup build-docker-image @@ -61,13 +69,13 @@ fmt-rust-workspace: bind check-fmt-rust-workspace: bind cargo fmt --check check-rust-workspace: bind - cargo check --features build_uarch + cargo check --features download_uarch test-rust-workspace: bind - cargo test --features build_uarch + cargo test --features download_uarch build-rust-workspace *ARGS: bind - cargo build {{ARGS}} --features build_uarch + cargo build {{ARGS}} --features download_uarch build-release-rust-workspace *ARGS: bind - cargo build --release {{ARGS}} --features build_uarch + cargo build --release {{ARGS}} --features download_uarch clean-rust-workspace: bind cargo clean diff --git a/machine/rust-bindings/cartesi-machine-sys/build.rs b/machine/rust-bindings/cartesi-machine-sys/build.rs index 3b08b5e3..527c6543 100644 --- a/machine/rust-bindings/cartesi-machine-sys/build.rs +++ b/machine/rust-bindings/cartesi-machine-sys/build.rs @@ -30,7 +30,7 @@ fn main() { } // static link - // println!("cargo:rustc-link-lib=slirp"); + println!("cargo:rustc-link-lib=slirp"); cfg_if::cfg_if! { if #[cfg(feature = "remote_machine")] { println!("cargo:rustc-link-lib=static=cartesi_jsonrpc"); @@ -167,7 +167,7 @@ mod build_cm { mod copy_uarch { use std::{env, fs, path::Path}; - fn copy(uarch_path: &Path) { + pub(crate) fn copy(uarch_path: &Path) { let uarch_pristine_hash_path = env::var("UARCH_PRISTINE_HASH_PATH").expect("`UARCH_PRISTINE_HASH_PATH` not set"); let uarch_pristine_ram_path =