Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ctb): AnchorStateRegistry output root verification [rfc] #10431

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,11 @@
"internalType": "uint256",
"name": "l2BlockNumber_",
"type": "uint256"
},
{
"internalType": "bool",
"name": "isVerified_",
"type": "bool"
}
],
"stateMutability": "pure",
Expand Down Expand Up @@ -738,4 +743,4 @@
"name": "ValidStep",
"type": "error"
}
]
]
4 changes: 2 additions & 2 deletions op-challenger/game/fault/contracts/faultdisputegame_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ func TestGetBlockRange(t *testing.T) {
expectedStart := uint64(65)
expectedEnd := uint64(102)
stubRpc.SetResponse(fdgAddr, methodStartingBlockNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedStart)})
stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedEnd)})
stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, rpcblock.Latest, nil, []interface{}{new(big.Int).SetUint64(expectedEnd), false})
clabby marked this conversation as resolved.
Show resolved Hide resolved
start, end, err := contract.GetBlockRange(context.Background())
require.NoError(t, err)
require.Equal(t, expectedStart, start)
Expand Down Expand Up @@ -465,7 +465,7 @@ func TestGetGameMetadata(t *testing.T) {
expectedStatus := types.GameStatusChallengerWon
block := rpcblock.ByNumber(889)
stubRpc.SetResponse(fdgAddr, methodL1Head, block, nil, []interface{}{expectedL1Head})
stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber)})
stubRpc.SetResponse(fdgAddr, methodL2BlockNumber, block, nil, []interface{}{new(big.Int).SetUint64(expectedL2BlockNumber), false})
clabby marked this conversation as resolved.
Show resolved Hide resolved
stubRpc.SetResponse(fdgAddr, methodRootClaim, block, nil, []interface{}{expectedRootClaim})
stubRpc.SetResponse(fdgAddr, methodStatus, block, nil, []interface{}{expectedStatus})
if version.version == vers080 {
Expand Down
6 changes: 4 additions & 2 deletions packages/contracts-bedrock/scripts/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -774,8 +774,10 @@ contract Deploy is Deployer {
/// @notice Deploy the AnchorStateRegistry
function deployAnchorStateRegistry() public broadcast returns (address addr_) {
console.log("Deploying AnchorStateRegistry implementation");
AnchorStateRegistry anchorStateRegistry =
new AnchorStateRegistry{ salt: _implSalt() }(DisputeGameFactory(mustGetAddress("DisputeGameFactoryProxy")));
AnchorStateRegistry anchorStateRegistry = new AnchorStateRegistry{ salt: _implSalt() }(
DisputeGameFactory(mustGetAddress("DisputeGameFactoryProxy")),
Duration.wrap(uint64(cfg.disputeGameFinalityDelaySeconds()))
);
save("AnchorStateRegistry", address(anchorStateRegistry));
console.log("AnchorStateRegistry deployed at %s", address(anchorStateRegistry));

Expand Down
8 changes: 4 additions & 4 deletions packages/contracts-bedrock/semver-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,16 +120,16 @@
"sourceCodeHash": "0x7c8b26cd263f6be144bace1f3faf0ec9265df0efb68ac34fa1fa7df7f608ab42"
},
"src/dispute/AnchorStateRegistry.sol": {
"initCodeHash": "0x0305c21e50829b9e07d43358d8c2c82f1449534c90d4391400d46e76d0503a49",
"sourceCodeHash": "0x56b069b33d080c2a45ee6fd340e5c5824ab4dc866eadb5b481b9026ebb12aa7c"
"initCodeHash": "0x18810917b72e7fda8a019a9be7fcb360d3b64e0291d506e9425728db3af877e9",
"sourceCodeHash": "0xa49560af1223ffe862383c58ad4b55ec9c3e34c3bd5358db1e7346922d71ff92"
},
"src/dispute/DisputeGameFactory.sol": {
"initCodeHash": "0x7a7cb8f2c95df2f9afb3ce9eaefe4a6f997ccce7ed8ffda5d425a65a2474a792",
"sourceCodeHash": "0x918c395ac5d77357f2551616aad0613e68893862edd14e554623eb16ee6ba148"
},
"src/dispute/FaultDisputeGame.sol": {
"initCodeHash": "0x8398caaff1da5d81730c95104c15b14c2fb7ff394bab005d9ec77372a2b1f5ca",
"sourceCodeHash": "0x5888aea16645a18ce54032c1787644afcdf07c6df2b7c6546caa957a047f03fc"
"initCodeHash": "0xae975410648f724f8c234d9d9f55f69fff66f25d184eafec828eb6873fad1473",
"sourceCodeHash": "0x91340f4dd0b5f70d9aebd8072d8b07595cc9b1fa46bdb857ff66b7d55fcec7ff"
},
"src/dispute/weth/DelayedWETH.sol": {
"initCodeHash": "0xb9bbe005874922cd8f499e7a0a092967cfca03e012c1e41912b0c77481c71777",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
"internalType": "contract IDisputeGameFactory",
"name": "_disputeGameFactory",
"type": "address"
},
{
"internalType": "Duration",
"name": "_gameFinalizationDelay",
"type": "uint64"
}
],
"stateMutability": "nonpayable",
Expand Down Expand Up @@ -85,8 +90,65 @@
"type": "function"
},
{
"inputs": [],
"name": "tryUpdateAnchorState",
"inputs": [
{
"internalType": "contract IDisputeGame",
"name": "",
"type": "address"
}
],
"name": "verifiedGames",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IFaultDisputeGame",
"name": "_disputeGame",
"type": "address"
},
{
"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": "verifyAnchor",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,9 +426,14 @@
"internalType": "uint256",
"name": "l2BlockNumber_",
"type": "uint256"
},
{
"internalType": "bool",
"name": "isVerified_",
"type": "bool"
}
],
"stateMutability": "pure",
"stateMutability": "view",
"type": "function"
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -449,9 +449,14 @@
"internalType": "uint256",
"name": "l2BlockNumber_",
"type": "uint256"
},
{
"internalType": "bool",
"name": "isVerified_",
"type": "bool"
}
],
"stateMutability": "pure",
"stateMutability": "view",
"type": "function"
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,12 @@
"offset": 0,
"slot": "1",
"type": "mapping(GameType => struct OutputRoot)"
},
{
"bytes": "32",
"label": "verifiedGames",
"offset": 0,
"slot": "2",
"type": "mapping(contract IDisputeGame => bool)"
}
]
95 changes: 78 additions & 17 deletions packages/contracts-bedrock/src/dispute/AnchorStateRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { IFaultDisputeGame } from "src/dispute/interfaces/IFaultDisputeGame.sol"
import { IDisputeGame } from "src/dispute/interfaces/IDisputeGame.sol";
import { IDisputeGameFactory } from "src/dispute/interfaces/IDisputeGameFactory.sol";

import { RLPReader } from "src/libraries/rlp/RLPReader.sol";
import { Hashing } from "src/libraries/Hashing.sol";
import { Types } from "src/libraries/Types.sol";
import "src/dispute/lib/Types.sol";

/// @title AnchorStateRegistry
Expand All @@ -24,18 +27,30 @@ contract AnchorStateRegistry is Initializable, IAnchorStateRegistry, ISemver {
}

/// @notice Semantic version.
/// @custom:semver 1.0.0
string public constant version = "1.0.0";
/// @custom:semver 1.1.0
string public constant version = "1.1.0";

/// @notice The index of the block number in the RLP-encoded block header.
/// @dev Consensus encoding reference:
clabby marked this conversation as resolved.
Show resolved Hide resolved
/// https://github.com/paradigmxyz/reth/blob/5f82993c23164ce8ccdc7bf3ae5085205383a5c8/crates/primitives/src/header.rs#L368
uint256 internal constant HEADER_BLOCK_NUMBER_INDEX = 8;

/// @notice DisputeGameFactory address.
IDisputeGameFactory internal immutable DISPUTE_GAME_FACTORY;

/// @notice Dispute game finalization delay.
Duration internal immutable DISPUTE_GAME_FINALIZATION_DELAY;

/// @inheritdoc IAnchorStateRegistry
mapping(GameType => OutputRoot) public anchors;

/// @inheritdoc IAnchorStateRegistry
mapping(IDisputeGame => bool) public verifiedGames;

/// @param _disputeGameFactory DisputeGameFactory address.
constructor(IDisputeGameFactory _disputeGameFactory) {
constructor(IDisputeGameFactory _disputeGameFactory, Duration _gameFinalizationDelay) {
DISPUTE_GAME_FACTORY = _disputeGameFactory;
DISPUTE_GAME_FINALIZATION_DELAY = _gameFinalizationDelay;

// Initialize the implementation with an empty array of starting anchor roots.
initialize(new StartingAnchorRoot[](0));
Expand All @@ -55,34 +70,80 @@ contract AnchorStateRegistry is Initializable, IAnchorStateRegistry, ISemver {
return DISPUTE_GAME_FACTORY;
}

/// @inheritdoc IAnchorStateRegistry
function tryUpdateAnchorState() external {
// Grab the game and game data.
IFaultDisputeGame game = IFaultDisputeGame(msg.sender);
(GameType gameType, Claim rootClaim, bytes memory extraData) = game.gameData();
/// @notice Verifies that the output root proposed as the root claim of a dispute game corresponds to the claimed
/// L2 block number.
/// @param _disputeGame The dispute game contract.
/// @param _outputRootProof The output root proof corresponding to the proposed output root in the dispute game.
/// @param _headerRLP The RLP-encoded block header corresponding to the L2 block in the output root proof.
function verifyAnchor(
IFaultDisputeGame _disputeGame,
Types.OutputRootProof calldata _outputRootProof,
bytes calldata _headerRLP
)
external
{
(GameType gameType, Claim rootClaim, bytes memory extraData) = _disputeGame.gameData();
(uint256 l2BlockNumber) = abi.decode(extraData, (uint256));

// Grab the verified address of the game based on the game data.
// slither-disable-next-line unused-return
(IDisputeGame factoryRegisteredGame,) =
DISPUTE_GAME_FACTORY.games({ _gameType: gameType, _rootClaim: rootClaim, _extraData: extraData });

// Must be a valid game.
require(
address(factoryRegisteredGame) == address(game),
address(factoryRegisteredGame) == address(_disputeGame),
"AnchorStateRegistry: fault dispute game not registered with factory"
);

// No need to update anything if the anchor state is already newer.
if (game.l2BlockNumber() <= anchors[gameType].l2BlockNumber) {
return;
}
require(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what happens if a game that commits to a later l2 block number resolves prior to this game? There would be no need to poke the anchor registry correct? If so, I’m thinking the offchain agent should account for this by calling the method to check it doesnt revert if this is true.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah no need! Should be caught in simulation, but updating the anchor can only advance the finalized claims about the safe L2 chain.

l2BlockNumber > anchors[gameType].l2BlockNumber,
"AnchorStateRegistry: block number of proposal does not advance anchor"
);

// Must be a game that resolved in favor of the state.
if (game.status() != GameStatus.DEFENDER_WINS) {
return;
require(
_disputeGame.status() == GameStatus.DEFENDER_WINS,
"AnchorStateRegistry: status of proposal is not DEFENDER_WINS"
);

require(
_disputeGame.resolvedAt().raw() + DISPUTE_GAME_FINALIZATION_DELAY.raw() <= block.timestamp,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contract now is looking like the OptimismPortal.
Why didn't we have this check before? Is that because we'd upgrade the ASR if it were to get poisoned by a blacklisted game? If so, why add the check now?

"AnchorStateRegistry: proposal not finalized"
);

// Verify the output root preimage.
require(
Hashing.hashOutputRootProof(_outputRootProof) == _disputeGame.rootClaim().raw(),
"AnchorStateRegistry: output root proof invalid"
);

// Verify the block preimage.
require(keccak256(_headerRLP) == _outputRootProof.latestBlockhash, "AnchorStateRegistry: header rlp invalid");

// 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 headerRLP = RLPReader.toRLPItem(_headerRLP);
RLPReader.RLPItem[] memory headerContents = RLPReader.readList(headerRLP);
bytes memory rawBlockNumber = RLPReader.readBytes(headerContents[HEADER_BLOCK_NUMBER_INDEX]);

require(rawBlockNumber.length <= 32, "AnchorStateRegistry: bad block header timestamp");

// 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)))
}

// Actually update the anchor state.
anchors[gameType] = OutputRoot({ l2BlockNumber: game.l2BlockNumber(), root: Hash.wrap(game.rootClaim().raw()) });
require(blockNumber == l2BlockNumber, "AnchorStateRegistry: block number mismatch");

// Update the anchor state.
anchors[gameType] = OutputRoot({ l2BlockNumber: l2BlockNumber, root: Hash.wrap(rootClaim.raw()) });

// Flag the game as verified.
verifiedGames[_disputeGame] = true;
}
}
13 changes: 6 additions & 7 deletions packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
Position internal constant ROOT_POSITION = Position.wrap(1);

/// @notice Semantic version.
/// @custom:semver 1.0.0
string public constant version = "1.0.0";
/// @custom:semver 1.1.0
string public constant version = "1.1.0";

/// @notice The starting timestamp of the game
Timestamp public createdAt;
Expand Down Expand Up @@ -186,7 +186,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {

// Do not allow the game to be initialized if the root claim corresponds to a block at or before the
// configured starting block number.
if (l2BlockNumber() <= rootBlockNumber) revert UnexpectedRootClaim(rootClaim());
(uint256 l2Number,) = l2BlockNumber();
if (l2Number <= rootBlockNumber) revert UnexpectedRootClaim(rootClaim());

// Set the root claim
claimData.push(
Expand Down Expand Up @@ -448,8 +449,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
}

/// @inheritdoc IFaultDisputeGame
function l2BlockNumber() public pure returns (uint256 l2BlockNumber_) {
function l2BlockNumber() public view returns (uint256 l2BlockNumber_, bool isVerified_) {
l2BlockNumber_ = _getArgUint256(0x54);
isVerified_ = ANCHOR_STATE_REGISTRY.verifiedGames(IDisputeGame(address(this)));
}

/// @inheritdoc IFaultDisputeGame
Expand Down Expand Up @@ -480,9 +482,6 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {

// Update the status and emit the resolved event, note that we're performing an assignment here.
emit Resolved(status = status_);

// Try to update the anchor state, this should not revert.
ANCHOR_STATE_REGISTRY.tryUpdateAnchorState();
}

/// @inheritdoc IFaultDisputeGame
Expand Down
Loading
Loading