diff --git a/op-chain-ops/deployer/opsm/opchain.go b/op-chain-ops/deployer/opsm/opchain.go index 79ad79569c3f..d600f200dcf1 100644 --- a/op-chain-ops/deployer/opsm/opchain.go +++ b/op-chain-ops/deployer/opsm/opchain.go @@ -9,6 +9,12 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/script" ) +// PermissionedGameStartingAnchorRoots is a root of bytes32(hex"dead") for the permissioned game at block 0, +// and no root for the permissionless game. +var PermissionedGameStartingAnchorRoots = []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xde, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +} + type DeployOPChainInput struct { OpChainProxyAdminOwner common.Address SystemConfigOwner common.Address @@ -27,6 +33,10 @@ func (input *DeployOPChainInput) InputSet() bool { return true } +func (input *DeployOPChainInput) StartingAnchorRoots() []byte { + return PermissionedGameStartingAnchorRoots +} + type DeployOPChainOutput struct { OpChainProxyAdmin common.Address AddressManager common.Address @@ -65,6 +75,7 @@ func DeployOPChain(host *script.Host, input DeployOPChainInput) (DeployOPChainOu return dco, fmt.Errorf("failed to insert DeployOPChainInput precompile: %w", err) } defer cleanupInput() + host.Label(inputAddr, "DeployOPChainInput") cleanupOutput, err := script.WithPrecompileAtAddress[*DeployOPChainOutput](host, outputAddr, &dco, script.WithFieldSetter[*DeployOPChainOutput]) @@ -72,6 +83,7 @@ func DeployOPChain(host *script.Host, input DeployOPChainInput) (DeployOPChainOu return dco, fmt.Errorf("failed to insert DeployOPChainOutput precompile: %w", err) } defer cleanupOutput() + host.Label(outputAddr, "DeployOPChainOutput") deployScript, cleanupDeploy, err := script.WithScript[DeployOPChainScript](host, "DeployOPChain.s.sol", "DeployOPChain") if err != nil { diff --git a/packages/contracts-bedrock/scripts/DeployImplementations.s.sol b/packages/contracts-bedrock/scripts/DeployImplementations.s.sol index 636053574331..433b1573efe4 100644 --- a/packages/contracts-bedrock/scripts/DeployImplementations.s.sol +++ b/packages/contracts-bedrock/scripts/DeployImplementations.s.sol @@ -617,14 +617,14 @@ contract DeployImplementations is Script { // The fault proofs contracts are configured as follows: // | Contract | Proxied | Deployment | MCP Ready | // |-------------------------|---------|-----------------------------------|------------| - // | DisputeGameFactory | Yes | Bespoke | Yes | - // | AnchorStateRegistry | Yes | Bespoke | No | - // | FaultDisputeGame | No | Bespoke | No | - // | PermissionedDisputeGame | No | Bespoke | No | - // | DelayedWETH | Yes | Two bespoke (one per DisputeGame) | No | - // | PreimageOracle | No | Shared | N/A | - // | MIPS | No | Shared | N/A | - // | OptimismPortal2 | Yes | Shared | No | + // | DisputeGameFactory | Yes | Bespoke | Yes | X + // | AnchorStateRegistry | Yes | Bespoke | No | X + // | FaultDisputeGame | No | Bespoke | No | Todo + // | PermissionedDisputeGame | No | Bespoke | No | Todo + // | DelayedWETH | Yes | Two bespoke (one per DisputeGame) | No | Todo: Proxies. + // | PreimageOracle | No | Shared | N/A | X + // | MIPS | No | Shared | N/A | X + // | OptimismPortal2 | Yes | Shared | No | X // // This script only deploys the shared contracts. The bespoke contracts are deployed by // `DeployOPChain.s.sol`. When the shared contracts are proxied, the contracts deployed here are diff --git a/packages/contracts-bedrock/scripts/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/DeployOPChain.s.sol index d2fec4e9e6d2..50fd9060ae91 100644 --- a/packages/contracts-bedrock/scripts/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/DeployOPChain.s.sol @@ -15,6 +15,7 @@ import { Constants } from "src/libraries/Constants.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; +import { Proxy } from "src/universal/Proxy.sol"; import { AddressManager } from "src/legacy/AddressManager.sol"; import { DelayedWETH } from "src/dispute/DelayedWETH.sol"; @@ -22,6 +23,7 @@ import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { AnchorStateRegistry } from "src/dispute/AnchorStateRegistry.sol"; import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol"; import { PermissionedDisputeGame } from "src/dispute/PermissionedDisputeGame.sol"; +import { GameType, GameTypes, Hash, OutputRoot } from "src/dispute/lib/Types.sol"; import { OPStackManager } from "src/L1/OPStackManager.sol"; import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol"; @@ -116,6 +118,26 @@ contract DeployOPChainInput is BaseDeployIO { return _l2ChainId; } + function startingAnchorRoots() public pure returns (bytes memory) { + // WARNING: For now always hardcode the starting permissioned game anchor root to 0xdead, + // and we do not set anything for the permissioned game. This is because we currently only + // support deploying straight to permissioned games, and the starting root does not + // matter for that, as long as it is non-zero, since no games will be played. We do not + // deploy the permissionless game (and therefore do not set a starting root for it here) + // because to to update to the permissionless game, we will need to update its starting + // anchor root and deploy a new permissioned dispute game contract anyway. + // + // You can `console.logBytes(abi.encode(defaultStartingAnchorRoots))` to get the bytes that + // are hardcoded into `op-chain-ops/deployer/opsm/opchain.go` + AnchorStateRegistry.StartingAnchorRoot[] memory defaultStartingAnchorRoots = + new AnchorStateRegistry.StartingAnchorRoot[](1); + defaultStartingAnchorRoots[0] = AnchorStateRegistry.StartingAnchorRoot({ + gameType: GameTypes.PERMISSIONED_CANNON, + outputRoot: OutputRoot({ root: Hash.wrap(bytes32(hex"dead")), l2BlockNumber: 0 }) + }); + return abi.encode(defaultStartingAnchorRoots); + } + // TODO: Check that opsm is proxied and it has an implementation. function opsmProxy() public view returns (OPStackManager) { require(address(_opsmProxy) != address(0), "DeployOPChainInput: not set"); @@ -162,7 +184,7 @@ contract DeployOPChainOutput is BaseDeployIO { // forgefmt: disable-end } - function checkOutput(DeployOPChainInput _doi) public view { + function checkOutput(DeployOPChainInput _doi) public { // With 16 addresses, we'd get a stack too deep error if we tried to do this inline as a // single call to `Solarray.addresses`. So we split it into two calls. address[] memory addrs1 = Solarray.addresses( @@ -266,7 +288,9 @@ contract DeployOPChainOutput is BaseDeployIO { // -------- Deployment Assertions -------- - function assertValidDeploy(DeployOPChainInput _doi) internal view { + function assertValidDeploy(DeployOPChainInput _doi) internal { + assertValidAnchorStateRegistryProxy(_doi); + assertValidAnchorStateRegistryImpl(_doi); assertValidDelayedWETHs(_doi); assertValidDisputeGameFactory(_doi); assertValidL1CrossDomainMessenger(_doi); @@ -279,6 +303,32 @@ contract DeployOPChainOutput is BaseDeployIO { // TODO add initialization assertions } + function assertValidAnchorStateRegistryProxy(DeployOPChainInput) internal { + // First we check the proxy as itself. + Proxy proxy = Proxy(payable(address(anchorStateRegistryProxy()))); + vm.prank(address(0)); + address admin = proxy.admin(); + require(admin == address(opChainProxyAdmin()), "ANCHORP-10"); + + // Then we check the proxy as ASR. + DeployUtils.assertInitialized({ _contractAddress: address(anchorStateRegistryProxy()), _slot: 0, _offset: 0 }); + + vm.prank(address(0)); + address impl = proxy.implementation(); + require(impl == address(anchorStateRegistryImpl()), "ANCHORP-20"); + require( + address(anchorStateRegistryProxy().disputeGameFactory()) == address(disputeGameFactoryProxy()), "ANCHORP-30" + ); + } + + function assertValidAnchorStateRegistryImpl(DeployOPChainInput) internal view { + AnchorStateRegistry registry = anchorStateRegistryImpl(); + + DeployUtils.assertInitialized({ _contractAddress: address(registry), _slot: 0, _offset: 0 }); + + require(address(registry.disputeGameFactory()) == address(disputeGameFactoryProxy()), "ANCHORI-10"); + } + function assertValidSystemConfig(DeployOPChainInput _doi) internal view { SystemConfig systemConfig = systemConfigProxy(); @@ -412,7 +462,8 @@ contract DeployOPChain is Script { roles: roles, basefeeScalar: _doi.basefeeScalar(), blobBasefeeScalar: _doi.blobBaseFeeScalar(), - l2ChainId: _doi.l2ChainId() + l2ChainId: _doi.l2ChainId(), + startingAnchorRoots: _doi.startingAnchorRoots() }); vm.broadcast(msg.sender); diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 91897e737ccf..db35c5b37429 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -32,8 +32,8 @@ "sourceCodeHash": "0xde4df0f9633dc0cdb1c9f634003ea5b0f7c5c1aebc407bc1b2f44c0ecf938649" }, "src/L1/OPStackManager.sol": { - "initCodeHash": "0x022b3f6a80eb637972dd0d9ce8666a037c4b916889f44f86771d8c3add9d615d", - "sourceCodeHash": "0xb085725e18c1a0cc1826b770e403ecad765fce686bb80555bf0f6c3c67b21cba" + "initCodeHash": "0x4bffecbd95e63f9bd04ab8e3c6a804cc25e0cd151ebeb7f8d6b9330332e6eb20", + "sourceCodeHash": "0x850f1eacc77f1a5c680625196618bc4b4332cb68924d9eddd57c749bedcd7c94" }, "src/L1/OptimismPortal.sol": { "initCodeHash": "0xbe2c0c81b3459014f287d8c89cdc0d27dde5d1f44e5d024fa1e4773ddc47c190", diff --git a/packages/contracts-bedrock/snapshots/abi/OPStackManager.json b/packages/contracts-bedrock/snapshots/abi/OPStackManager.json index 8f8d68515681..2ad0a4d1dc2c 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPStackManager.json +++ b/packages/contracts-bedrock/snapshots/abi/OPStackManager.json @@ -134,6 +134,11 @@ "internalType": "uint256", "name": "l2ChainId", "type": "uint256" + }, + { + "internalType": "bytes", + "name": "startingAnchorRoots", + "type": "bytes" } ], "internalType": "struct OPStackManager.DeployInput", diff --git a/packages/contracts-bedrock/snapshots/abi/OPStackManagerInterop.json b/packages/contracts-bedrock/snapshots/abi/OPStackManagerInterop.json index 8f8d68515681..2ad0a4d1dc2c 100644 --- a/packages/contracts-bedrock/snapshots/abi/OPStackManagerInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OPStackManagerInterop.json @@ -134,6 +134,11 @@ "internalType": "uint256", "name": "l2ChainId", "type": "uint256" + }, + { + "internalType": "bytes", + "name": "startingAnchorRoots", + "type": "bytes" } ], "internalType": "struct OPStackManager.DeployInput", diff --git a/packages/contracts-bedrock/src/L1/OPStackManager.sol b/packages/contracts-bedrock/src/L1/OPStackManager.sol index dad32c5628ea..f7d71233005e 100644 --- a/packages/contracts-bedrock/src/L1/OPStackManager.sol +++ b/packages/contracts-bedrock/src/L1/OPStackManager.sol @@ -55,6 +55,9 @@ contract OPStackManager is ISemver, Initializable { uint32 basefeeScalar; uint32 blobBasefeeScalar; uint256 l2ChainId; + // The correct type is AnchorStateRegistry.StartingAnchorRoot[] memory, + // but OP Deployer does not yet support structs. + bytes startingAnchorRoots; } /// @notice The full set of outputs from deploying a new OP Stack chain. @@ -115,8 +118,8 @@ contract OPStackManager is ISemver, Initializable { // -------- Constants and Variables -------- - /// @custom:semver 1.0.0-beta.3 - string public constant version = "1.0.0-beta.3"; + /// @custom:semver 1.0.0-beta.4 + string public constant version = "1.0.0-beta.4"; /// @notice Address of the SuperchainConfig contract shared by all chains. SuperchainConfig public immutable superchainConfig; @@ -206,8 +209,6 @@ contract OPStackManager is ISemver, Initializable { // -------- TODO: Placeholders -------- // For contracts we don't yet deploy, we set the outputs to dummy proxies so they have code to pass assertions. // We do these first, that way the disputeGameFactoryProxy is set when passed to the SystemConfig input. - output.anchorStateRegistryProxy = AnchorStateRegistry(deployProxy(l2ChainId, output.opChainProxyAdmin, "3")); - output.anchorStateRegistryImpl = AnchorStateRegistry(deployProxy(l2ChainId, output.opChainProxyAdmin, "4")); output.faultDisputeGame = FaultDisputeGame(deployProxy(l2ChainId, output.opChainProxyAdmin, "5")); output.permissionedDisputeGame = PermissionedDisputeGame(deployProxy(l2ChainId, output.opChainProxyAdmin, "6")); output.delayedWETHPermissionedGameProxy = @@ -266,7 +267,7 @@ contract OPStackManager is ISemver, Initializable { ); // -------- Set and Initialize Proxy Implementations -------- - Implementation storage impl; + Implementation memory impl; bytes memory data; impl = getLatestImplementation("L1ERC721Bridge"); @@ -293,10 +294,16 @@ contract OPStackManager is ISemver, Initializable { data = encodeL1StandardBridgeInitializer(impl.initializer, output); upgradeAndCall(output.opChainProxyAdmin, address(output.l1StandardBridgeProxy), impl.logic, data); + // TODO: also call setImplementation() once the dispute games are deployed. impl = getLatestImplementation("DisputeGameFactory"); data = encodeDisputeGameFactoryInitializer(impl.initializer, _input); upgradeAndCall(output.opChainProxyAdmin, address(output.disputeGameFactoryProxy), impl.logic, data); + impl.logic = address(output.anchorStateRegistryImpl); + impl.initializer = AnchorStateRegistry.initialize.selector; + data = encodeAnchorStateRegistryInitializer(impl.initializer, _input); + upgradeAndCall(output.opChainProxyAdmin, address(output.anchorStateRegistryProxy), impl.logic, data); + // -------- Finalize Deployment -------- // Transfer ownership of the ProxyAdmin from this contract to the specified owner. output.opChainProxyAdmin.transferOwnership(_input.roles.opChainProxyAdminOwner); @@ -345,9 +352,11 @@ contract OPStackManager is ISemver, Initializable { return Blueprint.deployFrom(blueprint.proxy, salt, abi.encode(_proxyAdmin)); } - /// @notice Returns the implementation data for a contract name. - function getLatestImplementation(string memory _name) internal view returns (Implementation storage) { - return implementations[latestRelease][_name]; + /// @notice Returns the implementation data for a contract name. Makes a copy of the internal + // Implementation struct in storage to prevent accidental mutation of the internal data. + function getLatestImplementation(string memory _name) internal view returns (Implementation memory) { + Implementation storage impl = implementations[latestRelease][_name]; + return Implementation({ logic: impl.logic, initializer: impl.initializer }); } // -------- Initializer Encoding -------- @@ -464,6 +473,21 @@ contract OPStackManager is ISemver, Initializable { return abi.encodeWithSelector(_selector, _input.roles.opChainProxyAdminOwner); } + function encodeAnchorStateRegistryInitializer( + bytes4 _selector, + DeployInput memory _input + ) + internal + view + virtual + returns (bytes memory) + { + // this line fails in the op-deployer tests because it is not passing in any data + AnchorStateRegistry.StartingAnchorRoot[] memory startingAnchorRoots = + abi.decode(_input.startingAnchorRoots, (AnchorStateRegistry.StartingAnchorRoot[])); + return abi.encodeWithSelector(_selector, startingAnchorRoots, superchainConfig); + } + /// @notice Returns default, standard config arguments for the SystemConfig initializer. /// This is used by subclasses to reduce code duplication. function defaultSystemConfigParams( diff --git a/packages/contracts-bedrock/test/DeployOPChain.t.sol b/packages/contracts-bedrock/test/DeployOPChain.t.sol index b0a4cb15507e..ef8fc06cc626 100644 --- a/packages/contracts-bedrock/test/DeployOPChain.t.sol +++ b/packages/contracts-bedrock/test/DeployOPChain.t.sol @@ -31,6 +31,8 @@ import { L1ERC721Bridge } from "src/L1/L1ERC721Bridge.sol"; import { L1StandardBridge } from "src/L1/L1StandardBridge.sol"; import { OptimismMintableERC20Factory } from "src/universal/OptimismMintableERC20Factory.sol"; +import { GameType, GameTypes, Hash, OutputRoot } from "src/dispute/lib/Types.sol"; + contract DeployOPChainInput_Test is Test { DeployOPChainInput doi; @@ -336,9 +338,29 @@ contract DeployOPChain_TestBase is Test { uint32 basefeeScalar = 100; uint32 blobBaseFeeScalar = 200; uint256 l2ChainId = 300; + AnchorStateRegistry.StartingAnchorRoot[] startingAnchorRoots; OPStackManager opsm = OPStackManager(address(0)); function setUp() public virtual { + // Set defaults for reference types + uint256 cannonBlock = 400; + uint256 permissionedBlock = 500; + startingAnchorRoots.push( + AnchorStateRegistry.StartingAnchorRoot({ + gameType: GameTypes.CANNON, + outputRoot: OutputRoot({ root: Hash.wrap(keccak256("defaultOutputRootCannon")), l2BlockNumber: cannonBlock }) + }) + ); + startingAnchorRoots.push( + AnchorStateRegistry.StartingAnchorRoot({ + gameType: GameTypes.PERMISSIONED_CANNON, + outputRoot: OutputRoot({ + root: Hash.wrap(keccak256("defaultOutputRootPermissioned")), + l2BlockNumber: permissionedBlock + }) + }) + ); + // Initialize deploy scripts. DeploySuperchain deploySuperchain = new DeploySuperchain(); (DeploySuperchainInput dsi, DeploySuperchainOutput dso) = deploySuperchain.etchIOContracts(); @@ -389,7 +411,7 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { return keccak256(abi.encode(_seed, _i)); } - function testFuzz_run_memory_succeeds(bytes32 _seed) public { + function testFuzz_run_memory_succeed(bytes32 _seed) public { opChainProxyAdminOwner = address(uint160(uint256(hash(_seed, 0)))); systemConfigOwner = address(uint160(uint256(hash(_seed, 1)))); batcher = address(uint160(uint256(hash(_seed, 2)))); @@ -398,7 +420,26 @@ contract DeployOPChain_Test is DeployOPChain_TestBase { challenger = address(uint160(uint256(hash(_seed, 5)))); basefeeScalar = uint32(uint256(hash(_seed, 6))); blobBaseFeeScalar = uint32(uint256(hash(_seed, 7))); - l2ChainId = uint256(uint256(hash(_seed, 8))); + l2ChainId = uint256(hash(_seed, 8)); + + // Set the initial anchor states. The typical usage we expect is to pass in one root per game type. + uint256 cannonBlock = uint256(hash(_seed, 9)); + uint256 permissionedBlock = uint256(hash(_seed, 10)); + startingAnchorRoots.push( + AnchorStateRegistry.StartingAnchorRoot({ + gameType: GameTypes.CANNON, + outputRoot: OutputRoot({ root: Hash.wrap(keccak256(abi.encode(_seed, 11))), l2BlockNumber: cannonBlock }) + }) + ); + startingAnchorRoots.push( + AnchorStateRegistry.StartingAnchorRoot({ + gameType: GameTypes.PERMISSIONED_CANNON, + outputRoot: OutputRoot({ + root: Hash.wrap(keccak256(abi.encode(_seed, 12))), + l2BlockNumber: permissionedBlock + }) + }) + ); doi.set(doi.opChainProxyAdminOwner.selector, opChainProxyAdminOwner); doi.set(doi.systemConfigOwner.selector, systemConfigOwner); diff --git a/packages/contracts-bedrock/test/L1/OPStackManager.t.sol b/packages/contracts-bedrock/test/L1/OPStackManager.t.sol index 0443c9f0c46d..6d9d7d134c33 100644 --- a/packages/contracts-bedrock/test/L1/OPStackManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPStackManager.t.sol @@ -62,7 +62,8 @@ contract OPStackManager_Deploy_Test is DeployOPChain_TestBase { }), basefeeScalar: _doi.basefeeScalar(), blobBasefeeScalar: _doi.blobBaseFeeScalar(), - l2ChainId: _doi.l2ChainId() + l2ChainId: _doi.l2ChainId(), + startingAnchorRoots: _doi.startingAnchorRoots() }); }