Commit 93e9d153 authored by clabby's avatar clabby Committed by GitHub

feat(ctb): Add new move type to FDG for OR counters (#10438)

* feat(ctb): Add new move type to FDG for OR counters

Adds a potential new move type to the `FaultDisputeGame` that allows for
a participant to reveal the preimage of the claimed output root to
display to the dispute game that the claimed L2 block number does not
match up with the block number that the block header within the output
root commits to.

The root output can be challenged with the new special move type iff:
1. The passed `OutputRootProof` hashes to equal the claimed output root.
1. The passed Header RLP hashes to equal the block hash within the
`OutputRootProof` above.
1. The claimed block number in the dispute game does not equal the block
number that the output root commits to.

If there is a successful challenge with the new move type, that claim
itself is inserted as a special case counter. In `resolveClaim`, the
contract will always consider the creator of the L2 block challenge the
winner of the bond. Notably, this only applies for the root claim
subgame.

* feat(ctb): Transition `RLPReader` to 4byte errors (#10439)

* feat(ctb): Transition `RLPReader` to 4byte errors

* semver

* update summary

* portal semver

* extra checks
parent 84723638
...@@ -8,4 +8,4 @@ GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (g ...@@ -8,4 +8,4 @@ GasBenchMark_L1StandardBridge_Finalize:test_finalizeETHWithdrawal_benchmark() (g
GasBenchMark_L2OutputOracle:test_proposeL2Output_benchmark() (gas: 92973) GasBenchMark_L2OutputOracle:test_proposeL2Output_benchmark() (gas: 92973)
GasBenchMark_OptimismPortal:test_depositTransaction_benchmark() (gas: 68453) GasBenchMark_OptimismPortal:test_depositTransaction_benchmark() (gas: 68453)
GasBenchMark_OptimismPortal:test_depositTransaction_benchmark_1() (gas: 68923) GasBenchMark_OptimismPortal:test_depositTransaction_benchmark_1() (gas: 68923)
GasBenchMark_OptimismPortal:test_proveWithdrawalTransaction_benchmark() (gas: 155612) GasBenchMark_OptimismPortal:test_proveWithdrawalTransaction_benchmark() (gas: 155618)
\ No newline at end of file \ No newline at end of file
...@@ -32,12 +32,12 @@ ...@@ -32,12 +32,12 @@
"sourceCodeHash": "0xf5fcf570721e25459fadbb37e02f9efe349e1c8afcbf1e3b5fdb09c9f612cdc0" "sourceCodeHash": "0xf5fcf570721e25459fadbb37e02f9efe349e1c8afcbf1e3b5fdb09c9f612cdc0"
}, },
"src/L1/OptimismPortal.sol": { "src/L1/OptimismPortal.sol": {
"initCodeHash": "0xc820f7434345b665a77bc17e8b486ba97b7ff9845d336a51f024a41599c4c155", "initCodeHash": "0xbb8e7b110d3aa0f4807cd790174d6fb6d98c44348c977007ad7a11aa7053f1c5",
"sourceCodeHash": "0x3d0e9dc8ed75b6f2e59444e9204341b18d343af02d6e37ecd9f759da2c8c118b" "sourceCodeHash": "0x3d0e9dc8ed75b6f2e59444e9204341b18d343af02d6e37ecd9f759da2c8c118b"
}, },
"src/L1/OptimismPortal2.sol": { "src/L1/OptimismPortal2.sol": {
"initCodeHash": "0x6bc8659932ab7eed221880a1d606704a0446d80cf4826f13623fc6cc57053751", "initCodeHash": "0x45cae622788a795c2fc4f4bc8e6b85d8edf284a1dc20e1b5fa01e88d737deb23",
"sourceCodeHash": "0x411ad7aea4d36d7a89ecb2f0dc2c6c200e5fb022b528b3beb1e1b37c557bc7dc" "sourceCodeHash": "0xea564dbff9831ad1bf0c1b345fbc3da4675cf112d2605ba94e1ef5c7b745b7ae"
}, },
"src/L1/ProtocolVersions.sol": { "src/L1/ProtocolVersions.sol": {
"initCodeHash": "0x72cd467e8bcf019c02675d72ab762e088bcc9cc0f1a4e9f587fa4589f7fdd1b8", "initCodeHash": "0x72cd467e8bcf019c02675d72ab762e088bcc9cc0f1a4e9f587fa4589f7fdd1b8",
...@@ -128,8 +128,8 @@ ...@@ -128,8 +128,8 @@
"sourceCodeHash": "0x918c395ac5d77357f2551616aad0613e68893862edd14e554623eb16ee6ba148" "sourceCodeHash": "0x918c395ac5d77357f2551616aad0613e68893862edd14e554623eb16ee6ba148"
}, },
"src/dispute/FaultDisputeGame.sol": { "src/dispute/FaultDisputeGame.sol": {
"initCodeHash": "0x8398caaff1da5d81730c95104c15b14c2fb7ff394bab005d9ec77372a2b1f5ca", "initCodeHash": "0x165b86287737cc00981215510f8f5fcd9197c953cefe14aa5d355fa2c88d257e",
"sourceCodeHash": "0x5888aea16645a18ce54032c1787644afcdf07c6df2b7c6546caa957a047f03fc" "sourceCodeHash": "0x5f1d3e7cbb1a705ad3b297313178bd7a8acfa2ddf1bde68be1b0b5468942d003"
}, },
"src/dispute/weth/DelayedWETH.sol": { "src/dispute/weth/DelayedWETH.sol": {
"initCodeHash": "0xb9bbe005874922cd8f499e7a0a092967cfca03e012c1e41912b0c77481c71777", "initCodeHash": "0xb9bbe005874922cd8f499e7a0a092967cfca03e012c1e41912b0c77481c71777",
......
...@@ -122,6 +122,46 @@ ...@@ -122,6 +122,46 @@
"stateMutability": "payable", "stateMutability": "payable",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"components": [
{
"internalType": "bytes32",
"name": "version",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "stateRoot",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "messagePasserStorageRoot",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "latestBlockhash",
"type": "bytes32"
}
],
"internalType": "struct Types.OutputRootProof",
"name": "_outputRootProof",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "_headerRLP",
"type": "bytes"
}
],
"name": "challengeRootL2Block",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -431,6 +471,19 @@ ...@@ -431,6 +471,19 @@
"stateMutability": "pure", "stateMutability": "pure",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "l2BlockNumberChallenged",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "l2ChainId", "name": "l2ChainId",
...@@ -812,6 +865,11 @@ ...@@ -812,6 +865,11 @@
"name": "AnchorRootNotFound", "name": "AnchorRootNotFound",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "BlockNumberMatches",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "BondTransferFailed", "name": "BondTransferFailed",
...@@ -847,11 +905,21 @@ ...@@ -847,11 +905,21 @@
"name": "ClockTimeExceeded", "name": "ClockTimeExceeded",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "ContentLengthMismatch",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "DuplicateStep", "name": "DuplicateStep",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "EmptyItem",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "GameDepthExceeded", "name": "GameDepthExceeded",
...@@ -872,11 +940,31 @@ ...@@ -872,11 +940,31 @@
"name": "InvalidClockExtension", "name": "InvalidClockExtension",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "InvalidDataRemainder",
"type": "error"
},
{
"inputs": [],
"name": "InvalidHeader",
"type": "error"
},
{
"inputs": [],
"name": "InvalidHeaderRLP",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "InvalidLocalIdent", "name": "InvalidLocalIdent",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "InvalidOutputRootProof",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "InvalidParent", "name": "InvalidParent",
...@@ -892,6 +980,11 @@ ...@@ -892,6 +980,11 @@
"name": "InvalidSplitDepth", "name": "InvalidSplitDepth",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "L2BlockNumberChallenged",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "MaxDepthTooLarge", "name": "MaxDepthTooLarge",
...@@ -907,6 +1000,11 @@ ...@@ -907,6 +1000,11 @@
"name": "OutOfOrderResolution", "name": "OutOfOrderResolution",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "UnexpectedList",
"type": "error"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -918,6 +1016,11 @@ ...@@ -918,6 +1016,11 @@
"name": "UnexpectedRootClaim", "name": "UnexpectedRootClaim",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "UnexpectedString",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "ValidStep", "name": "ValidStep",
......
...@@ -575,11 +575,31 @@ ...@@ -575,11 +575,31 @@
"name": "CallPaused", "name": "CallPaused",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "ContentLengthMismatch",
"type": "error"
},
{
"inputs": [],
"name": "EmptyItem",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "GasEstimation", "name": "GasEstimation",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "InvalidDataRemainder",
"type": "error"
},
{
"inputs": [],
"name": "InvalidHeader",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "LargeCalldata", "name": "LargeCalldata",
...@@ -619,5 +639,15 @@ ...@@ -619,5 +639,15 @@
"inputs": [], "inputs": [],
"name": "Unauthorized", "name": "Unauthorized",
"type": "error" "type": "error"
},
{
"inputs": [],
"name": "UnexpectedList",
"type": "error"
},
{
"inputs": [],
"name": "UnexpectedString",
"type": "error"
} }
] ]
\ No newline at end of file
...@@ -752,11 +752,31 @@ ...@@ -752,11 +752,31 @@
"name": "CallPaused", "name": "CallPaused",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "ContentLengthMismatch",
"type": "error"
},
{
"inputs": [],
"name": "EmptyItem",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "GasEstimation", "name": "GasEstimation",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "InvalidDataRemainder",
"type": "error"
},
{
"inputs": [],
"name": "InvalidHeader",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "LargeCalldata", "name": "LargeCalldata",
...@@ -776,5 +796,15 @@ ...@@ -776,5 +796,15 @@
"inputs": [], "inputs": [],
"name": "Unauthorized", "name": "Unauthorized",
"type": "error" "type": "error"
},
{
"inputs": [],
"name": "UnexpectedList",
"type": "error"
},
{
"inputs": [],
"name": "UnexpectedString",
"type": "error"
} }
] ]
\ No newline at end of file
...@@ -132,6 +132,46 @@ ...@@ -132,6 +132,46 @@
"stateMutability": "payable", "stateMutability": "payable",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"components": [
{
"internalType": "bytes32",
"name": "version",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "stateRoot",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "messagePasserStorageRoot",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "latestBlockhash",
"type": "bytes32"
}
],
"internalType": "struct Types.OutputRootProof",
"name": "_outputRootProof",
"type": "tuple"
},
{
"internalType": "bytes",
"name": "_headerRLP",
"type": "bytes"
}
],
"name": "challengeRootL2Block",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "challenger", "name": "challenger",
...@@ -454,6 +494,19 @@ ...@@ -454,6 +494,19 @@
"stateMutability": "pure", "stateMutability": "pure",
"type": "function" "type": "function"
}, },
{
"inputs": [],
"name": "l2BlockNumberChallenged",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [], "inputs": [],
"name": "l2ChainId", "name": "l2ChainId",
...@@ -853,6 +906,11 @@ ...@@ -853,6 +906,11 @@
"name": "BadAuth", "name": "BadAuth",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "BlockNumberMatches",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "BondTransferFailed", "name": "BondTransferFailed",
...@@ -888,11 +946,21 @@ ...@@ -888,11 +946,21 @@
"name": "ClockTimeExceeded", "name": "ClockTimeExceeded",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "ContentLengthMismatch",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "DuplicateStep", "name": "DuplicateStep",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "EmptyItem",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "GameDepthExceeded", "name": "GameDepthExceeded",
...@@ -913,11 +981,31 @@ ...@@ -913,11 +981,31 @@
"name": "InvalidClockExtension", "name": "InvalidClockExtension",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "InvalidDataRemainder",
"type": "error"
},
{
"inputs": [],
"name": "InvalidHeader",
"type": "error"
},
{
"inputs": [],
"name": "InvalidHeaderRLP",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "InvalidLocalIdent", "name": "InvalidLocalIdent",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "InvalidOutputRootProof",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "InvalidParent", "name": "InvalidParent",
...@@ -933,6 +1021,11 @@ ...@@ -933,6 +1021,11 @@
"name": "InvalidSplitDepth", "name": "InvalidSplitDepth",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "L2BlockNumberChallenged",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "MaxDepthTooLarge", "name": "MaxDepthTooLarge",
...@@ -948,6 +1041,11 @@ ...@@ -948,6 +1041,11 @@
"name": "OutOfOrderResolution", "name": "OutOfOrderResolution",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "UnexpectedList",
"type": "error"
},
{ {
"inputs": [ "inputs": [
{ {
...@@ -959,6 +1057,11 @@ ...@@ -959,6 +1057,11 @@
"name": "UnexpectedRootClaim", "name": "UnexpectedRootClaim",
"type": "error" "type": "error"
}, },
{
"inputs": [],
"name": "UnexpectedString",
"type": "error"
},
{ {
"inputs": [], "inputs": [],
"name": "ValidStep", "name": "ValidStep",
......
...@@ -27,53 +27,67 @@ ...@@ -27,53 +27,67 @@
"slot": "0", "slot": "0",
"type": "bool" "type": "bool"
}, },
{
"bytes": "1",
"label": "l2BlockNumberChallenged",
"offset": 18,
"slot": "0",
"type": "bool"
},
{
"bytes": "20",
"label": "l2BlockNumberChallenger",
"offset": 0,
"slot": "1",
"type": "address"
},
{ {
"bytes": "32", "bytes": "32",
"label": "claimData", "label": "claimData",
"offset": 0, "offset": 0,
"slot": "1", "slot": "2",
"type": "struct IFaultDisputeGame.ClaimData[]" "type": "struct IFaultDisputeGame.ClaimData[]"
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "credit", "label": "credit",
"offset": 0, "offset": 0,
"slot": "2", "slot": "3",
"type": "mapping(address => uint256)" "type": "mapping(address => uint256)"
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "claims", "label": "claims",
"offset": 0, "offset": 0,
"slot": "3", "slot": "4",
"type": "mapping(Hash => bool)" "type": "mapping(Hash => bool)"
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "subgames", "label": "subgames",
"offset": 0, "offset": 0,
"slot": "4", "slot": "5",
"type": "mapping(uint256 => uint256[])" "type": "mapping(uint256 => uint256[])"
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "resolvedSubgames", "label": "resolvedSubgames",
"offset": 0, "offset": 0,
"slot": "5", "slot": "6",
"type": "mapping(uint256 => bool)" "type": "mapping(uint256 => bool)"
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "resolutionCheckpoints", "label": "resolutionCheckpoints",
"offset": 0, "offset": 0,
"slot": "6", "slot": "7",
"type": "mapping(uint256 => struct IFaultDisputeGame.ResolutionCheckpoint)" "type": "mapping(uint256 => struct IFaultDisputeGame.ResolutionCheckpoint)"
}, },
{ {
"bytes": "64", "bytes": "64",
"label": "startingOutputRoot", "label": "startingOutputRoot",
"offset": 0, "offset": 0,
"slot": "7", "slot": "8",
"type": "struct OutputRoot" "type": "struct OutputRoot"
} }
] ]
\ No newline at end of file
...@@ -27,53 +27,67 @@ ...@@ -27,53 +27,67 @@
"slot": "0", "slot": "0",
"type": "bool" "type": "bool"
}, },
{
"bytes": "1",
"label": "l2BlockNumberChallenged",
"offset": 18,
"slot": "0",
"type": "bool"
},
{
"bytes": "20",
"label": "l2BlockNumberChallenger",
"offset": 0,
"slot": "1",
"type": "address"
},
{ {
"bytes": "32", "bytes": "32",
"label": "claimData", "label": "claimData",
"offset": 0, "offset": 0,
"slot": "1", "slot": "2",
"type": "struct IFaultDisputeGame.ClaimData[]" "type": "struct IFaultDisputeGame.ClaimData[]"
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "credit", "label": "credit",
"offset": 0, "offset": 0,
"slot": "2", "slot": "3",
"type": "mapping(address => uint256)" "type": "mapping(address => uint256)"
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "claims", "label": "claims",
"offset": 0, "offset": 0,
"slot": "3", "slot": "4",
"type": "mapping(Hash => bool)" "type": "mapping(Hash => bool)"
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "subgames", "label": "subgames",
"offset": 0, "offset": 0,
"slot": "4", "slot": "5",
"type": "mapping(uint256 => uint256[])" "type": "mapping(uint256 => uint256[])"
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "resolvedSubgames", "label": "resolvedSubgames",
"offset": 0, "offset": 0,
"slot": "5", "slot": "6",
"type": "mapping(uint256 => bool)" "type": "mapping(uint256 => bool)"
}, },
{ {
"bytes": "32", "bytes": "32",
"label": "resolutionCheckpoints", "label": "resolutionCheckpoints",
"offset": 0, "offset": 0,
"slot": "6", "slot": "7",
"type": "mapping(uint256 => struct IFaultDisputeGame.ResolutionCheckpoint)" "type": "mapping(uint256 => struct IFaultDisputeGame.ResolutionCheckpoint)"
}, },
{ {
"bytes": "64", "bytes": "64",
"label": "startingOutputRoot", "label": "startingOutputRoot",
"offset": 0, "offset": 0,
"slot": "7", "slot": "8",
"type": "struct OutputRoot" "type": "struct OutputRoot"
} }
] ]
\ No newline at end of file
...@@ -140,8 +140,8 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { ...@@ -140,8 +140,8 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver {
} }
/// @notice Semantic version. /// @notice Semantic version.
/// @custom:semver 3.9.0 /// @custom:semver 3.10.0
string public constant version = "3.9.0"; string public constant version = "3.10.0";
/// @notice Constructs the OptimismPortal contract. /// @notice Constructs the OptimismPortal contract.
constructor(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) { constructor(uint256 _proofMaturityDelaySeconds, uint256 _disputeGameFinalityDelaySeconds) {
......
...@@ -14,6 +14,9 @@ import { Clone } from "@solady/utils/Clone.sol"; ...@@ -14,6 +14,9 @@ import { Clone } from "@solady/utils/Clone.sol";
import { Types } from "src/libraries/Types.sol"; import { Types } from "src/libraries/Types.sol";
import { ISemver } from "src/universal/ISemver.sol"; import { ISemver } from "src/universal/ISemver.sol";
import { Types } from "src/libraries/Types.sol";
import { Hashing } from "src/libraries/Hashing.sol";
import { RLPReader } from "src/libraries/rlp/RLPReader.sol";
import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Types.sol";
import "src/dispute/lib/Errors.sol"; import "src/dispute/lib/Errors.sol";
...@@ -60,9 +63,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -60,9 +63,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
/// @notice The global root claim's position is always at gindex 1. /// @notice The global root claim's position is always at gindex 1.
Position internal constant ROOT_POSITION = Position.wrap(1); Position internal constant ROOT_POSITION = Position.wrap(1);
/// @notice The index of the block number in the RLP-encoded block header.
/// @dev Consensus encoding reference:
/// https://github.com/paradigmxyz/reth/blob/5f82993c23164ce8ccdc7bf3ae5085205383a5c8/crates/primitives/src/header.rs#L368
uint256 internal constant HEADER_BLOCK_NUMBER_INDEX = 8;
/// @notice Semantic version. /// @notice Semantic version.
/// @custom:semver 1.0.0 /// @custom:semver 1.1.0
string public constant version = "1.0.0"; string public constant version = "1.1.0";
/// @notice The starting timestamp of the game /// @notice The starting timestamp of the game
Timestamp public createdAt; Timestamp public createdAt;
...@@ -76,6 +84,13 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -76,6 +84,13 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
/// @notice Flag for the `initialize` function to prevent re-initialization. /// @notice Flag for the `initialize` function to prevent re-initialization.
bool internal initialized; bool internal initialized;
/// @notice Flag for whether or not the L2 block number claim has been invalidated via `challengeRootL2Block`.
bool public l2BlockNumberChallenged;
/// @notice The challenger of the L2 block number claim. Should always be `address(0)` if `l2BlockNumberChallenged`
/// is `false`. Should be the address of the challenger if `l2BlockNumberChallenged` is `true`.
address internal l2BlockNumberChallenger;
/// @notice An append-only array of all claims made during the dispute game. /// @notice An append-only array of all claims made during the dispute game.
ClaimData[] public claimData; ClaimData[] public claimData;
...@@ -321,6 +336,10 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -321,6 +336,10 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
revert CannotDefendRootClaim(); revert CannotDefendRootClaim();
} }
// INVARIANT: No moves against the root claim can be made after it has been challenged with
// `challengeRootL2Block`.`
if (l2BlockNumberChallenged && _challengeIndex == 0) revert L2BlockNumberChallenged();
// INVARIANT: A move can never surpass the `MAX_GAME_DEPTH`. The only option to counter a // INVARIANT: A move can never surpass the `MAX_GAME_DEPTH`. The only option to counter a
// claim at this depth is to perform a single instruction step on-chain via // claim at this depth is to perform a single instruction step on-chain via
// the `step` function to prove that the state transition produces an unexpected // the `step` function to prove that the state transition produces an unexpected
...@@ -462,6 +481,54 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -462,6 +481,54 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
startingRootHash_ = startingOutputRoot.root; startingRootHash_ = startingOutputRoot.root;
} }
/// @notice Challenges the root L2 block number by providing the preimage of the output root and the L2 block header
/// and showing that the committed L2 block number is incorrect relative to the claimed L2 block number.
/// @param _outputRootProof The output root proof.
/// @param _headerRLP The RLP-encoded L2 block header.
function challengeRootL2Block(
Types.OutputRootProof calldata _outputRootProof,
bytes calldata _headerRLP
)
external
{
// INVARIANT: Moves cannot be made unless the game is currently in progress.
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();
// The root L2 block claim can only be challenged once.
if (l2BlockNumberChallenged) revert L2BlockNumberChallenged();
// Verify the output root preimage.
if (Hashing.hashOutputRootProof(_outputRootProof) != rootClaim().raw()) revert InvalidOutputRootProof();
// Verify the block hash preimage.
if (keccak256(_headerRLP) != _outputRootProof.latestBlockhash) revert InvalidHeaderRLP();
// Decode the header RLP to find the number of the block. In the consensus encoding, the timestamp
// is the 9th element in the list that represents the block header.
RLPReader.RLPItem[] memory headerContents = RLPReader.readList(RLPReader.toRLPItem(_headerRLP));
bytes memory rawBlockNumber = RLPReader.readBytes(headerContents[HEADER_BLOCK_NUMBER_INDEX]);
// Sanity check the block number string length.
if (rawBlockNumber.length > 32) revert InvalidHeaderRLP();
// Convert the raw, left-aligned block number to a uint256 by aligning it as a big-endian
// number in the low-order bytes of a 32-byte word.
//
// SAFETY: The length of `rawBlockNumber` is checked above to ensure it is at most 32 bytes.
uint256 blockNumber;
assembly {
blockNumber := shr(shl(0x03, sub(0x20, mload(rawBlockNumber))), mload(add(rawBlockNumber, 0x20)))
}
// Ensure the block number does not match the block number claimed in the dispute game.
if (blockNumber == l2BlockNumber()) revert BlockNumberMatches();
// Issue a special counter to the root claim. This counter will always win the root claim subgame, and receive
// the bond from the root claimant.
l2BlockNumberChallenger = msg.sender;
l2BlockNumberChallenged = true;
}
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// `IDisputeGame` impl // // `IDisputeGame` impl //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
...@@ -565,16 +632,25 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -565,16 +632,25 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
if (checkpoint.subgameIndex == challengeIndicesLen) { if (checkpoint.subgameIndex == challengeIndicesLen) {
address countered = checkpoint.counteredBy; address countered = checkpoint.counteredBy;
// Once a subgame is resolved, we percolate the result up the DAG so subsequent calls to
// resolveClaim will not need to traverse this subgame.
subgameRootClaim.counteredBy = countered;
// Mark the subgame as resolved. // Mark the subgame as resolved.
resolvedSubgames[_claimIndex] = true; resolvedSubgames[_claimIndex] = true;
// If the parent was not successfully countered, pay out the parent's bond to the claimant. // Distribute the bond to the appropriate party.
// If the parent was successfully countered, pay out the parent's bond to the challenger. if (_claimIndex == 0 && l2BlockNumberChallenged) {
_distributeBond(countered == address(0) ? subgameRootClaim.claimant : countered, subgameRootClaim); // Special case: If the root claim has been challenged with the `challengeRootL2Block` function,
// the bond is always paid out to the issuer of that challenge.
address challenger = l2BlockNumberChallenger;
_distributeBond(challenger, subgameRootClaim);
subgameRootClaim.counteredBy = challenger;
} else {
// If the parent was not successfully countered, pay out the parent's bond to the claimant.
// If the parent was successfully countered, pay out the parent's bond to the challenger.
_distributeBond(countered == address(0) ? subgameRootClaim.claimant : countered, subgameRootClaim);
// Once a subgame is resolved, we percolate the result up the DAG so subsequent calls to
// resolveClaim will not need to traverse this subgame.
subgameRootClaim.counteredBy = countered;
}
} }
} }
...@@ -670,9 +746,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ...@@ -670,9 +746,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
credit[_recipient] = 0; credit[_recipient] = 0;
// Revert if the recipient has no credit to claim. // Revert if the recipient has no credit to claim.
if (recipientCredit == 0) { if (recipientCredit == 0) revert NoCreditToClaim();
revert NoCreditToClaim();
}
// Try to withdraw the WETH amount so it can be used here. // Try to withdraw the WETH amount so it can be used here.
WETH.withdraw(_recipient, recipientCredit); WETH.withdraw(_recipient, recipientCredit);
......
...@@ -45,7 +45,7 @@ error CannotDefendRootClaim(); ...@@ -45,7 +45,7 @@ error CannotDefendRootClaim();
/// @notice Thrown when a claim is attempting to be made that already exists. /// @notice Thrown when a claim is attempting to be made that already exists.
error ClaimAlreadyExists(); error ClaimAlreadyExists();
/// @notice Thrown when a given claim is invalid (0). /// @notice Thrown when a given claim is invalid.
error InvalidClaim(); error InvalidClaim();
/// @notice Thrown when an action that requires the game to be `IN_PROGRESS` is invoked when /// @notice Thrown when an action that requires the game to be `IN_PROGRESS` is invoked when
...@@ -104,6 +104,19 @@ error DuplicateStep(); ...@@ -104,6 +104,19 @@ error DuplicateStep();
/// @notice Thrown when an anchor root is not found for a given game type. /// @notice Thrown when an anchor root is not found for a given game type.
error AnchorRootNotFound(); error AnchorRootNotFound();
/// @notice Thrown when an output root proof is invalid.
error InvalidOutputRootProof();
/// @notice Thrown when header RLP is invalid with respect to the block hash in an output root proof.
error InvalidHeaderRLP();
/// @notice Thrown when there is a match between the block number in the output root proof and the block number
/// claimed in the dispute game.
error BlockNumberMatches();
/// @notice Thrown when the L2 block number claim has already been challenged.
error L2BlockNumberChallenged();
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// `PermissionedDisputeGame` Errors // // `PermissionedDisputeGame` Errors //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
/// @notice The length of an RLP item must be greater than zero to be decodable
error EmptyItem();
/// @notice The decoded item type for list is not a list item
error UnexpectedString();
/// @notice The RLP item has an invalid data remainder
error InvalidDataRemainder();
/// @notice Decoded item type for bytes is not a string item
error UnexpectedList();
/// @notice The length of the content must be greater than the RLP item length
error ContentLengthMismatch();
/// @notice Invalid RLP header for RLP item
error InvalidHeader();
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.8; pragma solidity ^0.8.8;
import "./RLPErrors.sol";
/// @custom:attribution https://github.com/hamdiallam/Solidity-RLP /// @custom:attribution https://github.com/hamdiallam/Solidity-RLP
/// @title RLPReader /// @title RLPReader
/// @notice RLPReader is a library for parsing RLP-encoded byte arrays into Solidity types. Adapted /// @notice RLPReader is a library for parsing RLP-encoded byte arrays into Solidity types. Adapted
...@@ -34,7 +36,7 @@ library RLPReader { ...@@ -34,7 +36,7 @@ library RLPReader {
/// @return out_ Output memory reference. /// @return out_ Output memory reference.
function toRLPItem(bytes memory _in) internal pure returns (RLPItem memory out_) { function toRLPItem(bytes memory _in) internal pure returns (RLPItem memory out_) {
// Empty arrays are not RLP items. // Empty arrays are not RLP items.
require(_in.length > 0, "RLPReader: length of an RLP item must be greater than zero to be decodable"); if (_in.length == 0) revert EmptyItem();
MemoryPointer ptr; MemoryPointer ptr;
assembly { assembly {
...@@ -50,9 +52,9 @@ library RLPReader { ...@@ -50,9 +52,9 @@ library RLPReader {
function readList(RLPItem memory _in) internal pure returns (RLPItem[] memory out_) { function readList(RLPItem memory _in) internal pure returns (RLPItem[] memory out_) {
(uint256 listOffset, uint256 listLength, RLPItemType itemType) = _decodeLength(_in); (uint256 listOffset, uint256 listLength, RLPItemType itemType) = _decodeLength(_in);
require(itemType == RLPItemType.LIST_ITEM, "RLPReader: decoded item type for list is not a list item"); if (itemType != RLPItemType.LIST_ITEM) revert UnexpectedString();
require(listOffset + listLength == _in.length, "RLPReader: list item has an invalid data remainder"); if (listOffset + listLength != _in.length) revert InvalidDataRemainder();
// Solidity in-memory arrays can't be increased in size, but *can* be decreased in size by // Solidity in-memory arrays can't be increased in size, but *can* be decreased in size by
// writing to the length. Since we can't know the number of RLP items without looping over // writing to the length. Since we can't know the number of RLP items without looping over
...@@ -97,9 +99,9 @@ library RLPReader { ...@@ -97,9 +99,9 @@ library RLPReader {
function readBytes(RLPItem memory _in) internal pure returns (bytes memory out_) { function readBytes(RLPItem memory _in) internal pure returns (bytes memory out_) {
(uint256 itemOffset, uint256 itemLength, RLPItemType itemType) = _decodeLength(_in); (uint256 itemOffset, uint256 itemLength, RLPItemType itemType) = _decodeLength(_in);
require(itemType == RLPItemType.DATA_ITEM, "RLPReader: decoded item type for bytes is not a data item"); if (itemType != RLPItemType.DATA_ITEM) revert UnexpectedList();
require(_in.length == itemOffset + itemLength, "RLPReader: bytes value contains an invalid remainder"); if (_in.length != itemOffset + itemLength) revert InvalidDataRemainder();
out_ = _copy(_in.ptr, itemOffset, itemLength); out_ = _copy(_in.ptr, itemOffset, itemLength);
} }
...@@ -131,7 +133,7 @@ library RLPReader { ...@@ -131,7 +133,7 @@ library RLPReader {
// Short-circuit if there's nothing to decode, note that we perform this check when // Short-circuit if there's nothing to decode, note that we perform this check when
// the user creates an RLP item via toRLPItem, but it's always possible for them to bypass // the user creates an RLP item via toRLPItem, but it's always possible for them to bypass
// that function and create an RLP item directly. So we need to check this anyway. // that function and create an RLP item directly. So we need to check this anyway.
require(_in.length > 0, "RLPReader: length of an RLP item must be greater than zero to be decodable"); if (_in.length == 0) revert EmptyItem();
MemoryPointer ptr = _in.ptr; MemoryPointer ptr = _in.ptr;
uint256 prefix; uint256 prefix;
...@@ -148,50 +150,37 @@ library RLPReader { ...@@ -148,50 +150,37 @@ library RLPReader {
// slither-disable-next-line variable-scope // slither-disable-next-line variable-scope
uint256 strLen = prefix - 0x80; uint256 strLen = prefix - 0x80;
require( if (_in.length <= strLen) revert ContentLengthMismatch();
_in.length > strLen, "RLPReader: length of content must be greater than string length (short string)"
);
bytes1 firstByteOfContent; bytes1 firstByteOfContent;
assembly { assembly {
firstByteOfContent := and(mload(add(ptr, 1)), shl(248, 0xff)) firstByteOfContent := and(mload(add(ptr, 1)), shl(248, 0xff))
} }
require( if (strLen == 1 && firstByteOfContent < 0x80) revert InvalidHeader();
strLen != 1 || firstByteOfContent >= 0x80,
"RLPReader: invalid prefix, single byte < 0x80 are not prefixed (short string)"
);
return (1, strLen, RLPItemType.DATA_ITEM); return (1, strLen, RLPItemType.DATA_ITEM);
} else if (prefix <= 0xbf) { } else if (prefix <= 0xbf) {
// Long string. // Long string.
uint256 lenOfStrLen = prefix - 0xb7; uint256 lenOfStrLen = prefix - 0xb7;
require( if (_in.length <= lenOfStrLen) revert ContentLengthMismatch();
_in.length > lenOfStrLen,
"RLPReader: length of content must be > than length of string length (long string)"
);
bytes1 firstByteOfContent; bytes1 firstByteOfContent;
assembly { assembly {
firstByteOfContent := and(mload(add(ptr, 1)), shl(248, 0xff)) firstByteOfContent := and(mload(add(ptr, 1)), shl(248, 0xff))
} }
require( if (firstByteOfContent == 0x00) revert InvalidHeader();
firstByteOfContent != 0x00, "RLPReader: length of content must not have any leading zeros (long string)"
);
uint256 strLen; uint256 strLen;
assembly { assembly {
strLen := shr(sub(256, mul(8, lenOfStrLen)), mload(add(ptr, 1))) strLen := shr(sub(256, mul(8, lenOfStrLen)), mload(add(ptr, 1)))
} }
require(strLen > 55, "RLPReader: length of content must be greater than 55 bytes (long string)"); if (strLen <= 55) revert InvalidHeader();
require( if (_in.length <= lenOfStrLen + strLen) revert ContentLengthMismatch();
_in.length > lenOfStrLen + strLen,
"RLPReader: length of content must be greater than total length (long string)"
);
return (1 + lenOfStrLen, strLen, RLPItemType.DATA_ITEM); return (1 + lenOfStrLen, strLen, RLPItemType.DATA_ITEM);
} else if (prefix <= 0xf7) { } else if (prefix <= 0xf7) {
...@@ -199,38 +188,30 @@ library RLPReader { ...@@ -199,38 +188,30 @@ library RLPReader {
// slither-disable-next-line variable-scope // slither-disable-next-line variable-scope
uint256 listLen = prefix - 0xc0; uint256 listLen = prefix - 0xc0;
require(_in.length > listLen, "RLPReader: length of content must be greater than list length (short list)"); if (_in.length <= listLen) revert ContentLengthMismatch();
return (1, listLen, RLPItemType.LIST_ITEM); return (1, listLen, RLPItemType.LIST_ITEM);
} else { } else {
// Long list. // Long list.
uint256 lenOfListLen = prefix - 0xf7; uint256 lenOfListLen = prefix - 0xf7;
require( if (_in.length <= lenOfListLen) revert ContentLengthMismatch();
_in.length > lenOfListLen,
"RLPReader: length of content must be > than length of list length (long list)"
);
bytes1 firstByteOfContent; bytes1 firstByteOfContent;
assembly { assembly {
firstByteOfContent := and(mload(add(ptr, 1)), shl(248, 0xff)) firstByteOfContent := and(mload(add(ptr, 1)), shl(248, 0xff))
} }
require( if (firstByteOfContent == 0x00) revert InvalidHeader();
firstByteOfContent != 0x00, "RLPReader: length of content must not have any leading zeros (long list)"
);
uint256 listLen; uint256 listLen;
assembly { assembly {
listLen := shr(sub(256, mul(8, lenOfListLen)), mload(add(ptr, 1))) listLen := shr(sub(256, mul(8, lenOfListLen)), mload(add(ptr, 1)))
} }
require(listLen > 55, "RLPReader: length of content must be greater than 55 bytes (long list)"); if (listLen <= 55) revert InvalidHeader();
require( if (_in.length <= lenOfListLen + listLen) revert ContentLengthMismatch();
_in.length > lenOfListLen + listLen,
"RLPReader: length of content must be greater than total length (long list)"
);
return (1 + lenOfListLen, listLen, RLPItemType.LIST_ITEM); return (1 + lenOfListLen, listLen, RLPItemType.LIST_ITEM);
} }
......
...@@ -5,12 +5,15 @@ import { Test } from "forge-std/Test.sol"; ...@@ -5,12 +5,15 @@ import { Test } from "forge-std/Test.sol";
import { Vm } from "forge-std/Vm.sol"; import { Vm } from "forge-std/Vm.sol";
import { DisputeGameFactory_Init } from "test/dispute/DisputeGameFactory.t.sol"; import { DisputeGameFactory_Init } from "test/dispute/DisputeGameFactory.t.sol";
import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol"; import { FaultDisputeGame, IDisputeGame } from "src/dispute/FaultDisputeGame.sol";
import { DelayedWETH } from "src/dispute/weth/DelayedWETH.sol"; import { DelayedWETH } from "src/dispute/weth/DelayedWETH.sol";
import { PreimageOracle } from "src/cannon/PreimageOracle.sol"; import { PreimageOracle } from "src/cannon/PreimageOracle.sol";
import "src/dispute/lib/Types.sol"; import "src/dispute/lib/Types.sol";
import "src/dispute/lib/Errors.sol"; import "src/dispute/lib/Errors.sol";
import { Types } from "src/libraries/Types.sol";
import { Hashing } from "src/libraries/Hashing.sol";
import { RLPWriter } from "src/libraries/rlp/RLPWriter.sol";
import { LibClock } from "src/dispute/lib/LibUDT.sol"; import { LibClock } from "src/dispute/lib/LibUDT.sol";
import { LibPosition } from "src/dispute/lib/LibPosition.sol"; import { LibPosition } from "src/dispute/lib/LibPosition.sol";
import { IPreimageOracle } from "src/dispute/interfaces/IBigStepper.sol"; import { IPreimageOracle } from "src/dispute/interfaces/IBigStepper.sol";
...@@ -590,6 +593,168 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -590,6 +593,168 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
gameProxy.attack{ value: 0 }(0, _dummyClaim()); gameProxy.attack{ value: 0 }(0, _dummyClaim());
} }
/// @dev Tests that challenging the root claim's L2 block number by providing the real preimage of the output root
/// succeeds.
function testFuzz_challengeRootL2Block_succeeds(
bytes32 _storageRoot,
bytes32 _withdrawalRoot,
uint256 _l2BlockNumber
)
public
{
_l2BlockNumber = bound(_l2BlockNumber, 0, type(uint256).max - 1);
(Types.OutputRootProof memory outputRootProof, bytes32 outputRoot, bytes memory headerRLP) =
_generateOutputRootProof(_storageRoot, _withdrawalRoot, abi.encodePacked(_l2BlockNumber));
// Create the dispute game with the output root at the wrong L2 block number.
IDisputeGame game = disputeGameFactory.create(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(_l2BlockNumber + 1));
// Challenge the L2 block number.
FaultDisputeGame fdg = FaultDisputeGame(address(game));
fdg.challengeRootL2Block(outputRootProof, headerRLP);
// Ensure that a duplicate challenge reverts.
vm.expectRevert(L2BlockNumberChallenged.selector);
fdg.challengeRootL2Block(outputRootProof, headerRLP);
// Warp past the clocks, resolve the game.
vm.warp(block.timestamp + 3 days + 12 hours + 1);
fdg.resolveClaim(0, 0);
fdg.resolve();
// Ensure the challenge was successful.
assertEq(uint8(fdg.status()), uint8(GameStatus.CHALLENGER_WINS));
assertTrue(fdg.l2BlockNumberChallenged());
}
/// @dev Tests that challenging the root claim's L2 block number by providing the real preimage of the output root
/// succeeds. Also, this claim should always receive the bond when there is another counter that is as far left
/// as possible.
function testFuzz_challengeRootL2Block_receivesBond_succeeds(
bytes32 _storageRoot,
bytes32 _withdrawalRoot,
uint256 _l2BlockNumber
)
public
{
vm.deal(address(0xb0b), 1 ether);
_l2BlockNumber = bound(_l2BlockNumber, 0, type(uint256).max - 1);
(Types.OutputRootProof memory outputRootProof, bytes32 outputRoot, bytes memory headerRLP) =
_generateOutputRootProof(_storageRoot, _withdrawalRoot, abi.encodePacked(_l2BlockNumber));
// Create the dispute game with the output root at the wrong L2 block number.
disputeGameFactory.setInitBond(GAME_TYPE, 0.1 ether);
uint256 balanceBefore = address(this).balance;
IDisputeGame game = disputeGameFactory.create{ value: 0.1 ether }(
GAME_TYPE, Claim.wrap(outputRoot), abi.encode(_l2BlockNumber + 1)
);
FaultDisputeGame fdg = FaultDisputeGame(address(game));
// Attack the root as 0xb0b
uint256 bond = _getRequiredBond(0);
vm.prank(address(0xb0b));
fdg.attack{ value: bond }(0, Claim.wrap(0));
// Challenge the L2 block number as 0xace. This claim should receive the root claim's bond.
vm.prank(address(0xace));
fdg.challengeRootL2Block(outputRootProof, headerRLP);
// Warp past the clocks, resolve the game.
vm.warp(block.timestamp + 3 days + 12 hours + 1);
fdg.resolveClaim(1, 0);
fdg.resolveClaim(0, 0);
fdg.resolve();
// Ensure the challenge was successful.
assertEq(uint8(fdg.status()), uint8(GameStatus.CHALLENGER_WINS));
// Wait for the withdrawal delay.
vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds);
// Claim credit
vm.expectRevert(NoCreditToClaim.selector);
fdg.claimCredit(address(this));
fdg.claimCredit(address(0xb0b));
fdg.claimCredit(address(0xace));
// Ensure that the party who challenged the L2 block number with the special move received the bond.
// - Root claim loses their bond
// - 0xace receives the root claim's bond
// - 0xb0b receives their bond back
assertEq(address(this).balance, balanceBefore - 0.1 ether);
assertEq(address(0xb0b).balance, 1 ether);
assertEq(address(0xace).balance, 0.1 ether);
}
/// @dev Tests that challenging the root claim's L2 block number by providing the real preimage of the output root
/// never succeeds.
function testFuzz_challengeRootL2Block_rightBlockNumber_reverts(
bytes32 _storageRoot,
bytes32 _withdrawalRoot,
uint256 _l2BlockNumber
)
public
{
_l2BlockNumber = bound(_l2BlockNumber, 1, type(uint256).max);
(Types.OutputRootProof memory outputRootProof, bytes32 outputRoot, bytes memory headerRLP) =
_generateOutputRootProof(_storageRoot, _withdrawalRoot, abi.encodePacked(_l2BlockNumber));
// Create the dispute game with the output root at the wrong L2 block number.
IDisputeGame game = disputeGameFactory.create(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(_l2BlockNumber));
// Challenge the L2 block number.
FaultDisputeGame fdg = FaultDisputeGame(address(game));
vm.expectRevert(BlockNumberMatches.selector);
fdg.challengeRootL2Block(outputRootProof, headerRLP);
// Warp past the clocks, resolve the game.
vm.warp(block.timestamp + 3 days + 12 hours + 1);
fdg.resolveClaim(0, 0);
fdg.resolve();
// Ensure the challenge was successful.
assertEq(uint8(fdg.status()), uint8(GameStatus.DEFENDER_WINS));
}
/// @dev Tests that challenging the root claim's L2 block number with a bad output root proof reverts.
function test_challengeRootL2Block_badProof_reverts() public {
Types.OutputRootProof memory outputRootProof =
Types.OutputRootProof({ version: 0, stateRoot: 0, messagePasserStorageRoot: 0, latestBlockhash: 0 });
vm.expectRevert(InvalidOutputRootProof.selector);
gameProxy.challengeRootL2Block(outputRootProof, hex"");
}
/// @dev Tests that challenging the root claim's L2 block number with a bad output root proof reverts.
function test_challengeRootL2Block_badHeaderRLP_reverts() public {
Types.OutputRootProof memory outputRootProof =
Types.OutputRootProof({ version: 0, stateRoot: 0, messagePasserStorageRoot: 0, latestBlockhash: 0 });
bytes32 outputRoot = Hashing.hashOutputRootProof(outputRootProof);
// Create the dispute game with the output root at the wrong L2 block number.
IDisputeGame game = disputeGameFactory.create(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(1));
FaultDisputeGame fdg = FaultDisputeGame(address(game));
vm.expectRevert(InvalidHeaderRLP.selector);
fdg.challengeRootL2Block(outputRootProof, hex"");
}
/// @dev Tests that challenging the root claim's L2 block number with a bad output root proof reverts.
function test_challengeRootL2Block_badHeaderRLPBlockNumberLength_reverts() public {
(Types.OutputRootProof memory outputRootProof, bytes32 outputRoot,) =
_generateOutputRootProof(0, 0, new bytes(64));
// Create the dispute game with the output root at the wrong L2 block number.
IDisputeGame game = disputeGameFactory.create(GAME_TYPE, Claim.wrap(outputRoot), abi.encode(1));
FaultDisputeGame fdg = FaultDisputeGame(address(game));
vm.expectRevert(InvalidHeaderRLP.selector);
fdg.challengeRootL2Block(outputRootProof, hex"");
}
/// @dev Tests that a claim cannot be stepped against twice. /// @dev Tests that a claim cannot be stepped against twice.
function test_step_duplicateStep_reverts() public { function test_step_duplicateStep_reverts() public {
// Give the test contract some ether // Give the test contract some ether
...@@ -1456,6 +1621,39 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -1456,6 +1621,39 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
} }
/// @dev Helper to generate a mock RLP encoded header (with only a real block number) & an output root proof.
function _generateOutputRootProof(
bytes32 _storageRoot,
bytes32 _withdrawalRoot,
bytes memory _l2BlockNumber
)
internal
pure
returns (Types.OutputRootProof memory proof_, bytes32 root_, bytes memory rlp_)
{
// L2 Block header
bytes[] memory rawHeaderRLP = new bytes[](9);
rawHeaderRLP[0] = hex"83FACADE";
rawHeaderRLP[1] = hex"83FACADE";
rawHeaderRLP[2] = hex"83FACADE";
rawHeaderRLP[3] = hex"83FACADE";
rawHeaderRLP[4] = hex"83FACADE";
rawHeaderRLP[5] = hex"83FACADE";
rawHeaderRLP[6] = hex"83FACADE";
rawHeaderRLP[7] = hex"83FACADE";
rawHeaderRLP[8] = RLPWriter.writeBytes(_l2BlockNumber);
rlp_ = RLPWriter.writeList(rawHeaderRLP);
// Output root
proof_ = Types.OutputRootProof({
version: 0,
stateRoot: _storageRoot,
messagePasserStorageRoot: _withdrawalRoot,
latestBlockhash: keccak256(rlp_)
});
root_ = Hashing.hashOutputRootProof(proof_);
}
/// @dev Helper to get the required bond for the given claim index. /// @dev Helper to get the required bond for the given claim index.
function _getRequiredBond(uint256 _claimIndex) internal view returns (uint256 bond_) { function _getRequiredBond(uint256 _claimIndex) internal view returns (uint256 bond_) {
(,,,,, Position parent,) = gameProxy.claimData(_claimIndex); (,,,,, Position parent,) = gameProxy.claimData(_claimIndex);
......
...@@ -21,7 +21,7 @@ contract DeploymentSummary is DeploymentSummaryCode { ...@@ -21,7 +21,7 @@ contract DeploymentSummary is DeploymentSummaryCode {
address internal constant l1StandardBridgeProxyAddress = 0x0c8b5822b6e02CDa722174F19A1439A7495a3fA6; address internal constant l1StandardBridgeProxyAddress = 0x0c8b5822b6e02CDa722174F19A1439A7495a3fA6;
address internal constant l2OutputOracleAddress = 0x19652082F846171168Daf378C4fD3ee85a0D4A60; address internal constant l2OutputOracleAddress = 0x19652082F846171168Daf378C4fD3ee85a0D4A60;
address internal constant l2OutputOracleProxyAddress = 0x8B71b41D4dBEb2b6821d44692d3fACAAf77480Bb; address internal constant l2OutputOracleProxyAddress = 0x8B71b41D4dBEb2b6821d44692d3fACAAf77480Bb;
address internal constant optimismPortalAddress = 0xb7B7121AEcAd4F33131222efFFA34aB6c382ef4c; address internal constant optimismPortalAddress = 0xFc9AD479AC641888D36EE5Dc265bcD3ecceBDc58;
address internal constant optimismPortalProxyAddress = 0x978e3286EB805934215a88694d80b09aDed68D90; address internal constant optimismPortalProxyAddress = 0x978e3286EB805934215a88694d80b09aDed68D90;
address internal constant protocolVersionsAddress = 0xfbfD64a6C0257F613feFCe050Aa30ecC3E3d7C3F; address internal constant protocolVersionsAddress = 0xfbfD64a6C0257F613feFCe050Aa30ecC3E3d7C3F;
address internal constant protocolVersionsProxyAddress = 0x416C42991d05b31E9A6dC209e91AD22b79D87Ae6; address internal constant protocolVersionsProxyAddress = 0x416C42991d05b31E9A6dC209e91AD22b79D87Ae6;
...@@ -437,7 +437,7 @@ contract DeploymentSummary is DeploymentSummaryCode { ...@@ -437,7 +437,7 @@ contract DeploymentSummary is DeploymentSummaryCode {
value = hex"000000000000000000000000000000000000000000000000000000000000000a"; value = hex"000000000000000000000000000000000000000000000000000000000000000a";
vm.store(systemOwnerSafeAddress, slot, value); vm.store(systemOwnerSafeAddress, slot, value);
slot = hex"360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; slot = hex"360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
value = hex"000000000000000000000000b7b7121aecad4f33131222efffa34ab6c382ef4c"; value = hex"000000000000000000000000fc9ad479ac641888d36ee5dc265bcd3eccebdc58";
vm.store(optimismPortalProxyAddress, slot, value); vm.store(optimismPortalProxyAddress, slot, value);
slot = hex"0000000000000000000000000000000000000000000000000000000000000000"; slot = hex"0000000000000000000000000000000000000000000000000000000000000000";
value = hex"0000000000000000000000000000000000000000000000000000000000000001"; value = hex"0000000000000000000000000000000000000000000000000000000000000001";
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -4,6 +4,7 @@ pragma solidity 0.8.15; ...@@ -4,6 +4,7 @@ pragma solidity 0.8.15;
import { stdError } from "forge-std/Test.sol"; import { stdError } from "forge-std/Test.sol";
import { Test } from "forge-std/Test.sol"; import { Test } from "forge-std/Test.sol";
import { RLPReader } from "src/libraries/rlp/RLPReader.sol"; import { RLPReader } from "src/libraries/rlp/RLPReader.sol";
import "src/libraries/rlp/RLPErrors.sol";
contract RLPReader_readBytes_Test is Test { contract RLPReader_readBytes_Test is Test {
function test_readBytes_bytestring00_succeeds() external pure { function test_readBytes_bytestring00_succeeds() external pure {
...@@ -19,27 +20,27 @@ contract RLPReader_readBytes_Test is Test { ...@@ -19,27 +20,27 @@ contract RLPReader_readBytes_Test is Test {
} }
function test_readBytes_revertListItem_reverts() external { function test_readBytes_revertListItem_reverts() external {
vm.expectRevert("RLPReader: decoded item type for bytes is not a data item"); vm.expectRevert(UnexpectedList.selector);
RLPReader.readBytes(hex"c7c0c1c0c3c0c1c0"); RLPReader.readBytes(hex"c7c0c1c0c3c0c1c0");
} }
function test_readBytes_invalidStringLength_reverts() external { function test_readBytes_invalidStringLength_reverts() external {
vm.expectRevert("RLPReader: length of content must be > than length of string length (long string)"); vm.expectRevert(ContentLengthMismatch.selector);
RLPReader.readBytes(hex"b9"); RLPReader.readBytes(hex"b9");
} }
function test_readBytes_invalidListLength_reverts() external { function test_readBytes_invalidListLength_reverts() external {
vm.expectRevert("RLPReader: length of content must be > than length of list length (long list)"); vm.expectRevert(ContentLengthMismatch.selector);
RLPReader.readBytes(hex"ff"); RLPReader.readBytes(hex"ff");
} }
function test_readBytes_invalidRemainder_reverts() external { function test_readBytes_invalidRemainder_reverts() external {
vm.expectRevert("RLPReader: bytes value contains an invalid remainder"); vm.expectRevert(InvalidDataRemainder.selector);
RLPReader.readBytes(hex"800a"); RLPReader.readBytes(hex"800a");
} }
function test_readBytes_invalidPrefix_reverts() external { function test_readBytes_invalidPrefix_reverts() external {
vm.expectRevert("RLPReader: invalid prefix, single byte < 0x80 are not prefixed (short string)"); vm.expectRevert(InvalidHeader.selector);
RLPReader.readBytes(hex"810a"); RLPReader.readBytes(hex"810a");
} }
} }
...@@ -135,101 +136,101 @@ contract RLPReader_readList_Test is Test { ...@@ -135,101 +136,101 @@ contract RLPReader_readList_Test is Test {
} }
function test_readList_invalidShortList_reverts() external { function test_readList_invalidShortList_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than list length (short list)"); vm.expectRevert(ContentLengthMismatch.selector);
RLPReader.readList(hex"efdebd"); RLPReader.readList(hex"efdebd");
} }
function test_readList_longStringLength_reverts() external { function test_readList_longStringLength_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than list length (short list)"); vm.expectRevert(ContentLengthMismatch.selector);
RLPReader.readList(hex"efb83600"); RLPReader.readList(hex"efb83600");
} }
function test_readList_notLongEnough_reverts() external { function test_readList_notLongEnough_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than list length (short list)"); vm.expectRevert(ContentLengthMismatch.selector);
RLPReader.readList(hex"efdebdaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); RLPReader.readList(hex"efdebdaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
} }
function test_readList_int32Overflow_reverts() external { function test_readList_int32Overflow_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than total length (long string)"); vm.expectRevert(ContentLengthMismatch.selector);
RLPReader.readList(hex"bf0f000000000000021111"); RLPReader.readList(hex"bf0f000000000000021111");
} }
function test_readList_int32Overflow2_reverts() external { function test_readList_int32Overflow2_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than total length (long list)"); vm.expectRevert(ContentLengthMismatch.selector);
RLPReader.readList(hex"ff0f000000000000021111"); RLPReader.readList(hex"ff0f000000000000021111");
} }
function test_readList_incorrectLengthInArray_reverts() external { function test_readList_incorrectLengthInArray_reverts() external {
vm.expectRevert("RLPReader: length of content must not have any leading zeros (long string)"); vm.expectRevert(InvalidHeader.selector);
RLPReader.readList(hex"b9002100dc2b275d0f74e8a53e6f4ec61b27f24278820be3f82ea2110e582081b0565df0"); RLPReader.readList(hex"b9002100dc2b275d0f74e8a53e6f4ec61b27f24278820be3f82ea2110e582081b0565df0");
} }
function test_readList_leadingZerosInLongLengthArray1_reverts() external { function test_readList_leadingZerosInLongLengthArray1_reverts() external {
vm.expectRevert("RLPReader: length of content must not have any leading zeros (long string)"); vm.expectRevert(InvalidHeader.selector);
RLPReader.readList( RLPReader.readList(
hex"b90040000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" hex"b90040000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"
); );
} }
function test_readList_leadingZerosInLongLengthArray2_reverts() external { function test_readList_leadingZerosInLongLengthArray2_reverts() external {
vm.expectRevert("RLPReader: length of content must not have any leading zeros (long string)"); vm.expectRevert(InvalidHeader.selector);
RLPReader.readList(hex"b800"); RLPReader.readList(hex"b800");
} }
function test_readList_leadingZerosInLongLengthList1_reverts() external { function test_readList_leadingZerosInLongLengthList1_reverts() external {
vm.expectRevert("RLPReader: length of content must not have any leading zeros (long list)"); vm.expectRevert(InvalidHeader.selector);
RLPReader.readList( RLPReader.readList(
hex"fb00000040000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" hex"fb00000040000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"
); );
} }
function test_readList_nonOptimalLongLengthArray1_reverts() external { function test_readList_nonOptimalLongLengthArray1_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than 55 bytes (long string)"); vm.expectRevert(InvalidHeader.selector);
RLPReader.readList(hex"b81000112233445566778899aabbccddeeff"); RLPReader.readList(hex"b81000112233445566778899aabbccddeeff");
} }
function test_readList_nonOptimalLongLengthArray2_reverts() external { function test_readList_nonOptimalLongLengthArray2_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than 55 bytes (long string)"); vm.expectRevert(InvalidHeader.selector);
RLPReader.readList(hex"b801ff"); RLPReader.readList(hex"b801ff");
} }
function test_readList_invalidValue_reverts() external { function test_readList_invalidValue_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than string length (short string)"); vm.expectRevert(ContentLengthMismatch.selector);
RLPReader.readList(hex"91"); RLPReader.readList(hex"91");
} }
function test_readList_invalidRemainder_reverts() external { function test_readList_invalidRemainder_reverts() external {
vm.expectRevert("RLPReader: list item has an invalid data remainder"); vm.expectRevert(InvalidDataRemainder.selector);
RLPReader.readList(hex"c000"); RLPReader.readList(hex"c000");
} }
function test_readList_notEnoughContentForString1_reverts() external { function test_readList_notEnoughContentForString1_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than total length (long string)"); vm.expectRevert(ContentLengthMismatch.selector);
RLPReader.readList(hex"ba010000aabbccddeeff"); RLPReader.readList(hex"ba010000aabbccddeeff");
} }
function test_readList_notEnoughContentForString2_reverts() external { function test_readList_notEnoughContentForString2_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than total length (long string)"); vm.expectRevert(ContentLengthMismatch.selector);
RLPReader.readList(hex"b840ffeeddccbbaa99887766554433221100"); RLPReader.readList(hex"b840ffeeddccbbaa99887766554433221100");
} }
function test_readList_notEnoughContentForList1_reverts() external { function test_readList_notEnoughContentForList1_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than total length (long list)"); vm.expectRevert(ContentLengthMismatch.selector);
RLPReader.readList(hex"f90180"); RLPReader.readList(hex"f90180");
} }
function test_readList_notEnoughContentForList2_reverts() external { function test_readList_notEnoughContentForList2_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than total length (long list)"); vm.expectRevert(ContentLengthMismatch.selector);
RLPReader.readList(hex"ffffffffffffffffff0001020304050607"); RLPReader.readList(hex"ffffffffffffffffff0001020304050607");
} }
function test_readList_longStringLessThan56Bytes_reverts() external { function test_readList_longStringLessThan56Bytes_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than 55 bytes (long string)"); vm.expectRevert(InvalidHeader.selector);
RLPReader.readList(hex"b80100"); RLPReader.readList(hex"b80100");
} }
function test_readList_longListLessThan56Bytes_reverts() external { function test_readList_longListLessThan56Bytes_reverts() external {
vm.expectRevert("RLPReader: length of content must be greater than 55 bytes (long list)"); vm.expectRevert(InvalidHeader.selector);
RLPReader.readList(hex"f80100"); RLPReader.readList(hex"f80100");
} }
} }
...@@ -4,6 +4,7 @@ pragma solidity 0.8.15; ...@@ -4,6 +4,7 @@ pragma solidity 0.8.15;
import { Test } from "forge-std/Test.sol"; import { Test } from "forge-std/Test.sol";
import { MerkleTrie } from "src/libraries/trie/MerkleTrie.sol"; import { MerkleTrie } from "src/libraries/trie/MerkleTrie.sol";
import { FFIInterface } from "test/setup/FFIInterface.sol"; import { FFIInterface } from "test/setup/FFIInterface.sol";
import "src/libraries/rlp/RLPErrors.sol";
contract MerkleTrie_get_Test is Test { contract MerkleTrie_get_Test is Test {
FFIInterface constant ffi = FFIInterface(address(uint160(uint256(keccak256(abi.encode("optimism.ffi")))))); FFIInterface constant ffi = FFIInterface(address(uint160(uint256(keccak256(abi.encode("optimism.ffi"))))));
...@@ -186,7 +187,7 @@ contract MerkleTrie_get_Test is Test { ...@@ -186,7 +187,7 @@ contract MerkleTrie_get_Test is Test {
hex"f84580a0582eed8dd051b823d13f8648cdcd08aa2d8dac239f458863c4620e8c4d605debca83206262856176616c32ca83206363856176616c3380808080808080808080808080"; hex"f84580a0582eed8dd051b823d13f8648cdcd08aa2d8dac239f458863c4620e8c4d605debca83206262856176616c32ca83206363856176616c3380808080808080808080808080";
proof[4] = hex"ca83206262856176616c32"; proof[4] = hex"ca83206262856176616c32";
vm.expectRevert("RLPReader: decoded item type for list is not a list item"); vm.expectRevert(UnexpectedString.selector);
MerkleTrie.get(key, proof, root); MerkleTrie.get(key, proof, root);
} }
...@@ -198,7 +199,7 @@ contract MerkleTrie_get_Test is Test { ...@@ -198,7 +199,7 @@ contract MerkleTrie_get_Test is Test {
proof[1] = hex"d780808080808080808080c32081aac32081ab8080808080"; proof[1] = hex"d780808080808080808080c32081aac32081ab8080808080";
proof[2] = hex"c32081aa000000000000000000000000000000"; proof[2] = hex"c32081aa000000000000000000000000000000";
vm.expectRevert("RLPReader: list item has an invalid data remainder"); vm.expectRevert(InvalidDataRemainder.selector);
MerkleTrie.get(key, proof, root); MerkleTrie.get(key, proof, root);
} }
...@@ -328,7 +329,7 @@ contract MerkleTrie_get_Test is Test { ...@@ -328,7 +329,7 @@ contract MerkleTrie_get_Test is Test {
// Generate an invalid test case where the proof is malformed. // Generate an invalid test case where the proof is malformed.
(bytes32 root, bytes memory key,, bytes[] memory proof) = ffi.getMerkleTrieFuzzCase("corrupted_proof"); (bytes32 root, bytes memory key,, bytes[] memory proof) = ffi.getMerkleTrieFuzzCase("corrupted_proof");
vm.expectRevert("RLPReader: decoded item type for list is not a list item"); vm.expectRevert(UnexpectedString.selector);
MerkleTrie.get(key, proof, root); MerkleTrie.get(key, proof, root);
} }
...@@ -338,7 +339,7 @@ contract MerkleTrie_get_Test is Test { ...@@ -338,7 +339,7 @@ contract MerkleTrie_get_Test is Test {
// length designates within the RLP list encoding. // length designates within the RLP list encoding.
(bytes32 root, bytes memory key,, bytes[] memory proof) = ffi.getMerkleTrieFuzzCase("invalid_data_remainder"); (bytes32 root, bytes memory key,, bytes[] memory proof) = ffi.getMerkleTrieFuzzCase("invalid_data_remainder");
vm.expectRevert("RLPReader: list item has an invalid data remainder"); vm.expectRevert(InvalidDataRemainder.selector);
MerkleTrie.get(key, proof, root); MerkleTrie.get(key, proof, root);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment