Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 26 additions & 7 deletions .github/actions/setup-tools/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,32 @@ runs:
cache-bin: false

- name: Install just
uses: extractions/setup-just@53165ef7e734c5c07cb06b3c8e7b647c5aa16db3 # v4.0.0
with:
# Pinned like the other toolchain components: a floating "latest"
# upgrades CI's toolchain silently mid-week (1.56 -> 1.57
# landed between two runs of this workflow and muddied a
# two-day CI hunt). Bump deliberately.
just-version: "1.57.0"
shell: bash
run: |
# Pin deliberately: floating latest silently changes CI's toolchain.
# Fetch the pinned asset directly: release-list lookup has reported
# this published version missing during CI setup.
version=1.57.0
case "$(uname -m)" in
x86_64)
target=x86_64-unknown-linux-musl
checksum=45b548094283cb9739af8f13273b8cddeee869f5b4ef2bb631b1f311cb566155
;;
aarch64)
target=aarch64-unknown-linux-musl
checksum=f225044a81adea6e0b3a8b9370aaf374e6af76c8735ae263ac993df55fd137ec
;;
*) echo "Unsupported just host architecture: $(uname -m)" >&2; exit 1 ;;
esac
install_dir=$(mktemp -d "${RUNNER_TEMP}/just.XXXXXX")
archive="${install_dir}/just.tar.gz"
curl --fail --location --retry 3 \
"https://github.com/casey/just/releases/download/${version}/just-${version}-${target}.tar.gz" \
--output "$archive"
printf '%s %s\n' "$checksum" "$archive" | sha256sum --check
tar -xzf "$archive" -C "$install_dir" just
"$install_dir/just" --version
echo "$install_dir" >> "$GITHUB_PATH"

- name: Set Foundry version
id: set-foundry-version
Expand Down
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
# Changelog

## Unreleased

A new deployment generation: the consensus and application-factory bytecode
changes, moving deterministic deployment addresses. This node requires a
DaveConsensus that caps each epoch at the Machine's input span; earlier
uncapped deployments are unsupported: the node stalls at the first seal
past the span.

### Contracts

- DaveConsensus seals at most 2^24 pending inputs, retaining excess inputs
for later epochs. Only processed, accepted inputs become finalized.
- DaveAppFactory binds application addresses to the full consensus settings,
so other settings cannot occupy a predicted application address. Anyone can
still occupy one through the shared ApplicationFactory, which makes
`newDaveApp` revert: do not send value to a predicted address before
`DaveAppCreated`.

### Node

- Once the dispute has a result, skip sentry claims, staging and acceptance
while foreclosure is visible. Dispute moves and bond recovery continue;
exit 30 only after recovery completes and foreclosure is finalized.
A restart resumes the same work.
- Page when an input makes the application terminal, after its batch commits,
while continuing to defend the true state.
- Split full pending epochs at the input span, check every later seal against
those boundaries, and stall at the first seal past the span.
- Check a candidate against the already-read standing before waiting under
active matches; a contradiction discards and refolds the event cache.

### For developers

- Rebuild the devnet bundle with `just rollups-contracts::build-devnet` after
the contract changes.
- Fetch pinned just assets directly and verify their checksums instead of
relying on release-list discovery.

## v3.0.0-alpha.6

A new deployment generation (cartesi/dave#287). Nodes from earlier releases
Expand Down
38 changes: 25 additions & 13 deletions cartesi-rollups/contracts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ truth.
consensus, migrates the app's validator to it, renounces ownership.
All steps run inside one `newDaveApp` call, so there is no
partial-completion window; `calculateDaveAppAddress` predicts both
addresses for pre-funding and configuration.
addresses for configuration (see the squatting non-claim before funding).
- `src/ISentryErrors.sol` - sentry error selectors.

## Invariants to protect
Expand All @@ -76,7 +76,8 @@ truth.
staged value is echoed back to or from a claim.
- One live tournament at a time - the current sealed epoch's. Acceptance
settles, samples the next epoch's input bounds from the InputBox at
that moment, records the outputs root in a write-once mapping, and
that moment, capped at the Machine's 2^24-input span with excess left
pending, records the outputs root in a write-once mapping, and
instantiates the next tournament from the settled state.
- A sentry slot claims at most once per epoch (bitmap by slot ID).
Rotation moves a slot to a new address but preserves an already-placed
Expand All @@ -89,9 +90,15 @@ truth.
padding rule the off-chain commitment builders mirror
(docs/computation-hash.md). Changing it desynchronizes every
commitment producer.
- The application address binds the template, withdrawal configuration and
every consensus setting, so `newDaveApp` from any caller can land a
predicted address only with the pair that address was predicted for.

## Change guardrails

- `MAX_INPUTS_PER_EPOCH` must equal the node's `Structure::max_inputs`.
If it moves, update the reader's eager split and docs/epoch-lifecycle.md
with it, and validate the cross-implementation boundary.
- Preserve the deployed ABI, events, error selectors, and the ERC-165
surface (`IDataProvider`, `IOutputsMerkleRootValidator`) unless the
task explicitly authorizes a compatibility break.
Expand Down Expand Up @@ -119,17 +126,17 @@ just test-prt-gas # includes the leaf-proof subsidy through this provid
just bind # after any interface change
```

Honest coverage statement: `DaveConsensus` has no dedicated Foundry unit
suite today. Its behavior is exercised through `test/DaveAppFactory.t.sol`
(construction, stage and accept, `wasInputFinalized`, staging reverts on bad
proofs, root failure, sentry rotation, a winner exhausting the payment
callback), the leaf-proof gas FFI fixture (`test/gas/`), the node's
integration tests, and the e2e smoke. `test/NodeProofs.t.sol` and
`test/NodeWitnesses.t.sol` check that the contracts' verifiers and the step
accept the node's proofs and witnesses, without running `DaveConsensus`. Do
not mistake any of these for a unit suite when changing staging,
sentry, or settlement logic; adding focused tests with such a change is
expected.
Honest coverage statement: `test/DaveConsensusCapacity.t.sol` uses a mocked
fixture to cover capped epoch storage and events, backlog rollover and boundary
counts. `test/DaveAppFactory.t.sol` exercises construction, stage and accept,
`wasInputFinalized`, staging reverts on bad proofs, root failure, sentry
rotation and a winner exhausting the payment callback. The factory tests pin
the consensus-settings address binding. The leaf-proof gas FFI fixture
(`test/gas/`), node integration tests and e2e smoke add coverage.
`test/NodeProofs.t.sol` and `test/NodeWitnesses.t.sol` check that the contracts'
verifiers and the step accept the node's proofs and witnesses, without running
`DaveConsensus`. When changing staging, sentry or settlement logic, add focused
tests rather than assuming the capacity fixture covers it.

## Explicit non-claims

Expand All @@ -140,3 +147,8 @@ expected.
delay. The staging period is a reaction window, not a fraud proof.
- Epochs have no inherent duration: sealing bounds are sampled at
acceptance time, so epoch length is a function of settlement cadence.
- The shared ApplicationFactory's salt does not bind its caller, so anyone
can occupy a predicted application address with an application that has
no validator. `newDaveApp` then reverts for those settings and salt, and
value sent there first is stranded. Do not send value to a predicted
address before `DaveAppCreated` (docs/todo.md).
18 changes: 16 additions & 2 deletions cartesi-rollups/contracts/src/DaveAppFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ contract DaveAppFactory is IDaveAppFactory {
WithdrawalConfig calldata withdrawalConfig,
bytes32 salt
) external override returns (IApplication appContract, IDaveConsensus daveConsensus) {
appContract = _newApplication(templateHash, withdrawalConfig, salt);
appContract = _newApplication(
templateHash, withdrawalConfig, _applicationSalt(claimStagingPeriod, sentryManager, sentries, salt)
);
daveConsensus =
_newDaveConsensus(address(appContract), templateHash, claimStagingPeriod, sentryManager, sentries, salt);
appContract.migrateToOutputsMerkleRootValidator(daveConsensus);
Expand All @@ -55,7 +57,9 @@ contract DaveAppFactory is IDaveAppFactory {
WithdrawalConfig calldata withdrawalConfig,
bytes32 salt
) external view override returns (address appContractAddress, address daveConsensusAddress) {
appContractAddress = _calculateApplicationAddress(templateHash, withdrawalConfig, salt);
appContractAddress = _calculateApplicationAddress(
templateHash, withdrawalConfig, _applicationSalt(claimStagingPeriod, sentryManager, sentries, salt)
);
daveConsensusAddress = _calculateDaveConsensusAddress(
appContractAddress, templateHash, claimStagingPeriod, sentryManager, sentries, salt
);
Expand Down Expand Up @@ -103,6 +107,16 @@ contract DaveAppFactory is IDaveAppFactory {
);
}

function _applicationSalt(
uint256 claimStagingPeriod,
address sentryManager,
address[] calldata sentries,
bytes32 salt
) internal pure returns (bytes32) {
// Different consensus settings must never occupy the same application address.
return keccak256(abi.encode(claimStagingPeriod, sentryManager, sentries, salt));
}

/// @notice Calculates the address of a `DaveConsensus` contract.
function _calculateDaveConsensusAddress(
address appContract,
Expand Down
17 changes: 14 additions & 3 deletions cartesi-rollups/contracts/src/DaveConsensus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {ITournamentFactory} from "prt-contracts/ITournamentFactory.sol";
import {Machine} from "prt-contracts/types/Machine.sol";
import {Tree} from "prt-contracts/types/Tree.sol";

import {EmulatorConstants} from "step/src/EmulatorConstants.sol";
import {Memory} from "step/src/Memory.sol";

import {IDaveConsensus} from "./IDaveConsensus.sol";
Expand All @@ -33,6 +34,10 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
using LibBinaryMerkleTree for bytes;
using LibMachineValidityProof for MachineValidityProof;

/// @notice The state transition and node coordinates cannot represent
/// input counters beyond this span; excess inputs stay pending.
uint256 constant MAX_INPUTS_PER_EPOCH = 1 << EmulatorConstants.ROLLUP_LOG2_MAX_ADVANCE_STATES_PER_EPOCH;

/// @notice The input box contract
IInputBox immutable _INPUT_BOX;

Expand Down Expand Up @@ -133,7 +138,7 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
emit ConsensusCreation(inputBox, appContract, tournamentFactory);

// Initialize first sealed epoch
uint256 inputIndexUpperBound = inputBox.getNumberOfInputs(appContract);
uint256 inputIndexUpperBound = _nextInputIndexUpperBound(0);
_inputIndexUpperBound = inputIndexUpperBound;
ITournament tournament = tournamentFactory.instantiate(initialMachineStateHash, this);
_tournament = tournament;
Expand Down Expand Up @@ -259,10 +264,11 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
Machine.Hash finalMachineStateHash = _stagedPostEpochMachineStateHash;
bytes32 outputsMerkleRoot = _stagedPostEpochOutputsMerkleRoot;

// Seal current accumulating epoch, save settled output tree and machine state hash
// Settle the staged result, then seal at most one input span;
// the backlog waits for later epochs.
_epochNumber++;
_inputIndexLowerBound = _inputIndexUpperBound;
_inputIndexUpperBound = _INPUT_BOX.getNumberOfInputs(_APP_CONTRACT);
_inputIndexUpperBound = _nextInputIndexUpperBound(_inputIndexLowerBound);
_outputsMerkleRoots[outputsMerkleRoot] = true;
_lastFinalizedMachineStateHash = finalMachineStateHash;
_isTournamentResultStaged = false;
Expand Down Expand Up @@ -461,6 +467,11 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker {
return proof.validate(Machine.Hash.unwrap(finalMachineStateHash));
}

function _nextInputIndexUpperBound(uint256 lowerBound) internal view returns (uint256) {
uint256 pendingInputs = _INPUT_BOX.getNumberOfInputs(_APP_CONTRACT) - lowerBound;
return lowerBound + pendingInputs.min(MAX_INPUTS_PER_EPOCH);
}

function _doAllSentriesAgreeWithStagedTournamentResult() internal view returns (bool) {
return _NUM_OF_SENTRIES > 0 && _claimCount[_epochNumber][_stagedPostEpochMachineStateHash] == _NUM_OF_SENTRIES;
}
Expand Down
3 changes: 3 additions & 0 deletions cartesi-rollups/contracts/src/IDaveAppFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ interface IDaveAppFactory is IApplicationFactoryErrors, ISentryErrors {
/// @param salt A 32-byte value used to add entropy to the addresses
/// @return appContractAddress The application contract address
/// @return daveConsensusAddress The Dave consensus contract address
/// @dev Do not send value to a predicted address before `DaveAppCreated`:
/// anyone can occupy the application address through the shared
/// application factory, with no validator, making `newDaveApp` revert.
function calculateDaveAppAddress(
bytes32 templateHash,
uint256 claimStagingPeriod,
Expand Down
9 changes: 5 additions & 4 deletions cartesi-rollups/contracts/src/IDaveConsensus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {ISentryErrors} from "./ISentryErrors.sol";
/// @notice Consensus contract with Dave tournaments.
///
/// @notice This contract validates only one application,
/// which read inputs from the InputBox contract.
/// which reads inputs from the InputBox contract.
///
/// @notice This contract also manages epoch boundaries, which
/// are defined in terms of input indices. We represent them
Expand All @@ -41,9 +41,10 @@ import {ISentryErrors} from "./ISentryErrors.sol";
///
/// @notice At any given time, there is always one sealed epoch.
/// Prior to it, every epoch has been settled.
/// After it, the next epoch is accumulating inputs. Once this epoch is settled,
/// the accumulating epoch will be sealed, and a new
/// accumulating epoch will be created.
/// After it, inputs accumulate in the InputBox. Once the sealed epoch settles,
/// the next epoch seals at most 2^24 pending inputs. Any excess remains pending
/// for later epochs; the seal's index bounds define its inputs, not block ranges.
/// The first epoch is sealed the same way at construction.
/// Every sealed epoch has an associated tournament.
/// Once a tournament is finished, and a winner commitment is declared,
/// it can then be staged by anyone. After the claim staging period is elapsed,
Expand Down
76 changes: 76 additions & 0 deletions cartesi-rollups/contracts/test/DaveAppFactorySquatting.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

pragma solidity ^0.8.30;

import {WithdrawalConfig} from "cartesi-rollups-contracts-3.0.0/src/common/WithdrawalConfig.sol";
import {IApplication} from "cartesi-rollups-contracts-3.0.0/src/dapp/IApplication.sol";
import {RollupsTest} from "cartesi-rollups-contracts-3.0.0/test/util/RollupsTest.sol";
import {ITournament} from "prt-contracts/ITournament.sol";
import {ITournamentFactory} from "prt-contracts/ITournamentFactory.sol";
import {DaveAppFactory} from "src/DaveAppFactory.sol";
import {IDaveConsensus} from "src/IDaveConsensus.sol";

contract DaveAppFactorySquattingTest is RollupsTest {
DaveAppFactory factory;
ITournamentFactory constant TOURNAMENT_FACTORY = ITournamentFactory(address(0x100));
bytes32 constant TEMPLATE = bytes32(uint256(1));

function setUp() external {
vm.mockCall(
address(TOURNAMENT_FACTORY),
abi.encodeWithSelector(ITournamentFactory.instantiate.selector),
abi.encode(ITournament(address(0x200)))
);
factory = new DaveAppFactory(_contracts.core.inputBox, _contracts.core.applicationFactory, TOURNAMENT_FACTORY);
}

function testAnotherCallerCanOnlyDeployTheExactPredictedDavePair() external {
WithdrawalConfig memory config;
address[] memory sentries = new address[](0);
(address predicted, address expectedConsensus) =
factory.calculateDaveAppAddress(TEMPLATE, 10, address(0), sentries, config, 0);
vm.deal(predicted, 1 ether);
vm.prank(address(0xbad));
(IApplication app, IDaveConsensus consensus) = factory.newDaveApp(TEMPLATE, 10, address(0), sentries, config, 0);
assertEq(address(app), predicted);
assertEq(address(consensus), expectedConsensus);
assertEq(address(app.getOutputsMerkleRootValidator()), expectedConsensus);
assertEq(app.owner(), address(0));
assertEq(consensus.getClaimStagingPeriod(), 10);
assertEq(predicted.balance, 1 ether);
}

function testDifferentConsensusSettingsCannotSquatTheSameApplication() external {
WithdrawalConfig memory config;
address[] memory sentries = new address[](0);
(address victim, address expectedConsensus) =
factory.calculateDaveAppAddress(TEMPLATE, 10, address(0), sentries, config, 0);
(address attacker,) = factory.calculateDaveAppAddress(TEMPLATE, 0, address(0), sentries, config, 0);
assertNotEq(victim, attacker);
vm.prank(address(0xbad));
factory.newDaveApp(TEMPLATE, 0, address(0), sentries, config, 0);
(IApplication app, IDaveConsensus consensus) = factory.newDaveApp(TEMPLATE, 10, address(0), sentries, config, 0);
assertEq(address(app), victim);
assertEq(address(consensus), expectedConsensus);
}

function testFuzzAllConsensusSettingsAreBoundToTheApplicationAddress(uint64 period, address manager, bytes32 salt)
external
view
{
WithdrawalConfig memory config;
address[] memory sentries = new address[](0);
(address original,) = factory.calculateDaveAppAddress(TEMPLATE, period, manager, sentries, config, salt);
(address otherPeriod,) =
factory.calculateDaveAppAddress(TEMPLATE, uint256(period) + 1, manager, sentries, config, salt);
assertNotEq(original, otherPeriod);
(address otherManager,) =
factory.calculateDaveAppAddress(TEMPLATE, period, address(uint160(manager) ^ 1), sentries, config, salt);
assertNotEq(original, otherManager);
sentries = new address[](1);
sentries[0] = address(1);
(address otherSentries,) = factory.calculateDaveAppAddress(TEMPLATE, period, manager, sentries, config, salt);
assertNotEq(original, otherSentries);
}
}
Loading
Loading