From 18f6a9c160cee63242296e0cc0226e15932919fa Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Mon, 15 Jun 2026 12:40:11 +0100 Subject: [PATCH] [MEL] Add additional, AI-generated, coverage tests --- package.json | 2 + test/ai/foundry/Deserialize.t.sol | 125 +++++ test/ai/foundry/OneStepProofEntry.t.sol | 205 +++++++ .../OneStepProverHostIoErrorPaths.t.sol | 96 ++++ test/ai/foundry/RollupCoverage.t.sol | 498 ++++++++++++++++++ test/ai/hardhat/globalState.spec.ts | 62 +++ 6 files changed, 988 insertions(+) create mode 100644 test/ai/foundry/Deserialize.t.sol create mode 100644 test/ai/foundry/OneStepProofEntry.t.sol create mode 100644 test/ai/foundry/OneStepProverHostIoErrorPaths.t.sol create mode 100644 test/ai/foundry/RollupCoverage.t.sol create mode 100644 test/ai/hardhat/globalState.spec.ts diff --git a/package.json b/package.json index e9d06c4d..505a01e6 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,8 @@ "test:foundry": "FOUNDRY_PROFILE=test forge test --gas-limit 10000000000", "test:update": "yarn run test:signatures || yarn run test:storage", "test:bytecodes": "hardhat clean && forge clean && hardhat run scripts/compareBytecodes.ts", + "test:ai:foundry": "FOUNDRY_PROFILE=test FOUNDRY_TEST=test/ai/foundry forge test --gas-limit 10000000000", + "test:ai:hardhat": "DISABLE_GAS_REPORTER=true hardhat --network hardhat test test/ai/hardhat/*.ts", "metadatahash": "yarn build:all && hardhat run scripts/printMetadataHashes.ts", "upload-4bytes": "forge build && find ./out -type f -name \"*.json\" -exec cast upload-signature {} + | grep -v Duplicated:", "postinstall": "patch-package", diff --git a/test/ai/foundry/Deserialize.t.sol b/test/ai/foundry/Deserialize.t.sol new file mode 100644 index 00000000..4234abcd --- /dev/null +++ b/test/ai/foundry/Deserialize.t.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../../../src/state/Deserialize.sol"; +import "../../../src/state/MELState.sol"; + +// Exposes the internal Deserialize.melState for testing. The harness mirrors +// OneStepProofEntry.proveOneStep's usage (decode into a memory local, then hash) and keeps +// only the hash + offset in storage. Reading individual struct fields into extra locals, or +// putting the 14-field struct in storage / an external return, overflows the stack under +// the optimizer-off test profile. The hash equality is sufficient: abi.encode is +// order-sensitive, so any field/width/ordering decode error changes the hash or the offset. +contract DeserializeHarness { + using MELStateLib for MELState; + + bytes32 public decodedHash; + uint256 public lastOffset; + + function decode(bytes calldata proof, uint256 offset) external { + MELState memory s; + uint256 newOffset; + (s, newOffset) = Deserialize.melState(proof, offset); + lastOffset = newOffset; + decodedHash = s.hash(); + } +} + +// Deserialize is referenced by no existing test; this locks in the 14-field decode and the exact +// byte width (274 bytes). +contract DeserializeTest is Test { + using MELStateLib for MELState; + + DeserializeHarness harness; + + function setUp() public { + harness = new DeserializeHarness(); + } + + // Serialize a MELState exactly as Deserialize.melState expects to read it. Split into + // small abi.encodePacked groups joined with bytes.concat — a single 14-arg packed encode + // overflows the stack under the optimizer-off test profile. + function _serializeMELState( + MELState memory s + ) internal pure returns (bytes memory) { + bytes memory a = abi.encodePacked( + s.version, // u16 + s.parentChainId, // u64 + s.parentChainBlockNumber, // u64 + uint256(uint160(s.batchPostingTargetAddress)), // addr read as u256 + uint256(uint160(s.delayedMessagePostingTargetAddress)) // addr read as u256 + ); + bytes memory b = abi.encodePacked( + s.parentChainBlockHash, // b32 + s.parentChainPreviousBlockHash, // b32 + s.batchCount, // u64 + s.msgCount, // u64 + s.localMsgAccumulator // b32 + ); + bytes memory c = abi.encodePacked( + s.delayedMessagesRead, // u64 + s.delayedMessagesSeen, // u64 + s.delayedMessageInboxAcc, // b32 + s.delayedMessageOutboxAcc // b32 + ); + return bytes.concat(a, b, c); + } + + function _sampleMELState() internal pure returns (MELState memory s) { + s.version = 7; + s.parentChainId = 42161; + s.parentChainBlockNumber = 123456789; + s.batchPostingTargetAddress = address(0xBEEF); + s.delayedMessagePostingTargetAddress = address(0xCAFE); + s.parentChainBlockHash = keccak256("parentChainBlockHash"); + s.parentChainPreviousBlockHash = keccak256("parentChainPreviousBlockHash"); + s.batchCount = 9; + s.msgCount = 100; + s.localMsgAccumulator = keccak256("localMsgAccumulator"); + s.delayedMessagesRead = 5; + s.delayedMessagesSeen = 6; + s.delayedMessageInboxAcc = keccak256("delayedMessageInboxAcc"); + s.delayedMessageOutboxAcc = keccak256("delayedMessageOutboxAcc"); + } + + // A fully-populated MELState round-trips through serialization and Deserialize.melState, + // and the offset advances by the exact field-width sum (274 bytes). + function testDeserializeMELStateRoundTrip() public { + MELState memory want = _sampleMELState(); + bytes memory proof = _serializeMELState(want); + // 2 + 8 + 8 + 32 + 32 + 32 + 32 + 8 + 8 + 32 + 8 + 8 + 32 + 32 = 274 + assertEq(proof.length, 274, "unexpected serialized length"); + + harness.decode(proof, 0); + + // hash equality proves every field decoded correctly and in order (abi.encode is + // order-sensitive); the offset assertion pins the total decoded width. + assertEq(harness.decodedHash(), want.hash(), "hash mismatch after round-trip"); + assertEq(harness.lastOffset(), 274, "offset must advance by the full struct width"); + } + + // A single wrong field must change the decoded hash (guards against a no-op round-trip). + function testDeserializeMELStateDetectsFieldChange() public { + MELState memory want = _sampleMELState(); + harness.decode(_serializeMELState(want), 0); + bytes32 baseline = harness.decodedHash(); + + MELState memory mutated = want; + mutated.msgCount = want.msgCount + 1; + harness.decode(_serializeMELState(mutated), 0); + assertTrue(harness.decodedHash() != baseline, "hash should change when a field changes"); + } + + // Decoding works at a non-zero start offset (leading bytes are skipped). + function testDeserializeMELStateAtOffset() public { + MELState memory want = _sampleMELState(); + bytes memory prefix = hex"aabbccdd"; + bytes memory proof = abi.encodePacked(prefix, _serializeMELState(want)); + + harness.decode(proof, prefix.length); + + assertEq(harness.decodedHash(), want.hash(), "hash mismatch after offset round-trip"); + assertEq(harness.lastOffset(), prefix.length + 274, "offset must advance from start"); + } +} diff --git a/test/ai/foundry/OneStepProofEntry.t.sol b/test/ai/foundry/OneStepProofEntry.t.sol new file mode 100644 index 00000000..0f6f9464 --- /dev/null +++ b/test/ai/foundry/OneStepProofEntry.t.sol @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../../../src/osp/OneStepProofEntry.sol"; +import "../../../src/osp/OneStepProver0.sol"; +import "../../../src/osp/OneStepProverMemory.sol"; +import "../../../src/osp/OneStepProverMath.sol"; +import "../../../src/osp/OneStepProverHostIo.sol"; + +// Covers OneStepProofEntry.proveOneStep's halted-machine path, which is exercised by no +// existing test. +// +// For a halted machine MachineLib.hash is just keccak256("Machine finished:", globalStateHash), +// so we can hand-serialize a minimal all-empty machine whose hash depends only on the +// globalStateHash, then append a serialized GlobalState + MELState as proveOneStep expects: +// proof = [serialized machine][serialized GlobalState][serialized MELState] +contract OneStepProofEntryHaltedMachineTest is Test { + using GlobalStateLib for GlobalState; + using MELStateLib for MELState; + + OneStepProofEntry osp; + bytes32 constant WASM_ROOT = keccak256("INITIAL_WASM_MODULE_ROOT"); + + function setUp() public { + osp = new OneStepProofEntry( + new OneStepProver0(), + new OneStepProverMemory(), + new OneStepProverMath(), + new OneStepProverHostIo(address(0), address(0)) + ); + } + + // Serialize a minimal FINISHED machine with empty stacks, matching Deserialize.machine. + // Only the globalStateHash field matters for a halted machine's hash. Packed encodes are + // chunked to small arg counts (optimizer-off stack limit). + function _serializeMachine( + bytes32 gsHash + ) internal pure returns (bytes memory) { + bytes memory p1 = abi.encodePacked(uint8(1), bytes32(0), uint256(0)); // status FINISHED + valueStack + bytes memory p2 = abi.encodePacked(bytes32(0), bytes32(0)); // valueMultiStack + bytes memory p3 = abi.encodePacked(bytes32(0), uint256(0)); // internalStack + bytes memory p4 = abi.encodePacked(bytes32(0), uint8(0)); // frameStack window (empty) + bytes memory p5 = abi.encodePacked(bytes32(0), bytes32(0)); // frameMultiStack + bytes memory p6 = abi.encodePacked(gsHash, uint32(0), uint32(0), uint32(0)); // gsHash, idxs, pc + bytes memory p7 = abi.encodePacked(bytes32(0), bytes32(0)); // recoveryPc, modulesRoot + return bytes.concat(p1, p2, p3, p4, p5, p6, p7); + } + + function _serializeGlobalState( + GlobalState memory g + ) internal pure returns (bytes memory) { + bytes memory a = + abi.encodePacked(g.bytes32Vals[0], g.bytes32Vals[1], g.bytes32Vals[2], g.bytes32Vals[3]); + bytes memory b = abi.encodePacked(g.u64Vals[0], g.u64Vals[1]); + return bytes.concat(a, b); + } + + function _serializeMELState( + MELState memory m + ) internal pure returns (bytes memory) { + bytes memory a = abi.encodePacked( + m.version, + m.parentChainId, + m.parentChainBlockNumber, + uint256(uint160(m.batchPostingTargetAddress)), + uint256(uint160(m.delayedMessagePostingTargetAddress)) + ); + bytes memory b = abi.encodePacked( + m.parentChainBlockHash, + m.parentChainPreviousBlockHash, + m.batchCount, + m.msgCount, + m.localMsgAccumulator + ); + bytes memory c = abi.encodePacked( + m.delayedMessagesRead, + m.delayedMessagesSeen, + m.delayedMessageInboxAcc, + m.delayedMessageOutboxAcc + ); + return bytes.concat(a, b, c); + } + + // machineGsHashField is the globalStateHash written into the serialized machine (which + // determines beforeHash); g is the GlobalState appended after the machine. + function _buildProof( + bytes32 machineGsHashField, + GlobalState memory g, + MELState memory m + ) internal pure returns (bytes memory) { + return bytes.concat( + _serializeMachine(machineGsHashField), _serializeGlobalState(g), _serializeMELState(m) + ); + } + + function _finishedHash( + bytes32 gsHash + ) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("Machine finished:", gsHash)); + } + + // Build a MELState whose hash we commit into the GlobalState's MEL slot. + function _melState( + bytes32 parentChainBlockHash, + uint64 msgCount + ) internal pure returns (MELState memory m) { + m.version = 0; + m.parentChainBlockHash = parentChainBlockHash; + m.msgCount = msgCount; + } + + function _execCtx( + bytes32 target + ) internal pure returns (ExecutionContext memory ctx) { + ctx.targetParentChainBlockHash = target; + ctx.initialWasmModuleRoot = WASM_ROOT; + } + + // Halted machine whose committed MEL slot doesn't match the supplied MELState. + function testRevertProveOneStepBadMELState() public { + MELState memory m = _melState(keccak256("PCBH"), 1); + GlobalState memory g; + g.bytes32Vals[2] = keccak256("WRONG_MEL_HASH"); // != m.hash() + bytes32 gsHash = g.hash(); + bytes memory proof = _buildProof(gsHash, g, m); + + vm.expectRevert("BAD_MEL_STATE"); + osp.proveOneStep(_execCtx(keccak256("PCBH")), 0, _finishedHash(gsHash), proof); + } + + // Halted machine whose supplied GlobalState doesn't hash to the machine's committed + // globalStateHash. + function testRevertProveOneStepBadGlobalState() public { + MELState memory m = _melState(keccak256("PCBH"), 1); + GlobalState memory g; + g.bytes32Vals[2] = m.hash(); + bytes32 wrongGsHash = keccak256("WRONG_GLOBAL_STATE"); // != g.hash() + bytes memory proof = _buildProof(wrongGsHash, g, m); + + vm.expectRevert("BAD_GLOBAL_STATE"); + osp.proveOneStep(_execCtx(keccak256("PCBH")), 0, _finishedHash(wrongGsHash), proof); + } + + // FINISHED, step 0, parentChainBlockHash != target -> kickstart (restart). + function testProveOneStepKickstartBlockHashNotReached() public { + bytes32 reached = keccak256("REACHED"); + bytes32 target = keccak256("TARGET"); // != reached + MELState memory m = _melState(reached, 5); + GlobalState memory g; + g.bytes32Vals[2] = m.hash(); + g.u64Vals[1] = 5; // ExecutedMsgCount == msgCount, so only the block-hash branch fires + bytes32 gsHash = g.hash(); + bytes memory proof = _buildProof(gsHash, g, m); + + bytes32 afterHash = osp.proveOneStep(_execCtx(target), 0, _finishedHash(gsHash), proof); + assertEq(afterHash, osp.getStartMachineHash(gsHash, WASM_ROOT), "should kickstart machine"); + } + + // FINISHED, step 0, at target but ExecutedMsgCount < msgCount -> kickstart (restart). + function testProveOneStepKickstartMessagesUnexecuted() public { + bytes32 target = keccak256("TARGET"); + MELState memory m = _melState(target, 5); // parentChainBlockHash == target + GlobalState memory g; + g.bytes32Vals[2] = m.hash(); + g.u64Vals[1] = 3; // ExecutedMsgCount (3) < msgCount (5) + bytes32 gsHash = g.hash(); + bytes memory proof = _buildProof(gsHash, g, m); + + bytes32 afterHash = osp.proveOneStep(_execCtx(target), 0, _finishedHash(gsHash), proof); + assertEq(afterHash, osp.getStartMachineHash(gsHash, WASM_ROOT), "should kickstart machine"); + } + + // FINISHED, step 0, at target and all messages executed -> no kickstart, return mach.hash(). + function testProveOneStepFinishedNoKickstart() public { + bytes32 target = keccak256("TARGET"); + MELState memory m = _melState(target, 5); + GlobalState memory g; + g.bytes32Vals[2] = m.hash(); + g.u64Vals[1] = 5; // ExecutedMsgCount == msgCount + bytes32 gsHash = g.hash(); + bytes memory proof = _buildProof(gsHash, g, m); + + bytes32 beforeHash = _finishedHash(gsHash); + bytes32 afterHash = osp.proveOneStep(_execCtx(target), 0, beforeHash, proof); + assertEq(afterHash, beforeHash, "no kickstart: machine hash unchanged"); + } + + // A nonzero machineStep with the machine already FINISHED also takes the no-kickstart + // return (the kickstart predicate requires machineStep == 0). + function testProveOneStepNonZeroStepNoKickstart() public { + bytes32 reached = keccak256("REACHED"); + bytes32 target = keccak256("TARGET"); // != reached, but step != 0 so no restart + MELState memory m = _melState(reached, 5); + GlobalState memory g; + g.bytes32Vals[2] = m.hash(); + g.u64Vals[1] = 0; + bytes32 gsHash = g.hash(); + bytes memory proof = _buildProof(gsHash, g, m); + + bytes32 beforeHash = _finishedHash(gsHash); + bytes32 afterHash = osp.proveOneStep(_execCtx(target), 1, beforeHash, proof); + assertEq(afterHash, beforeHash, "machineStep != 0 must not kickstart"); + } +} diff --git a/test/ai/foundry/OneStepProverHostIoErrorPaths.t.sol b/test/ai/foundry/OneStepProverHostIoErrorPaths.t.sol new file mode 100644 index 00000000..bdb92eb0 --- /dev/null +++ b/test/ai/foundry/OneStepProverHostIoErrorPaths.t.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../../../src/osp/OneStepProverHostIo.sol"; + +// Error/edge branches of OneStepProverHostIo that the existing +// test/foundry/OneStepProverHostIo.t.sol does not reach: +// - GET_END_PARENT_CHAIN_BLOCK_HASH with an invalid destination leaf -> ERRORED +// - GET_END_PARENT_CHAIN_BLOCK_HASH with an inconsistent committed memory root -> WRONG_MEM_ROOT +// - keccak ReadPreImage with an unrecognized proofType -> UNKNOWN_PREIMAGE_PROOF +contract OneStepProverHostIoErrorPathsTest is Test { + using ValueStackLib for ValueStack; + + OneStepProverHostIo prover; + + function setUp() public { + prover = new OneStepProverHostIo(address(0), address(0)); + } + + // Single-leaf memory proof: leaf at index 0, no counterparts. + function buildMerkleProof( + bytes32 leafContents + ) internal pure returns (bytes memory) { + bytes memory proofData = new bytes(32 + 1); + assembly { + mstore(add(proofData, 32), leafContents) + } + proofData[32] = 0; // counterparts length + return proofData; + } + + // An out-of-bounds destination pointer errors the machine and leaves memory untouched. + function testGetEndParentChainBlockHashInvalidLeaf() public { + ExecutionContext memory context; + context.targetParentChainBlockHash = keccak256("TARGET"); + Machine memory mach; + Module memory mod; + Instruction memory inst; + inst.opcode = Instructions.GET_END_PARENT_CHAIN_BLOCK_HASH; + + // ptr = 32 with size = 32 -> ptr + 32 > size, so isValidLeaf is false + mach.valueStack.push(ValueLib.newI32(32)); + mod.moduleMemory.size = 32; + bytes32 originalRoot = keccak256(abi.encodePacked("Memory leaf:", bytes32(0))); + mod.moduleMemory.merkleRoot = originalRoot; + + // proof can be empty: the opcode returns before proveLeaf + (Machine memory resultMach, Module memory resultMod) = + prover.executeOneStep(context, mach, mod, inst, ""); + + assertTrue(resultMach.status == MachineStatus.ERRORED, "machine should be ERRORED"); + assertEq(resultMod.moduleMemory.merkleRoot, originalRoot, "memory root must be unchanged"); + } + + // A committed memory root inconsistent with the proven leaf reverts in proveLeaf. + function testGetEndParentChainBlockHashWrongMemRoot() public { + ExecutionContext memory context; + context.targetParentChainBlockHash = keccak256("TARGET"); + Machine memory mach; + Module memory mod; + Instruction memory inst; + inst.opcode = Instructions.GET_END_PARENT_CHAIN_BLOCK_HASH; + + mach.valueStack.push(ValueLib.newI32(0)); // valid leaf + mod.moduleMemory.size = 32; + // committed root is for some OTHER leaf, but the proof is built for leaf bytes32(0) + mod.moduleMemory.merkleRoot = + keccak256(abi.encodePacked("Memory leaf:", keccak256("OTHER"))); + bytes memory proof = buildMerkleProof(bytes32(0)); + + vm.expectRevert("WRONG_MEM_ROOT"); + prover.executeOneStep(context, mach, mod, inst, proof); + } + + // A keccak256 ReadPreImage request with an unrecognized proofType reverts. + function testReadPreImageUnknownKeccakProof() public { + ExecutionContext memory context; + Machine memory mach; + Module memory mod; + Instruction memory inst; + inst.opcode = Instructions.READ_PRE_IMAGE; + inst.argumentData = 0; // keccak256 preimage + + bytes32 fullHash = keccak256("SOME_PREIMAGE"); + mach.valueStack.push(ValueLib.newI32(0)); // ptr + mach.valueStack.push(ValueLib.newI32(0)); // preimageOffset + mod.moduleMemory.size = 32; + mod.moduleMemory.merkleRoot = keccak256(abi.encodePacked("Memory leaf:", fullHash)); + // proofType = 2 is neither 0 (full preimage) nor 1 (HashProofHelper) + bytes memory proof = abi.encodePacked(buildMerkleProof(fullHash), uint8(2)); + + vm.expectRevert("UNKNOWN_PREIMAGE_PROOF"); + prover.executeOneStep(context, mach, mod, inst, proof); + } +} diff --git a/test/ai/foundry/RollupCoverage.t.sol b/test/ai/foundry/RollupCoverage.t.sol new file mode 100644 index 00000000..4f38f5a5 --- /dev/null +++ b/test/ai/foundry/RollupCoverage.t.sol @@ -0,0 +1,498 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "../../foundry/Rollup.t.sol"; + +// Additional coverage for the rollup side: negative/positive paths in +// RollupCore.createNewAssertion, RollupUserLogic.stakeOnNewAssertion, and the RollupAdminLogic +// admin surface that the existing test/foundry/Rollup.t.sol does not exercise. +// +// Inherits RollupTest to reuse its full deployment harness, constants and helpers +// (testSuccessCreateAssertion, firstState/firstMELState, etc). As a result the base suite's +// tests also run under this contract — that redundancy is the cost of harness reuse. +contract RollupCoverageTest is RollupTest { + using GlobalStateLib for GlobalState; + using MELStateLib for MELState; + + // Re-declared locally for vm.expectEmit (not declared in RollupTest). MELConfigSet is + // inherited from RollupTest. + event InboxSet(address inbox); + event AssertionCreated( + bytes32 indexed assertionHash, + bytes32 indexed parentAssertionHash, + AssertionInputs assertion, + bytes32 nextParentChainBlockHash, + bytes32 wasmModuleRoot, + uint256 requiredStake, + address challengeManager, + uint64 confirmPeriodBlocks + ); + + // --------------------------------------------------------------------------------------- + // createNewAssertion + RollupUserLogic + // --------------------------------------------------------------------------------------- + + // A wrong nextParentChainBlockHash in the before-config no longer hashes to the prev + // (genesis) stored config; locks in that nextParentChainBlockHash is part of the domain. + function testRevertWrongConfigHash() public { + AssertionState memory beforeState; + beforeState.machineStatus = MachineStatus.FINISHED; + AssertionInputs memory inputs = AssertionInputs({ + beforeStateData: BeforeStateData({ + prevPrevAssertionHash: bytes32(0), + configData: ConfigData({ + wasmModuleRoot: WASM_MODULE_ROOT, + requiredStake: BASE_STAKE, + challengeManager: address(challengeManager), + confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, + nextParentChainBlockHash: keccak256("WRONG_CONFIG_TARGET") + }) + }), + beforeState: beforeState, + afterState: firstState, + afterMELState: firstMELState + }); + + vm.prank(validator1); + vm.expectRevert("CONFIG_HASH_MISMATCH"); + userRollup.newStakeOnNewAssertion({ + tokenAmount: BASE_STAKE, + assertion: inputs, + expectedAssertionHash: bytes32(0), + _withdrawalAddress: validator1Withdrawal + }); + } + + // A non-terminal afterState (RUNNING) is rejected. + function testRevertBadAfterStatus() public { + AssertionState memory beforeState; + beforeState.machineStatus = MachineStatus.FINISHED; + AssertionState memory afterState = firstState; + afterState.machineStatus = MachineStatus.RUNNING; + + AssertionInputs memory inputs = AssertionInputs({ + beforeStateData: BeforeStateData({ + prevPrevAssertionHash: bytes32(0), + configData: ConfigData({ + wasmModuleRoot: WASM_MODULE_ROOT, + requiredStake: BASE_STAKE, + challengeManager: address(challengeManager), + confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, + nextParentChainBlockHash: firstAssertionParentChainBlockHash + }) + }), + beforeState: beforeState, + afterState: afterState, + afterMELState: firstMELState + }); + + vm.prank(validator1); + vm.expectRevert("BAD_AFTER_STATUS"); + userRollup.newStakeOnNewAssertion({ + tokenAmount: BASE_STAKE, + assertion: inputs, + expectedAssertionHash: bytes32(0), + _withdrawalAddress: validator1Withdrawal + }); + } + + // An assertion cannot be built on top of an ERRORED before-state. We first create an + // errored assertion on genesis, then attempt a follow-up using that errored state as the + // before-state (so it authenticates against the errored prev, then hits BAD_PREV_STATUS). + function testRevertBadPrevStatus() public { + // 1) errored assertion on genesis + AssertionState memory genesisBefore; + genesisBefore.machineStatus = MachineStatus.FINISHED; + AssertionState memory erroredState = firstState; + erroredState.machineStatus = MachineStatus.ERRORED; + bytes32 erroredHash = RollupLib.assertionHash(genesisHash, erroredState); + + vm.prank(validator1); + userRollup.newStakeOnNewAssertion({ + tokenAmount: BASE_STAKE, + assertion: AssertionInputs({ + beforeStateData: BeforeStateData({ + prevPrevAssertionHash: bytes32(0), + configData: ConfigData({ + wasmModuleRoot: WASM_MODULE_ROOT, + requiredStake: BASE_STAKE, + challengeManager: address(challengeManager), + confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, + nextParentChainBlockHash: firstAssertionParentChainBlockHash + }) + }), + beforeState: genesisBefore, + afterState: erroredState, + afterMELState: firstMELState + }), + expectedAssertionHash: erroredHash, + _withdrawalAddress: validator1Withdrawal + }); + bytes32 nextParentChainBlockHash = blockhash(block.number - 1); + + // 2) follow-up whose before-state is the errored state -> BAD_PREV_STATUS + AssertionState memory afterState = firstState; // FINISHED, MEL slot consistent + vm.roll(block.number + 75); + vm.prank(validator2); + vm.expectRevert("BAD_PREV_STATUS"); + userRollup.newStakeOnNewAssertion({ + tokenAmount: BASE_STAKE, + assertion: AssertionInputs({ + beforeStateData: BeforeStateData({ + prevPrevAssertionHash: genesisHash, + configData: ConfigData({ + wasmModuleRoot: WASM_MODULE_ROOT, + requiredStake: BASE_STAKE, + challengeManager: address(challengeManager), + confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, + nextParentChainBlockHash: nextParentChainBlockHash + }) + }), + beforeState: erroredState, + afterState: afterState, + afterMELState: firstMELState + }), + expectedAssertionHash: bytes32(0), + _withdrawalAddress: validator2Withdrawal + }); + } + + // A follow-up whose ExecutedMsgCount regresses below the before-state is rejected. + function testRevertExecutedMessagesBackwards() public { + ( + bytes32 beforeAssertionHash, + AssertionState memory beforeState, + MELState memory beforeMELState, + bytes32 nextParentChainBlockHash + ) = testSuccessCreateAssertion(); + + MELState memory afterMELState = beforeMELState; + afterMELState.parentChainBlockHash = nextParentChainBlockHash; + + AssertionState memory afterState; + afterState.machineStatus = MachineStatus.FINISHED; + afterState.globalState.u64Vals[0] = beforeState.globalState.u64Vals[0]; + // ExecutedMsgCount goes backwards (before is INITIAL_MSG_COUNT = 1) + afterState.globalState.u64Vals[1] = beforeState.globalState.u64Vals[1] - 1; + afterState.globalState.bytes32Vals[2] = afterMELState.hash(); + bytes32 expectedHash = RollupLib.assertionHash(beforeAssertionHash, afterState); + + vm.roll(block.number + 75); + vm.prank(validator1); + vm.expectRevert("INBOX_BACKWARDS"); + userRollup.stakeOnNewAssertion({ + assertion: AssertionInputs({ + beforeStateData: BeforeStateData({ + prevPrevAssertionHash: genesisHash, + configData: ConfigData({ + wasmModuleRoot: WASM_MODULE_ROOT, + requiredStake: BASE_STAKE, + challengeManager: address(challengeManager), + confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, + nextParentChainBlockHash: nextParentChainBlockHash + }) + }), + beforeState: beforeState, + afterState: afterState, + afterMELState: afterMELState + }), + expectedAssertionHash: expectedHash + }); + } + + // An overflow assertion (msgCount > ExecutedMsgCount) that makes no execution progress + // over its before-state is rejected. + function testRevertOverflowStandstill() public { + ( + bytes32 beforeAssertionHash, + AssertionState memory beforeState, + MELState memory beforeMELState, + bytes32 nextParentChainBlockHash + ) = testSuccessCreateAssertion(); + + MELState memory afterMELState = beforeMELState; + afterMELState.parentChainBlockHash = nextParentChainBlockHash; + afterMELState.msgCount = beforeMELState.msgCount + 4; // extraction ran ahead + + AssertionState memory afterState; + afterState.machineStatus = MachineStatus.FINISHED; + afterState.globalState.u64Vals[0] = beforeState.globalState.u64Vals[0]; + // no execution progress: ExecutedMsgCount equals the before-state's + afterState.globalState.u64Vals[1] = beforeState.globalState.u64Vals[1]; + afterState.globalState.bytes32Vals[2] = afterMELState.hash(); + bytes32 expectedHash = RollupLib.assertionHash(beforeAssertionHash, afterState); + + vm.roll(block.number + 75); + vm.prank(validator1); + vm.expectRevert("OVERFLOW_STANDSTILL"); + userRollup.stakeOnNewAssertion({ + assertion: AssertionInputs({ + beforeStateData: BeforeStateData({ + prevPrevAssertionHash: genesisHash, + configData: ConfigData({ + wasmModuleRoot: WASM_MODULE_ROOT, + requiredStake: BASE_STAKE, + challengeManager: address(challengeManager), + confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, + nextParentChainBlockHash: nextParentChainBlockHash + }) + }), + beforeState: beforeState, + afterState: afterState, + afterMELState: afterMELState + }), + expectedAssertionHash: expectedHash + }); + } + + // A non-overflow assertion created sooner than minimumAssertionPeriod (75) is rejected. + function testRevertCreateAssertionTimeDelta() public { + ( + bytes32 beforeAssertionHash, + AssertionState memory beforeState, + MELState memory beforeMELState, + bytes32 nextParentChainBlockHash + ) = testSuccessCreateAssertion(); + + MELState memory afterMELState = beforeMELState; + afterMELState.parentChainBlockHash = nextParentChainBlockHash; + afterMELState.msgCount = beforeMELState.msgCount + 1; + + AssertionState memory afterState; + afterState.machineStatus = MachineStatus.FINISHED; + afterState.globalState.u64Vals[0] = beforeState.globalState.u64Vals[0] + 1; + // execution keeps up with extraction => NOT an overflow assertion + afterState.globalState.u64Vals[1] = beforeState.globalState.u64Vals[1] + 1; + afterState.globalState.bytes32Vals[2] = afterMELState.hash(); + bytes32 expectedHash = RollupLib.assertionHash(beforeAssertionHash, afterState); + + // only +1 block (< minimumAssertionPeriod), but >= 1 so SAME_BLOCK_ASSERTION passes + vm.roll(block.number + 1); + vm.prank(validator1); + vm.expectRevert("TIME_DELTA"); + userRollup.stakeOnNewAssertion({ + assertion: AssertionInputs({ + beforeStateData: BeforeStateData({ + prevPrevAssertionHash: genesisHash, + configData: ConfigData({ + wasmModuleRoot: WASM_MODULE_ROOT, + requiredStake: BASE_STAKE, + challengeManager: address(challengeManager), + confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, + nextParentChainBlockHash: nextParentChainBlockHash + }) + }), + beforeState: beforeState, + afterState: afterState, + afterMELState: afterMELState + }), + expectedAssertionHash: expectedHash + }); + } + + // An overflow assertion (with execution progress) is exempt from minimumAssertionPeriod + // and can be created back-to-back (+1 block). + function testSuccessOverflowAssertionSkipsTimeDelta() public { + ( + bytes32 beforeAssertionHash, + AssertionState memory beforeState, + MELState memory beforeMELState, + bytes32 nextParentChainBlockHash + ) = testSuccessCreateAssertion(); + + MELState memory afterMELState = beforeMELState; + afterMELState.parentChainBlockHash = nextParentChainBlockHash; + afterMELState.msgCount = beforeMELState.msgCount + 4; // extraction ahead of execution + + AssertionState memory afterState; + afterState.machineStatus = MachineStatus.FINISHED; + afterState.globalState.u64Vals[0] = beforeState.globalState.u64Vals[0] + 1; + // some progress over before-state (so it's not a standstill), but still < msgCount + afterState.globalState.u64Vals[1] = beforeState.globalState.u64Vals[1] + 1; + afterState.globalState.bytes32Vals[2] = afterMELState.hash(); + bytes32 expectedHash = RollupLib.assertionHash(beforeAssertionHash, afterState); + + vm.roll(block.number + 1); // back-to-back; would fail TIME_DELTA if not overflow + vm.prank(validator1); + userRollup.stakeOnNewAssertion({ + assertion: AssertionInputs({ + beforeStateData: BeforeStateData({ + prevPrevAssertionHash: genesisHash, + configData: ConfigData({ + wasmModuleRoot: WASM_MODULE_ROOT, + requiredStake: BASE_STAKE, + challengeManager: address(challengeManager), + confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, + nextParentChainBlockHash: nextParentChainBlockHash + }) + }), + beforeState: beforeState, + afterState: afterState, + afterMELState: afterMELState + }), + expectedAssertionHash: expectedHash + }); + + assertTrue( + userRollup.getAssertion(expectedHash).status == AssertionStatus.Pending, + "overflow assertion should have been created" + ); + } + + // --------------------------------------------------------------------------------------- + // RollupAdminLogic admin surface + // --------------------------------------------------------------------------------------- + + // The first MEL config must be version 0. + function testRevertSetMELConfigInitialVersionNonZero() public { + vm.prank(upgradeExecutorAddr); + vm.expectRevert("INVALID_MEL_VERSION"); + adminRollup.setMELConfig(1, address(0xdead01), address(0xdead02)); + } + + // A later MEL config must strictly increase the version; success path v0 -> v2. + function testSuccessSetMELConfigVersionIncrease() public { + address inbox2 = address(0xdead11); + address seqInbox2 = address(0xdead12); + + vm.startPrank(upgradeExecutorAddr); + adminRollup.setMELConfig(0, address(0xdead01), address(0xdead02)); + bytes32 hash0 = userRollup.currentMelConfigHash(); + adminRollup.setMELConfig(2, inbox2, seqInbox2); + vm.stopPrank(); + + bytes32 hash2 = userRollup.currentMelConfigHash(); + assertTrue(hash2 != hash0, "currentMelConfigHash should advance"); + + (uint64 version, address inbox, address sequencerInbox,) = userRollup.melConfig(hash2); + assertEq(uint256(version), 2, "stored version should be 2"); + assertEq(inbox, inbox2, "stored inbox"); + assertEq(sequencerInbox, seqInbox2, "stored sequencerInbox"); + assertEq(address(userRollup.inbox()), inbox2, "inbox pointer should update"); + } + + // Re-setting the same version (not strictly increasing) is rejected. + function testRevertSetMELConfigVersionNotIncreasing() public { + vm.startPrank(upgradeExecutorAddr); + adminRollup.setMELConfig(0, address(0xdead01), address(0xdead02)); + vm.expectRevert("INVALID_MEL_VERSION"); + adminRollup.setMELConfig(0, address(0xdead01), address(0xdead02)); + vm.stopPrank(); + } + + // setMELConfig is gated to the proxy admin (upgrade executor). + function testRevertSetMELConfigNotOwner() public { + vm.expectRevert(); + adminRollup.setMELConfig(0, address(0xdead01), address(0xdead02)); + } + + // Standalone setInbox updates the inbox pointer and emits InboxSet. + function testSuccessSetInbox() public { + address newInbox = address(0xb0b1); + vm.expectEmit(true, true, true, true); + emit InboxSet(newInbox); + vm.prank(upgradeExecutorAddr); + adminRollup.setInbox(IInboxBase(newInbox)); + assertEq(address(userRollup.inbox()), newInbox, "inbox pointer should update"); + } + + // setInbox is gated to the proxy admin (upgrade executor). + function testRevertSetInboxNotOwner() public { + vm.expectRevert(); + adminRollup.setInbox(IInboxBase(address(0xb0b1))); + } + + // --------------------------------------------------------------------------------------- + // Hash-domain / event / genesis + // --------------------------------------------------------------------------------------- + + // configHash unit test — the preimage includes nextParentChainBlockHash. + function testConfigHash() public { + bytes32 expected = keccak256( + abi.encodePacked( + WASM_MODULE_ROOT, + uint256(BASE_STAKE), + address(challengeManager), + CONFIRM_PERIOD_BLOCKS, + firstAssertionParentChainBlockHash + ) + ); + bytes32 actual = RollupLib.configHash( + WASM_MODULE_ROOT, + BASE_STAKE, + address(challengeManager), + CONFIRM_PERIOD_BLOCKS, + firstAssertionParentChainBlockHash + ); + assertEq(actual, expected, "configHash must commit nextParentChainBlockHash"); + } + + // AssertionCreated carries nextParentChainBlockHash == blockhash(block.number - 1). + function testAssertionCreatedEvent() public { + AssertionState memory beforeState; + beforeState.machineStatus = MachineStatus.FINISHED; + AssertionInputs memory inputs = AssertionInputs({ + beforeStateData: BeforeStateData({ + prevPrevAssertionHash: bytes32(0), + configData: ConfigData({ + wasmModuleRoot: WASM_MODULE_ROOT, + requiredStake: BASE_STAKE, + challengeManager: address(challengeManager), + confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, + nextParentChainBlockHash: firstAssertionParentChainBlockHash + }) + }), + beforeState: beforeState, + afterState: firstState, + afterMELState: firstMELState + }); + bytes32 expectedHash = RollupLib.assertionHash(genesisHash, firstState); + + vm.expectEmit(true, true, false, true); + emit AssertionCreated( + expectedHash, + genesisHash, + inputs, + blockhash(block.number - 1), + WASM_MODULE_ROOT, + BASE_STAKE, + address(challengeManager), + CONFIRM_PERIOD_BLOCKS + ); + vm.prank(validator1); + userRollup.newStakeOnNewAssertion({ + tokenAmount: BASE_STAKE, + assertion: inputs, + expectedAssertionHash: expectedHash, + _withdrawalAddress: validator1Withdrawal + }); + } + + // The genesis config hash commits nextParentChainBlockHash = blockhash(block.number - 1) + // captured at initialize time. + function testGenesisConfigHashCommitsParentChainBlockHash() public { + // the correct config validates against the stored genesis config hash + userRollup.validateConfig( + genesisHash, + ConfigData({ + wasmModuleRoot: WASM_MODULE_ROOT, + requiredStake: BASE_STAKE, + challengeManager: address(challengeManager), + confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, + nextParentChainBlockHash: firstAssertionParentChainBlockHash + }) + ); + // a wrong nextParentChainBlockHash does not -> it is part of the genesis config domain + vm.expectRevert("CONFIG_HASH_MISMATCH"); + userRollup.validateConfig( + genesisHash, + ConfigData({ + wasmModuleRoot: WASM_MODULE_ROOT, + requiredStake: BASE_STAKE, + challengeManager: address(challengeManager), + confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, + nextParentChainBlockHash: keccak256("WRONG_GENESIS_TARGET") + }) + ); + } +} diff --git a/test/ai/hardhat/globalState.spec.ts b/test/ai/hardhat/globalState.spec.ts new file mode 100644 index 00000000..2bba0bee --- /dev/null +++ b/test/ai/hardhat/globalState.spec.ts @@ -0,0 +1,62 @@ +import { assert } from 'chai' +import { ethers } from 'hardhat' +import { hash as tsGlobalStateHash } from '../../contract/common/globalStateLib' + +// The TypeScript GlobalState hash helper must agree with the on-chain GlobalStateLib.hash on the +// current layout (4 x bytes32 + 2 x u64). SimpleOneStepProofEntry's getMachineHash returns +// globalState.hash() directly for a FINISHED state, giving us an on-chain oracle without extra +// scaffolding. This locks the TS mirror to the contract and would catch the helper drifting from +// the committed layout (e.g. the MELStateHash/NextMsgHash bytes32 slots). +describe('GlobalState hash (TS vs Solidity)', function () { + const keccak = (s: string) => + ethers.utils.keccak256(ethers.utils.toUtf8Bytes(s)) + + const MAX_U64 = '18446744073709551615' // 2**64 - 1 + + it('TS globalStateLib.hash matches on-chain GlobalStateLib.hash for populated states', async function () { + const factory = await ethers.getContractFactory('SimpleOneStepProofEntry') + const osp = await factory.deploy() + await osp.deployed() + + const states = [ + // all-zero + { + bytes32Vals: [ + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ethers.constants.HashZero, + ], + u64Vals: [0, 0], + }, + // populated MEL slots: [BlockHash, SendRoot, MELStateHash, NextMsgHash], [MsgCount, ExecutedMsgCount] + { + bytes32Vals: [ + keccak('blockHash'), + keccak('sendRoot'), + keccak('melStateHash'), + keccak('nextMsgHash'), + ], + u64Vals: [7, 5], + }, + // boundary u64 values + { + bytes32Vals: [keccak('a'), keccak('b'), keccak('c'), keccak('d')], + u64Vals: [MAX_U64, '12345'], + }, + ] + + for (const state of states) { + const onchain = await osp.getMachineHash({ + globalState: state, + machineStatus: 1, // FINISHED + }) + const ts = tsGlobalStateHash(state as any) + assert.equal( + onchain, + ts, + 'TS globalStateLib.hash disagrees with on-chain GlobalStateLib.hash' + ) + } + }) +})