From 4bd1791758c511b5ae91e2747c8f2107012c0829 Mon Sep 17 00:00:00 2001 From: Stephen Chen <20940639+stephenctw@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:47:16 +0800 Subject: [PATCH 1/9] test(prt-contracts): enable `Match` unit tests --- prt/contracts/test/Match.t.sol | 97 +++++++++++++--------------------- 1 file changed, 38 insertions(+), 59 deletions(-) diff --git a/prt/contracts/test/Match.t.sol b/prt/contracts/test/Match.t.sol index 4e4fbe95..3703dad4 100644 --- a/prt/contracts/test/Match.t.sol +++ b/prt/contracts/test/Match.t.sol @@ -18,19 +18,14 @@ import "src/CanonicalConstants.sol"; pragma solidity ^0.8.0; -// TODO: we cannot set the height of a match anymore -// To properly test, we'll need to swap the implementation of -// ArbitrationConstants. -// Or wait until `mockCall` works on internal calls `https://github.com/foundry-rs/foundry/issues/432` - -/* contract MatchTest is Test { using Tree for Tree.Node; using Machine for Machine.Hash; using Match for Match.Id; + using Match for Match.IdHash; using Match for Match.State; - uint256 MAX_LOG2_SIZE = ArbitrationConstants.height(0); + Tree.Node constant ONE_NODE = Tree.Node.wrap(bytes32(uint256(1))); Match.State leftDivergenceMatch; Match.State rightDivergenceMatch; @@ -38,26 +33,20 @@ contract MatchTest is Test { Match.IdHash rightDivergenceMatchId; function setUp() public { - Tree.Node leftDivergenceCommitment1 = Tree.ZERO_NODE.join( - Tree.ZERO_NODE - ); - Tree.Node rightDivergenceCommitment1 = Tree.ZERO_NODE.join( - Tree.ZERO_NODE - ); + Tree.Node leftDivergenceCommitment1 = + Tree.ZERO_NODE.join(Tree.ZERO_NODE); + Tree.Node rightDivergenceCommitment1 = + Tree.ZERO_NODE.join(Tree.ZERO_NODE); - Tree.Node leftDivergenceCommitment2 = Tree - .Node - .wrap(bytes32(uint256(1))) - .join(Tree.ZERO_NODE); - Tree.Node rightDivergenceCommitment2 = Tree.ZERO_NODE.join( - Tree.Node.wrap(bytes32(uint256(1))) - ); + Tree.Node leftDivergenceCommitment2 = ONE_NODE.join(Tree.ZERO_NODE); + Tree.Node rightDivergenceCommitment2 = Tree.ZERO_NODE.join(ONE_NODE); (leftDivergenceMatchId, leftDivergenceMatch) = Match.createMatch( leftDivergenceCommitment1, leftDivergenceCommitment2, - Tree.Node.wrap(bytes32(uint256(1))), + ONE_NODE, Tree.ZERO_NODE, + 0, 1 ); @@ -65,7 +54,8 @@ contract MatchTest is Test { rightDivergenceCommitment1, rightDivergenceCommitment2, Tree.ZERO_NODE, - Tree.Node.wrap(bytes32(uint256(1))), + ONE_NODE, + 0, 1 ); } @@ -75,22 +65,17 @@ contract MatchTest is Test { !leftDivergenceMatch.agreesOnLeftNode(Tree.ZERO_NODE), "left node should diverge" ); - ( - Machine.Hash _finalHashOne, - Machine.Hash _finalHashTwo - ) = leftDivergenceMatch.setDivergenceOnLeftLeaf(Tree.ZERO_NODE); leftDivergenceMatch.height = 2; + (Machine.Hash _finalHashOne, Machine.Hash _finalHashTwo) = + leftDivergenceMatch._setDivergenceOnLeftLeaf(Tree.ZERO_NODE); assertTrue( - _finalHashOne.eq(Tree.ZERO_NODE.toMachineHash()), - "hash one should be zero" + _finalHashOne.eq(ONE_NODE.toMachineHash()), "hash one should be 1" ); assertTrue( - _finalHashTwo.eq( - Tree.Node.wrap(bytes32(uint256(1))).toMachineHash() - ), - "hash two should be 1" + _finalHashTwo.eq(Tree.ZERO_NODE.toMachineHash()), + "hash two should be zero" ); } @@ -99,22 +84,17 @@ contract MatchTest is Test { rightDivergenceMatch.agreesOnLeftNode(Tree.ZERO_NODE), "left node should match" ); - ( - Machine.Hash _finalHashOne, - Machine.Hash _finalHashTwo - ) = rightDivergenceMatch.setDivergenceOnRightLeaf(Tree.ZERO_NODE); rightDivergenceMatch.height = 2; + (Machine.Hash _finalHashOne, Machine.Hash _finalHashTwo) = + rightDivergenceMatch._setDivergenceOnRightLeaf(Tree.ZERO_NODE); assertTrue( - _finalHashOne.eq(Tree.ZERO_NODE.toMachineHash()), - "hash one should be zero" + _finalHashOne.eq(ONE_NODE.toMachineHash()), "hash one should be 1" ); assertTrue( - _finalHashTwo.eq( - Tree.Node.wrap(bytes32(uint256(1))).toMachineHash() - ), - "hash two should be 1" + _finalHashTwo.eq(Tree.ZERO_NODE.toMachineHash()), + "hash two should be zero" ); } @@ -123,22 +103,17 @@ contract MatchTest is Test { !leftDivergenceMatch.agreesOnLeftNode(Tree.ZERO_NODE), "left node should diverge" ); - ( - Machine.Hash _finalHashOne, - Machine.Hash _finalHashTwo - ) = leftDivergenceMatch.setDivergenceOnLeftLeaf(Tree.ZERO_NODE); leftDivergenceMatch.height = 3; + (Machine.Hash _finalHashOne, Machine.Hash _finalHashTwo) = + leftDivergenceMatch._setDivergenceOnLeftLeaf(Tree.ZERO_NODE); assertTrue( _finalHashOne.eq(Tree.ZERO_NODE.toMachineHash()), "hash one should be zero" ); assertTrue( - _finalHashTwo.eq( - Tree.Node.wrap(bytes32(uint256(1))).toMachineHash() - ), - "hash two should be 1" + _finalHashTwo.eq(ONE_NODE.toMachineHash()), "hash two should be 1" ); } @@ -147,23 +122,27 @@ contract MatchTest is Test { rightDivergenceMatch.agreesOnLeftNode(Tree.ZERO_NODE), "left node should match" ); - ( - Machine.Hash _finalHashOne, - Machine.Hash _finalHashTwo - ) = rightDivergenceMatch.setDivergenceOnRightLeaf(Tree.ZERO_NODE); rightDivergenceMatch.height = 3; + (Machine.Hash _finalHashOne, Machine.Hash _finalHashTwo) = + rightDivergenceMatch._setDivergenceOnRightLeaf(Tree.ZERO_NODE); assertTrue( _finalHashOne.eq(Tree.ZERO_NODE.toMachineHash()), "hash one should be zero" ); assertTrue( - _finalHashTwo.eq( - Tree.Node.wrap(bytes32(uint256(1))).toMachineHash() - ), - "hash two should be 1" + _finalHashTwo.eq(ONE_NODE.toMachineHash()), "hash two should be 1" ); } + + function testEqual() public { + assertTrue(leftDivergenceMatchId.eq(leftDivergenceMatchId)); + assertTrue(rightDivergenceMatchId.eq(rightDivergenceMatchId)); + assertTrue(!leftDivergenceMatchId.eq(rightDivergenceMatchId)); + assertTrue(!rightDivergenceMatchId.eq(leftDivergenceMatchId)); + + vm.expectRevert("matches are not equal"); + leftDivergenceMatchId.requireEq(rightDivergenceMatchId); + } } -*/ From d7c9e9f9639b6bb1e0588da858a4f0fe94ccfaf9 Mon Sep 17 00:00:00 2001 From: Stephen Chen <20940639+stephenctw@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:00:44 +0800 Subject: [PATCH 2/9] fix(prt-contracts): make `forge coverage` work --- prt/contracts/.gitignore | 4 +--- prt/contracts/coverage.sh | 9 +++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/prt/contracts/.gitignore b/prt/contracts/.gitignore index 3f738c3f..a54db637 100644 --- a/prt/contracts/.gitignore +++ b/prt/contracts/.gitignore @@ -16,6 +16,4 @@ docs/ # Coverage report lcov.info - -# Test -test/uarch-log +lcov.info.pruned diff --git a/prt/contracts/coverage.sh b/prt/contracts/coverage.sh index aac315d2..d657fcee 100755 --- a/prt/contracts/coverage.sh +++ b/prt/contracts/coverage.sh @@ -1,3 +1,8 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail + +# requires lcov - sudo apt-get install lcov forge coverage --report lcov -genhtml -o report --branch-coverage lcov.info +# removes irrelevance from coverage report +lcov --remove ./lcov.info '*script*' '*step*' '*test*' -o ./lcov.info.pruned +genhtml -o report --branch-coverage lcov.info.pruned From d24473ac39b46bd80439e084cea2c8b9477ea222 Mon Sep 17 00:00:00 2001 From: Stephen Chen <20940639+stephenctw@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:58:56 +0800 Subject: [PATCH 3/9] test(prt-contracts): full coverage for `Clock` --- prt/contracts/test/Clock.t.sol | 82 +++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/prt/contracts/test/Clock.t.sol b/prt/contracts/test/Clock.t.sol index c2f3bbda..29b2b401 100644 --- a/prt/contracts/test/Clock.t.sol +++ b/prt/contracts/test/Clock.t.sol @@ -16,15 +16,9 @@ import "src/tournament/libs/Clock.sol"; pragma solidity ^0.8.0; -library ExternalClock { - function advanceClockExternal(Clock.State storage state) external { - Clock.advanceClock(state); - } -} - contract ClockTest is Test { using Clock for Clock.State; - using ExternalClock for Clock.State; + using Time for Time.Duration; using Time for Time.Instant; Clock.State clock1; @@ -42,15 +36,6 @@ contract ClockTest is Test { ); } - function testMax() public view { - Time.Duration max = clock1.max(clock2); - assertEq( - Time.Duration.unwrap(max), - 30, - "should return max of two paused clocks" - ); - } - function testAdvanceClock() public { assertTrue(clock1.startInstant.isZero(), "clock1 should be set paused"); @@ -63,6 +48,27 @@ contract ClockTest is Test { assertTrue(clock1.startInstant.isZero(), "clock1 should be set paused"); } + function testMax() public view { + Time.Duration max = clock1.max(clock2); + assertEq( + Time.Duration.unwrap(max), + 30, + "should return max of two paused clocks" + ); + + Time.Duration max2 = clock2.max(clock1); + assertEq( + Time.Duration.unwrap(max2), + 30, + "should return max of two paused clocks" + ); + } + + function testNewClock() public { + vm.expectRevert("can't create clock with zero time"); + Clock.setNewPaused(clock2, Time.currentTime(), Time.Duration.wrap(0)); + } + function testTimeLeft() public { assertTrue(clock1.hasTimeLeft(), "clock1 should have time left"); @@ -76,6 +82,48 @@ contract ClockTest is Test { assertTrue(!clock1.hasTimeLeft(), "clock1 should run out of time"); vm.expectRevert("can't advance clock with no time left"); - clock1.advanceClockExternal(); + clock1.advanceClock(); + } + + function testTimeout() public { + clock1.advanceClock(); + clock2.advanceClock(); + + vm.roll(Time.Instant.unwrap(clock1.startInstant) + clock1Allowance - 1); + assertTrue( + clock1.timeSinceTimeout().isZero(), + "clock1 shouldn't be timeout yet" + ); + + vm.roll(Time.Instant.unwrap(clock2.startInstant) + clock2Allowance - 1); + assertTrue( + clock2.timeSinceTimeout().isZero(), + "clock2 shouldn't be timeout yet" + ); + + vm.roll(Time.Instant.unwrap(clock1.startInstant) + clock1Allowance); + assertTrue(clock1.timeSinceTimeout().isZero(), "clock1 just timeout"); + + vm.roll(Time.Instant.unwrap(clock2.startInstant) + clock2Allowance); + assertTrue(clock2.timeSinceTimeout().isZero(), "clock2 just timeout"); + + vm.roll( + Time.Instant.unwrap(clock1.startInstant) + clock1Allowance + 100 + ); + assertTrue( + clock1.timeSinceTimeout().gt(Time.ZERO_DURATION), + "clock1 should be timeout" + ); + + vm.roll(Time.Instant.unwrap(clock2.startInstant) + clock2Allowance + 1); + assertTrue( + clock2.timeSinceTimeout().gt(Time.ZERO_DURATION), + "clock2 shouldn be timeout" + ); + } + + function testTimeout2() public { + vm.expectRevert("a paused clock can't timeout"); + clock1.timeSinceTimeout(); } } From e336972b62a768f069b77ac86b82511d55505e5a Mon Sep 17 00:00:00 2001 From: Stephen Chen <20940639+stephenctw@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:33:17 +0800 Subject: [PATCH 4/9] test(prt-contracts): split `MultiTournament` test --- prt/contracts/test/BottomTournament.t.sol | 148 +++++++ prt/contracts/test/MiddleTournament.t.sol | 222 +++++++++++ prt/contracts/test/MultiTournament.t.sol | 466 ---------------------- prt/contracts/test/TopTournament.t.sol | 142 +++++++ prt/contracts/test/Tournament.t.sol | 28 +- 5 files changed, 539 insertions(+), 467 deletions(-) create mode 100644 prt/contracts/test/BottomTournament.t.sol create mode 100644 prt/contracts/test/MiddleTournament.t.sol delete mode 100644 prt/contracts/test/MultiTournament.t.sol create mode 100644 prt/contracts/test/TopTournament.t.sol diff --git a/prt/contracts/test/BottomTournament.t.sol b/prt/contracts/test/BottomTournament.t.sol new file mode 100644 index 00000000..2852d5de --- /dev/null +++ b/prt/contracts/test/BottomTournament.t.sol @@ -0,0 +1,148 @@ +// Copyright 2023 Cartesi Pte. Ltd. + +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +import "forge-std/Test.sol"; + +import "./Util.sol"; +import "src/tournament/factories/MultiLevelTournamentFactory.sol"; +import "src/CanonicalConstants.sol"; + +pragma solidity ^0.8.0; + +contract BottomTournamentTest is Util, Test { + using Tree for Tree.Node; + using Time for Time.Instant; + using Match for Match.Id; + using Match for Match.State; + using Machine for Machine.Hash; + + MultiLevelTournamentFactory immutable factory; + TopTournament topTournament; + MiddleTournament middleTournament; + BottomTournament bottomTournament; + + event newInnerTournament(Match.IdHash indexed, NonRootTournament); + + constructor() { + factory = Util.instantiateTournamentFactory(); + } + + function setUp() public {} + + function testBottom() public { + topTournament = Util.initializePlayer0Tournament(factory); + + // pair commitment, expect a match + // player 1 joins tournament + Util.joinTournament(topTournament, 1, 0); + + Match.Id memory _matchId = Util.matchId(1, 0); + Match.State memory _match = + topTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to left tree + uint256 _playerToSeal = + Util.advanceMatch01AtLevel(topTournament, _matchId, 0); + + // expect new inner created + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + topTournament, _matchId, _playerToSeal + ); + + assertEq( + topTournament.getMatchCycle(_matchId.hashFromId()), + 0, + "agree cycle should be zero" + ); + + Vm.Log[] memory _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + middleTournament = MiddleTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + Util.joinTournament(middleTournament, 0, 1); + Util.joinTournament(middleTournament, 1, 1); + + _matchId = Util.matchId(1, 1); + _match = middleTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to left tree + _playerToSeal = + Util.advanceMatch01AtLevel(middleTournament, _matchId, 1); + + // expect new inner created (middle 2) + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + middleTournament, _matchId, _playerToSeal + ); + + assertEq( + middleTournament.getMatchCycle(_matchId.hashFromId()), + 0, + "agree cycle should be zero" + ); + + _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + bottomTournament = BottomTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + Util.joinTournament(bottomTournament, 0, 2); + Util.joinTournament(bottomTournament, 1, 2); + + _matchId = Util.matchId(1, 2); + _match = bottomTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to left tree + _playerToSeal = + Util.advanceMatch01AtLevel(bottomTournament, _matchId, 2); + + // seal match + Util.sealLeafMatch(bottomTournament, _matchId, _playerToSeal); + + assertEq( + bottomTournament.getMatchCycle(_matchId.hashFromId()), + 0, + "agree cycle should be zero" + ); + + vm.expectRevert(); + // win match, expect revert + Util.winLeafMatch(bottomTournament, _matchId, _playerToSeal); + } +} diff --git a/prt/contracts/test/MiddleTournament.t.sol b/prt/contracts/test/MiddleTournament.t.sol new file mode 100644 index 00000000..1d1a20ca --- /dev/null +++ b/prt/contracts/test/MiddleTournament.t.sol @@ -0,0 +1,222 @@ +// Copyright 2023 Cartesi Pte. Ltd. + +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +import "forge-std/Test.sol"; + +import "./Util.sol"; +import "src/tournament/factories/MultiLevelTournamentFactory.sol"; +import "src/CanonicalConstants.sol"; + +pragma solidity ^0.8.0; + +contract MiddleTournamentTest is Util, Test { + using Tree for Tree.Node; + using Time for Time.Instant; + using Match for Match.Id; + using Match for Match.State; + using Machine for Machine.Hash; + + MultiLevelTournamentFactory immutable factory; + TopTournament topTournament; + MiddleTournament middleTournament; + + event newInnerTournament(Match.IdHash indexed, NonRootTournament); + + constructor() { + factory = Util.instantiateTournamentFactory(); + } + + function setUp() public {} + + function testInnerWinner() public { + topTournament = Util.initializePlayer0Tournament(factory); + + // pair commitment, expect a match + // player 1 joins tournament + Util.joinTournament(topTournament, 1, 0); + + Match.Id memory _matchId = Util.matchId(1, 0); + Match.State memory _match = + topTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to left tree + uint256 _playerToSeal = + Util.advanceMatch01AtLevel(topTournament, _matchId, 0); + + // expect new inner created + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + topTournament, _matchId, _playerToSeal + ); + + Vm.Log[] memory _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + middleTournament = MiddleTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + (bool _finished, Tree.Node _winner,) = + middleTournament.innerTournamentWinner(); + assertFalse(_finished, "winner should be zero node"); + + // player 0 should win after fast forward time to inner tournament finishes + uint256 _t = vm.getBlockNumber(); + // the delay is increased when a match is created + uint256 _rootTournamentFinish = _t + + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE) + + Time.Duration.unwrap(ArbitrationConstants.MATCH_EFFORT); + Util.joinTournament(middleTournament, 0, 1); + + vm.roll(_rootTournamentFinish); + (_finished, _winner,) = middleTournament.innerTournamentWinner(); + topTournament.winInnerMatch( + middleTournament, + playerNodes[0][ArbitrationConstants.height(0) - 1], + playerNodes[0][ArbitrationConstants.height(0) - 1] + ); + + { + (bool _finishedTop, Tree.Node _commitment, Machine.Hash _finalState) + = topTournament.arbitrationResult(); + + uint256 _winnerPlayer = 0; + assertTrue( + _commitment.eq( + playerNodes[_winnerPlayer][ArbitrationConstants.height(0)] + ), + "winner should be player 0" + ); + assertTrue(_finishedTop, "tournament should be finished"); + assertTrue( + _finalState.eq(Util.finalStates[_winnerPlayer]), + "final state should match" + ); + } + + //create another tournament for other test + topTournament = Util.initializePlayer0Tournament(factory); + + // pair commitment, expect a match + // player 1 joins tournament + Util.joinTournament(topTournament, 1, 0); + + _matchId = Util.matchId(1, 0); + _match = topTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to left tree + _playerToSeal = Util.advanceMatch01AtLevel(topTournament, _matchId, 0); + + // expect new inner created + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + topTournament, _matchId, _playerToSeal + ); + + _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + middleTournament = MiddleTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + (_finished, _winner,) = middleTournament.innerTournamentWinner(); + assertTrue(_winner.isZero(), "winner should be zero node"); + + _t = vm.getBlockNumber(); + // the delay is increased when a match is created + _rootTournamentFinish = + _t + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE); + uint256 _middleTournamentFinish = _rootTournamentFinish + + Time.Duration.unwrap(ArbitrationConstants.MATCH_EFFORT); + + Util.joinTournament(middleTournament, 0, 1); + + //let player 1 join, then timeout player 0 + Util.joinTournament(middleTournament, 1, 1); + + (Clock.State memory _player0Clock,) = middleTournament.getCommitment( + playerNodes[0][ArbitrationConstants.height(1)] + ); + _matchId = Util.matchId(1, 1); + _match = middleTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + vm.expectRevert(Tournament.WinByTimeout.selector); + middleTournament.winMatchByTimeout( + _matchId, + playerNodes[1][ArbitrationConstants.height(1) - 1], + playerNodes[1][ArbitrationConstants.height(1) - 1] + ); + + vm.roll( + Time.Instant.unwrap( + _player0Clock.startInstant.add(_player0Clock.allowance) + ) + ); + middleTournament.winMatchByTimeout( + _matchId, + playerNodes[1][ArbitrationConstants.height(1) - 1], + playerNodes[1][ArbitrationConstants.height(1) - 1] + ); + + _match = middleTournament.getMatch(_matchId.hashFromId()); + assertFalse(_match.exists(), "match should be deleted"); + + vm.roll(_middleTournamentFinish); + (_finished, _winner,) = middleTournament.innerTournamentWinner(); + topTournament.winInnerMatch( + middleTournament, + playerNodes[1][ArbitrationConstants.height(0) - 1], + playerNodes[1][ArbitrationConstants.height(0) - 1] + ); + + { + vm.roll(_rootTournamentFinish); + (bool _finishedTop, Tree.Node _commitment, Machine.Hash _finalState) + = topTournament.arbitrationResult(); + + uint256 _winnerPlayer = 1; + assertTrue( + _commitment.eq( + playerNodes[_winnerPlayer][ArbitrationConstants.height(0)] + ), + "winner should be player 1" + ); + assertTrue(_finishedTop, "tournament should be finished"); + assertTrue( + _finalState.eq(Util.finalStates[_winnerPlayer]), + "final state should match" + ); + } + } +} diff --git a/prt/contracts/test/MultiTournament.t.sol b/prt/contracts/test/MultiTournament.t.sol deleted file mode 100644 index 6aa44e02..00000000 --- a/prt/contracts/test/MultiTournament.t.sol +++ /dev/null @@ -1,466 +0,0 @@ -// Copyright 2023 Cartesi Pte. Ltd. - -// SPDX-License-Identifier: Apache-2.0 -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use -// this file except in compliance with the License. You may obtain a copy of the -// License at http://www.apache.org/licenses/LICENSE-2.0 - -// Unless required by applicable law or agreed to in writing, software distributed -// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, either express or implied. See the License for the -// specific language governing permissions and limitations under the License. - -import "forge-std/Test.sol"; - -import "./Util.sol"; -import "src/tournament/factories/MultiLevelTournamentFactory.sol"; -import "src/CanonicalConstants.sol"; - -pragma solidity ^0.8.0; - -contract MultiTournamentTest is Util, Test { - using Tree for Tree.Node; - using Time for Time.Instant; - using Match for Match.Id; - using Match for Match.State; - using Machine for Machine.Hash; - - MultiLevelTournamentFactory immutable factory; - TopTournament topTournament; - MiddleTournament middleTournament; - BottomTournament bottomTournament; - - event matchCreated( - Tree.Node indexed one, Tree.Node indexed two, Tree.Node leftOfTwo - ); - event newInnerTournament(Match.IdHash indexed, NonRootTournament); - - constructor() { - factory = Util.instantiateTournamentFactory(); - } - - function setUp() public {} - - function testRootWinner() public { - topTournament = Util.initializePlayer0Tournament(factory); - - // no winner before tournament finished - (bool _finished, Tree.Node _winner, Machine.Hash _finalState) = - topTournament.arbitrationResult(); - - assertTrue(_winner.isZero(), "winner should be zero node"); - assertFalse(_finished, "tournament shouldn't be finished"); - assertTrue( - _finalState.eq(Machine.ZERO_STATE), "final state should be zero" - ); - - // player 0 should win after fast forward time to tournament finishes - uint256 _t = vm.getBlockNumber(); - uint256 _tournamentFinish = - _t + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE); - - vm.roll(_tournamentFinish); - (_finished, _winner, _finalState) = topTournament.arbitrationResult(); - - uint256 _winnerPlayer = 0; - assertTrue( - _winner.eq( - playerNodes[_winnerPlayer][ArbitrationConstants.height(0)] - ), - "winner should be player 0" - ); - assertTrue(_finished, "tournament should be finished"); - assertTrue( - _finalState.eq(Util.finalStates[_winnerPlayer]), - "final state should match" - ); - - // rewind time in half and pair commitment, expect a match - vm.roll(_t); - // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); - - // no dangling commitment available, should revert - vm.roll(_tournamentFinish); - (_finished, _winner, _finalState) = topTournament.arbitrationResult(); - - // tournament not finished when still match going on - assertTrue(_winner.isZero(), "winner should be zero node"); - assertFalse(_finished, "tournament shouldn't be finished"); - assertTrue( - _finalState.eq(Machine.ZERO_STATE), "final state should be zero" - ); - } - - function testBottom() public { - topTournament = Util.initializePlayer0Tournament(factory); - - // pair commitment, expect a match - // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); - - Match.Id memory _matchId = Util.matchId(1, 0); - Match.State memory _match = - topTournament.getMatch(_matchId.hashFromId()); - assertTrue(_match.exists(), "match should exist"); - - // advance match to end, this match will always advance to left tree - uint256 _playerToSeal = - Util.advanceMatch01AtLevel(topTournament, _matchId, 0); - - // expect new inner created - vm.recordLogs(); - - // seal match - Util.sealInnerMatchAndCreateInnerTournament( - topTournament, _matchId, _playerToSeal - ); - - assertEq( - topTournament.getMatchCycle(_matchId.hashFromId()), - 0, - "agree cycle should be zero" - ); - - Vm.Log[] memory _entries = vm.getRecordedLogs(); - assertEq(_entries[0].topics.length, 2); - assertEq( - _entries[0].topics[0], - keccak256("newInnerTournament(bytes32,address)") - ); - assertEq( - _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) - ); - - middleTournament = MiddleTournament( - address(bytes20(bytes32(_entries[0].data) << (12 * 8))) - ); - - Util.joinTournament(middleTournament, 0, 1); - Util.joinTournament(middleTournament, 1, 1); - - _matchId = Util.matchId(1, 1); - _match = middleTournament.getMatch(_matchId.hashFromId()); - assertTrue(_match.exists(), "match should exist"); - - // advance match to end, this match will always advance to left tree - _playerToSeal = - Util.advanceMatch01AtLevel(middleTournament, _matchId, 1); - - // expect new inner created (middle 2) - vm.recordLogs(); - - // seal match - Util.sealInnerMatchAndCreateInnerTournament( - middleTournament, _matchId, _playerToSeal - ); - - assertEq( - middleTournament.getMatchCycle(_matchId.hashFromId()), - 0, - "agree cycle should be zero" - ); - - _entries = vm.getRecordedLogs(); - assertEq(_entries[0].topics.length, 2); - assertEq( - _entries[0].topics[0], - keccak256("newInnerTournament(bytes32,address)") - ); - assertEq( - _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) - ); - - bottomTournament = BottomTournament( - address(bytes20(bytes32(_entries[0].data) << (12 * 8))) - ); - - Util.joinTournament(bottomTournament, 0, 2); - Util.joinTournament(bottomTournament, 1, 2); - - _matchId = Util.matchId(1, 2); - _match = bottomTournament.getMatch(_matchId.hashFromId()); - assertTrue(_match.exists(), "match should exist"); - - // advance match to end, this match will always advance to left tree - _playerToSeal = - Util.advanceMatch01AtLevel(bottomTournament, _matchId, 2); - - // seal match - Util.sealLeafMatch(bottomTournament, _matchId, _playerToSeal); - - assertEq( - bottomTournament.getMatchCycle(_matchId.hashFromId()), - 0, - "agree cycle should be zero" - ); - - vm.expectRevert(); - // win match, expect revert - Util.winLeafMatch(bottomTournament, _matchId, _playerToSeal); - } - - function testInner() public { - topTournament = Util.initializePlayer0Tournament(factory); - - // pair commitment, expect a match - // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); - - Match.Id memory _matchId = Util.matchId(1, 0); - Match.State memory _match = - topTournament.getMatch(_matchId.hashFromId()); - assertTrue(_match.exists(), "match should exist"); - - // advance match to end, this match will always advance to left tree - uint256 _playerToSeal = - Util.advanceMatch01AtLevel(topTournament, _matchId, 0); - - // seal match - Util.sealInnerMatchAndCreateInnerTournament( - topTournament, _matchId, _playerToSeal - ); - - assertEq( - topTournament.getMatchCycle(_matchId.hashFromId()), - 0, - "agree cycle should be zero" - ); - - topTournament = Util.initializePlayer0Tournament(factory); - - // pair commitment, expect a match - // player 2 joins tournament - Util.joinTournament(topTournament, 2, 0); - - _matchId = Util.matchId(2, 0); - _match = topTournament.getMatch(_matchId.hashFromId()); - assertTrue(_match.exists(), "match should exist"); - - // advance match to end, this match will always advance to right tree - _playerToSeal = Util.advanceMatch02AtLevel(topTournament, _matchId, 0); - - // seal match - Util.sealInnerMatchAndCreateInnerTournament( - topTournament, _matchId, _playerToSeal - ); - - uint256 step = 1 << ArbitrationConstants.log2step(0); - uint256 leaf_position = (1 << ArbitrationConstants.height(0)) - 1; - - assertEq( - topTournament.getMatchCycle(_matchId.hashFromId()), - step * leaf_position, - "agree cycle should be the second right most leaf" - ); - } - - function testInnerWinner() public { - topTournament = Util.initializePlayer0Tournament(factory); - - // pair commitment, expect a match - // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); - - Match.Id memory _matchId = Util.matchId(1, 0); - Match.State memory _match = - topTournament.getMatch(_matchId.hashFromId()); - assertTrue(_match.exists(), "match should exist"); - - // advance match to end, this match will always advance to left tree - uint256 _playerToSeal = - Util.advanceMatch01AtLevel(topTournament, _matchId, 0); - - // expect new inner created - vm.recordLogs(); - - // seal match - Util.sealInnerMatchAndCreateInnerTournament( - topTournament, _matchId, _playerToSeal - ); - - Vm.Log[] memory _entries = vm.getRecordedLogs(); - assertEq(_entries[0].topics.length, 2); - assertEq( - _entries[0].topics[0], - keccak256("newInnerTournament(bytes32,address)") - ); - assertEq( - _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) - ); - - middleTournament = MiddleTournament( - address(bytes20(bytes32(_entries[0].data) << (12 * 8))) - ); - - (bool _finished, Tree.Node _winner,) = - middleTournament.innerTournamentWinner(); - assertFalse(_finished, "winner should be zero node"); - - // player 0 should win after fast forward time to inner tournament finishes - uint256 _t = vm.getBlockNumber(); - // the delay is increased when a match is created - uint256 _rootTournamentFinish = _t - + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE) - + Time.Duration.unwrap(ArbitrationConstants.MATCH_EFFORT); - Util.joinTournament(middleTournament, 0, 1); - - vm.roll(_rootTournamentFinish); - (_finished, _winner,) = middleTournament.innerTournamentWinner(); - topTournament.winInnerMatch( - middleTournament, - playerNodes[0][ArbitrationConstants.height(0) - 1], - playerNodes[0][ArbitrationConstants.height(0) - 1] - ); - - { - (bool _finishedTop, Tree.Node _commitment, Machine.Hash _finalState) - = topTournament.arbitrationResult(); - - uint256 _winnerPlayer = 0; - assertTrue( - _commitment.eq( - playerNodes[_winnerPlayer][ArbitrationConstants.height(0)] - ), - "winner should be player 0" - ); - assertTrue(_finishedTop, "tournament should be finished"); - assertTrue( - _finalState.eq(Util.finalStates[_winnerPlayer]), - "final state should match" - ); - } - - //create another tournament for other test - topTournament = Util.initializePlayer0Tournament(factory); - - // pair commitment, expect a match - // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); - - _matchId = Util.matchId(1, 0); - _match = topTournament.getMatch(_matchId.hashFromId()); - assertTrue(_match.exists(), "match should exist"); - - // advance match to end, this match will always advance to left tree - _playerToSeal = Util.advanceMatch01AtLevel(topTournament, _matchId, 0); - - // expect new inner created - vm.recordLogs(); - - // seal match - Util.sealInnerMatchAndCreateInnerTournament( - topTournament, _matchId, _playerToSeal - ); - - _entries = vm.getRecordedLogs(); - assertEq(_entries[0].topics.length, 2); - assertEq( - _entries[0].topics[0], - keccak256("newInnerTournament(bytes32,address)") - ); - assertEq( - _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) - ); - - middleTournament = MiddleTournament( - address(bytes20(bytes32(_entries[0].data) << (12 * 8))) - ); - - (_finished, _winner,) = middleTournament.innerTournamentWinner(); - assertTrue(_winner.isZero(), "winner should be zero node"); - - _t = vm.getBlockNumber(); - // the delay is increased when a match is created - _rootTournamentFinish = - _t + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE); - uint256 _middleTournamentFinish = _rootTournamentFinish - + Time.Duration.unwrap(ArbitrationConstants.MATCH_EFFORT); - - Util.joinTournament(middleTournament, 0, 1); - - //let player 1 join, then timeout player 0 - Util.joinTournament(middleTournament, 1, 1); - - (Clock.State memory _player0Clock,) = middleTournament.getCommitment( - playerNodes[0][ArbitrationConstants.height(1)] - ); - _matchId = Util.matchId(1, 1); - _match = middleTournament.getMatch(_matchId.hashFromId()); - assertTrue(_match.exists(), "match should exist"); - - vm.expectRevert(Tournament.WinByTimeout.selector); - middleTournament.winMatchByTimeout( - _matchId, - playerNodes[1][ArbitrationConstants.height(1) - 1], - playerNodes[1][ArbitrationConstants.height(1) - 1] - ); - - vm.roll( - Time.Instant.unwrap( - _player0Clock.startInstant.add(_player0Clock.allowance) - ) - ); - middleTournament.winMatchByTimeout( - _matchId, - playerNodes[1][ArbitrationConstants.height(1) - 1], - playerNodes[1][ArbitrationConstants.height(1) - 1] - ); - - _match = middleTournament.getMatch(_matchId.hashFromId()); - assertFalse(_match.exists(), "match should be deleted"); - - vm.roll(_middleTournamentFinish); - (_finished, _winner,) = middleTournament.innerTournamentWinner(); - topTournament.winInnerMatch( - middleTournament, - playerNodes[1][ArbitrationConstants.height(0) - 1], - playerNodes[1][ArbitrationConstants.height(0) - 1] - ); - - { - vm.roll(_rootTournamentFinish); - (bool _finishedTop, Tree.Node _commitment, Machine.Hash _finalState) - = topTournament.arbitrationResult(); - - uint256 _winnerPlayer = 1; - assertTrue( - _commitment.eq( - playerNodes[_winnerPlayer][ArbitrationConstants.height(0)] - ), - "winner should be player 1" - ); - assertTrue(_finishedTop, "tournament should be finished"); - assertTrue( - _finalState.eq(Util.finalStates[_winnerPlayer]), - "final state should match" - ); - } - } - - function testEliminateByTimeout() public { - topTournament = Util.initializePlayer0Tournament(factory); - - // pair commitment, expect a match - // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); - - Match.Id memory _matchId = Util.matchId(1, 0); - Match.State memory _match = - topTournament.getMatch(_matchId.hashFromId()); - assertTrue(_match.exists(), "match should exist"); - - uint256 _t = vm.getBlockNumber(); - // the delay is increased when a match is created - uint256 _rootTournamentFinish = - _t + 2 * Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE); - - vm.roll(_rootTournamentFinish - 1); - // cannot eliminate match when both blocks still have time - vm.expectRevert(Tournament.EliminateByTimeout.selector); - topTournament.eliminateMatchByTimeout(_matchId); - - vm.roll(_rootTournamentFinish); - topTournament.eliminateMatchByTimeout(_matchId); - } -} diff --git a/prt/contracts/test/TopTournament.t.sol b/prt/contracts/test/TopTournament.t.sol new file mode 100644 index 00000000..34256a76 --- /dev/null +++ b/prt/contracts/test/TopTournament.t.sol @@ -0,0 +1,142 @@ +// Copyright 2023 Cartesi Pte. Ltd. + +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +import "forge-std/Test.sol"; + +import "./Util.sol"; +import "src/tournament/factories/MultiLevelTournamentFactory.sol"; +import "src/CanonicalConstants.sol"; + +pragma solidity ^0.8.0; + +contract TopTournamentTest is Util, Test { + using Tree for Tree.Node; + using Time for Time.Instant; + using Match for Match.Id; + using Match for Match.State; + using Machine for Machine.Hash; + + MultiLevelTournamentFactory immutable factory; + TopTournament topTournament; + + constructor() { + factory = Util.instantiateTournamentFactory(); + } + + function setUp() public {} + + function testRootWinner() public { + topTournament = Util.initializePlayer0Tournament(factory); + + // no winner before tournament finished + (bool _finished, Tree.Node _winner, Machine.Hash _finalState) = + topTournament.arbitrationResult(); + + assertTrue(_winner.isZero(), "winner should be zero node"); + assertFalse(_finished, "tournament shouldn't be finished"); + assertTrue( + _finalState.eq(Machine.ZERO_STATE), "final state should be zero" + ); + + // player 0 should win after fast forward time to tournament finishes + uint256 _t = vm.getBlockNumber(); + uint256 _tournamentFinish = + _t + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE); + + vm.roll(_tournamentFinish); + (_finished, _winner, _finalState) = topTournament.arbitrationResult(); + + uint256 _winnerPlayer = 0; + assertTrue( + _winner.eq( + playerNodes[_winnerPlayer][ArbitrationConstants.height(0)] + ), + "winner should be player 0" + ); + assertTrue(_finished, "tournament should be finished"); + assertTrue( + _finalState.eq(Util.finalStates[_winnerPlayer]), + "final state should match" + ); + + // rewind time in half and pair commitment, expect a match + vm.roll(_t); + // player 1 joins tournament + Util.joinTournament(topTournament, 1, 0); + + // no dangling commitment available, should revert + vm.roll(_tournamentFinish); + (_finished, _winner, _finalState) = topTournament.arbitrationResult(); + + // tournament not finished when still match going on + assertTrue(_winner.isZero(), "winner should be zero node"); + assertFalse(_finished, "tournament shouldn't be finished"); + assertTrue( + _finalState.eq(Machine.ZERO_STATE), "final state should be zero" + ); + } + + function testInner() public { + topTournament = Util.initializePlayer0Tournament(factory); + + // pair commitment, expect a match + // player 1 joins tournament + Util.joinTournament(topTournament, 1, 0); + + Match.Id memory _matchId = Util.matchId(1, 0); + Match.State memory _match = + topTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to left tree + uint256 _playerToSeal = + Util.advanceMatch01AtLevel(topTournament, _matchId, 0); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + topTournament, _matchId, _playerToSeal + ); + + assertEq( + topTournament.getMatchCycle(_matchId.hashFromId()), + 0, + "agree cycle should be zero" + ); + + topTournament = Util.initializePlayer0Tournament(factory); + + // pair commitment, expect a match + // player 2 joins tournament + Util.joinTournament(topTournament, 2, 0); + + _matchId = Util.matchId(2, 0); + _match = topTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to right tree + _playerToSeal = Util.advanceMatch02AtLevel(topTournament, _matchId, 0); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + topTournament, _matchId, _playerToSeal + ); + + uint256 step = 1 << ArbitrationConstants.log2step(0); + uint256 leaf_position = (1 << ArbitrationConstants.height(0)) - 1; + + assertEq( + topTournament.getMatchCycle(_matchId.hashFromId()), + step * leaf_position, + "agree cycle should be the second right most leaf" + ); + } +} diff --git a/prt/contracts/test/Tournament.t.sol b/prt/contracts/test/Tournament.t.sol index 2f8d0b4a..2094f5be 100644 --- a/prt/contracts/test/Tournament.t.sol +++ b/prt/contracts/test/Tournament.t.sol @@ -23,6 +23,7 @@ contract TournamentTest is Util, Test { using Tree for Tree.Node; using Time for Time.Instant; using Match for Match.Id; + using Match for Match.State; using Machine for Machine.Hash; MultiLevelTournamentFactory immutable factory; @@ -32,7 +33,6 @@ contract TournamentTest is Util, Test { event matchCreated( Tree.Node indexed one, Tree.Node indexed two, Tree.Node leftOfTwo ); - event newInnerTournament(Match.IdHash indexed, NonRootTournament); constructor() { factory = Util.instantiateTournamentFactory(); @@ -169,4 +169,30 @@ contract TournamentTest is Util, Test { "final state should match" ); } + + function testEliminateByTimeout() public { + topTournament = Util.initializePlayer0Tournament(factory); + + // pair commitment, expect a match + // player 1 joins tournament + Util.joinTournament(topTournament, 1, 0); + + Match.Id memory _matchId = Util.matchId(1, 0); + Match.State memory _match = + topTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + uint256 _t = vm.getBlockNumber(); + // the delay is increased when a match is created + uint256 _rootTournamentFinish = + _t + 2 * Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE); + + vm.roll(_rootTournamentFinish - 1); + // cannot eliminate match when both blocks still have time + vm.expectRevert(Tournament.EliminateByTimeout.selector); + topTournament.eliminateMatchByTimeout(_matchId); + + vm.roll(_rootTournamentFinish); + topTournament.eliminateMatchByTimeout(_matchId); + } } From 7744958a22b4359f922c36e750a9db8c199683cc Mon Sep 17 00:00:00 2001 From: Stephen Chen <20940639+stephenctw@users.noreply.github.com> Date: Tue, 4 Feb 2025 00:08:25 +0800 Subject: [PATCH 5/9] refactor(prt-contracts): cleanup code and tests --- prt/contracts/src/CanonicalConstants.sol | 4 +- .../abstracts/NonLeafTournament.sol | 10 -- .../src/tournament/libs/Commitment.sol | 17 +- prt/contracts/src/tournament/libs/Merkle.sol | 51 ------ prt/contracts/test/BottomTournament.t.sol | 161 ++++++++++++++++-- prt/contracts/test/MiddleTournament.t.sol | 27 +-- prt/contracts/test/TopTournament.t.sol | 21 ++- prt/contracts/test/Tournament.t.sol | 31 ++-- prt/contracts/test/TournamentFactory.t.sol | 2 +- prt/contracts/test/Util.sol | 127 +++++--------- 10 files changed, 256 insertions(+), 195 deletions(-) delete mode 100644 prt/contracts/src/tournament/libs/Merkle.sol diff --git a/prt/contracts/src/CanonicalConstants.sol b/prt/contracts/src/CanonicalConstants.sol index e751e48a..2ca17513 100644 --- a/prt/contracts/src/CanonicalConstants.sol +++ b/prt/contracts/src/CanonicalConstants.sol @@ -35,13 +35,13 @@ library ArbitrationConstants { /// @return log2step gap of each leaf in the tournament[level] function log2step(uint64 level) internal pure returns (uint64) { - uint64[LEVELS] memory arr = [uint64(44), uint64(28), uint64(0)]; + uint64[LEVELS] memory arr = [uint64(44), uint64(27), uint64(0)]; return arr[level]; } /// @return height of the tournament[level] tree which is calculated by subtracting the log2step[level] from the log2step[level - 1] function height(uint64 level) internal pure returns (uint64) { - uint64[LEVELS] memory arr = [uint64(48), uint64(16), uint64(28)]; + uint64[LEVELS] memory arr = [uint64(48), uint64(17), uint64(27)]; return arr[level]; } } diff --git a/prt/contracts/src/tournament/abstracts/NonLeafTournament.sol b/prt/contracts/src/tournament/abstracts/NonLeafTournament.sol index 62d3e30d..274825b5 100644 --- a/prt/contracts/src/tournament/abstracts/NonLeafTournament.sol +++ b/prt/contracts/src/tournament/abstracts/NonLeafTournament.sol @@ -33,16 +33,6 @@ abstract contract NonLeafTournament is Tournament { // event newInnerTournament(Match.IdHash indexed, NonRootTournament); - // - // Modifiers - // - modifier onlyInnerTournament() { - Match.IdHash matchIdHash = - matchIdFromInnerTournaments[NonRootTournament(msg.sender)]; - matches[matchIdHash].requireExist(); - _; - } - // // Constructor // diff --git a/prt/contracts/src/tournament/libs/Commitment.sol b/prt/contracts/src/tournament/libs/Commitment.sol index 8e5a1719..ea781d4c 100644 --- a/prt/contracts/src/tournament/libs/Commitment.sol +++ b/prt/contracts/src/tournament/libs/Commitment.sol @@ -7,12 +7,12 @@ import "../../CanonicalConstants.sol"; import "../../Tree.sol"; import "../../Machine.sol"; -// import "./Merkle.sol"; - library Commitment { using Tree for Tree.Node; using Commitment for Tree.Node; + error CommitmentMismatch(Tree.Node received, Tree.Node expected); + function requireState( Tree.Node commitment, uint64 treeHeight, @@ -23,20 +23,29 @@ library Commitment { Tree.Node expectedCommitment = getRoot(Machine.Hash.unwrap(state), treeHeight, position, hashProof); - require(commitment.eq(expectedCommitment), "commitment state mismatch"); + require( + commitment.eq(expectedCommitment), + CommitmentMismatch(commitment, expectedCommitment) + ); } function isEven(uint256 x) private pure returns (bool) { return x % 2 == 0; } + error LengthMismatch(uint64 treeHeight, uint64 siblingsLength); + function getRoot( bytes32 leaf, uint64 treeHeight, uint256 position, bytes32[] calldata siblings ) internal pure returns (Tree.Node) { - assert(treeHeight == siblings.length); + uint64 siblingsLength = uint64(siblings.length); + require( + treeHeight == siblingsLength, + LengthMismatch(treeHeight, siblingsLength) + ); for (uint256 i = 0; i < treeHeight; i++) { if (isEven(position >> i)) { diff --git a/prt/contracts/src/tournament/libs/Merkle.sol b/prt/contracts/src/tournament/libs/Merkle.sol deleted file mode 100644 index b42dab16..00000000 --- a/prt/contracts/src/tournament/libs/Merkle.sol +++ /dev/null @@ -1,51 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.17; - -library Merkle { - function getRootWithValue( - uint64 _position, - bytes8 _value, - bytes32[] memory _proof - ) public pure returns (bytes32) { - bytes32 _runningHash = keccak256(abi.encodePacked(_value)); - - return getRootWithDrive(_position, 3, _runningHash, _proof); - } - - function getRootWithHash( - uint64 _position, - bytes32 _hash, - bytes32[] memory _proof - ) public pure returns (bytes32) { - return getRootWithDrive(_position, 3, _hash, _proof); - } - - function getRootWithDrive( - uint64 _position, - uint8 _logOfSize, - bytes32 _drive, - bytes32[] memory _siblings - ) public pure returns (bytes32) { - require(_logOfSize >= 3, "Must be at least a word"); - require(_logOfSize <= 64, "Cannot be bigger than the machine itself"); - - uint64 _size = uint64(2) ** _logOfSize; - - require(((_size - 1) & _position) == 0, "Position is not aligned"); - require( - _siblings.length == 64 - _logOfSize, "Proof length does not match" - ); - - for (uint64 _i = 0; _i < _siblings.length; _i++) { - if ((_position & (_size << _i)) == 0) { - _drive = keccak256(abi.encodePacked(_drive, _siblings[_i])); - } else { - _drive = keccak256(abi.encodePacked(_siblings[_i], _drive)); - } - } - - return _drive; - } -} diff --git a/prt/contracts/test/BottomTournament.t.sol b/prt/contracts/test/BottomTournament.t.sol index 2852d5de..d5f5af80 100644 --- a/prt/contracts/test/BottomTournament.t.sol +++ b/prt/contracts/test/BottomTournament.t.sol @@ -38,21 +38,23 @@ contract BottomTournamentTest is Util, Test { function setUp() public {} - function testBottom() public { + function testBottom1() public { topTournament = Util.initializePlayer0Tournament(factory); // pair commitment, expect a match // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); + uint256 _opponent = 1; + uint64 _height = 0; + Util.joinTournament(topTournament, _opponent); - Match.Id memory _matchId = Util.matchId(1, 0); + Match.Id memory _matchId = Util.matchId(_opponent, _height); Match.State memory _match = topTournament.getMatch(_matchId.hashFromId()); assertTrue(_match.exists(), "match should exist"); // advance match to end, this match will always advance to left tree uint256 _playerToSeal = - Util.advanceMatch01AtLevel(topTournament, _matchId, 0); + Util.advanceMatch(topTournament, _matchId, _opponent); // expect new inner created vm.recordLogs(); @@ -61,6 +63,7 @@ contract BottomTournamentTest is Util, Test { Util.sealInnerMatchAndCreateInnerTournament( topTournament, _matchId, _playerToSeal ); + _height += 1; assertEq( topTournament.getMatchCycle(_matchId.hashFromId()), @@ -82,16 +85,15 @@ contract BottomTournamentTest is Util, Test { address(bytes20(bytes32(_entries[0].data) << (12 * 8))) ); - Util.joinTournament(middleTournament, 0, 1); - Util.joinTournament(middleTournament, 1, 1); + Util.joinTournament(middleTournament, 0); + Util.joinTournament(middleTournament, _opponent); - _matchId = Util.matchId(1, 1); + _matchId = Util.matchId(_opponent, _height); _match = middleTournament.getMatch(_matchId.hashFromId()); assertTrue(_match.exists(), "match should exist"); // advance match to end, this match will always advance to left tree - _playerToSeal = - Util.advanceMatch01AtLevel(middleTournament, _matchId, 1); + _playerToSeal = Util.advanceMatch(middleTournament, _matchId, _opponent); // expect new inner created (middle 2) vm.recordLogs(); @@ -100,6 +102,7 @@ contract BottomTournamentTest is Util, Test { Util.sealInnerMatchAndCreateInnerTournament( middleTournament, _matchId, _playerToSeal ); + _height += 1; assertEq( middleTournament.getMatchCycle(_matchId.hashFromId()), @@ -121,16 +124,15 @@ contract BottomTournamentTest is Util, Test { address(bytes20(bytes32(_entries[0].data) << (12 * 8))) ); - Util.joinTournament(bottomTournament, 0, 2); - Util.joinTournament(bottomTournament, 1, 2); + Util.joinTournament(bottomTournament, 0); + Util.joinTournament(bottomTournament, _opponent); - _matchId = Util.matchId(1, 2); + _matchId = Util.matchId(_opponent, _height); _match = bottomTournament.getMatch(_matchId.hashFromId()); assertTrue(_match.exists(), "match should exist"); // advance match to end, this match will always advance to left tree - _playerToSeal = - Util.advanceMatch01AtLevel(bottomTournament, _matchId, 2); + _playerToSeal = Util.advanceMatch(bottomTournament, _matchId, _opponent); // seal match Util.sealLeafMatch(bottomTournament, _matchId, _playerToSeal); @@ -145,4 +147,135 @@ contract BottomTournamentTest is Util, Test { // win match, expect revert Util.winLeafMatch(bottomTournament, _matchId, _playerToSeal); } + + function testBottom2() public { + topTournament = Util.initializePlayer0Tournament(factory); + + // pair commitment, expect a match + // player 2 joins tournament + uint256 _opponent = 2; + uint64 _height = 0; + Util.joinTournament(topTournament, _opponent); + + Match.Id memory _matchId = Util.matchId(_opponent, _height); + Match.State memory _match = + topTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to right tree + uint256 _playerToSeal = + Util.advanceMatch(topTournament, _matchId, _opponent); + + // expect new inner created + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + topTournament, _matchId, _playerToSeal + ); + _height += 1; + + uint256 cycle = ( + 1 + << ( + ArbitrationConstants.height(0) + + ArbitrationConstants.log2step(0) + ) + ) - (1 << ArbitrationConstants.log2step(0)); + assertEq( + topTournament.getMatchCycle(_matchId.hashFromId()), + cycle, + "agree cycle should be 4951760157141503507410452480" + ); + + Vm.Log[] memory _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + middleTournament = MiddleTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + Util.joinTournament(middleTournament, 0); + Util.joinTournament(middleTournament, _opponent); + + _matchId = Util.matchId(_opponent, _height); + _match = middleTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to right tree + _playerToSeal = Util.advanceMatch(middleTournament, _matchId, _opponent); + + // expect new inner created (middle 2) + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + middleTournament, _matchId, _playerToSeal + ); + _height += 1; + + cycle = ( + 1 + << ( + ArbitrationConstants.height(0) + + ArbitrationConstants.log2step(0) + ) + ) - (1 << ArbitrationConstants.log2step(1)); + assertEq( + middleTournament.getMatchCycle(_matchId.hashFromId()), + cycle, + "agree cycle should be 4951760157141521099462279168" + ); + + _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + bottomTournament = BottomTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + Util.joinTournament(bottomTournament, 0); + Util.joinTournament(bottomTournament, _opponent); + + _matchId = Util.matchId(_opponent, _height); + _match = bottomTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to right tree + _playerToSeal = Util.advanceMatch(bottomTournament, _matchId, _opponent); + + // seal match + Util.sealLeafMatch(bottomTournament, _matchId, _playerToSeal); + + cycle = ( + 1 + << ( + ArbitrationConstants.height(0) + + ArbitrationConstants.log2step(0) + ) + ) - (1 << ArbitrationConstants.log2step(2)); + assertEq( + bottomTournament.getMatchCycle(_matchId.hashFromId()), + cycle, + "agree cycle should be 4951760157141521099596496895" + ); + + vm.expectRevert(); + // win match, expect revert + Util.winLeafMatch(bottomTournament, _matchId, _playerToSeal); + } } diff --git a/prt/contracts/test/MiddleTournament.t.sol b/prt/contracts/test/MiddleTournament.t.sol index 1d1a20ca..2c57d832 100644 --- a/prt/contracts/test/MiddleTournament.t.sol +++ b/prt/contracts/test/MiddleTournament.t.sol @@ -42,16 +42,18 @@ contract MiddleTournamentTest is Util, Test { // pair commitment, expect a match // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); + uint256 _opponent = 1; + uint64 _height = 0; + Util.joinTournament(topTournament, _opponent); - Match.Id memory _matchId = Util.matchId(1, 0); + Match.Id memory _matchId = Util.matchId(_opponent, _height); Match.State memory _match = topTournament.getMatch(_matchId.hashFromId()); assertTrue(_match.exists(), "match should exist"); // advance match to end, this match will always advance to left tree uint256 _playerToSeal = - Util.advanceMatch01AtLevel(topTournament, _matchId, 0); + Util.advanceMatch(topTournament, _matchId, _opponent); // expect new inner created vm.recordLogs(); @@ -60,6 +62,7 @@ contract MiddleTournamentTest is Util, Test { Util.sealInnerMatchAndCreateInnerTournament( topTournament, _matchId, _playerToSeal ); + _height += 1; Vm.Log[] memory _entries = vm.getRecordedLogs(); assertEq(_entries[0].topics.length, 2); @@ -85,7 +88,7 @@ contract MiddleTournamentTest is Util, Test { uint256 _rootTournamentFinish = _t + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE) + Time.Duration.unwrap(ArbitrationConstants.MATCH_EFFORT); - Util.joinTournament(middleTournament, 0, 1); + Util.joinTournament(middleTournament, 0); vm.roll(_rootTournamentFinish); (_finished, _winner,) = middleTournament.innerTournamentWinner(); @@ -118,14 +121,15 @@ contract MiddleTournamentTest is Util, Test { // pair commitment, expect a match // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); + _height = 0; + Util.joinTournament(topTournament, _opponent); - _matchId = Util.matchId(1, 0); + _matchId = Util.matchId(_opponent, _height); _match = topTournament.getMatch(_matchId.hashFromId()); assertTrue(_match.exists(), "match should exist"); // advance match to end, this match will always advance to left tree - _playerToSeal = Util.advanceMatch01AtLevel(topTournament, _matchId, 0); + _playerToSeal = Util.advanceMatch(topTournament, _matchId, _opponent); // expect new inner created vm.recordLogs(); @@ -134,6 +138,7 @@ contract MiddleTournamentTest is Util, Test { Util.sealInnerMatchAndCreateInnerTournament( topTournament, _matchId, _playerToSeal ); + _height += 1; _entries = vm.getRecordedLogs(); assertEq(_entries[0].topics.length, 2); @@ -159,15 +164,15 @@ contract MiddleTournamentTest is Util, Test { uint256 _middleTournamentFinish = _rootTournamentFinish + Time.Duration.unwrap(ArbitrationConstants.MATCH_EFFORT); - Util.joinTournament(middleTournament, 0, 1); + Util.joinTournament(middleTournament, 0); //let player 1 join, then timeout player 0 - Util.joinTournament(middleTournament, 1, 1); + Util.joinTournament(middleTournament, _opponent); (Clock.State memory _player0Clock,) = middleTournament.getCommitment( - playerNodes[0][ArbitrationConstants.height(1)] + playerNodes[0][ArbitrationConstants.height(_height)] ); - _matchId = Util.matchId(1, 1); + _matchId = Util.matchId(_opponent, _height); _match = middleTournament.getMatch(_matchId.hashFromId()); assertTrue(_match.exists(), "match should exist"); diff --git a/prt/contracts/test/TopTournament.t.sol b/prt/contracts/test/TopTournament.t.sol index 34256a76..1dc4725f 100644 --- a/prt/contracts/test/TopTournament.t.sol +++ b/prt/contracts/test/TopTournament.t.sol @@ -71,7 +71,8 @@ contract TopTournamentTest is Util, Test { // rewind time in half and pair commitment, expect a match vm.roll(_t); // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); + uint256 _opponent = 1; + Util.joinTournament(topTournament, _opponent); // no dangling commitment available, should revert vm.roll(_tournamentFinish); @@ -90,21 +91,24 @@ contract TopTournamentTest is Util, Test { // pair commitment, expect a match // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); + uint256 _opponent = 1; + uint64 _height = 0; + Util.joinTournament(topTournament, _opponent); - Match.Id memory _matchId = Util.matchId(1, 0); + Match.Id memory _matchId = Util.matchId(_opponent, _height); Match.State memory _match = topTournament.getMatch(_matchId.hashFromId()); assertTrue(_match.exists(), "match should exist"); // advance match to end, this match will always advance to left tree uint256 _playerToSeal = - Util.advanceMatch01AtLevel(topTournament, _matchId, 0); + Util.advanceMatch(topTournament, _matchId, _opponent); // seal match Util.sealInnerMatchAndCreateInnerTournament( topTournament, _matchId, _playerToSeal ); + _height += 1; assertEq( topTournament.getMatchCycle(_matchId.hashFromId()), @@ -116,19 +120,22 @@ contract TopTournamentTest is Util, Test { // pair commitment, expect a match // player 2 joins tournament - Util.joinTournament(topTournament, 2, 0); + _opponent = 2; + _height = 0; + Util.joinTournament(topTournament, _opponent); - _matchId = Util.matchId(2, 0); + _matchId = Util.matchId(_opponent, _height); _match = topTournament.getMatch(_matchId.hashFromId()); assertTrue(_match.exists(), "match should exist"); // advance match to end, this match will always advance to right tree - _playerToSeal = Util.advanceMatch02AtLevel(topTournament, _matchId, 0); + _playerToSeal = Util.advanceMatch(topTournament, _matchId, _opponent); // seal match Util.sealInnerMatchAndCreateInnerTournament( topTournament, _matchId, _playerToSeal ); + _height += 1; uint256 step = 1 << ArbitrationConstants.log2step(0); uint256 leaf_position = (1 << ArbitrationConstants.height(0)) - 1; diff --git a/prt/contracts/test/Tournament.t.sol b/prt/contracts/test/Tournament.t.sol index 2094f5be..b4a1d69c 100644 --- a/prt/contracts/test/Tournament.t.sol +++ b/prt/contracts/test/Tournament.t.sol @@ -43,10 +43,6 @@ contract TournamentTest is Util, Test { function testJoinTournament() public { topTournament = Util.initializePlayer0Tournament(factory); - // duplicate commitment should be reverted - vm.expectRevert("clock is initialized"); - Util.joinTournament(topTournament, 0, 0); - // pair commitment, expect a match vm.expectEmit(true, true, false, true, address(topTournament)); emit matchCreated( @@ -55,9 +51,18 @@ contract TournamentTest is Util, Test { playerNodes[1][ArbitrationConstants.height(0) - 1] ); // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); + uint256 _opponent = 1; + Util.joinTournament(topTournament, _opponent); } + // function testDuplicateJoinTournament() public { + // topTournament = Util.initializePlayer0Tournament(factory); + + // // duplicate commitment should be reverted + // vm.expectRevert("clock is initialized"); + // Util.joinTournament(topTournament, 0); + // } + function testTimeout() public { topTournament = Util.initializePlayer0Tournament(factory); @@ -68,9 +73,11 @@ contract TournamentTest is Util, Test { + Time.Duration.unwrap(ArbitrationConstants.MATCH_EFFORT); // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); + uint256 _opponent = 1; + uint64 _height = 0; + Util.joinTournament(topTournament, _opponent); - Match.Id memory _matchId = Util.matchId(1, 0); + Match.Id memory _matchId = Util.matchId(_opponent, _height); assertFalse( topTournament.canWinMatchByTimeout(_matchId), "shouldn't be able to win match by timeout" @@ -122,11 +129,11 @@ contract TournamentTest is Util, Test { + Time.Duration.unwrap(ArbitrationConstants.MATCH_EFFORT); // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); + Util.joinTournament(topTournament, _opponent); // player 0 should win after fast forward time to player 1 timeout // player 1 timeout first because he's supposed to advance match after player 0 advanced - _matchId = Util.matchId(1, 0); + _matchId = Util.matchId(_opponent, _height); topTournament.advanceMatch( _matchId, @@ -175,9 +182,11 @@ contract TournamentTest is Util, Test { // pair commitment, expect a match // player 1 joins tournament - Util.joinTournament(topTournament, 1, 0); + uint256 _opponent = 1; + uint64 _height = 0; + Util.joinTournament(topTournament, _opponent); - Match.Id memory _matchId = Util.matchId(1, 0); + Match.Id memory _matchId = Util.matchId(_opponent, _height); Match.State memory _match = topTournament.getMatch(_matchId.hashFromId()); assertTrue(_match.exists(), "match should exist"); diff --git a/prt/contracts/test/TournamentFactory.t.sol b/prt/contracts/test/TournamentFactory.t.sol index d3fd82fe..9fa71e1f 100644 --- a/prt/contracts/test/TournamentFactory.t.sol +++ b/prt/contracts/test/TournamentFactory.t.sol @@ -33,7 +33,7 @@ contract TournamentFactoryTest is Util, Test { function testRootTournament() public { RootTournament rootTournament = RootTournament( address( - singleLevelfactory.instantiateSingleLevel( + singleLevelfactory.instantiate( Util.ONE_STATE, IDataProvider(address(0)) ) ) diff --git a/prt/contracts/test/Util.sol b/prt/contracts/test/Util.sol index 1e31f3cd..dba0c31d 100644 --- a/prt/contracts/test/Util.sol +++ b/prt/contracts/test/Util.sol @@ -10,6 +10,8 @@ // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. +import "forge-std/console.sol"; + import "src/tournament/libs/Match.sol"; import "src/CanonicalConstants.sol"; import "src/CanonicalTournamentParametersProvider.sol"; @@ -96,13 +98,13 @@ contract Util { return _proof; } - // advance match between player 0 and player 1 - function advanceMatch01AtLevel( + // advance match between player 0 and opponent + function advanceMatch( Tournament _tournament, Match.Id memory _matchId, - uint64 _level + uint256 _opponent ) internal returns (uint256 _playerToSeal) { - uint256 _current = ArbitrationConstants.height(_level); + (,,, uint64 _current) = _tournament.tournamentLevelConstants(); for (_current; _current > 1; _current -= 1) { if (_playerToSeal == 0) { // advance match alternately until it can be sealed @@ -114,47 +116,25 @@ contract Util { playerNodes[0][_current - 2], playerNodes[0][_current - 2] ); - _playerToSeal = 1; + _playerToSeal = _opponent; } else { - _tournament.advanceMatch( - _matchId, - playerNodes[1][_current - 1], - playerNodes[1][_current - 1], - playerNodes[1][_current - 2], - playerNodes[1][_current - 2] - ); - _playerToSeal = 0; - } - } - } - - // advance match between player 0 and player 2 - function advanceMatch02AtLevel( - Tournament _tournament, - Match.Id memory _matchId, - uint64 _level - ) internal returns (uint256 _playerToSeal) { - uint256 _current = ArbitrationConstants.height(_level); - for (_current; _current > 1; _current -= 1) { - if (_playerToSeal == 0) { - // advance match alternately until it can be sealed - // starts with player 0 - _tournament.advanceMatch( - _matchId, - playerNodes[0][_current - 1], - playerNodes[0][_current - 1], - playerNodes[0][_current - 2], - playerNodes[0][_current - 2] - ); - _playerToSeal = 2; - } else { - _tournament.advanceMatch( - _matchId, - playerNodes[0][_current - 1], - playerNodes[2][_current - 1], - playerNodes[0][_current - 2], - playerNodes[2][_current - 2] - ); + if (_playerToSeal == 1) { + _tournament.advanceMatch( + _matchId, + playerNodes[1][_current - 1], + playerNodes[1][_current - 1], + playerNodes[1][_current - 2], + playerNodes[1][_current - 2] + ); + } else { + _tournament.advanceMatch( + _matchId, + playerNodes[0][_current - 1], + playerNodes[2][_current - 1], + playerNodes[0][_current - 2], + playerNodes[2][_current - 2] + ); + } _playerToSeal = 0; } } @@ -166,48 +146,26 @@ contract Util { returns (TopTournament _topTournament) { _topTournament = TopTournament( - address( - _factory.instantiateTop(ONE_STATE, IDataProvider(address(0))) - ) + address(_factory.instantiate(ONE_STATE, IDataProvider(address(0)))) ); // player 0 joins tournament - joinTournament(_topTournament, 0, 0); + joinTournament(_topTournament, 0); } // _player joins _tournament at _level - function joinTournament( - Tournament _tournament, - uint256 _player, - uint64 _level - ) internal { - if (_player == 0) { - _tournament.joinTournament( - ONE_STATE, - generateFinalStateProof( - _player, ArbitrationConstants.height(_level) - ), - playerNodes[0][ArbitrationConstants.height(_level) - 1], - playerNodes[0][ArbitrationConstants.height(_level) - 1] - ); - } else if (_player == 1) { - _tournament.joinTournament( - TWO_STATE, - generateFinalStateProof( - _player, ArbitrationConstants.height(_level) - ), - playerNodes[1][ArbitrationConstants.height(_level) - 1], - playerNodes[1][ArbitrationConstants.height(_level) - 1] - ); - } else if (_player == 2) { - _tournament.joinTournament( - TWO_STATE, - generateFinalStateProof( - _player, ArbitrationConstants.height(_level) - ), - playerNodes[0][ArbitrationConstants.height(_level) - 1], - playerNodes[2][ArbitrationConstants.height(_level) - 1] - ); - } + function joinTournament(Tournament _tournament, uint256 _player) internal { + (,,, uint64 height) = _tournament.tournamentLevelConstants(); + Tree.Node _left = _player == 1 + ? playerNodes[1][height - 1] + : playerNodes[0][height - 1]; + Tree.Node _right = playerNodes[_player][height - 1]; + Machine.Hash _final_state = _player == 0 ? ONE_STATE : TWO_STATE; + _tournament.joinTournament( + _final_state, + generateFinalStateProof(_player, height), + _left, + _right + ); } function sealLeafMatch( @@ -215,6 +173,7 @@ contract Util { Match.Id memory _matchId, uint256 _player ) internal { + (,,, uint64 height) = _tournament.tournamentLevelConstants(); Tree.Node _left = _player == 1 ? playerNodes[1][0] : playerNodes[0][0]; Tree.Node _right = playerNodes[_player][0]; // Machine.Hash state = _player == 1 ? ONE_STATE : Machine.ZERO_STATE; @@ -224,7 +183,7 @@ contract Util { _left, _right, ONE_STATE, - generateDivergenceProof(_player, ArbitrationConstants.height(0)) + generateDivergenceProof(_player, height) ); } @@ -245,16 +204,16 @@ contract Util { Match.Id memory _matchId, uint256 _player ) internal { + (,,, uint64 height) = _tournament.tournamentLevelConstants(); Tree.Node _left = _player == 1 ? playerNodes[1][0] : playerNodes[0][0]; Tree.Node _right = playerNodes[_player][0]; - // Machine.Hash state = _player == 1 ? ONE_STATE : Machine.ZERO_STATE; _tournament.sealInnerMatchAndCreateInnerTournament( _matchId, _left, _right, ONE_STATE, - generateDivergenceProof(_player, ArbitrationConstants.height(0)) + generateDivergenceProof(_player, height) ); } From 5cdaf6ed7ad9013db0b25f71b25da230842b6543 Mon Sep 17 00:00:00 2001 From: Stephen Chen <20940639+stephenctw@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:50:26 +0800 Subject: [PATCH 6/9] test(prt-contracts): full coverage for `Time` and `Machine` --- prt/contracts/test/Libs.t.sol | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 prt/contracts/test/Libs.t.sol diff --git a/prt/contracts/test/Libs.t.sol b/prt/contracts/test/Libs.t.sol new file mode 100644 index 00000000..db314dbd --- /dev/null +++ b/prt/contracts/test/Libs.t.sol @@ -0,0 +1,44 @@ +// Copyright 2023 Cartesi Pte. Ltd. + +// SPDX-License-Identifier: Apache-2.0 +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +import "forge-std/Test.sol"; + +import "src/Machine.sol"; +import "src/tournament/libs/Time.sol"; + +pragma solidity ^0.8.0; + +contract LibraryTest is Test { + using Machine for Machine.Hash; + using Time for Time.Duration; + + function testTimeSub() public pure { + Time.Duration l = Time.Duration.wrap(25); + Time.Duration r = Time.Duration.wrap(25); + assertEq(Time.Duration.unwrap(l.sub(r)), 0); + + l = Time.Duration.wrap(26); + r = Time.Duration.wrap(25); + assertEq(Time.Duration.unwrap(l.sub(r)), 1); + } + + function testTimeSubRevert() public { + vm.expectRevert(); + Time.Duration l = Time.Duration.wrap(25); + Time.Duration r = Time.Duration.wrap(35); + l.sub(r); + } + + function testMachineNotInitialized() public pure { + assertTrue(Machine.ZERO_STATE.notInitialized()); + } +} From 668c41dbcbfc42e761fa20cb5c848dae52627071 Mon Sep 17 00:00:00 2001 From: Stephen Chen <20940639+stephenctw@users.noreply.github.com> Date: Mon, 10 Feb 2025 19:57:24 +0800 Subject: [PATCH 7/9] test(prt-contracts): improve `LeafTournament` coverage --- prt/contracts/test/BottomTournament.t.sol | 359 +++++++++++++++++++++- prt/contracts/test/Util.sol | 51 +++ 2 files changed, 408 insertions(+), 2 deletions(-) diff --git a/prt/contracts/test/BottomTournament.t.sol b/prt/contracts/test/BottomTournament.t.sol index d5f5af80..badd76b1 100644 --- a/prt/contracts/test/BottomTournament.t.sol +++ b/prt/contracts/test/BottomTournament.t.sol @@ -38,7 +38,7 @@ contract BottomTournamentTest is Util, Test { function setUp() public {} - function testBottom1() public { + function testComputeStep() public { topTournament = Util.initializePlayer0Tournament(factory); // pair commitment, expect a match @@ -148,7 +148,7 @@ contract BottomTournamentTest is Util, Test { Util.winLeafMatch(bottomTournament, _matchId, _playerToSeal); } - function testBottom2() public { + function testComputeReset() public { topTournament = Util.initializePlayer0Tournament(factory); // pair commitment, expect a match @@ -278,4 +278,359 @@ contract BottomTournamentTest is Util, Test { // win match, expect revert Util.winLeafMatch(bottomTournament, _matchId, _playerToSeal); } + + function testRollupsCmio() public { + topTournament = Util.initializePlayer0RollupsTournament(factory); + + // pair commitment, expect a match + // player 1 joins tournament + uint256 _opponent = 1; + uint64 _height = 0; + Util.joinTournament(topTournament, _opponent); + + Match.Id memory _matchId = Util.matchId(_opponent, _height); + Match.State memory _match = + topTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to left tree + uint256 _playerToSeal = + Util.advanceMatch(topTournament, _matchId, _opponent); + + // expect new inner created + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + topTournament, _matchId, _playerToSeal + ); + _height += 1; + + assertEq( + topTournament.getMatchCycle(_matchId.hashFromId()), + 0, + "agree cycle should be zero" + ); + + Vm.Log[] memory _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + middleTournament = MiddleTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + Util.joinTournament(middleTournament, 0); + Util.joinTournament(middleTournament, _opponent); + + _matchId = Util.matchId(_opponent, _height); + _match = middleTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to left tree + _playerToSeal = Util.advanceMatch(middleTournament, _matchId, _opponent); + + // expect new inner created (middle 2) + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + middleTournament, _matchId, _playerToSeal + ); + _height += 1; + + assertEq( + middleTournament.getMatchCycle(_matchId.hashFromId()), + 0, + "agree cycle should be zero" + ); + + _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + bottomTournament = BottomTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + Util.joinTournament(bottomTournament, 0); + Util.joinTournament(bottomTournament, _opponent); + + _matchId = Util.matchId(_opponent, _height); + _match = bottomTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to left tree + _playerToSeal = Util.advanceMatch(bottomTournament, _matchId, _opponent); + + // seal match + Util.sealLeafMatch(bottomTournament, _matchId, _playerToSeal); + + assertEq( + bottomTournament.getMatchCycle(_matchId.hashFromId()), + 0, + "agree cycle should be zero" + ); + + vm.expectRevert(); + // win match, expect revert + Util.winLeafMatchRollupsWithInput( + bottomTournament, _matchId, _playerToSeal + ); + } + + function testRollupsStep() public { + topTournament = Util.initializePlayer0RollupsTournament(factory); + + // pair commitment, expect a match + // player 1 joins tournament + uint256 _opponent = 1; + uint64 _height = 0; + Util.joinTournament(topTournament, _opponent); + + Match.Id memory _matchId = Util.matchId(_opponent, _height); + Match.State memory _match = + topTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to left tree + uint256 _playerToSeal = + Util.advanceMatch(topTournament, _matchId, _opponent); + + // expect new inner created + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + topTournament, _matchId, _playerToSeal + ); + _height += 1; + + assertEq( + topTournament.getMatchCycle(_matchId.hashFromId()), + 0, + "agree cycle should be zero" + ); + + Vm.Log[] memory _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + middleTournament = MiddleTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + Util.joinTournament(middleTournament, 0); + Util.joinTournament(middleTournament, _opponent); + + _matchId = Util.matchId(_opponent, _height); + _match = middleTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to left tree + _playerToSeal = Util.advanceMatch(middleTournament, _matchId, _opponent); + + // expect new inner created (middle 2) + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + middleTournament, _matchId, _playerToSeal + ); + _height += 1; + + assertEq( + middleTournament.getMatchCycle(_matchId.hashFromId()), + 0, + "agree cycle should be zero" + ); + + _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + bottomTournament = BottomTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + Util.joinTournament(bottomTournament, 0); + Util.joinTournament(bottomTournament, _opponent); + + _matchId = Util.matchId(_opponent, _height); + _match = bottomTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to left tree + _playerToSeal = Util.advanceMatch(bottomTournament, _matchId, _opponent); + + // seal match + Util.sealLeafMatch(bottomTournament, _matchId, _playerToSeal); + + assertEq( + bottomTournament.getMatchCycle(_matchId.hashFromId()), + 0, + "agree cycle should be zero" + ); + + vm.expectRevert(); + // win match, expect revert + Util.winLeafMatchRollupsWithoutInput( + bottomTournament, _matchId, _playerToSeal + ); + } + + function testRollupsReset() public { + topTournament = Util.initializePlayer0RollupsTournament(factory); + + // pair commitment, expect a match + // player 2 joins tournament + uint256 _opponent = 2; + uint64 _height = 0; + Util.joinTournament(topTournament, _opponent); + + Match.Id memory _matchId = Util.matchId(_opponent, _height); + Match.State memory _match = + topTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to right tree + uint256 _playerToSeal = + Util.advanceMatch(topTournament, _matchId, _opponent); + + // expect new inner created + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + topTournament, _matchId, _playerToSeal + ); + _height += 1; + + uint256 cycle = ( + 1 + << ( + ArbitrationConstants.height(0) + + ArbitrationConstants.log2step(0) + ) + ) - (1 << ArbitrationConstants.log2step(0)); + assertEq( + topTournament.getMatchCycle(_matchId.hashFromId()), + cycle, + "agree cycle should be 4951760157141503507410452480" + ); + + Vm.Log[] memory _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + middleTournament = MiddleTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + Util.joinTournament(middleTournament, 0); + Util.joinTournament(middleTournament, _opponent); + + _matchId = Util.matchId(_opponent, _height); + _match = middleTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to right tree + _playerToSeal = Util.advanceMatch(middleTournament, _matchId, _opponent); + + // expect new inner created (middle 2) + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + middleTournament, _matchId, _playerToSeal + ); + _height += 1; + + cycle = ( + 1 + << ( + ArbitrationConstants.height(0) + + ArbitrationConstants.log2step(0) + ) + ) - (1 << ArbitrationConstants.log2step(1)); + assertEq( + middleTournament.getMatchCycle(_matchId.hashFromId()), + cycle, + "agree cycle should be 4951760157141521099462279168" + ); + + _entries = vm.getRecordedLogs(); + assertEq(_entries[0].topics.length, 2); + assertEq( + _entries[0].topics[0], + keccak256("newInnerTournament(bytes32,address)") + ); + assertEq( + _entries[0].topics[1], Match.IdHash.unwrap(_matchId.hashFromId()) + ); + + bottomTournament = BottomTournament( + address(bytes20(bytes32(_entries[0].data) << (12 * 8))) + ); + + Util.joinTournament(bottomTournament, 0); + Util.joinTournament(bottomTournament, _opponent); + + _matchId = Util.matchId(_opponent, _height); + _match = bottomTournament.getMatch(_matchId.hashFromId()); + assertTrue(_match.exists(), "match should exist"); + + // advance match to end, this match will always advance to right tree + _playerToSeal = Util.advanceMatch(bottomTournament, _matchId, _opponent); + + // seal match + Util.sealLeafMatch(bottomTournament, _matchId, _playerToSeal); + + cycle = ( + 1 + << ( + ArbitrationConstants.height(0) + + ArbitrationConstants.log2step(0) + ) + ) - (1 << ArbitrationConstants.log2step(2)); + assertEq( + bottomTournament.getMatchCycle(_matchId.hashFromId()), + cycle, + "agree cycle should be 4951760157141521099596496895" + ); + + vm.expectRevert(); + // win match, expect revert + Util.winLeafMatch(bottomTournament, _matchId, _playerToSeal); + } } diff --git a/prt/contracts/test/Util.sol b/prt/contracts/test/Util.sol index dba0c31d..62abb0ca 100644 --- a/prt/contracts/test/Util.sol +++ b/prt/contracts/test/Util.sol @@ -152,6 +152,21 @@ contract Util { joinTournament(_topTournament, 0); } + // create new _topTournament and player 0 joins it + function initializePlayer0RollupsTournament( + MultiLevelTournamentFactory _factory + ) internal returns (TopTournament _topTournament) { + _topTournament = TopTournament( + address( + _factory.instantiate( + ONE_STATE, IDataProvider(address(0x12345678901234567890)) + ) + ) + ); + // player 0 joins tournament + joinTournament(_topTournament, 0); + } + // _player joins _tournament at _level function joinTournament(Tournament _tournament, uint256 _player) internal { (,,, uint64 height) = _tournament.tournamentLevelConstants(); @@ -199,6 +214,42 @@ contract Util { _tournament.winLeafMatch(_matchId, _left, _right, new bytes(0)); } + function winLeafMatchRollupsWithInput( + LeafTournament _tournament, + Match.Id memory _matchId, + uint256 _player + ) internal { + Tree.Node _left = _player == 1 ? playerNodes[1][0] : playerNodes[0][0]; + Tree.Node _right = playerNodes[_player][0]; + // Machine.Hash state = _player == 1 ? ONE_STATE : Machine.ZERO_STATE; + + uint256 length = 20; + _tournament.winLeafMatch( + _matchId, + _left, + _right, + abi.encodePacked(abi.encodePacked(length), new bytes(20)) + ); + } + + function winLeafMatchRollupsWithoutInput( + LeafTournament _tournament, + Match.Id memory _matchId, + uint256 _player + ) internal { + Tree.Node _left = _player == 1 ? playerNodes[1][0] : playerNodes[0][0]; + Tree.Node _right = playerNodes[_player][0]; + // Machine.Hash state = _player == 1 ? ONE_STATE : Machine.ZERO_STATE; + + uint256 length = 0; + _tournament.winLeafMatch( + _matchId, + _left, + _right, + abi.encodePacked(abi.encodePacked(length), new bytes(0)) + ); + } + function sealInnerMatchAndCreateInnerTournament( NonLeafTournament _tournament, Match.Id memory _matchId, From 4f10794011a6eea954ff266b9a47b36d242259c7 Mon Sep 17 00:00:00 2001 From: Stephen Chen <20940639+stephenctw@users.noreply.github.com> Date: Tue, 18 Feb 2025 18:13:42 +0800 Subject: [PATCH 8/9] test(prt-contracts): use `foundry@1.0.0` stable and update tests --- prt/contracts/test/Clock.t.sol | 35 +++++++++++++++++++++++++++++----- prt/contracts/test/Libs.t.sol | 11 ++++++++++- prt/contracts/test/Match.t.sol | 8 +++++++- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/prt/contracts/test/Clock.t.sol b/prt/contracts/test/Clock.t.sol index 29b2b401..1a712e2a 100644 --- a/prt/contracts/test/Clock.t.sol +++ b/prt/contracts/test/Clock.t.sol @@ -16,6 +16,27 @@ import "src/tournament/libs/Clock.sol"; pragma solidity ^0.8.0; +library ExternalClock { + function advanceClock(Clock.State storage state) external { + Clock.advanceClock(state); + } + + function setNewPaused( + Clock.State storage state, + Time.Instant checkinInstant, + Time.Duration initialAllowance + ) external { + Clock.setNewPaused(state, checkinInstant, initialAllowance); + } + + function timeSinceTimeout(Clock.State storage state) + external + returns (Time.Duration) + { + return Clock.timeSinceTimeout(state); + } +} + contract ClockTest is Test { using Clock for Clock.State; using Time for Time.Duration; @@ -23,6 +44,7 @@ contract ClockTest is Test { Clock.State clock1; Clock.State clock2; + Clock.State clock3; uint64 constant clock1Allowance = 20; uint64 constant clock2Allowance = 30; @@ -34,6 +56,9 @@ contract ClockTest is Test { Clock.setNewPaused( clock2, Time.currentTime(), Time.Duration.wrap(clock2Allowance) ); + Clock.setNewPaused( + clock3, Time.currentTime(), Time.Duration.wrap(clock2Allowance) + ); } function testAdvanceClock() public { @@ -66,7 +91,9 @@ contract ClockTest is Test { function testNewClock() public { vm.expectRevert("can't create clock with zero time"); - Clock.setNewPaused(clock2, Time.currentTime(), Time.Duration.wrap(0)); + ExternalClock.setNewPaused( + clock2, Time.currentTime(), Time.Duration.wrap(0) + ); } function testTimeLeft() public { @@ -82,7 +109,7 @@ contract ClockTest is Test { assertTrue(!clock1.hasTimeLeft(), "clock1 should run out of time"); vm.expectRevert("can't advance clock with no time left"); - clock1.advanceClock(); + ExternalClock.advanceClock(clock1); } function testTimeout() public { @@ -120,10 +147,8 @@ contract ClockTest is Test { clock2.timeSinceTimeout().gt(Time.ZERO_DURATION), "clock2 shouldn be timeout" ); - } - function testTimeout2() public { vm.expectRevert("a paused clock can't timeout"); - clock1.timeSinceTimeout(); + ExternalClock.timeSinceTimeout(clock3); } } diff --git a/prt/contracts/test/Libs.t.sol b/prt/contracts/test/Libs.t.sol index db314dbd..e4aa0e6c 100644 --- a/prt/contracts/test/Libs.t.sol +++ b/prt/contracts/test/Libs.t.sol @@ -17,6 +17,15 @@ import "src/tournament/libs/Time.sol"; pragma solidity ^0.8.0; +library ExternalTime { + function sub(Time.Duration left, Time.Duration right) + external + returns (Time.Duration) + { + return Time.sub(left, right); + } +} + contract LibraryTest is Test { using Machine for Machine.Hash; using Time for Time.Duration; @@ -35,7 +44,7 @@ contract LibraryTest is Test { vm.expectRevert(); Time.Duration l = Time.Duration.wrap(25); Time.Duration r = Time.Duration.wrap(35); - l.sub(r); + ExternalTime.sub(l, r); } function testMachineNotInitialized() public pure { diff --git a/prt/contracts/test/Match.t.sol b/prt/contracts/test/Match.t.sol index 3703dad4..2a7cfc81 100644 --- a/prt/contracts/test/Match.t.sol +++ b/prt/contracts/test/Match.t.sol @@ -18,6 +18,12 @@ import "src/CanonicalConstants.sol"; pragma solidity ^0.8.0; +library ExternalMatch { + function requireEq(Match.IdHash left, Match.IdHash right) external { + Match.requireEq(left, right); + } +} + contract MatchTest is Test { using Tree for Tree.Node; using Machine for Machine.Hash; @@ -143,6 +149,6 @@ contract MatchTest is Test { assertTrue(!rightDivergenceMatchId.eq(leftDivergenceMatchId)); vm.expectRevert("matches are not equal"); - leftDivergenceMatchId.requireEq(rightDivergenceMatchId); + ExternalMatch.requireEq(leftDivergenceMatchId, rightDivergenceMatchId); } } From 87a45524761a95d3f04975f101a704d7824c8330 Mon Sep 17 00:00:00 2001 From: Stephen Chen <20940639+stephenctw@users.noreply.github.com> Date: Fri, 21 Feb 2025 17:36:16 +0800 Subject: [PATCH 9/9] test(prt-contracts): improve `Match.t.sol` --- .../tournament/abstracts/LeafTournament.sol | 1 - .../abstracts/NonLeafTournament.sol | 1 - .../src/tournament/abstracts/Tournament.sol | 1 - prt/contracts/src/tournament/libs/Match.sol | 4 + prt/contracts/test/Match.t.sol | 268 ++++++++++++++++-- 5 files changed, 255 insertions(+), 20 deletions(-) diff --git a/prt/contracts/src/tournament/abstracts/LeafTournament.sol b/prt/contracts/src/tournament/abstracts/LeafTournament.sol index 635f90ff..27e670a0 100644 --- a/prt/contracts/src/tournament/abstracts/LeafTournament.sol +++ b/prt/contracts/src/tournament/abstracts/LeafTournament.sol @@ -32,7 +32,6 @@ abstract contract LeafTournament is Tournament { Match.State storage _matchState = matches[_matchId.hashFromId()]; _matchState.requireExist(); _matchState.requireCanBeFinalized(); - _matchState.requireParentHasChildren(_leftLeaf, _rightLeaf); // Unpause clocks { diff --git a/prt/contracts/src/tournament/abstracts/NonLeafTournament.sol b/prt/contracts/src/tournament/abstracts/NonLeafTournament.sol index 274825b5..36eb4c03 100644 --- a/prt/contracts/src/tournament/abstracts/NonLeafTournament.sol +++ b/prt/contracts/src/tournament/abstracts/NonLeafTournament.sol @@ -49,7 +49,6 @@ abstract contract NonLeafTournament is Tournament { ) external tournamentNotFinished { Match.State storage _matchState = matches[_matchId.hashFromId()]; _matchState.requireCanBeFinalized(); - _matchState.requireParentHasChildren(_leftLeaf, _rightLeaf); // Pause clocks Time.Duration _maxDuration; { diff --git a/prt/contracts/src/tournament/abstracts/Tournament.sol b/prt/contracts/src/tournament/abstracts/Tournament.sol index 93c3aff1..22690716 100644 --- a/prt/contracts/src/tournament/abstracts/Tournament.sol +++ b/prt/contracts/src/tournament/abstracts/Tournament.sol @@ -166,7 +166,6 @@ abstract contract Tournament { Match.State storage _matchState = matches[_matchId.hashFromId()]; _matchState.requireExist(); _matchState.requireCanBeAdvanced(); - _matchState.requireParentHasChildren(_leftNode, _rightNode); _matchState.advanceMatch( _matchId, _leftNode, _rightNode, _newLeftNode, _newRightNode diff --git a/prt/contracts/src/tournament/libs/Match.sol b/prt/contracts/src/tournament/libs/Match.sol index c130e8da..b74e1b54 100644 --- a/prt/contracts/src/tournament/libs/Match.sol +++ b/prt/contracts/src/tournament/libs/Match.sol @@ -107,6 +107,8 @@ library Match { Tree.Node newLeftNode, Tree.Node newRightNode ) internal { + state.requireParentHasChildren(leftNode, rightNode); + if (!state.agreesOnLeftNode(leftNode)) { // go down left in Commitment tree leftNode.requireChildren(newLeftNode, newRightNode); @@ -136,6 +138,8 @@ library Match { internal returns (Machine.Hash divergentStateOne, Machine.Hash divergentStateTwo) { + state.requireParentHasChildren(leftLeaf, rightLeaf); + if (!state.agreesOnLeftNode(leftLeaf)) { // Divergence is in the left leaf! (divergentStateOne, divergentStateTwo) = diff --git a/prt/contracts/test/Match.t.sol b/prt/contracts/test/Match.t.sol index 2a7cfc81..089391f9 100644 --- a/prt/contracts/test/Match.t.sol +++ b/prt/contracts/test/Match.t.sol @@ -19,9 +19,45 @@ import "src/CanonicalConstants.sol"; pragma solidity ^0.8.0; library ExternalMatch { - function requireEq(Match.IdHash left, Match.IdHash right) external { + function requireEq(Match.IdHash left, Match.IdHash right) external pure { Match.requireEq(left, right); } + + function advanceMatch( + Match.State storage state, + Match.Id calldata id, + Tree.Node leftNode, + Tree.Node rightNode, + Tree.Node newLeftNode, + Tree.Node newRightNode + ) external { + Match.advanceMatch( + state, id, leftNode, rightNode, newLeftNode, newRightNode + ); + } + + function sealMatch( + Match.State storage state, + Match.Id calldata id, + Machine.Hash initialState, + Tree.Node leftLeaf, + Tree.Node rightLeaf, + Machine.Hash agreeState, + bytes32[] calldata agreeStateProof + ) + external + returns (Machine.Hash divergentStateOne, Machine.Hash divergentStateTwo) + { + return Match.sealMatch( + state, + id, + initialState, + leftLeaf, + rightLeaf, + agreeState, + agreeStateProof + ); + } } contract MatchTest is Test { @@ -33,21 +69,23 @@ contract MatchTest is Test { Tree.Node constant ONE_NODE = Tree.Node.wrap(bytes32(uint256(1))); + Match.State advanceMatchStateLeft; + Match.State advanceMatchStateRight; + Match.State leftDivergenceMatch; Match.State rightDivergenceMatch; - Match.IdHash leftDivergenceMatchId; - Match.IdHash rightDivergenceMatchId; - function setUp() public { - Tree.Node leftDivergenceCommitment1 = - Tree.ZERO_NODE.join(Tree.ZERO_NODE); - Tree.Node rightDivergenceCommitment1 = - Tree.ZERO_NODE.join(Tree.ZERO_NODE); + Match.IdHash leftDivergenceMatchIdHash; + Match.IdHash rightDivergenceMatchIdHash; - Tree.Node leftDivergenceCommitment2 = ONE_NODE.join(Tree.ZERO_NODE); - Tree.Node rightDivergenceCommitment2 = Tree.ZERO_NODE.join(ONE_NODE); + Tree.Node leftDivergenceCommitment1 = Tree.ZERO_NODE.join(Tree.ZERO_NODE); + Tree.Node rightDivergenceCommitment1 = Tree.ZERO_NODE.join(Tree.ZERO_NODE); - (leftDivergenceMatchId, leftDivergenceMatch) = Match.createMatch( + Tree.Node leftDivergenceCommitment2 = ONE_NODE.join(Tree.ZERO_NODE); + Tree.Node rightDivergenceCommitment2 = Tree.ZERO_NODE.join(ONE_NODE); + + function setUp() public { + (leftDivergenceMatchIdHash, leftDivergenceMatch) = Match.createMatch( leftDivergenceCommitment1, leftDivergenceCommitment2, ONE_NODE, @@ -56,7 +94,7 @@ contract MatchTest is Test { 1 ); - (rightDivergenceMatchId, rightDivergenceMatch) = Match.createMatch( + (rightDivergenceMatchIdHash, rightDivergenceMatch) = Match.createMatch( rightDivergenceCommitment1, rightDivergenceCommitment2, Tree.ZERO_NODE, @@ -66,6 +104,194 @@ contract MatchTest is Test { ); } + function testAdvanceMatchLeft() public { + Tree.Node leftDivergenceCommitment3 = + leftDivergenceCommitment1.join(Tree.ZERO_NODE); + Tree.Node leftDivergenceCommitment4 = + leftDivergenceCommitment2.join(Tree.ZERO_NODE); + + (, advanceMatchStateLeft) = Match.createMatch( + leftDivergenceCommitment3, + leftDivergenceCommitment4, + leftDivergenceCommitment2, + Tree.ZERO_NODE, + 0, + 2 + ); + advanceMatchStateLeft.requireExist(); + + Match.Id memory id = + Match.Id(leftDivergenceCommitment3, leftDivergenceCommitment4); + + advanceMatchStateLeft.requireCanBeAdvanced(); + ExternalMatch.advanceMatch( + advanceMatchStateLeft, + id, + leftDivergenceCommitment1, + Tree.ZERO_NODE, + Tree.ZERO_NODE, + Tree.ZERO_NODE + ); + + assertEq(advanceMatchStateLeft.currentHeight, 1); + assertTrue(advanceMatchStateLeft.leftNode.eq(Tree.ZERO_NODE)); + assertTrue(advanceMatchStateLeft.rightNode.eq(Tree.ZERO_NODE)); + + advanceMatchStateLeft.requireCanBeFinalized(); + ExternalMatch.sealMatch( + advanceMatchStateLeft, + id, + Machine.ZERO_STATE, + ONE_NODE, + Tree.ZERO_NODE, + Machine.ZERO_STATE, + new bytes32[](0) + ); + + advanceMatchStateLeft.requireIsFinished(); + ( + Machine.Hash agreeHash, + uint256 agreeCycle, + Machine.Hash finalStateOne, + Machine.Hash finalStateTwo + ) = advanceMatchStateLeft.getDivergence(0); + } + + function testAdvanceMatchRight() public { + Tree.Node rightDivergenceCommitment3 = + Tree.ZERO_NODE.join(rightDivergenceCommitment1); + Tree.Node rightDivergenceCommitment4 = + Tree.ZERO_NODE.join(rightDivergenceCommitment2); + + (, advanceMatchStateRight) = Match.createMatch( + rightDivergenceCommitment3, + rightDivergenceCommitment4, + Tree.ZERO_NODE, + rightDivergenceCommitment2, + 0, + 2 + ); + advanceMatchStateRight.requireExist(); + + Match.Id memory id = + Match.Id(rightDivergenceCommitment3, rightDivergenceCommitment4); + + advanceMatchStateRight.requireCanBeAdvanced(); + ExternalMatch.advanceMatch( + advanceMatchStateRight, + id, + Tree.ZERO_NODE, + rightDivergenceCommitment1, + Tree.ZERO_NODE, + Tree.ZERO_NODE + ); + + assertEq(advanceMatchStateRight.currentHeight, 1); + assertTrue(advanceMatchStateRight.leftNode.eq(Tree.ZERO_NODE)); + assertTrue(advanceMatchStateRight.rightNode.eq(Tree.ZERO_NODE)); + + bytes32[] memory proof = new bytes32[](2); + proof[0] = Tree.Node.unwrap(ONE_NODE); + proof[1] = Tree.Node.unwrap(Tree.ZERO_NODE); + + advanceMatchStateRight.requireCanBeFinalized(); + ExternalMatch.sealMatch( + advanceMatchStateRight, + id, + Machine.ZERO_STATE, + Tree.ZERO_NODE, + ONE_NODE, + Machine.ZERO_STATE, + proof + ); + + advanceMatchStateRight.requireIsFinished(); + ( + Machine.Hash agreeHash, + uint256 agreeCycle, + Machine.Hash finalStateOne, + Machine.Hash finalStateTwo + ) = advanceMatchStateRight.getDivergence(0); + } + + function testAdvanceMatchRight2() public { + Tree.Node rightDivergenceCommitment3 = + Tree.ZERO_NODE.join(rightDivergenceCommitment1); + Tree.Node rightDivergenceCommitment4 = + Tree.ZERO_NODE.join(rightDivergenceCommitment2); + Tree.Node rightDivergenceCommitment5 = + Tree.ZERO_NODE.join(rightDivergenceCommitment3); + Tree.Node rightDivergenceCommitment6 = + Tree.ZERO_NODE.join(rightDivergenceCommitment4); + + (, advanceMatchStateRight) = Match.createMatch( + rightDivergenceCommitment5, + rightDivergenceCommitment6, + Tree.ZERO_NODE, + rightDivergenceCommitment4, + 0, + 3 + ); + advanceMatchStateRight.requireExist(); + + Match.Id memory id = + Match.Id(rightDivergenceCommitment5, rightDivergenceCommitment6); + + advanceMatchStateRight.requireCanBeAdvanced(); + ExternalMatch.advanceMatch( + advanceMatchStateRight, + id, + Tree.ZERO_NODE, + rightDivergenceCommitment3, + Tree.ZERO_NODE, + rightDivergenceCommitment1 + ); + + assertEq(advanceMatchStateRight.currentHeight, 2); + assertTrue(advanceMatchStateRight.leftNode.eq(Tree.ZERO_NODE)); + assertTrue( + advanceMatchStateRight.rightNode.eq(rightDivergenceCommitment1) + ); + + advanceMatchStateRight.requireCanBeAdvanced(); + ExternalMatch.advanceMatch( + advanceMatchStateRight, + id, + Tree.ZERO_NODE, + rightDivergenceCommitment2, + Tree.ZERO_NODE, + ONE_NODE + ); + + assertEq(advanceMatchStateRight.currentHeight, 1); + assertTrue(advanceMatchStateRight.leftNode.eq(Tree.ZERO_NODE)); + assertTrue(advanceMatchStateRight.rightNode.eq(ONE_NODE)); + + bytes32[] memory proof = new bytes32[](3); + proof[0] = Tree.Node.unwrap(Tree.ZERO_NODE); + proof[1] = Tree.Node.unwrap(Tree.ZERO_NODE); + proof[2] = Tree.Node.unwrap(Tree.ZERO_NODE); + + advanceMatchStateRight.requireCanBeFinalized(); + ExternalMatch.sealMatch( + advanceMatchStateRight, + id, + Machine.ZERO_STATE, + Tree.ZERO_NODE, + Tree.ZERO_NODE, + Machine.ZERO_STATE, + proof + ); + + advanceMatchStateRight.requireIsFinished(); + ( + Machine.Hash agreeHash, + uint256 agreeCycle, + Machine.Hash finalStateOne, + Machine.Hash finalStateTwo + ) = advanceMatchStateRight.getDivergence(0); + } + function testDivergenceLeftWithEvenHeight() public { assertTrue( !leftDivergenceMatch.agreesOnLeftNode(Tree.ZERO_NODE), @@ -143,12 +369,20 @@ contract MatchTest is Test { } function testEqual() public { - assertTrue(leftDivergenceMatchId.eq(leftDivergenceMatchId)); - assertTrue(rightDivergenceMatchId.eq(rightDivergenceMatchId)); - assertTrue(!leftDivergenceMatchId.eq(rightDivergenceMatchId)); - assertTrue(!rightDivergenceMatchId.eq(leftDivergenceMatchId)); + assertTrue(leftDivergenceMatchIdHash.eq(leftDivergenceMatchIdHash)); + assertTrue(rightDivergenceMatchIdHash.eq(rightDivergenceMatchIdHash)); + assertTrue(!leftDivergenceMatchIdHash.eq(rightDivergenceMatchIdHash)); + assertTrue(!rightDivergenceMatchIdHash.eq(leftDivergenceMatchIdHash)); vm.expectRevert("matches are not equal"); - ExternalMatch.requireEq(leftDivergenceMatchId, rightDivergenceMatchId); + ExternalMatch.requireEq( + leftDivergenceMatchIdHash, rightDivergenceMatchIdHash + ); + } + + function testIdHash() public { + Match.Id memory id = Match.Id(Tree.ZERO_NODE, Tree.ZERO_NODE); + Match.IdHash idHash = id.hashFromId(); + idHash.requireExist(); } }