From 0fb88e3d9b9d157f9b127888e4c2942a711e6e59 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Thu, 11 Jun 2026 11:33:15 +0100 Subject: [PATCH 1/7] Update challenge and osp tests --- test/MockAssertionChain.sol | 4 +- test/foundry/Rollup.t.sol | 2 +- .../challengeV2/ArrayUtilsLib.t.sol | 2 +- .../challengeV2/ChallengeEdgeLib.t.sol | 2 +- .../challengeV2/EdgeChallengeManager.t.sol | 60 +++++++++++-------- .../challengeV2/EdgeChallengeManagerLib.t.sol | 27 ++++----- .../MerkleTreeAccumulatorLib.t.sol | 4 +- test/{ => foundry}/challengeV2/StateTools.sol | 12 ++-- .../challengeV2/UintUtilsLib.t.sol | 2 +- test/{ => foundry}/challengeV2/Utils.sol | 6 +- test/stakingPool/AssertionStakingPool.t.sol | 2 +- 11 files changed, 66 insertions(+), 57 deletions(-) rename test/{ => foundry}/challengeV2/ArrayUtilsLib.t.sol (98%) rename test/{ => foundry}/challengeV2/ChallengeEdgeLib.t.sol (99%) rename test/{ => foundry}/challengeV2/EdgeChallengeManager.t.sol (97%) rename test/{ => foundry}/challengeV2/EdgeChallengeManagerLib.t.sol (99%) rename test/{ => foundry}/challengeV2/MerkleTreeAccumulatorLib.t.sol (99%) rename test/{ => foundry}/challengeV2/StateTools.sol (70%) rename test/{ => foundry}/challengeV2/UintUtilsLib.t.sol (98%) rename test/{ => foundry}/challengeV2/Utils.sol (96%) diff --git a/test/MockAssertionChain.sol b/test/MockAssertionChain.sol index e2c0067e1..c1cf4d7b5 100644 --- a/test/MockAssertionChain.sol +++ b/test/MockAssertionChain.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import {IAssertionChain} from "../src/challengeV2/IAssertionChain.sol"; import "../src/bridge/IBridge.sol"; import "../src/rollup/RollupLib.sol"; -import "./challengeV2/StateTools.sol"; +import "./foundry/challengeV2/StateTools.sol"; struct MockAssertion { bytes32 predecessorId; @@ -36,7 +36,7 @@ contract MockAssertionChain is IAssertionChain { return assertions[assertionHash].height != 0; } - function stakeToken() public view returns (address) { + function stakeToken() public pure returns (address) { return address(0); } diff --git a/test/foundry/Rollup.t.sol b/test/foundry/Rollup.t.sol index 810f5f03d..eacaaa241 100644 --- a/test/foundry/Rollup.t.sol +++ b/test/foundry/Rollup.t.sol @@ -16,7 +16,7 @@ import "../../src/osp/OneStepProverMath.sol"; import "../../src/osp/OneStepProverHostIo.sol"; import "../../src/osp/OneStepProofEntry.sol"; import "../../src/challengeV2/EdgeChallengeManager.sol"; -import "./../challengeV2/Utils.sol"; +import "./challengeV2/Utils.sol"; import "../../src/libraries/Error.sol"; diff --git a/test/challengeV2/ArrayUtilsLib.t.sol b/test/foundry/challengeV2/ArrayUtilsLib.t.sol similarity index 98% rename from test/challengeV2/ArrayUtilsLib.t.sol rename to test/foundry/challengeV2/ArrayUtilsLib.t.sol index 16399e195..39d738d6c 100644 --- a/test/challengeV2/ArrayUtilsLib.t.sol +++ b/test/foundry/challengeV2/ArrayUtilsLib.t.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import "../../src/challengeV2/libraries/ArrayUtilsLib.sol"; +import "../../../src/challengeV2/libraries/ArrayUtilsLib.sol"; import "./Utils.sol"; /// forge-config: default.allow_internal_expect_revert = true diff --git a/test/challengeV2/ChallengeEdgeLib.t.sol b/test/foundry/challengeV2/ChallengeEdgeLib.t.sol similarity index 99% rename from test/challengeV2/ChallengeEdgeLib.t.sol rename to test/foundry/challengeV2/ChallengeEdgeLib.t.sol index 71834457c..ffd716ab6 100644 --- a/test/challengeV2/ChallengeEdgeLib.t.sol +++ b/test/foundry/challengeV2/ChallengeEdgeLib.t.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; import "./Utils.sol"; -import "../../src/challengeV2/libraries/ChallengeEdgeLib.sol"; +import "../../../src/challengeV2/libraries/ChallengeEdgeLib.sol"; contract TestChallengeEdge { function levelToType( diff --git a/test/challengeV2/EdgeChallengeManager.t.sol b/test/foundry/challengeV2/EdgeChallengeManager.t.sol similarity index 97% rename from test/challengeV2/EdgeChallengeManager.t.sol rename to test/foundry/challengeV2/EdgeChallengeManager.t.sol index 226eb55fa..6ccc17b5b 100644 --- a/test/challengeV2/EdgeChallengeManager.t.sol +++ b/test/foundry/challengeV2/EdgeChallengeManager.t.sol @@ -6,11 +6,11 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; import "./Utils.sol"; -import "../MockAssertionChain.sol"; -import "../../src/challengeV2/EdgeChallengeManager.sol"; +import "../../MockAssertionChain.sol"; +import "../../../src/challengeV2/EdgeChallengeManager.sol"; import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -import "../ERC20Mock.sol"; +import "../../ERC20Mock.sol"; import "./StateTools.sol"; contract MockOneStepProofEntry is IOneStepProofEntry { @@ -50,7 +50,7 @@ contract EdgeChallengeManagerTest is Test { StateToolsLib.randomState(rand, 4, genesisBlockHash, MachineStatus.FINISHED); bytes32 genesisStateHash = StateToolsLib.mockMachineHash(genesisState); bytes32 genesisAfterStateHash = genesisState.hash(); - AssertionStateData genesisStateData = AssertionStateData(genesisState, bytes32(0), bytes32(0)); + AssertionStateData genesisStateData = AssertionStateData(genesisState, bytes32(0)); uint8 public NUM_BIGSTEP_LEVEL = 3; uint256 public START_BLOCK = block.number; @@ -69,8 +69,7 @@ contract EdgeChallengeManagerTest is Test { MerkleTreeAccumulatorLib.root(ProofUtils.expansionFromLeaves(genesisStates(), 0, 1)); uint256 genesisHeight = 2; - uint64 inboxMsgCountGenesis = 7; - uint64 inboxMsgCountAssertion = 12; + bytes32 firstParentChainBlockHash = blockhash(block.number - 1); bytes32 h1 = rand.hash(); bytes32 h2 = rand.hash(); @@ -129,7 +128,7 @@ contract EdgeChallengeManagerTest is Test { challengeManager.stakeToken().approve(address(challengeManager), type(uint256).max); genesisAssertionHash = assertionChain.addAssertionUnsafe( - 0, genesisHeight, inboxMsgCountGenesis, genesisState, 0 + 0, genesisHeight, firstParentChainBlockHash, genesisState, 0 ); return (assertionChain, challengeManager, genesisAssertionHash); } @@ -312,13 +311,13 @@ contract EdgeChallengeManagerTest is Test { AssertionState memory a1State = StateToolsLib.randomState( rand, - GlobalStateLib.getInboxPosition(genesisState.globalState), + GlobalStateLib.getMELExecutedMsgCount(genesisState.globalState), h1, MachineStatus.FINISHED ); AssertionState memory a2State = StateToolsLib.randomState( rand, - GlobalStateLib.getInboxPosition(genesisState.globalState), + GlobalStateLib.getMELExecutedMsgCount(genesisState.globalState), h2, MachineStatus.FINISHED ); @@ -332,12 +331,16 @@ contract EdgeChallengeManagerTest is Test { ); a2State.endHistoryRoot = MerkleTreeAccumulatorLib.root(a2RandomStatesExp); + // Roll a few blocks + _safeVmRoll(block.number + NUM_BLOCK_WAIT); + bytes32 nextParentChainBlockHash = blockhash(block.number - 1); + // add one since heights are zero indexed in the history states bytes32 a1 = assertionChain.addAssertion( - genesis, genesisHeight + height1, inboxMsgCountAssertion, genesisState, a1State, 0 + genesis, genesisHeight + height1, nextParentChainBlockHash, genesisState, a1State, 0 ); bytes32 a2 = assertionChain.addAssertion( - genesis, genesisHeight + height1, inboxMsgCountAssertion, genesisState, a2State, 0 + genesis, genesisHeight + height1, nextParentChainBlockHash, genesisState, a2State, 0 ); return EdgeInitData({ @@ -348,14 +351,13 @@ contract EdgeChallengeManagerTest is Test { a2: a2, a1State: a1State, a2State: a2State, - a1Data: AssertionStateData(a1State, genesis, bytes32(0)), - a2Data: AssertionStateData(a2State, genesis, bytes32(0)) + a1Data: AssertionStateData(a1State, genesis), + a2Data: AssertionStateData(a2State, genesis) }); } function testWhitelist() public { - (MockAssertionChain assertionChain, EdgeChallengeManager challengeManager, bytes32 genesis) - = deploy(); + (MockAssertionChain assertionChain, EdgeChallengeManager challengeManager,) = deploy(); assertionChain.setValidatorWhitelistDisabled(false); @@ -373,7 +375,7 @@ contract EdgeChallengeManagerTest is Test { AssertionState memory a1State = StateToolsLib.randomState( rand, - GlobalStateLib.getInboxPosition(genesisState.globalState), + GlobalStateLib.getMELExecutedMsgCount(genesisState.globalState), h1, MachineStatus.FINISHED ); @@ -383,8 +385,12 @@ contract EdgeChallengeManagerTest is Test { ); a1State.endHistoryRoot = MerkleTreeAccumulatorLib.root(exp); + // Roll a few blocks + _safeVmRoll(block.number + NUM_BLOCK_WAIT); + bytes32 nextParentChainBlockHash = blockhash(block.number - 1); + bytes32 a1 = assertionChain.addAssertion( - genesis, genesisHeight + height1, inboxMsgCountAssertion, genesisState, a1State, 0 + genesis, genesisHeight + height1, nextParentChainBlockHash, genesisState, a1State, 0 ); vm.expectRevert(abi.encodeWithSelector(AssertionNoSibling.selector)); @@ -401,7 +407,7 @@ contract EdgeChallengeManagerTest is Test { proof: abi.encode( ProofUtils.generateInclusionProof(ProofUtils.rehashed(states), states.length - 1), genesisStateData, - AssertionStateData(a1State, genesisAssertionHash, bytes32(0)) + AssertionStateData(a1State, genesisAssertionHash) ) }) ); @@ -602,7 +608,7 @@ contract EdgeChallengeManagerTest is Test { function testCanConfirmByTime() public { (EdgeInitData memory ei,,, bytes32 edgeId) = testCanCreateEdgeWithStake(); - _safeVmRoll(START_BLOCK + challengePeriodBlock); + _safeVmRoll(START_BLOCK + NUM_BLOCK_WAIT + challengePeriodBlock); ei.challengeManager.confirmEdgeByTime(edgeId, ei.a1Data); @@ -704,9 +710,13 @@ contract EdgeChallengeManagerTest is Test { function testRevertConfirmAnotherRival() public { (EdgeInitData memory ei, bytes32 edge1Id) = testCanConfirmByChildren(); + // Roll a few blocks + _safeVmRoll(block.number + NUM_BLOCK_WAIT); + bytes32 nextParentChainBlockHash = blockhash(block.number - 1); + AssertionState memory a2State = StateToolsLib.randomState( rand, - GlobalStateLib.getInboxPosition(genesisState.globalState), + GlobalStateLib.getMELExecutedMsgCount(genesisState.globalState), h2, MachineStatus.FINISHED ); @@ -715,7 +725,7 @@ contract EdgeChallengeManagerTest is Test { ); a2State.endHistoryRoot = MerkleTreeAccumulatorLib.root(exp2); bytes32 a2 = ei.assertionChain.addAssertion( - ei.genesis, genesisHeight + height1, inboxMsgCountAssertion, genesisState, a2State, 0 + ei.genesis, genesisHeight + height1, nextParentChainBlockHash, genesisState, a2State, 0 ); bytes32 edge2Id = createLayerZeroEdge(ei.challengeManager, a2, a2State, states2, exp2); @@ -1645,7 +1655,7 @@ contract EdgeChallengeManagerTest is Test { bytes memory typeSpecificProof1 = abi.encode( ProofUtils.generateInclusionProof(ProofUtils.rehashed(states), states.length - 1), genesisStateData, - AssertionStateData(endState, genesisAssertionHash, bytes32(0)) + AssertionStateData(endState, genesisAssertionHash) ); return challengeManager.createLayerZeroEdge( @@ -2052,7 +2062,9 @@ contract EdgeChallengeManagerTest is Test { ) ); - _safeVmRoll(START_BLOCK + (NUM_BIGSTEP_LEVEL + 2) * (NUM_BLOCK_WAIT) + challengePeriodBlock); + _safeVmRoll( + START_BLOCK + (NUM_BIGSTEP_LEVEL + 2) * (NUM_BLOCK_WAIT * 2) + challengePeriodBlock + ); BisectionChildren[] memory allWinners = toDynamic(local.smallStepBisection.edges1); for (uint256 i = 0; i < NUM_BIGSTEP_LEVEL; ++i) { @@ -2074,7 +2086,7 @@ contract EdgeChallengeManagerTest is Test { requiredStake: 0, challengeManager: address(0), confirmPeriodBlocks: 0, - nextInboxPosition: inboxMsgCountGenesis + nextParentChainBlockHash: firstParentChainBlockHash }), ProofUtils.generateInclusionProof(ProofUtils.rehashed(genesisStates()), 0), ProofUtils.generateInclusionProof(ProofUtils.rehashed(firstStates), 1) diff --git a/test/challengeV2/EdgeChallengeManagerLib.t.sol b/test/foundry/challengeV2/EdgeChallengeManagerLib.t.sol similarity index 99% rename from test/challengeV2/EdgeChallengeManagerLib.t.sol rename to test/foundry/challengeV2/EdgeChallengeManagerLib.t.sol index 0c2a0d95b..4b07cd14f 100644 --- a/test/challengeV2/EdgeChallengeManagerLib.t.sol +++ b/test/foundry/challengeV2/EdgeChallengeManagerLib.t.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import "../MockAssertionChain.sol"; -import "../../src/challengeV2/EdgeChallengeManager.sol"; +import "../../MockAssertionChain.sol"; +import "../../../src/challengeV2/EdgeChallengeManager.sol"; import "./Utils.sol"; contract MockOneStepProofEntry is IOneStepProofEntry { @@ -1362,7 +1362,7 @@ contract EdgeChallengeManagerLibTest is Test { uint256 bigStepHeight, uint256 smallStepHeight, uint256 level - ) internal returns (uint256) { + ) internal pure returns (uint256) { uint256 stepSize = 1; uint256 maxLevelIndex = numBigStepLevel + 1; for (uint256 i = level; i < maxLevelIndex; i++) { @@ -1445,8 +1445,7 @@ contract EdgeChallengeManagerLibTest is Test { proof: abi.encodePacked(states1[startHeight + 1]) }); ExecutionContext memory e = ExecutionContext({ - maxInboxMessagesRead: 0, - bridge: IBridge(address(0)), + targetParentChainBlockHash: bytes32(0), initialWasmModuleRoot: bytes32(0) }); data.beforeProof = ProofUtils.generateInclusionProof( @@ -1611,11 +1610,9 @@ contract EdgeChallengeManagerLibTest is Test { function randomAssertionState( IOneStepProofEntry os ) private returns (ExecStateVars memory) { + uint64 msgCount = uint64(uint256(rand.hash())); AssertionState memory assertionState = AssertionState( - GlobalState( - [rand.hash(), rand.hash()], - [uint64(uint256(rand.hash())), uint64(uint256(rand.hash()))] - ), + GlobalState([rand.hash(), rand.hash(), rand.hash(), rand.hash()], [msgCount, msgCount]), MachineStatus.FINISHED, bytes32(0) ); @@ -1707,8 +1704,8 @@ contract EdgeChallengeManagerLibTest is Test { ProofUtils.generateInclusionProof( ProofUtils.rehashed(roots.states), expectedEndHeight ), - AssertionStateData(ard.startState, bytes32(0), bytes32(0)), - AssertionStateData(ard.endState, bytes32(0), bytes32(0)) + AssertionStateData(ard.startState, bytes32(0)), + AssertionStateData(ard.endState, bytes32(0)) ); if (mode == 147) { proof = ""; @@ -1995,7 +1992,7 @@ contract EdgeChallengeManagerLibTest is Test { AssertionState genesisState = StateToolsLib.randomState(rand, 4, genesisBlockHash, MachineStatus.FINISHED); bytes32 genesisStateHash = StateToolsLib.mockMachineHash(genesisState); - AssertionStateData genesisStateData = AssertionStateData(genesisState, bytes32(0), bytes32(0)); + AssertionStateData genesisStateData = AssertionStateData(genesisState, bytes32(0)); bytes32 genesisAssertionHash = rand.hash(); uint256 height1 = 32; @@ -2017,7 +2014,7 @@ contract EdgeChallengeManagerLibTest is Test { bytes memory typeSpecificProof1 = abi.encode( ProofUtils.generateInclusionProof(ProofUtils.rehashed(states), states.length - 1), genesisStateData, - AssertionStateData(endState, genesisAssertionHash, bytes32(0)) + AssertionStateData(endState, genesisAssertionHash) ); bytes memory prefixProof = abi.encode( ProofUtils.expansionFromLeaves(states, 0, 1), @@ -2300,13 +2297,13 @@ contract EdgeChallengeManagerLibTest is Test { bytes32 h2 = rand.hash(); AssertionState memory a1State = StateToolsLib.randomState( rand, - GlobalStateLib.getInboxPosition(genesisState.globalState), + GlobalStateLib.getMELExecutedMsgCount(genesisState.globalState), h1, MachineStatus.FINISHED ); AssertionState memory a2State = StateToolsLib.randomState( rand, - GlobalStateLib.getInboxPosition(genesisState.globalState), + GlobalStateLib.getMELExecutedMsgCount(genesisState.globalState), h2, MachineStatus.FINISHED ); diff --git a/test/challengeV2/MerkleTreeAccumulatorLib.t.sol b/test/foundry/challengeV2/MerkleTreeAccumulatorLib.t.sol similarity index 99% rename from test/challengeV2/MerkleTreeAccumulatorLib.t.sol rename to test/foundry/challengeV2/MerkleTreeAccumulatorLib.t.sol index f0a6cf2c2..ed2def266 100644 --- a/test/challengeV2/MerkleTreeAccumulatorLib.t.sol +++ b/test/foundry/challengeV2/MerkleTreeAccumulatorLib.t.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import "../../src/challengeV2/libraries/MerkleTreeAccumulatorLib.sol"; -import "../../src/libraries/MerkleLib.sol"; +import "../../../src/challengeV2/libraries/MerkleTreeAccumulatorLib.sol"; +import "../../../src/libraries/MerkleLib.sol"; import "./Utils.sol"; /// forge-config: default.allow_internal_expect_revert = true diff --git a/test/challengeV2/StateTools.sol b/test/foundry/challengeV2/StateTools.sol similarity index 70% rename from test/challengeV2/StateTools.sol rename to test/foundry/challengeV2/StateTools.sol index 26aac4f56..689739908 100644 --- a/test/challengeV2/StateTools.sol +++ b/test/foundry/challengeV2/StateTools.sol @@ -5,9 +5,9 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import "../../src/state/GlobalState.sol"; -import "../../src/state/Machine.sol"; -import "../../src/rollup/RollupLib.sol"; +import "../../../src/state/GlobalState.sol"; +import "../../../src/state/Machine.sol"; +import "../../../src/rollup/RollupLib.sol"; import "./Utils.sol"; library StateToolsLib { @@ -15,12 +15,12 @@ library StateToolsLib { function randomState( Random rand, - uint256 inboxMsgCountProcessed, + uint256 melMsgCount, bytes32 blockHash, MachineStatus ms ) internal returns (AssertionState memory) { - bytes32[2] memory bytes32Vals = [blockHash, rand.hash()]; - uint64[2] memory u64Vals = [uint64(inboxMsgCountProcessed), uint64(uint256(rand.hash()))]; + bytes32[4] memory bytes32Vals = [blockHash, rand.hash(), rand.hash(), rand.hash()]; + uint64[2] memory u64Vals = [uint64(melMsgCount), uint64(melMsgCount)]; // We assume executedMsgCount == msgCount for simplicity GlobalState memory gs = GlobalState({bytes32Vals: bytes32Vals, u64Vals: u64Vals}); return AssertionState({globalState: gs, machineStatus: ms, endHistoryRoot: bytes32(0)}); diff --git a/test/challengeV2/UintUtilsLib.t.sol b/test/foundry/challengeV2/UintUtilsLib.t.sol similarity index 98% rename from test/challengeV2/UintUtilsLib.t.sol rename to test/foundry/challengeV2/UintUtilsLib.t.sol index e8a274686..4d70120a4 100644 --- a/test/challengeV2/UintUtilsLib.t.sol +++ b/test/foundry/challengeV2/UintUtilsLib.t.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import "../../src/challengeV2/libraries/UintUtilsLib.sol"; +import "../../../src/challengeV2/libraries/UintUtilsLib.sol"; import "./Utils.sol"; /// forge-config: default.allow_internal_expect_revert = true diff --git a/test/challengeV2/Utils.sol b/test/foundry/challengeV2/Utils.sol similarity index 96% rename from test/challengeV2/Utils.sol rename to test/foundry/challengeV2/Utils.sol index 2e2367ea1..73d2291a3 100644 --- a/test/challengeV2/Utils.sol +++ b/test/foundry/challengeV2/Utils.sol @@ -4,9 +4,9 @@ // pragma solidity ^0.8.17; -import "../../src/challengeV2/libraries/MerkleTreeAccumulatorLib.sol"; -import "../../src/challengeV2/libraries/UintUtilsLib.sol"; -import "../../src/challengeV2/libraries/ArrayUtilsLib.sol"; +import "../../../src/challengeV2/libraries/MerkleTreeAccumulatorLib.sol"; +import "../../../src/challengeV2/libraries/UintUtilsLib.sol"; +import "../../../src/challengeV2/libraries/ArrayUtilsLib.sol"; import "forge-std/Test.sol"; contract Random { diff --git a/test/stakingPool/AssertionStakingPool.t.sol b/test/stakingPool/AssertionStakingPool.t.sol index 3b6cdf2aa..26f703099 100644 --- a/test/stakingPool/AssertionStakingPool.t.sol +++ b/test/stakingPool/AssertionStakingPool.t.sol @@ -16,7 +16,7 @@ import "../../src/osp/OneStepProverMath.sol"; import "../../src/osp/OneStepProverHostIo.sol"; import "../../src/osp/OneStepProofEntry.sol"; import "../../src/challengeV2/EdgeChallengeManager.sol"; -import "../challengeV2/Utils.sol"; +import "../foundry/challengeV2/Utils.sol"; import "../../src/libraries/Error.sol"; From cb62030c945d9c5cfdd2617c5bed45b483898b8a Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Thu, 11 Jun 2026 13:44:06 +0100 Subject: [PATCH 2/7] Add new Rollup and OSP tests --- test/foundry/OneStepProverHostIo.t.sol | 191 +++++++++++++++++++++++++ test/foundry/Rollup.t.sol | 139 ++++++++++++++++++ 2 files changed, 330 insertions(+) diff --git a/test/foundry/OneStepProverHostIo.t.sol b/test/foundry/OneStepProverHostIo.t.sol index 3e318a0f7..f96ac68cd 100644 --- a/test/foundry/OneStepProverHostIo.t.sol +++ b/test/foundry/OneStepProverHostIo.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import "forge-std/Test.sol"; import "../../src/osp/OneStepProverHostIo.sol"; +import "../../src/osp/HashProofHelper.sol"; import {ICustomDAProofValidator} from "../../src/osp/ICustomDAProofValidator.sol"; contract OneStepProverHostIoPublic is OneStepProverHostIo { @@ -409,4 +410,194 @@ contract OneStepProverHostIoTest is Test { vm.expectRevert("INVALID_CUSTOM_DA_RESPONSE"); ospHostIo.executeReadPreImagePublic(context, mach, mod, inst, proof); } + + // The opcode writes execCtx.targetParentChainBlockHash into the target memory leaf. + function testGetEndParentChainBlockHashSuccess() public { + OneStepProverHostIoPublic ospHostIo = new OneStepProverHostIoPublic( + address(mockCustomDAProofValidator), address(mockHashProofHelper) + ); + + bytes32 targetHash = keccak256("TARGET_PARENT_CHAIN_BLOCK_HASH"); + + ExecutionContext memory context; + context.targetParentChainBlockHash = targetHash; + Machine memory mach; + Module memory mod; + Instruction memory inst; + inst.opcode = Instructions.GET_END_PARENT_CHAIN_BLOCK_HASH; + + mach.valueStack.push(ValueLib.newI32(0)); // ptr to the destination leaf + mod.moduleMemory.size = 32; + mod.moduleMemory.merkleRoot = keccak256(abi.encodePacked("Memory leaf:", bytes32(0))); + bytes memory proof = buildMerkleProof(bytes32(0)); + + (Machine memory resultMach, Module memory resultMod) = + ospHostIo.executeOneStep(context, mach, mod, inst, proof); + + assertTrue(resultMach.status == MachineStatus.RUNNING, "machine should still be running"); + assertEq( + resultMod.moduleMemory.merkleRoot, + keccak256(abi.encodePacked("Memory leaf:", targetHash)), + "target parent chain block hash not written to memory" + ); + } + + // A previously proven preimage part is read from the HashProofHelper into memory. + function testReadPreImageHashProofHelperSuccess() public { + HashProofHelper hashProofHelper = new HashProofHelper(); + OneStepProverHostIoPublic ospHostIo = new OneStepProverHostIoPublic( + address(mockCustomDAProofValidator), address(hashProofHelper) + ); + + bytes32 preimageWord = hex"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; + bytes memory preimage = abi.encodePacked(preimageWord); + bytes32 fullHash = keccak256(preimage); + hashProofHelper.proveWithFullPreimage(preimage, 0); + + ExecutionContext memory context; + Machine memory mach; + Module memory mod; + Instruction memory inst; + inst.opcode = Instructions.READ_PRE_IMAGE; + inst.argumentData = 0; // keccak256 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)); + bytes memory proof = abi.encodePacked(buildMerkleProof(fullHash), uint8(1)); // proofType 1 + + (Machine memory resultMach, Module memory resultMod) = + ospHostIo.executeOneStep(context, mach, mod, inst, proof); + + assertTrue(resultMach.status == MachineStatus.RUNNING, "machine should still be running"); + // the full 32-byte preimage is written into the destination leaf + assertEq( + resultMod.moduleMemory.merkleRoot, + keccak256(abi.encodePacked("Memory leaf:", preimageWord)), + "preimage part not written to memory" + ); + // the number of bytes read is pushed onto the stack + assertEq(resultMach.valueStack.peek().contents, 32, "wrong bytes-read count"); + } + + // Requesting a HashProofHelper proof when no helper is configured reverts. + function testReadPreImageHashProofHelperNotSet() public { + OneStepProverHostIoPublic ospHostIo = + new OneStepProverHostIoPublic(address(mockCustomDAProofValidator), address(0)); + + bytes32 fullHash = keccak256("UNPROVEN_PREIMAGE"); + + ExecutionContext memory context; + Machine memory mach; + Module memory mod; + Instruction memory inst; + inst.opcode = Instructions.READ_PRE_IMAGE; + inst.argumentData = 0; // keccak256 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)); + bytes memory proof = abi.encodePacked(buildMerkleProof(fullHash), uint8(1)); // proofType 1 + + vm.expectRevert("HASH_PROOF_HELPER_NOT_SET"); + ospHostIo.executeOneStep(context, mach, mod, inst, proof); + } + + // Requesting a preimage part that was never proven reverts with NotProven. + function testReadPreImageHashProofHelperNotProven() public { + HashProofHelper hashProofHelper = new HashProofHelper(); + OneStepProverHostIoPublic ospHostIo = new OneStepProverHostIoPublic( + address(mockCustomDAProofValidator), address(hashProofHelper) + ); + + bytes32 fullHash = keccak256("UNPROVEN_PREIMAGE"); + + ExecutionContext memory context; + Machine memory mach; + Module memory mod; + Instruction memory inst; + inst.opcode = Instructions.READ_PRE_IMAGE; + inst.argumentData = 0; // keccak256 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)); + bytes memory proof = abi.encodePacked(buildMerkleProof(fullHash), uint8(1)); // proofType 1 + + vm.expectRevert( + abi.encodeWithSelector(IHashProofHelper.NotProven.selector, fullHash, uint64(0)) + ); + ospHostIo.executeOneStep(context, mach, mod, inst, proof); + } + + // A preimage too large for one transaction can be uploaded across multiple + // proveWithSplitPreimage calls and then read through the prover. + function testReadPreImageHashProofHelperSplitPreimage() public { + HashProofHelper hashProofHelper = new HashProofHelper(); + OneStepProverHostIoPublic ospHostIo = new OneStepProverHostIoPublic( + address(mockCustomDAProofValidator), address(hashProofHelper) + ); + + bytes32 preimageWord = hex"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; + // 200-byte preimage: the leading word we read, followed by pseudo-random filler + // (deterministic, derived from keccak so the test stays reproducible). + // Uploaded as a 136-byte (round-aligned) chunk and a final 64-byte chunk. + bytes memory chunk1 = abi.encodePacked( + preimageWord, + keccak256("filler-0"), + keccak256("filler-1"), + keccak256("filler-2"), + bytes8(keccak256("filler-3")) + ); // 32 + 32 + 32 + 32 + 8 = 136 bytes + bytes memory chunk2 = abi.encodePacked(keccak256("filler-4"), keccak256("filler-5")); // 64 bytes + bytes memory preimage = abi.encodePacked(chunk1, chunk2); + + hashProofHelper.proveWithSplitPreimage(chunk1, 0, 0); // non-final chunk + bytes32 fullHash = hashProofHelper.proveWithSplitPreimage(chunk2, 0, 1); // final chunk + assertEq(fullHash, keccak256(preimage), "split-proof hash mismatch"); + + ExecutionContext memory context; + Machine memory mach; + Module memory mod; + Instruction memory inst; + inst.opcode = Instructions.READ_PRE_IMAGE; + inst.argumentData = 0; // keccak256 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)); + bytes memory proof = abi.encodePacked(buildMerkleProof(fullHash), uint8(1)); // proofType 1 + + (Machine memory resultMach, Module memory resultMod) = + ospHostIo.executeOneStep(context, mach, mod, inst, proof); + + assertTrue(resultMach.status == MachineStatus.RUNNING, "machine should still be running"); + // the first 32 bytes of the preimage are written into the leaf + assertEq( + resultMod.moduleMemory.merkleRoot, + keccak256(abi.encodePacked("Memory leaf:", preimageWord)), + "preimage part not written to memory" + ); + assertEq(resultMach.valueStack.peek().contents, 32, "wrong bytes-read count"); + } + + // The executable READ_INBOX_MESSAGE host I/O path was removed; the opcode is no longer dispatched. + function testReadInboxMessageOpcodeReverts() public { + OneStepProverHostIoPublic ospHostIo = new OneStepProverHostIoPublic( + address(mockCustomDAProofValidator), address(mockHashProofHelper) + ); + + ExecutionContext memory context; + Machine memory mach; + Module memory mod; + Instruction memory inst; + inst.opcode = Instructions.READ_INBOX_MESSAGE; + + vm.expectRevert("INVALID_HOSTIO_OPCODE"); + ospHostIo.executeOneStep(context, mach, mod, inst, ""); + } } diff --git a/test/foundry/Rollup.t.sol b/test/foundry/Rollup.t.sol index eacaaa241..8823539a6 100644 --- a/test/foundry/Rollup.t.sol +++ b/test/foundry/Rollup.t.sol @@ -106,6 +106,13 @@ contract RollupTest is Test { address validatorWalletCreator ); + event MELConfigSet( + uint16 indexed melVersion, + address indexed inbox, + address indexed sequencerInbox, + uint64 activationBlock + ); + IReader4844 dummyReader4844 = IReader4844(address(137)); BridgeCreator.BridgeTemplates ethBasedTemplates = BridgeCreator.BridgeTemplates({ bridge: new Bridge(), @@ -1756,4 +1763,136 @@ contract RollupTest is Test { vm.prank(upgradeExecutorAddr); adminRollup.decreaseBaseStake(BASE_STAKE - 1, data.nextParentChainBlockHash); } + + // The committed MELState hash in the afterState must match the provided afterMELState. + function testRevertInvalidMELState() 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 + }); + inputs.afterState.globalState.bytes32Vals[2] = keccak256("CORRUPTED_MEL_HASH"); + + vm.prank(validator1); + vm.expectRevert("INVALID_MEL_STATE"); + userRollup.newStakeOnNewAssertion({ + tokenAmount: BASE_STAKE, + assertion: inputs, + expectedAssertionHash: bytes32(0), + _withdrawalAddress: validator1Withdrawal + }); + } + + // The afterMELState must end on the parent chain block hash committed by the prev config. + function testRevertBadParentChainBlockHash() 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 + }); + inputs.afterMELState.parentChainBlockHash = keccak256("WRONG_PARENT_CHAIN_BLOCK_HASH"); + // keep the committed MELState hash consistent so the INVALID_MEL_STATE check passes + inputs.afterState.globalState.bytes32Vals[2] = inputs.afterMELState.hash(); + + vm.prank(validator1); + vm.expectRevert("BAD_PARENT_CHAIN_BLOCK_HASH"); + userRollup.newStakeOnNewAssertion({ + tokenAmount: BASE_STAKE, + assertion: inputs, + expectedAssertionHash: bytes32(0), + _withdrawalAddress: validator1Withdrawal + }); + } + + // Two assertions cannot be created in the same block, as they would share the same + // nextParentChainBlockHash target. + function testRevertSameBlockAssertion() public { + ( + , + AssertionState memory beforeState, + MELState memory beforeMELState, + bytes32 nextParentChainBlockHash + ) = testSuccessCreateAssertion(); + + MELState memory afterMELState = beforeMELState; + afterMELState.msgCount += 1; + afterMELState.parentChainBlockHash = nextParentChainBlockHash; + + AssertionState memory afterState; + afterState.machineStatus = MachineStatus.FINISHED; + afterState.globalState.u64Vals[0] = beforeState.globalState.u64Vals[0] + 1; // MsgCount + afterState.globalState.u64Vals[1] = beforeState.globalState.u64Vals[1] + 1; // ExecutedMsgCount + afterState.globalState.bytes32Vals[2] = afterMELState.hash(); // MELState hash + + AssertionInputs memory inputs = 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 + }); + + // no vm.roll: the second assertion is created in the same block as the first + vm.prank(validator1); + vm.expectRevert("SAME_BLOCK_ASSERTION"); + userRollup.stakeOnNewAssertion({assertion: inputs, expectedAssertionHash: bytes32(0)}); + } + + function testSuccessSetMELConfig() public { + address melInbox = address(0xdead01); + address melSeqInbox = address(0xdead02); + + vm.expectEmit(true, true, true, true); + emit MELConfigSet(0, melInbox, melSeqInbox, uint64(block.number)); + + vm.prank(upgradeExecutorAddr); + adminRollup.setMELConfig(0, melInbox, melSeqInbox); + + bytes32 melConfigHash = userRollup.currentMelConfigHash(); + assertTrue(melConfigHash != bytes32(0), "currentMelConfigHash not set"); + + (uint64 version, address inbox, address sequencerInbox, uint64 activationBlock) = + userRollup.melConfig(melConfigHash); + assertEq(uint256(version), 0, "wrong version"); + assertEq(inbox, melInbox, "wrong stored inbox"); + assertEq(sequencerInbox, melSeqInbox, "wrong stored sequencerInbox"); + assertEq(uint256(activationBlock), block.number, "wrong activation block"); + + assertEq(address(userRollup.inbox()), melInbox, "inbox pointer not updated"); + assertEq( + address(userRollup.sequencerInbox()), melSeqInbox, "sequencerInbox pointer not updated" + ); + } } From e1557b192051d3d525585c5512b599067901c00e Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Fri, 12 Jun 2026 12:08:17 +0100 Subject: [PATCH 3/7] Add HashProofHelper tests and add them to CI --- .github/workflows/contract-tests.yml | 3 + package.json | 1 + test/foundry/HashProofHelper.t.sol | 244 +++++++++++++++++++++++++++ test/prover/hash-proofs.ts | 49 +++--- 4 files changed, 276 insertions(+), 21 deletions(-) create mode 100644 test/foundry/HashProofHelper.t.sol diff --git a/.github/workflows/contract-tests.yml b/.github/workflows/contract-tests.yml index 5cc21c2c6..ee91c8109 100644 --- a/.github/workflows/contract-tests.yml +++ b/.github/workflows/contract-tests.yml @@ -88,6 +88,9 @@ jobs: - name: Run tests run: yarn hardhat --network hardhat test test/contract/*.spec.ts + - name: Run prover tests + run: yarn test:prover + - name: Interface compatibility run: yarn run test:compatibility diff --git a/package.json b/package.json index 5b2e28ea5..027b38bf0 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "build:0.7": "INTERFACE_TESTER_SOLC_VERSION=0.7.0 yarn run build", "test": "DISABLE_GAS_REPORTER=true hardhat --network hardhat test test/contract/*.spec.ts", "test:4844": "DISABLE_GAS_REPORTER=true hardhat --network hardhat test test/contract/*.spec.4844.ts", + "test:prover": "DISABLE_GAS_REPORTER=true hardhat --network hardhat test test/prover/hash-proofs.ts", "test:compatibility": "yarn run build:0.6 && yarn run build:0.7", "test:storage": "./test/storage/test.bash", "test:signatures": "./test/signatures/test-sigs.bash", diff --git a/test/foundry/HashProofHelper.t.sol b/test/foundry/HashProofHelper.t.sol new file mode 100644 index 000000000..1a6f53e03 --- /dev/null +++ b/test/foundry/HashProofHelper.t.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import "../../src/osp/HashProofHelper.sol"; + +contract HashProofHelperTest is Test { + HashProofHelper hph; + HashProofHelper hphFull; + HashProofHelper hphSplit; + + uint256 constant KECCAK_ROUND_INPUT = 136; + + function setUp() public { + hph = new HashProofHelper(); + hphFull = new HashProofHelper(); + hphSplit = new HashProofHelper(); + } + + // A non-final chunk whose length is not a multiple of the keccak round size is rejected. + function testRevertNotBlockAligned() public { + bytes memory data = new bytes(100); // not a multiple of 136 + vm.expectRevert("NOT_BLOCK_ALIGNED"); + hph.proveWithSplitPreimage(data, 0, 0); // flags 0 => non-final + } + + // The offset must stay constant across the chunks of a single split proof. + function testRevertDiffOffset() public { + bytes memory chunk1 = new bytes(KECCAK_ROUND_INPUT); // round-aligned, non-final + hph.proveWithSplitPreimage(chunk1, 0, 0); // establishes in-progress state at offset 0 + bytes memory chunk2 = new bytes(64); + vm.expectRevert("DIFF_OFFSET"); + hph.proveWithSplitPreimage(chunk2, 5, 1); // final chunk with a different offset + } + + // Requesting a part that was never proven reverts with NotProven. + function testRevertGetPreimagePartNotProven() public { + bytes32 fullHash = keccak256("never-proven"); + vm.expectRevert( + abi.encodeWithSelector(IHashProofHelper.NotProven.selector, fullHash, uint64(0)) + ); + hph.getPreimagePart(fullHash, 0); + } + + // An offset past the end of a full preimage proves an empty (but present) part. + function testFullPreimageOffsetPastEndEmptyPart() public { + bytes memory data = "hello world"; // 11 bytes + uint64 offset = 100; + bytes32 fullHash = hph.proveWithFullPreimage(data, offset); + assertEq(fullHash, keccak256(data), "hash mismatch"); + assertEq(hph.getPreimagePart(fullHash, offset).length, 0, "part should be empty"); + } + + // Same boundary, proved through the split path. + function testSplitPreimageOffsetPastEndEmptyPart() public { + bytes memory data = "hello world"; // 11 bytes + uint64 offset = 100; + bytes32 fullHash = hph.proveWithSplitPreimage(data, offset, 1); // single final chunk + assertEq(fullHash, keccak256(data), "hash mismatch"); + assertEq(hph.getPreimagePart(fullHash, offset).length, 0, "part should be empty"); + } + + // The clear flag (bit 1) discards stale in-progress state before a new proof. + function testSplitPreimageClearFlagResetsState() public { + // leave a stale, in-progress chunk in the caller's keccak state + hph.proveWithSplitPreimage(new bytes(KECCAK_ROUND_INPUT), 0, 0); + + // prove a different preimage from scratch: flags = isFinal (1) | clear (2) = 3 + bytes memory realData = "the real preimage"; + bytes32 fullHash = hph.proveWithSplitPreimage(realData, 0, 3); + + // the stale chunk must not contribute to the hash + assertEq(fullHash, keccak256(realData), "stale state was not cleared"); + assertEq(hph.getPreimagePart(fullHash, 0), realData, "wrong proven part"); + } + + // Builds a deterministic, varied byte string of the given length. + function _fill( + uint256 len + ) internal pure returns (bytes memory b) { + b = new bytes(len); + for (uint256 i = 0; i < len; i++) { + b[i] = bytes1(uint8(i)); + } + } + + // Copies data[start:end] out of a memory buffer. + function _slice( + bytes memory data, + uint256 start, + uint256 end + ) internal pure returns (bytes memory out) { + out = new bytes(end - start); + for (uint256 i = 0; i < out.length; i++) { + out[i] = data[start + i]; + } + } + + function testEmptyPreimage() public { + bytes memory data = new bytes(0); + bytes32 expected = keccak256(data); + + bytes32 fullHash = hph.proveWithFullPreimage(data, 0); + assertEq(fullHash, expected, "full empty hash"); + assertEq(hph.getPreimagePart(fullHash, 0).length, 0, "full empty part"); + + bytes32 splitHash = hph.proveWithSplitPreimage(data, 0, 1); // single empty final chunk + assertEq(splitHash, expected, "split empty hash"); + assertEq(hph.getPreimagePart(splitHash, 0).length, 0, "split empty part"); + } + + // Lengths that are exact multiples of the keccak rate require an extra padding-only block. + function testBlockMultipleLength() public { + bytes memory d136 = _fill(136); + assertEq(hph.proveWithSplitPreimage(d136, 0, 1), keccak256(d136), "136-byte hash"); + + bytes memory d272 = _fill(272); + assertEq(hph.proveWithSplitPreimage(d272, 0, 1), keccak256(d272), "272-byte hash"); + } + + // A single split call whose chunk spans multiple keccak blocks. + function testSingleChunkLargerThanRound() public { + bytes memory d200 = _fill(200); + bytes32 h200 = hph.proveWithSplitPreimage(d200, 0, 1); + assertEq(h200, keccak256(d200), "200-byte single chunk hash"); + assertEq(hph.getPreimagePart(h200, 0), _slice(d200, 0, 32), "200-byte part"); + + bytes memory d400 = _fill(400); + assertEq( + hph.proveWithSplitPreimage(d400, 0, 1), keccak256(d400), "400-byte single chunk hash" + ); + + // a non-final two-block chunk followed by a final remainder + bytes memory full = _fill(300); + hph.proveWithSplitPreimage(_slice(full, 0, 272), 0, 0); // non-final, 2 blocks + bytes32 h = hph.proveWithSplitPreimage(_slice(full, 272, 300), 0, 1); // final 28 bytes + assertEq(h, keccak256(full), "272 non-final + remainder hash"); + } + + // In-progress split state is keyed by msg.sender, so two senders interleaving split proofs + // (of different preimages, at different offsets) must not corrupt each other. + function testSplitPreimagePerSenderIsolation() public { + address alice = address(0xa11ce); + address bob = address(0xb0b); + + bytes memory aliceData = _fill(200); // 136 (non-final) + 64 (final) + bytes memory bobData = _fill(168); // 136 (non-final) + 32 (final) + uint64 aliceOffset = 0; + uint64 bobOffset = 50; + + // interleave the two senders' chunks + vm.prank(alice); + hph.proveWithSplitPreimage(_slice(aliceData, 0, 136), aliceOffset, 0); + vm.prank(bob); + hph.proveWithSplitPreimage(_slice(bobData, 0, 136), bobOffset, 0); + vm.prank(alice); + bytes32 aliceHash = hph.proveWithSplitPreimage(_slice(aliceData, 136, 200), aliceOffset, 1); + vm.prank(bob); + bytes32 bobHash = hph.proveWithSplitPreimage(_slice(bobData, 136, 168), bobOffset, 1); + + // each sender's proof finalizes to its own preimage, uncorrupted by the other's chunks + assertEq(aliceHash, keccak256(aliceData), "alice hash corrupted by interleaving"); + assertEq(bobHash, keccak256(bobData), "bob hash corrupted by interleaving"); + assertEq( + hph.getPreimagePart(aliceHash, aliceOffset), _slice(aliceData, 0, 32), "alice part" + ); + assertEq(hph.getPreimagePart(bobHash, bobOffset), _slice(bobData, 50, 82), "bob part"); + } + + // clearSplitProof() discards the caller's in-progress state. + function testClearSplitProofDiscardsState() public { + // leave a stale, in-progress chunk in the caller's keccak state + hph.proveWithSplitPreimage(new bytes(KECCAK_ROUND_INPUT), 0, 0); + + // explicitly clear it + hph.clearSplitProof(); + + // a fresh proof (no clear flag, same offset) must not include the stale chunk + bytes memory realData = "the real preimage"; + bytes32 fullHash = hph.proveWithSplitPreimage(realData, 0, 1); + assertEq(fullHash, keccak256(realData), "stale state not discarded by clearSplitProof"); + assertEq(hph.getPreimagePart(fullHash, 0), realData, "wrong proven part"); + } + + // An empty final chunk after full blocks triggers the padding-only round. + function testEmptyFinalChunk() public { + bytes memory d136 = _fill(136); + hph.proveWithSplitPreimage(d136, 0, 0); // non-final + assertEq( + hph.proveWithSplitPreimage(new bytes(0), 0, 1), keccak256(d136), "136 + empty final" + ); + + bytes memory d272 = _fill(272); + hph.proveWithSplitPreimage(d272, 0, 0); // non-final, 2 blocks + assertEq( + hph.proveWithSplitPreimage(new bytes(0), 0, 1), keccak256(d272), "272 + empty final" + ); + } + + // Differential property: full and split proofs agree on the hash and the proven part for + // any valid input, and neither reverts. Uses randomized chunk sizes (multiples of 136) and + // covers length 0 and offsets past the end of the preimage. + function testFuzz_fullVsSplit( + bytes calldata data, + uint64 rawOffset, + uint256 chunkSeed + ) public { + vm.assume(data.length <= 600); + uint64 offset = + data.length == 0 ? rawOffset : uint64(uint256(rawOffset) % (2 * data.length + 1)); + + bytes32 fullHash = hphFull.proveWithFullPreimage(data, offset); + + bytes32 splitHash; + if (data.length == 0) { + splitHash = hphSplit.proveWithSplitPreimage(data, offset, 1); + } else { + uint256 provenLen = 0; + uint256 seed = chunkSeed; + while (provenLen < data.length) { + uint256 numBlocks = 1 + (seed % 3); // 1..3 keccak blocks per chunk + seed = uint256(keccak256(abi.encode(seed))); + uint256 next = provenLen + numBlocks * KECCAK_ROUND_INPUT; + if (next >= data.length) next = data.length; + uint256 flags = next == data.length ? 1 : 0; // final only on the last chunk + splitHash = hphSplit.proveWithSplitPreimage(data[provenLen:next], offset, flags); + provenLen = next; + } + } + + assertEq(fullHash, keccak256(data), "full hash mismatch"); + assertEq(splitHash, fullHash, "split hash mismatch"); + + bytes memory expected; + if (offset < data.length) { + uint256 end = uint256(offset) + 32; + if (end > data.length) end = data.length; + expected = data[offset:end]; + } + assertLe(expected.length, 32, "part > 32 bytes"); + assertEq(hphFull.getPreimagePart(fullHash, offset), expected, "full part mismatch"); + assertEq(hphSplit.getPreimagePart(splitHash, offset), expected, "split part mismatch"); + } +} diff --git a/test/prover/hash-proofs.ts b/test/prover/hash-proofs.ts index da9df553b..b72195d2b 100644 --- a/test/prover/hash-proofs.ts +++ b/test/prover/hash-proofs.ts @@ -12,11 +12,14 @@ describe('HashProofHelper', function () { ).address ) - for (let i = 0; i < 16; i += 1) { - const len = Math.floor(Math.random() * 256) - const data = Math.floor(Math.random() * 256) - const offset = Math.floor(Math.random() * 256) - const bytes = Array(len).fill(data) + for (let i = 0; i < 32; i += 1) { + const len = Math.floor(Math.random() * 512) + const bytes = [] + for (let j = 0; j < len; j += 1) { + bytes.push(Math.floor(Math.random() * 256)) + } + // offset range overlaps len so we also exercise offset >= len (empty part) + const offset = Math.floor(Math.random() * 512) const hash = ethers.utils.keccak256(bytes) const proofTx = await hashProofHelper.proveWithFullPreimage(bytes, offset) @@ -24,14 +27,14 @@ describe('HashProofHelper', function () { const log = hashProofHelper.interface.parseLog(receipt.logs[0]) const provenPart = await hashProofHelper.getPreimagePart(hash, offset) - let dataHex = data.toString(16) - dataHex = '00'.slice(dataHex.length) + dataHex const partLen = Math.min(32, Math.max(0, len - offset)) - const partString = '0x' + dataHex.repeat(partLen) + const expectedPart = ethers.utils.hexlify( + bytes.slice(offset, offset + partLen) + ) assert.equal(log.args['fullHash'], hash) assert.equal(log.args['offset'], offset) - assert.equal(log.args['part'], partString) - assert.equal(provenPart, partString) + assert.equal(log.args['part'], expectedPart) + assert.equal(provenPart, expectedPart) } }) @@ -45,18 +48,22 @@ describe('HashProofHelper', function () { ).address ) - for (let i = 0; i < 16; i += 1) { - const len = Math.floor(Math.random() * 1024) - const data = Math.floor(Math.random() * 256) - const offset = Math.floor(Math.random() * 256) - const bytes = Array(len).fill(data) + for (let i = 0; i < 32; i += 1) { + const len = Math.floor(Math.random() * 4096) + const bytes = [] + for (let j = 0; j < len; j += 1) { + bytes.push(Math.floor(Math.random() * 256)) + } + // offset range overlaps len so we also exercise offset >= len (empty part) + const offset = Math.floor(Math.random() * 4096) const hash = ethers.utils.keccak256(bytes) let provenLen = 0 let provenPart = null let log = null while (provenPart === null) { - let nextPartialLen = 136 * (1 + Math.floor(Math.random() * 2)) + // chunks of 1 to 4 keccak blocks; the final chunk takes the remainder + let nextPartialLen = 136 * (1 + Math.floor(Math.random() * 4)) if (nextPartialLen > len - provenLen) { nextPartialLen = len - provenLen } @@ -75,15 +82,15 @@ describe('HashProofHelper', function () { provenLen = newProvenLen } - let dataHex = data.toString(16) - dataHex = '00'.slice(dataHex.length) + dataHex const partLen = Math.min(32, Math.max(0, len - offset)) - const partString = '0x' + dataHex.repeat(partLen) + const expectedPart = ethers.utils.hexlify( + bytes.slice(offset, offset + partLen) + ) assert.isNotNull(log) assert.equal(log!.args['fullHash'], hash) assert.equal(log!.args['offset'], offset) - assert.equal(log!.args['part'], partString) - assert.equal(provenPart, partString) + assert.equal(log!.args['part'], expectedPart) + assert.equal(provenPart, expectedPart) } }) }) From 944dd89884e033125d6e84f6e13c8620a145aaf6 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Fri, 12 Jun 2026 12:31:56 +0100 Subject: [PATCH 4/7] Generalize prover tests command --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 027b38bf0..e9d06c4d9 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "build:0.7": "INTERFACE_TESTER_SOLC_VERSION=0.7.0 yarn run build", "test": "DISABLE_GAS_REPORTER=true hardhat --network hardhat test test/contract/*.spec.ts", "test:4844": "DISABLE_GAS_REPORTER=true hardhat --network hardhat test test/contract/*.spec.4844.ts", - "test:prover": "DISABLE_GAS_REPORTER=true hardhat --network hardhat test test/prover/hash-proofs.ts", + "test:prover": "DISABLE_GAS_REPORTER=true hardhat --network hardhat test test/prover/*.ts", "test:compatibility": "yarn run build:0.6 && yarn run build:0.7", "test:storage": "./test/storage/test.bash", "test:signatures": "./test/signatures/test-sigs.bash", From 07c54039c21257dd4364bf7871960a633d82436c Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Mon, 15 Jun 2026 12:09:38 +0100 Subject: [PATCH 5/7] Minor fix --- test/contract/common/globalStateLib.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/contract/common/globalStateLib.ts b/test/contract/common/globalStateLib.ts index 513fa4cab..de2a8fb83 100644 --- a/test/contract/common/globalStateLib.ts +++ b/test/contract/common/globalStateLib.ts @@ -3,7 +3,7 @@ import { solidityKeccak256 } from 'ethers/lib/utils' export function hash(state: GlobalStateStruct) { return solidityKeccak256( - ['string', 'bytes32', 'bytes32', 'uint64', 'uint64'], + ['string', 'bytes32', 'bytes32', 'bytes32', 'bytes32', 'uint64', 'uint64'], [ 'Global state:', state.bytes32Vals[0], From 2a38770b7ae5e0ce896ce322f2c88de7756cee03 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Tue, 28 Jul 2026 22:00:18 +0100 Subject: [PATCH 6/7] Fix broken tests --- deploy/OneStepProverHostIoCreator.js | 7 +++++-- test/foundry/challengeV2/EdgeChallengeManagerLib.t.sol | 4 +++- test/foundry/challengeV2/StateTools.sol | 2 +- test/prover/one-step-proof.ts | 9 +-------- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/deploy/OneStepProverHostIoCreator.js b/deploy/OneStepProverHostIoCreator.js index 8174c09c0..f3df423da 100644 --- a/deploy/OneStepProverHostIoCreator.js +++ b/deploy/OneStepProverHostIoCreator.js @@ -5,9 +5,12 @@ module.exports = async hre => { await deploy('OneStepProverHostIo', { from: deployer, - args: [ethers.constants.AddressZero], + args: [ + ethers.constants.AddressZero, + (await deployments.get('HashProofHelper')).address, + ], }) } module.exports.tags = ['OneStepProverHostIo'] -module.exports.dependencies = [] +module.exports.dependencies = ['HashProofHelper'] diff --git a/test/foundry/challengeV2/EdgeChallengeManagerLib.t.sol b/test/foundry/challengeV2/EdgeChallengeManagerLib.t.sol index 4b07cd14f..36ab7db51 100644 --- a/test/foundry/challengeV2/EdgeChallengeManagerLib.t.sol +++ b/test/foundry/challengeV2/EdgeChallengeManagerLib.t.sol @@ -1612,7 +1612,9 @@ contract EdgeChallengeManagerLibTest is Test { ) private returns (ExecStateVars memory) { uint64 msgCount = uint64(uint256(rand.hash())); AssertionState memory assertionState = AssertionState( - GlobalState([rand.hash(), rand.hash(), rand.hash(), rand.hash()], [msgCount, msgCount]), + GlobalState( + [rand.hash(), rand.hash(), rand.hash(), rand.hash()], [0, 0, msgCount, msgCount] + ), MachineStatus.FINISHED, bytes32(0) ); diff --git a/test/foundry/challengeV2/StateTools.sol b/test/foundry/challengeV2/StateTools.sol index 689739908..151bda4d5 100644 --- a/test/foundry/challengeV2/StateTools.sol +++ b/test/foundry/challengeV2/StateTools.sol @@ -20,7 +20,7 @@ library StateToolsLib { MachineStatus ms ) internal returns (AssertionState memory) { bytes32[4] memory bytes32Vals = [blockHash, rand.hash(), rand.hash(), rand.hash()]; - uint64[2] memory u64Vals = [uint64(melMsgCount), uint64(melMsgCount)]; // We assume executedMsgCount == msgCount for simplicity + uint64[4] memory u64Vals = [0, 0, uint64(melMsgCount), uint64(melMsgCount)]; // We assume executedMsgCount == msgCount for simplicity GlobalState memory gs = GlobalState({bytes32Vals: bytes32Vals, u64Vals: u64Vals}); return AssertionState({globalState: gs, machineStatus: ms, endHistoryRoot: bytes32(0)}); diff --git a/test/prover/one-step-proof.ts b/test/prover/one-step-proof.ts index 3ff5238d1..14911e2d1 100644 --- a/test/prover/one-step-proof.ts +++ b/test/prover/one-step-proof.ts @@ -74,12 +74,6 @@ describe('OneStepProof', function () { await deployments.get('OneStepProofEntry') ).address ) - const bridge = await ethers.getContractAt( - 'BridgeStub', - ( - await deployments.get('BridgeStub') - ).address - ) const promises = [] const isdone = [] @@ -89,10 +83,9 @@ describe('OneStepProof', function () { ) const proof = proofs[i] isdone.push(false) - const inboxLimit = 1000000 const promise = osp .proveOneStep( - [inboxLimit, bridge.address], + [ethers.constants.HashZero, ethers.constants.HashZero], i, [...Buffer.from(proof.before, 'hex')], [...Buffer.from(proof.proof, 'hex')] From 5c103420b723ca776a370c9a0dfb57acc49d8b81 Mon Sep 17 00:00:00 2001 From: TucksonDev Date: Tue, 28 Jul 2026 22:32:02 +0100 Subject: [PATCH 7/7] Surface stakingPool tests --- test/{ => foundry}/ERC20Mock.sol | 0 test/{ => foundry}/MockAssertionChain.sol | 13 +- .../challengeV2/EdgeChallengeManager.t.sol | 4 +- .../challengeV2/EdgeChallengeManagerLib.t.sol | 2 +- .../stakingPool/AbsBoldStakingPool.t.sol | 4 +- .../stakingPool/AssertionStakingPool.t.sol | 166 ++++++++++-------- .../stakingPool/EdgeStakingPool.t.sol | 4 +- 7 files changed, 104 insertions(+), 89 deletions(-) rename test/{ => foundry}/ERC20Mock.sol (100%) rename test/{ => foundry}/MockAssertionChain.sol (94%) rename test/{ => foundry}/stakingPool/AbsBoldStakingPool.t.sol (98%) rename test/{ => foundry}/stakingPool/AssertionStakingPool.t.sol (69%) rename test/{ => foundry}/stakingPool/EdgeStakingPool.t.sol (96%) diff --git a/test/ERC20Mock.sol b/test/foundry/ERC20Mock.sol similarity index 100% rename from test/ERC20Mock.sol rename to test/foundry/ERC20Mock.sol diff --git a/test/MockAssertionChain.sol b/test/foundry/MockAssertionChain.sol similarity index 94% rename from test/MockAssertionChain.sol rename to test/foundry/MockAssertionChain.sol index c1cf4d7b5..7ee7e1461 100644 --- a/test/MockAssertionChain.sol +++ b/test/foundry/MockAssertionChain.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import {IAssertionChain} from "../src/challengeV2/IAssertionChain.sol"; -import "../src/bridge/IBridge.sol"; -import "../src/rollup/RollupLib.sol"; -import "./foundry/challengeV2/StateTools.sol"; +import {IAssertionChain} from "../../src/challengeV2/IAssertionChain.sol"; +import "../../src/bridge/IBridge.sol"; +import "../../src/rollup/RollupLib.sol"; +import "./challengeV2/StateTools.sol"; struct MockAssertion { bytes32 predecessorId; @@ -98,10 +98,7 @@ contract MockAssertionChain is IAssertionChain { bytes32 predecessorId, AssertionState memory afterState ) public pure returns (bytes32) { - return RollupLib.assertionHash({ - parentAssertionHash: predecessorId, - afterState: afterState - }); + return RollupLib.assertionHash({parentAssertionHash: predecessorId, afterState: afterState}); } function childCreated( diff --git a/test/foundry/challengeV2/EdgeChallengeManager.t.sol b/test/foundry/challengeV2/EdgeChallengeManager.t.sol index 6ccc17b5b..1222c2987 100644 --- a/test/foundry/challengeV2/EdgeChallengeManager.t.sol +++ b/test/foundry/challengeV2/EdgeChallengeManager.t.sol @@ -6,11 +6,11 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; import "./Utils.sol"; -import "../../MockAssertionChain.sol"; +import "../MockAssertionChain.sol"; import "../../../src/challengeV2/EdgeChallengeManager.sol"; import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; -import "../../ERC20Mock.sol"; +import "../ERC20Mock.sol"; import "./StateTools.sol"; contract MockOneStepProofEntry is IOneStepProofEntry { diff --git a/test/foundry/challengeV2/EdgeChallengeManagerLib.t.sol b/test/foundry/challengeV2/EdgeChallengeManagerLib.t.sol index 36ab7db51..a63ff37dd 100644 --- a/test/foundry/challengeV2/EdgeChallengeManagerLib.t.sol +++ b/test/foundry/challengeV2/EdgeChallengeManagerLib.t.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import "../../MockAssertionChain.sol"; +import "../MockAssertionChain.sol"; import "../../../src/challengeV2/EdgeChallengeManager.sol"; import "./Utils.sol"; diff --git a/test/stakingPool/AbsBoldStakingPool.t.sol b/test/foundry/stakingPool/AbsBoldStakingPool.t.sol similarity index 98% rename from test/stakingPool/AbsBoldStakingPool.t.sol rename to test/foundry/stakingPool/AbsBoldStakingPool.t.sol index b0862d8c1..e74bb5da7 100644 --- a/test/stakingPool/AbsBoldStakingPool.t.sol +++ b/test/foundry/stakingPool/AbsBoldStakingPool.t.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import "../../src/assertionStakingPool/AbsBoldStakingPool.sol"; -import "../../src/mocks/TestWETH9.sol"; +import "../../../src/assertionStakingPool/AbsBoldStakingPool.sol"; +import "../../../src/mocks/TestWETH9.sol"; contract FundsHolder { function withdraw(IERC20 stakeToken, uint256 amount) external { diff --git a/test/stakingPool/AssertionStakingPool.t.sol b/test/foundry/stakingPool/AssertionStakingPool.t.sol similarity index 69% rename from test/stakingPool/AssertionStakingPool.t.sol rename to test/foundry/stakingPool/AssertionStakingPool.t.sol index 26f703099..3f7316e99 100644 --- a/test/stakingPool/AssertionStakingPool.t.sol +++ b/test/foundry/stakingPool/AssertionStakingPool.t.sol @@ -3,33 +3,35 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; -import "../../src/rollup/RollupProxy.sol"; +import "../../../src/rollup/RollupProxy.sol"; -import "../../src/rollup/RollupCore.sol"; -import "../../src/rollup/RollupUserLogic.sol"; -import "../../src/rollup/RollupAdminLogic.sol"; -import "../../src/rollup/RollupCreator.sol"; +import "../../../src/rollup/RollupCore.sol"; +import "../../../src/rollup/RollupUserLogic.sol"; +import "../../../src/rollup/RollupAdminLogic.sol"; +import "../../../src/rollup/RollupCreator.sol"; -import "../../src/osp/OneStepProver0.sol"; -import "../../src/osp/OneStepProverMemory.sol"; -import "../../src/osp/OneStepProverMath.sol"; -import "../../src/osp/OneStepProverHostIo.sol"; -import "../../src/osp/OneStepProofEntry.sol"; -import "../../src/challengeV2/EdgeChallengeManager.sol"; -import "../foundry/challengeV2/Utils.sol"; +import "../../../src/osp/OneStepProver0.sol"; +import "../../../src/osp/OneStepProverMemory.sol"; +import "../../../src/osp/OneStepProverMath.sol"; +import "../../../src/osp/OneStepProverHostIo.sol"; +import "../../../src/osp/OneStepProofEntry.sol"; +import "../../../src/challengeV2/EdgeChallengeManager.sol"; +import "../challengeV2/Utils.sol"; -import "../../src/libraries/Error.sol"; +import "../../../src/libraries/Error.sol"; -import "../../src/mocks/TestWETH9.sol"; -import "../../src/mocks/UpgradeExecutorMock.sol"; +import "../../../src/mocks/TestWETH9.sol"; +import "../../../src/mocks/UpgradeExecutorMock.sol"; -import "../../src/assertionStakingPool/AssertionStakingPool.sol"; -import "../../src/assertionStakingPool/AssertionStakingPoolCreator.sol"; +import "../../../src/assertionStakingPool/AssertionStakingPool.sol"; +import "../../../src/assertionStakingPool/AssertionStakingPoolCreator.sol"; import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; contract AssertionPoolTest is Test { + using MELStateLib for MELState; + address constant owner = address(1337); address constant sequencer = address(7331); @@ -39,8 +41,11 @@ contract AssertionPoolTest is Test { uint256 constant MAX_DATA_SIZE = 117964; uint64 constant CHALLENGE_GRACE_PERIOD_BLOCKS = 10; + uint64 constant INITIAL_MSG_COUNT = 1; bytes32 constant FIRST_ASSERTION_BLOCKHASH = keccak256("FIRST_ASSERTION_BLOCKHASH"); bytes32 constant FIRST_ASSERTION_SENDROOT = keccak256("FIRST_ASSERTION_SENDROOT"); + bytes32 constant FIRST_ASSERTION_PARENT_CHAIN_BLOCKHASH = + keccak256("FIRST_ASSERTION_PARENT_CHAIN_BLOCKHASH"); IERC20 token; RollupUserLogic userRollup; @@ -50,12 +55,12 @@ contract AssertionPoolTest is Test { GlobalState emptyGlobalState; AssertionState emptyAssertionState = AssertionState(emptyGlobalState, MachineStatus.FINISHED, bytes32(0)); - bytes32 genesisHash = RollupLib.assertionHash({ - parentAssertionHash: bytes32(0), - afterState: emptyAssertionState, - inboxAcc: bytes32(0) - }); + bytes32 genesisHash = + RollupLib.assertionHash({parentAssertionHash: bytes32(0), afterState: emptyAssertionState}); AssertionState firstState; + MELState firstMELState; + uint64 firstAssertionParentChainBlockNumber; + bytes32 firstAssertionParentChainBlockHash; IAssertionStakingPool pool; @@ -74,9 +79,9 @@ contract AssertionPoolTest is Test { address rollupAddr; AssertionInputs assertionInputs; - bytes32 assertionHash; + bytes32 expectedAssertionHash; AssertionState afterState; - uint64 inboxcount; + MELState afterMELState; address upgradeExecutorAddr; event RollupCreated( @@ -115,17 +120,27 @@ contract AssertionPoolTest is Test { OneStepProver0 oneStepProver = new OneStepProver0(); OneStepProverMemory oneStepProverMemory = new OneStepProverMemory(); OneStepProverMath oneStepProverMath = new OneStepProverMath(); - OneStepProverHostIo oneStepProverHostIo = new OneStepProverHostIo(address(0)); + OneStepProverHostIo oneStepProverHostIo = new OneStepProverHostIo(address(0), address(0)); OneStepProofEntry oneStepProofEntry = new OneStepProofEntry( oneStepProver, oneStepProverMemory, oneStepProverMath, oneStepProverHostIo ); EdgeChallengeManager edgeChallengeManager = new EdgeChallengeManager(); BridgeCreator bridgeCreator = new BridgeCreator(ethBasedTemplates, erc20BasedTemplates); - RollupCreator rollupCreator = new RollupCreator(); RollupAdminLogic rollupAdminLogicImpl = new RollupAdminLogic(); RollupUserLogic rollupUserLogicImpl = new RollupUserLogic(); DeployHelper deployHelper = new DeployHelper(); IUpgradeExecutor upgradeExecutorLogic = new UpgradeExecutorMock(); + RollupCreator rollupCreator = new RollupCreator( + address(this), + bridgeCreator, + oneStepProofEntry, + edgeChallengeManager, + rollupAdminLogicImpl, + rollupUserLogicImpl, + upgradeExecutorLogic, + address(0), + deployHelper + ); rollupCreator.setTemplates( bridgeCreator, @@ -137,11 +152,7 @@ contract AssertionPoolTest is Test { address(0), deployHelper ); - AssertionState memory emptyState = AssertionState( - GlobalState([bytes32(0), bytes32(0)], [uint64(0), uint64(0)]), - MachineStatus.FINISHED, - bytes32(0) - ); + AssertionState memory genesisAssertionState = emptyAssertionState; token = new TestWETH9("Test", "TEST"); IWETH9(address(token)).deposit{value: 21 ether}(); @@ -167,8 +178,7 @@ contract AssertionPoolTest is Test { stakeToken: address(token), wasmModuleRoot: WASM_MODULE_ROOT, loserStakeEscrow: address(200001), - genesisAssertionState: emptyState, - genesisInboxCount: 0, + genesisAssertionState: genesisAssertionState, miniStakeValues: miniStakeValues, layerZeroBlockEdgeHeight: 2 ** 5, layerZeroBigStepEdgeHeight: 2 ** 5, @@ -217,49 +227,44 @@ contract AssertionPoolTest is Test { adminRollup.sequencerInbox().setIsBatchPoster(sequencer, true); vm.stopPrank(); - firstState.machineStatus = MachineStatus.FINISHED; - firstState.globalState.bytes32Vals[0] = FIRST_ASSERTION_BLOCKHASH; // blockhash - firstState.globalState.bytes32Vals[1] = FIRST_ASSERTION_SENDROOT; // sendroot - firstState.globalState.u64Vals[0] = 1; // inbox count - firstState.globalState.u64Vals[1] = 0; // pos in msg + // store the parent chain block information to be used in the next assertion + // (must be consistent with the the implementation of `initialize` in RollupAdminLogic) + firstAssertionParentChainBlockNumber = uint64(block.number - 1); + firstAssertionParentChainBlockHash = blockhash(block.number - 1); + + // First assertion to create after the genesis assertion + (firstState, firstMELState) = _mockAssertionState(); vm.roll(block.number + 75); - inboxcount = uint64(_createNewBatch()); AssertionState memory beforeState; beforeState.machineStatus = MachineStatus.FINISHED; - afterState.machineStatus = MachineStatus.FINISHED; - afterState.globalState.bytes32Vals[0] = FIRST_ASSERTION_BLOCKHASH; // blockhash - afterState.globalState.bytes32Vals[1] = FIRST_ASSERTION_SENDROOT; // sendroot - afterState.globalState.u64Vals[0] = 1; // inbox count - afterState.globalState.u64Vals[1] = 0; // pos in msg - - assertionHash = RollupLib.assertionHash({ - parentAssertionHash: genesisHash, - afterState: afterState, - inboxAcc: userRollup.bridge().sequencerInboxAccs(0) - }); + afterState = firstState; + afterMELState = firstMELState; + + expectedAssertionHash = + RollupLib.assertionHash({parentAssertionHash: genesisHash, afterState: afterState}); assertionInputs = AssertionInputs({ beforeStateData: BeforeStateData({ - sequencerBatchAcc: bytes32(0), prevPrevAssertionHash: bytes32(0), configData: ConfigData({ wasmModuleRoot: WASM_MODULE_ROOT, requiredStake: BASE_STAKE, challengeManager: address(challengeManager), confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, - nextInboxPosition: afterState.globalState.u64Vals[0] + nextParentChainBlockHash: firstAssertionParentChainBlockHash }) }), beforeState: beforeState, - afterState: afterState + afterState: afterState, + afterMELState: afterMELState }); aspcreator = new AssertionStakingPoolCreator(); vm.expectRevert(abi.encodeWithSelector(IAssertionStakingPool.EmptyAssertionId.selector)); pool = aspcreator.createPool(address(rollupAddr), bytes32(0)); - pool = aspcreator.createPool(address(rollupAddr), assertionHash); + pool = aspcreator.createPool(address(rollupAddr), expectedAssertionHash); token.transfer(staker1, staker1Bal); token.transfer(staker2, staker2Bal); @@ -283,26 +288,41 @@ contract AssertionPoolTest is Test { adminRollup.setValidatorWhitelistDisabled(true); } - function _createNewBatch() internal returns (uint256) { - uint256 count = userRollup.bridge().sequencerMessageCount(); - vm.startPrank(sequencer); - userRollup.sequencerInbox().addSequencerL2Batch({ - sequenceNumber: count, - data: "", - afterDelayedMessagesRead: 1, - gasRefunder: IGasRefunder(address(0)), - prevMessageCount: 0, - newMessageCount: 0 - }); - vm.stopPrank(); - assertEq(userRollup.bridge().sequencerMessageCount(), ++count); - return count; + function _mockAssertionState() internal view returns (AssertionState memory, MELState memory) { + MELState memory melState; + melState.version = 0; + melState.parentChainId = uint64(block.chainid); + melState.parentChainBlockNumber = firstAssertionParentChainBlockNumber; + melState.batchPostingTargetAddress = address(0); + melState.delayedMessagePostingTargetAddress = address(0); + melState.parentChainBlockHash = firstAssertionParentChainBlockHash; + melState.parentChainPreviousBlockHash = bytes32(0); + melState.batchCount = 1; + melState.msgCount = INITIAL_MSG_COUNT; + melState.localMsgAccumulator = bytes32(0); + melState.delayedMessagesRead = INITIAL_MSG_COUNT; + melState.delayedMessagesSeen = INITIAL_MSG_COUNT; + melState.delayedMessageInboxAcc = bytes32(0); + melState.delayedMessageOutboxAcc = bytes32(0); + + AssertionState memory assertionState; + assertionState.machineStatus = MachineStatus.FINISHED; + assertionState.globalState.bytes32Vals[0] = FIRST_ASSERTION_BLOCKHASH; // Blockhash + assertionState.globalState.bytes32Vals[1] = FIRST_ASSERTION_SENDROOT; // Sendroot + assertionState.globalState.bytes32Vals[2] = melState.hash(); // MELState hash + assertionState.globalState.bytes32Vals[3] = bytes32(0); // MEL NextMsgHash + assertionState.globalState.u64Vals[0] = 0; // InboxPosition (deprecated) + assertionState.globalState.u64Vals[1] = 0; // PositionInMessage (deprecated) + assertionState.globalState.u64Vals[2] = INITIAL_MSG_COUNT; // MsgCount + assertionState.globalState.u64Vals[3] = INITIAL_MSG_COUNT; // ExecutedMsgCount + + return (assertionState, melState); } function testGetPool() external { assertEq( address(pool), - address(aspcreator.getPool(rollupAddr, assertionHash)), + address(aspcreator.getPool(rollupAddr, expectedAssertionHash)), "getPool returns created pool's expected address" ); } @@ -353,9 +373,8 @@ contract AssertionPoolTest is Test { function _createAndConfirmAssertion() internal { _createAssertion(); vm.roll(userRollup.getAssertion(genesisHash).firstChildBlock + CONFIRM_PERIOD_BLOCKS + 1); - bytes32 inboxAccs = userRollup.bridge().sequencerInboxAccs(0); userRollup.confirmAssertion( - assertionHash, + expectedAssertionHash, genesisHash, firstState, bytes32(0), @@ -364,9 +383,8 @@ contract AssertionPoolTest is Test { requiredStake: BASE_STAKE, challengeManager: address(challengeManager), confirmPeriodBlocks: CONFIRM_PERIOD_BLOCKS, - nextInboxPosition: firstState.globalState.u64Vals[0] - }), - inboxAccs + nextParentChainBlockHash: firstAssertionParentChainBlockHash + }) ); } diff --git a/test/stakingPool/EdgeStakingPool.t.sol b/test/foundry/stakingPool/EdgeStakingPool.t.sol similarity index 96% rename from test/stakingPool/EdgeStakingPool.t.sol rename to test/foundry/stakingPool/EdgeStakingPool.t.sol index fff7da062..1adc07cbc 100644 --- a/test/stakingPool/EdgeStakingPool.t.sol +++ b/test/foundry/stakingPool/EdgeStakingPool.t.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "../ERC20Mock.sol"; -import "../../src/assertionStakingPool/EdgeStakingPoolCreator.sol"; -import "../../src/challengeV2/EdgeChallengeManager.sol"; +import "../../../src/assertionStakingPool/EdgeStakingPoolCreator.sol"; +import "../../../src/challengeV2/EdgeChallengeManager.sol"; contract MockChallengeManager { IERC20 public immutable stakeToken;