Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cartesi-rollups/contracts/script/measure-prt-leaf-gas.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -euo pipefail

readonly EXPECTED_FOUNDRY_VERSION="1.5.1-v1.5.1"
readonly EXPECTED_DEPENDENCIES_SHA256="bf5c94f033883d49e851fe57111f5031bfbbc1969c6027aedc6ac607815d4234"
readonly EXPECTED_DEPENDENCIES_SHA256="0390394d7559329a94913a96b298a798c16fb03446600ca746760d5942ae6f4d"
readonly EXPECTED_MACHINE_HASH="9b358eac8ebd2aa2c7ab4c00d098da7fd90906dc571ec83ec16e889fd220e0fb"
readonly EXPECTED_FOUNDRY_CONFIG='{"solc":"0.8.30","via_ir":true,"optimizer":true,"optimizer_runs":200,"evm_version":"prague"}'

Expand Down
24 changes: 15 additions & 9 deletions cartesi-rollups/contracts/src/DaveConsensus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
override
returns (
bool isFinished,
bool isTournamentFailed,
bool isTournamentResultStaged,
uint256 epochNumber,
Tree.Node winnerCommitment,
Expand All @@ -154,7 +155,8 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
{
epochNumber = _epochNumber;
isTournamentResultStaged = _isTournamentResultStaged;
(isFinished, winnerCommitment, winnerPostEpochMachineStateHash) = _tournamentResult(_tournament);
(isFinished, isTournamentFailed, winnerCommitment, winnerPostEpochMachineStateHash) =
_tournamentResult(_tournament);
}

function stageTournamentResult(uint256 epochNumber, MachineValidityProof calldata proof)
Expand All @@ -168,8 +170,9 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
// Check whether the tournament result is staged
require(!_isTournamentResultStaged, TournamentResultAlreadyStaged());

// Check tournament finished
(bool isFinished,, Machine.Hash finalMachineStateHash) = _tournamentResult(_tournament);
// Check tournament finished with a winner
(bool isFinished, bool isTournamentFailed,, Machine.Hash finalMachineStateHash) = _tournamentResult(_tournament);
require(!isTournamentFailed, ITournament.TournamentFailedNoWinner());
require(isFinished, TournamentNotFinishedYet());

// Validate post-epoch machine state and prove outputs Merkle root
Expand Down Expand Up @@ -426,20 +429,23 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
}

/// @notice Read the root tournament's result through its typed standing.
/// @dev A failed root (finished without a winner) reverts, preserving the
/// staging posture: such an epoch cannot be settled from this tournament.
/// @dev Total over the three root outcomes: a failed root (finished
/// without a winner) is reported, not reverted, so read paths never
/// revert on a normal terminal state. `stageTournamentResult` preserves
/// the revert posture: such an epoch cannot be settled from this
/// tournament.
function _tournamentResult(ITournament tournament)
internal
view
returns (bool finished, Tree.Node winnerCommitment, Machine.Hash finalMachineStateHash)
returns (bool finished, bool tournamentFailed, Tree.Node winnerCommitment, Machine.Hash finalMachineStateHash)
{
ITournament.TournamentStandingView memory standing = tournament.tournamentStanding();
if (standing.standing == ITournament.TournamentStanding.ROOT_WINNER) {
return (true, standing.candidate, standing.finalState);
return (true, false, standing.candidate, standing.finalState);
} else if (standing.standing == ITournament.TournamentStanding.ROOT_FAILED) {
revert ITournament.TournamentFailedNoWinner();
return (true, true, Tree.ZERO_NODE, Machine.ZERO_STATE);
} else {
return (false, Tree.ZERO_NODE, Machine.ZERO_STATE);
return (false, false, Tree.ZERO_NODE, Machine.ZERO_STATE);
}
}

Expand Down
12 changes: 9 additions & 3 deletions cartesi-rollups/contracts/src/IDaveConsensus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -262,16 +262,22 @@ interface IDaveConsensus is

/// @notice Check whether the tournament result of the current sealed epoch can be staged.
/// @return isFinished Whether the current sealed epoch tournament is finished
/// @return isTournamentFailed Whether the tournament finished without a winner,
/// in which case the epoch cannot be settled from this tournament
/// @return isTournamentResultStaged Whether the tournament result (if there is one) is staged
/// @return epochNumber The current sealed epoch number
/// @return winnerCommitment If the tournament has finished, the winner commitment
/// @return winnerPostEpochMachineStateHash If the tournament has finished, the winner post-epoch machine state hash
/// @dev Validators should only call `stageTournamentResult` if isFinished is true and isTournamentResultStaged is false.
/// @return winnerCommitment If the tournament has finished with a winner, the winner commitment
/// @return winnerPostEpochMachineStateHash If the tournament has finished with a winner, the winner post-epoch machine state hash
/// @dev Total over every terminal state: no standing makes this view
/// revert. Validators should only call `stageTournamentResult` if
/// isFinished is true, isTournamentFailed is false, and
/// isTournamentResultStaged is false.
function canStageTournamentResult()
external
view
returns (
bool isFinished,
bool isTournamentFailed,
bool isTournamentResultStaged,
uint256 epochNumber,
Tree.Node winnerCommitment,
Expand Down
106 changes: 67 additions & 39 deletions cartesi-rollups/contracts/test/DaveAppFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,15 @@ contract DaveAppFactoryTest is ConsensusTestUtils {
{
bool val1;
bool val2;
uint256 val3;
bool val3;
uint256 val4;

(val1, val2, val3,,) = daveConsensus.canStageTournamentResult();
(val1, val2, val3, val4,,) = daveConsensus.canStageTournamentResult();

assertFalse(val1); // isFinished
assertFalse(val2); // isTournamentResultStaged
assertEq(val3, 0); // epochNumber
assertFalse(val2); // isTournamentFailed
assertFalse(val3); // isTournamentResultStaged
assertEq(val4, 0); // epochNumber
}

// Check epoch acceptance readiness
Expand Down Expand Up @@ -352,17 +354,19 @@ contract DaveAppFactoryTest is ConsensusTestUtils {
{
bool val1;
bool val2;
uint256 val3;
Tree.Node val4;
Machine.Hash val5;
bool val3;
uint256 val4;
Tree.Node val5;
Machine.Hash val6;

(val1, val2, val3, val4, val5) = daveConsensus.canStageTournamentResult();
(val1, val2, val3, val4, val5, val6) = daveConsensus.canStageTournamentResult();

assertTrue(val1); // isFinished
assertFalse(val2); // isTournamentResultStaged
assertEq(val3, 0); // epochNumber
assertEq(Tree.Node.unwrap(val4), commitment);
assertEq(Machine.Hash.unwrap(val5), machineMerkleRoot);
assertFalse(val2); // isTournamentFailed
assertFalse(val3); // isTournamentResultStaged
assertEq(val4, 0); // epochNumber
assertEq(Tree.Node.unwrap(val5), commitment);
assertEq(Machine.Hash.unwrap(val6), machineMerkleRoot);
}

// Check epoch acceptance readiness
Expand Down Expand Up @@ -477,17 +481,19 @@ contract DaveAppFactoryTest is ConsensusTestUtils {
{
bool val1;
bool val2;
uint256 val3;
Tree.Node val4;
Machine.Hash val5;
bool val3;
uint256 val4;
Tree.Node val5;
Machine.Hash val6;

(val1, val2, val3, val4, val5) = daveConsensus.canStageTournamentResult();
(val1, val2, val3, val4, val5, val6) = daveConsensus.canStageTournamentResult();

assertTrue(val1); // isFinished
assertTrue(val2); // isTournamentResultStaged
assertEq(val3, 0); // epochNumber
assertEq(Tree.Node.unwrap(val4), commitment);
assertEq(Machine.Hash.unwrap(val5), machineMerkleRoot);
assertFalse(val2); // isTournamentFailed
assertTrue(val3); // isTournamentResultStaged
assertEq(val4, 0); // epochNumber
assertEq(Tree.Node.unwrap(val5), commitment);
assertEq(Machine.Hash.unwrap(val6), machineMerkleRoot);
}

// Check epoch acceptance readiness
Expand Down Expand Up @@ -567,17 +573,19 @@ contract DaveAppFactoryTest is ConsensusTestUtils {
{
bool val1;
bool val2;
uint256 val3;
Tree.Node val4;
Machine.Hash val5;
bool val3;
uint256 val4;
Tree.Node val5;
Machine.Hash val6;

(val1, val2, val3, val4, val5) = daveConsensus.canStageTournamentResult();
(val1, val2, val3, val4, val5, val6) = daveConsensus.canStageTournamentResult();

assertTrue(val1); // isFinished
assertTrue(val2); // isTournamentResultStaged
assertEq(val3, 0); // epochNumber
assertEq(Tree.Node.unwrap(val4), commitment);
assertEq(Machine.Hash.unwrap(val5), machineMerkleRoot);
assertFalse(val2); // isTournamentFailed
assertTrue(val3); // isTournamentResultStaged
assertEq(val4, 0); // epochNumber
assertEq(Tree.Node.unwrap(val5), commitment);
assertEq(Machine.Hash.unwrap(val6), machineMerkleRoot);
}

// Check epoch acceptance readiness
Expand Down Expand Up @@ -642,13 +650,15 @@ contract DaveAppFactoryTest is ConsensusTestUtils {
{
bool val1;
bool val2;
uint256 val3;
bool val3;
uint256 val4;

(val1, val2, val3,,) = daveConsensus.canStageTournamentResult();
(val1, val2, val3, val4,,) = daveConsensus.canStageTournamentResult();

assertFalse(val1); // isFinished
assertFalse(val2); // isTournamentResultStaged
assertEq(val3, 1); // epochNumber
assertFalse(val2); // isTournamentFailed
assertFalse(val3); // isTournamentResultStaged
assertEq(val4, 1); // epochNumber
}

// Check epoch acceptance readiness
Expand Down Expand Up @@ -951,12 +961,28 @@ contract DaveAppFactoryTest is ConsensusTestUtils {
hasCandidate: false,
candidate: Tree.ZERO_NODE,
finalState: Machine.ZERO_STATE,
parentCommitment: Tree.ZERO_NODE
parentCommitment: Tree.ZERO_NODE,
finishedAt: Time.currentTime(),
winnerExpiresAt: Time.ZERO_INSTANT
});
vm.mockCall(address(tournament), abi.encodeCall(ITournament.tournamentStanding, ()), abi.encode(failedStanding));

vm.expectRevert(ITournament.TournamentFailedNoWinner.selector);
daveConsensus.canStageTournamentResult();
{
(
bool isFinished,
bool isTournamentFailed,
bool isTournamentResultStaged,
uint256 epochNumber,
Tree.Node winnerCommitment,
Machine.Hash winnerPostEpochMachineStateHash
) = daveConsensus.canStageTournamentResult();
assertTrue(isFinished);
assertTrue(isTournamentFailed);
assertFalse(isTournamentResultStaged);
assertEq(epochNumber, 0);
assertEq(Tree.Node.unwrap(winnerCommitment), bytes32(0));
assertEq(Machine.Hash.unwrap(winnerPostEpochMachineStateHash), bytes32(0));
}

// The tournament standing is checked before the machine validity proof,
// so any proof can be provided as to reach the expected revert.
Expand Down Expand Up @@ -1341,13 +1367,15 @@ contract DaveAppFactoryTest is ConsensusTestUtils {
{
bool val1;
bool val2;
uint256 val3;
bool val3;
uint256 val4;

(val1, val2, val3,,) = daveConsensus.canStageTournamentResult();
(val1, val2, val3, val4,,) = daveConsensus.canStageTournamentResult();

assertFalse(val1); // isFinished
assertFalse(val2); // isTournamentResultStaged
assertEq(val3, 0); // epochNumber
assertFalse(val2); // isTournamentFailed
assertFalse(val3); // isTournamentResultStaged
assertEq(val4, 0); // epochNumber
}

assertEq(address(daveConsensus.getInputBox()), address(_contracts.core.inputBox));
Expand Down
8 changes: 6 additions & 2 deletions cartesi-rollups/contracts/test/gas/PrtLeafProofGasFfi.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ contract RevertLeafWinTwoFfiTest is LeafTournamentGasFixture {

abstract contract InputLeafWinFfiTest is LeafTournamentGasFixture {
uint256 internal constant SECOND_INPUT_COUNTER = 1 << 68;
uint256 internal constant WIN_LEAF_MATCH_RETAINED_HEADROOM = 1_000;
// The selection adopts the maximum rounded recommendation exactly: the
// two-winning orientation. The one-winning orientation rounds 1,000
// units lower, so the alternate records that slack explicitly.
uint256 internal constant WIN_LEAF_MATCH_RETAINED_HEADROOM = 0;
uint256 internal constant WIN_LEAF_MATCH_ALTERNATE_HEADROOM = 1_000;

function _payloads(uint256 targetPayloadSize) internal pure returns (uint256[] memory sizes) {
sizes = new uint256[](2);
Expand Down Expand Up @@ -118,7 +122,7 @@ contract MaximumInputLeafWinOneFfiTest is InputLeafWinFfiTest {
function testMeasureMaximumInputWithOneWinning() public {
Measurement memory result = _measureLeafWin("maximum input one wins");
assertEq(
_roundUpToThousand(_minimumReviewedAllocation(result)) + WIN_LEAF_MATCH_RETAINED_HEADROOM,
_roundUpToThousand(_minimumReviewedAllocation(result)) + WIN_LEAF_MATCH_ALTERNATE_HEADROOM,
Gas.WIN_LEAF_MATCH
);
}
Expand Down
13 changes: 13 additions & 0 deletions cartesi-rollups/node/src/epoch_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,19 @@ impl<AS: ArenaSender> EpochManager<AS> {
.call()
.await?;

// A failed root is a documented terminal state, not a local
// contradiction: the ticked Hero path already logs and idles on
// FailedNoWinner, and this path also runs with no Hero (Absent),
// where crashing would loop on restart. stageTournamentResult's
// TournamentFailedNoWinner revert remains the write-side guard.
if can_stage.isTournamentFailed {
log::error!(
"dispute tournament for epoch {} finished without a winner; settlement is impossible, notify all users!",
can_stage.epochNumber
);
return Ok(None);
}

if !can_stage.isFinished || can_stage.isTournamentResultStaged {
trace!("tournament result not ready to be staged");
return Ok(None);
Expand Down
Loading
Loading