diff --git a/permissionless-arbitration/contracts/test/Clock.t.sol b/permissionless-arbitration/contracts/test/Clock.t.sol new file mode 100644 index 00000000..dfa44751 --- /dev/null +++ b/permissionless-arbitration/contracts/test/Clock.t.sol @@ -0,0 +1,74 @@ +// 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/Clock.sol"; + +pragma solidity ^0.8.0; + +contract ClockTest is Test { + using Clock for Clock.State; + using Time for Time.Instant; + + Clock.State clock1; + Clock.State clock2; + + uint64 constant clock1Allowance = 20; + uint64 constant clock2Allowance = 30; + + function setUp() public { + Clock.setNewPaused( + clock1, Time.currentTime(), Time.Duration.wrap(clock1Allowance) + ); + Clock.setNewPaused( + clock2, Time.currentTime(), Time.Duration.wrap(clock2Allowance) + ); + } + + function testMax() public { + 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"); + + clock1.advanceClock(); + assertTrue( + !clock1.startInstant.isZero(), "clock1 should be set running" + ); + + clock1.advanceClock(); + assertTrue(clock1.startInstant.isZero(), "clock1 should be set paused"); + } + + function testTimeLeft() public { + assertTrue(clock1.hasTimeLeft(), "clock1 should have time left"); + + clock1.advanceClock(); + assertTrue(clock1.hasTimeLeft(), "clock1 should have time left"); + + vm.warp(block.timestamp + clock1Allowance - 1); + assertTrue(clock1.hasTimeLeft(), "clock1 should have time left"); + + vm.warp(block.timestamp + clock1Allowance); + assertTrue(!clock1.hasTimeLeft(), "clock1 should run out of time"); + + vm.expectRevert("can't advance clock with no time left"); + clock1.advanceClock(); + } +} diff --git a/permissionless-arbitration/contracts/test/Match.t.sol b/permissionless-arbitration/contracts/test/Match.t.sol index 9cea84e7..de439bb0 100644 --- a/permissionless-arbitration/contracts/test/Match.t.sol +++ b/permissionless-arbitration/contracts/test/Match.t.sol @@ -21,6 +21,7 @@ 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 { diff --git a/permissionless-arbitration/contracts/test/MultiTournament.t.sol b/permissionless-arbitration/contracts/test/MultiTournament.t.sol index 5cbb62a1..bb9407b3 100644 --- a/permissionless-arbitration/contracts/test/MultiTournament.t.sol +++ b/permissionless-arbitration/contracts/test/MultiTournament.t.sol @@ -10,7 +10,6 @@ // 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 "forge-std/Test.sol"; import "./Util.sol"; @@ -23,11 +22,13 @@ 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; TournamentFactory immutable factory; TopTournament topTournament; MiddleTournament middleTournament; + BottomTournament bottomTournament; event matchCreated( Tree.Node indexed one, Tree.Node indexed two, Tree.Node leftOfTwo @@ -56,7 +57,7 @@ contract MultiTournamentTest is Util, Test { // player 0 should win after fast forward time to tournament finishes uint256 _t = block.timestamp; uint256 _tournamentFinish = - _t + 1 + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE); + _t + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE); vm.warp(_tournamentFinish); (_finished, _winner, _finalState) = topTournament.arbitrationResult(); @@ -77,7 +78,7 @@ contract MultiTournamentTest is Util, Test { // rewind time in half and pair commitment, expect a match vm.warp(_t); // player 1 joins tournament - Util.joinTopTournament(topTournament, 1); + Util.joinTournament(topTournament, 1, 0); // no dangling commitment available, should revert vm.warp(_tournamentFinish); @@ -91,14 +92,107 @@ contract MultiTournamentTest is Util, Test { ); } + 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 + ); + + 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 (bottom) + vm.recordLogs(); + + // seal match + Util.sealInnerMatchAndCreateInnerTournament( + middleTournament, _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()) + ); + + 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); + + 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.joinTopTournament(topTournament, 1); + 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 = @@ -113,9 +207,11 @@ contract MultiTournamentTest is Util, Test { // pair commitment, expect a match // player 2 joins tournament - Util.joinTopTournament(topTournament, 2); + 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); @@ -131,9 +227,12 @@ contract MultiTournamentTest is Util, Test { // pair commitment, expect a match // player 1 joins tournament - Util.joinTopTournament(topTournament, 1); + 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 = @@ -168,10 +267,10 @@ contract MultiTournamentTest is Util, Test { // player 0 should win after fast forward time to inner tournament finishes uint256 _t = block.timestamp; // the delay is increased when a match is created - uint256 _rootTournamentFinish = _t + 1 + uint256 _rootTournamentFinish = _t + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE) + Time.Duration.unwrap(ArbitrationConstants.MATCH_EFFORT); - Util.joinMiddleTournament(middleTournament, 0, 1); + Util.joinTournament(middleTournament, 0, 1); vm.warp(_rootTournamentFinish); (_finished, _winner,) = middleTournament.innerTournamentWinner(); @@ -204,9 +303,11 @@ contract MultiTournamentTest is Util, Test { // pair commitment, expect a match // player 1 joins tournament - Util.joinTopTournament(topTournament, 1); + 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); @@ -239,30 +340,43 @@ contract MultiTournamentTest is Util, Test { _t = block.timestamp; // the delay is increased when a match is created _rootTournamentFinish = - _t + 1 + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE); + _t + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE); uint256 _middleTournamentFinish = _rootTournamentFinish + Time.Duration.unwrap(ArbitrationConstants.MATCH_EFFORT); - Util.joinMiddleTournament(middleTournament, 0, 1); + Util.joinTournament(middleTournament, 0, 1); //let player 1 join, then timeout player 0 - Util.joinMiddleTournament(middleTournament, 1, 1); + 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("cannot win by timeout"); + middleTournament.winMatchByTimeout( + _matchId, + playerNodes[1][ArbitrationConstants.height(1) - 1], + playerNodes[1][ArbitrationConstants.height(1) - 1] + ); + vm.warp( Time.Instant.unwrap( _player0Clock.startInstant.add(_player0Clock.allowance) ) ); - _matchId = Util.matchId(1, 1); 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.warp(_middleTournamentFinish); (_finished, _winner,) = middleTournament.innerTournamentWinner(); topTournament.winInnerMatch( diff --git a/permissionless-arbitration/contracts/test/Tournament.t.sol b/permissionless-arbitration/contracts/test/Tournament.t.sol index 05d2f91a..9f1d4992 100644 --- a/permissionless-arbitration/contracts/test/Tournament.t.sol +++ b/permissionless-arbitration/contracts/test/Tournament.t.sol @@ -45,7 +45,7 @@ contract TournamentTest is Util, Test { // duplicate commitment should be reverted vm.expectRevert("clock is initialized"); - Util.joinTopTournament(topTournament, 0); + Util.joinTournament(topTournament, 0, 0); // pair commitment, expect a match vm.expectEmit(true, true, false, true, address(topTournament)); @@ -55,7 +55,7 @@ contract TournamentTest is Util, Test { playerNodes[1][ArbitrationConstants.height(0) - 1] ); // player 1 joins tournament - Util.joinTopTournament(topTournament, 1); + Util.joinTournament(topTournament, 1, 0); } function testTimeout() public { @@ -63,12 +63,12 @@ contract TournamentTest is Util, Test { uint256 _t = block.timestamp; // the delay is increased when a match is created - uint256 _tournamentFinishWithMatch = _t + 1 + uint256 _tournamentFinishWithMatch = _t + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE) + Time.Duration.unwrap(ArbitrationConstants.MATCH_EFFORT); // player 1 joins tournament - Util.joinTopTournament(topTournament, 1); + Util.joinTournament(topTournament, 1, 0); Match.Id memory _matchId = Util.matchId(1, 0); assertFalse( @@ -117,12 +117,12 @@ contract TournamentTest is Util, Test { _t = block.timestamp; // the delay is increased when a match is created - _tournamentFinishWithMatch = _t + 1 + _tournamentFinishWithMatch = _t + Time.Duration.unwrap(ArbitrationConstants.MAX_ALLOWANCE) + Time.Duration.unwrap(ArbitrationConstants.MATCH_EFFORT); // player 1 joins tournament - Util.joinTopTournament(topTournament, 1); + Util.joinTournament(topTournament, 1, 0); // 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 diff --git a/permissionless-arbitration/contracts/test/Util.sol b/permissionless-arbitration/contracts/test/Util.sol index 6b9c4617..11b9daca 100644 --- a/permissionless-arbitration/contracts/test/Util.sol +++ b/permissionless-arbitration/contracts/test/Util.sol @@ -84,7 +84,7 @@ contract Util { // advance match between player 0 and player 1 function advanceMatch01AtLevel( - TopTournament _topTournament, + Tournament _tournament, Match.Id memory _matchId, uint64 _level ) internal returns (uint256 _playerToSeal) { @@ -93,7 +93,7 @@ contract Util { if (_playerToSeal == 0) { // advance match alternately until it can be sealed // starts with player 0 - _topTournament.advanceMatch( + _tournament.advanceMatch( _matchId, playerNodes[0][_current - 1], playerNodes[0][_current - 1], @@ -102,7 +102,7 @@ contract Util { ); _playerToSeal = 1; } else { - _topTournament.advanceMatch( + _tournament.advanceMatch( _matchId, playerNodes[1][_current - 1], playerNodes[1][_current - 1], @@ -116,7 +116,7 @@ contract Util { // advance match between player 0 and player 2 function advanceMatch02AtLevel( - TopTournament _topTournament, + Tournament _tournament, Match.Id memory _matchId, uint64 _level ) internal returns (uint256 _playerToSeal) { @@ -125,7 +125,7 @@ contract Util { if (_playerToSeal == 0) { // advance match alternately until it can be sealed // starts with player 0 - _topTournament.advanceMatch( + _tournament.advanceMatch( _matchId, playerNodes[0][_current - 1], playerNodes[0][_current - 1], @@ -134,7 +134,7 @@ contract Util { ); _playerToSeal = 2; } else { - _topTournament.advanceMatch( + _tournament.advanceMatch( _matchId, playerNodes[0][_current - 1], playerNodes[2][_current - 1], @@ -154,59 +154,31 @@ contract Util { _topTournament = TopTournament(address(_factory.instantiateTop(ONE_STATE))); // player 0 joins tournament - joinTopTournament(_topTournament, 0); + joinTournament(_topTournament, 0, 0); } - // _player joins _topTournament - function joinTopTournament(TopTournament _topTournament, uint256 _player) - internal - { - if (_player == 0) { - _topTournament.joinTournament( - ONE_STATE, - generateProof(_player, ArbitrationConstants.height(0)), - playerNodes[0][ArbitrationConstants.height(0) - 1], - playerNodes[0][ArbitrationConstants.height(0) - 1] - ); - } else if (_player == 1) { - _topTournament.joinTournament( - TWO_STATE, - generateProof(_player, ArbitrationConstants.height(0)), - playerNodes[1][ArbitrationConstants.height(0) - 1], - playerNodes[1][ArbitrationConstants.height(0) - 1] - ); - } else if (_player == 2) { - _topTournament.joinTournament( - TWO_STATE, - generateProof(_player, ArbitrationConstants.height(0)), - playerNodes[0][ArbitrationConstants.height(0) - 1], - playerNodes[2][ArbitrationConstants.height(0) - 1] - ); - } - } - - // _player joins _middleTournament at _level - function joinMiddleTournament( - MiddleTournament _middleTournament, + // _player joins _tournament at _level + function joinTournament( + Tournament _tournament, uint256 _player, uint64 _level ) internal { if (_player == 0) { - _middleTournament.joinTournament( + _tournament.joinTournament( ONE_STATE, generateProof(_player, ArbitrationConstants.height(_level)), playerNodes[0][ArbitrationConstants.height(_level) - 1], playerNodes[0][ArbitrationConstants.height(_level) - 1] ); } else if (_player == 1) { - _middleTournament.joinTournament( + _tournament.joinTournament( TWO_STATE, generateProof(_player, ArbitrationConstants.height(_level)), playerNodes[1][ArbitrationConstants.height(_level) - 1], playerNodes[1][ArbitrationConstants.height(_level) - 1] ); } else if (_player == 2) { - _middleTournament.joinTournament( + _tournament.joinTournament( TWO_STATE, generateProof(_player, ArbitrationConstants.height(_level)), playerNodes[0][ArbitrationConstants.height(_level) - 1], @@ -215,8 +187,38 @@ contract Util { } } + function sealLeafMatch( + 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; + + _tournament.sealLeafMatch( + _matchId, + _left, + _right, + ONE_STATE, + generateProof(_player, ArbitrationConstants.height(0)) + ); + } + + function winLeafMatch( + 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; + + _tournament.winLeafMatch(_matchId, _left, _right, new bytes(0)); + } + function sealInnerMatchAndCreateInnerTournament( - TopTournament _topTournament, + NonLeafTournament _tournament, Match.Id memory _matchId, uint256 _player ) internal { @@ -224,12 +226,12 @@ contract Util { Tree.Node _right = playerNodes[_player][0]; // Machine.Hash state = _player == 1 ? ONE_STATE : Machine.ZERO_STATE; - _topTournament.sealInnerMatchAndCreateInnerTournament( + _tournament.sealInnerMatchAndCreateInnerTournament( _matchId, _left, _right, ONE_STATE, - Util.generateProof(_player, ArbitrationConstants.height(0)) + generateProof(_player, ArbitrationConstants.height(0)) ); }