Commit 3fc04bcf authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

op-deployer: Add manage command for interop dependencies (#13394)

* op-deployer: Add manage command for interop dependencies

You can now run `op-deployer manage dependencies ...` to add/remove remote chains from a local chain's Interop dependency set.

* semgrep
parent cc168516
...@@ -4,6 +4,8 @@ import ( ...@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/manage"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/bootstrap" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/bootstrap"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/inspect" "github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/inspect"
...@@ -52,6 +54,11 @@ func main() { ...@@ -52,6 +54,11 @@ func main() {
Usage: "inspects the state of a deployment", Usage: "inspects the state of a deployment",
Subcommands: inspect.Commands, Subcommands: inspect.Commands,
}, },
{
Name: "manage",
Usage: "performs individual operations on a chain",
Subcommands: manage.Commands,
},
} }
app.Writer = os.Stdout app.Writer = os.Stdout
app.ErrWriter = os.Stderr app.ErrWriter = os.Stderr
......
...@@ -17,6 +17,10 @@ import ( ...@@ -17,6 +17,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/env"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/retryproxy" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/retryproxy"
altda "github.com/ethereum-optimism/optimism/op-alt-da" altda "github.com/ethereum-optimism/optimism/op-alt-da"
...@@ -800,6 +804,47 @@ func TestIntentConfiguration(t *testing.T) { ...@@ -800,6 +804,47 @@ func TestIntentConfiguration(t *testing.T) {
} }
} }
func TestManageDependencies(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
l1ChainID := uint64(999)
l1ChainIDBig := new(big.Int).SetUint64(l1ChainID)
opts, intent, st := setupGenesisChain(t, l1ChainID)
intent.UseInterop = true
require.NoError(t, deployer.ApplyPipeline(ctx, opts))
dk, err := devkeys.NewMnemonicDevKeys(devkeys.TestMnemonic)
require.NoError(t, err)
sysConfigOwner, err := dk.Address(devkeys.SystemConfigOwner.Key(l1ChainIDBig))
require.NoError(t, err)
// Have to recreate the host again since deployer.ApplyPipeline
// doesn't expose the host directly.
loc, _ := testutil.LocalArtifacts(t)
afacts, _, err := artifacts.Download(ctx, loc, artifacts.NoopDownloadProgressor)
require.NoError(t, err)
host, err := env.DefaultScriptHost(
broadcaster.NoopBroadcaster(),
opts.Logger,
sysConfigOwner,
afacts,
)
require.NoError(t, err)
host.ImportState(st.L1StateDump.Data)
require.NoError(t, opcm.ManageDependencies(host, opcm.ManageDependenciesInput{
ChainId: big.NewInt(1234),
SystemConfig: st.Chains[0].SystemConfigProxyAddress,
Remove: false,
}))
}
func setupGenesisChain(t *testing.T, l1ChainID uint64) (deployer.ApplyPipelineOpts, *state.Intent, *state.State) { func setupGenesisChain(t *testing.T, l1ChainID uint64) (deployer.ApplyPipelineOpts, *state.Intent, *state.State) {
lgr := testlog.Logger(t, slog.LevelDebug) lgr := testlog.Logger(t, slog.LevelDebug)
......
package manage
import (
"context"
"crypto/ecdsa"
"fmt"
"strings"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/artifacts"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/bootstrap"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/broadcaster"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/opcm"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/env"
op_service "github.com/ethereum-optimism/optimism/op-service"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
"github.com/ethereum-optimism/optimism/op-service/ctxinterrupt"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/urfave/cli/v2"
)
type DependenciesConfig struct {
L1RPCUrl string
PrivateKey string
Logger log.Logger
ArtifactsLocator *artifacts.Locator
ChainID common.Hash
SystemConfig common.Address
Remove bool
privateKeyECDSA *ecdsa.PrivateKey
}
func (c *DependenciesConfig) Check() error {
if c.L1RPCUrl == "" {
return fmt.Errorf("l1RPCUrl must be specified")
}
if c.PrivateKey == "" {
return fmt.Errorf("private key must be specified")
}
privECDSA, err := crypto.HexToECDSA(strings.TrimPrefix(c.PrivateKey, "0x"))
if err != nil {
return fmt.Errorf("failed to parse private key: %w", err)
}
c.privateKeyECDSA = privECDSA
if c.Logger == nil {
return fmt.Errorf("logger must be specified")
}
if c.ArtifactsLocator == nil {
return fmt.Errorf("artifacts locator must be specified")
}
if c.ChainID == (common.Hash{}) {
return fmt.Errorf("chain ID must be specified")
}
if c.SystemConfig == (common.Address{}) {
return fmt.Errorf("system config must be specified")
}
return nil
}
func DependenciesCLI(cliCtx *cli.Context) error {
logCfg := oplog.ReadCLIConfig(cliCtx)
l := oplog.NewLogger(oplog.AppOut(cliCtx), logCfg)
oplog.SetGlobalLogHandler(l.Handler())
l1RPCUrl := cliCtx.String(deployer.L1RPCURLFlagName)
privateKey := cliCtx.String(deployer.PrivateKeyFlagName)
artifactsURLStr := cliCtx.String(bootstrap.ArtifactsLocatorFlagName)
artifactsLocator := new(artifacts.Locator)
if err := artifactsLocator.UnmarshalText([]byte(artifactsURLStr)); err != nil {
return fmt.Errorf("failed to parse artifacts URL: %w", err)
}
chainID, err := op_service.Parse256BitChainID(cliCtx.String(ChainIDFlagName))
if err != nil {
return fmt.Errorf("failed to parse chain ID: %w", err)
}
systemConfig := common.HexToAddress(cliCtx.String(SystemConfigFlagName))
remove := cliCtx.Bool(RemoveFlagName)
cfg := DependenciesConfig{
L1RPCUrl: l1RPCUrl,
PrivateKey: privateKey,
Logger: l,
ArtifactsLocator: artifactsLocator,
ChainID: chainID,
SystemConfig: systemConfig,
Remove: remove,
}
ctx := ctxinterrupt.WithCancelOnInterrupt(cliCtx.Context)
return Dependencies(ctx, cfg)
}
func Dependencies(ctx context.Context, cfg DependenciesConfig) error {
if err := cfg.Check(); err != nil {
return err
}
lgr := cfg.Logger
progressor := func(curr, total int64) {
lgr.Info("artifacts download progress", "current", curr, "total", total)
}
artifactsFS, cleanup, err := artifacts.Download(ctx, cfg.ArtifactsLocator, progressor)
if err != nil {
return fmt.Errorf("failed to download artifacts: %w", err)
}
defer func() {
if err := cleanup(); err != nil {
lgr.Warn("failed to clean up artifacts", "err", err)
}
}()
l1RPC, err := rpc.Dial(cfg.L1RPCUrl)
if err != nil {
return fmt.Errorf("failed to connect to L1 RPC: %w", err)
}
l1Client := ethclient.NewClient(l1RPC)
chainID, err := l1Client.ChainID(ctx)
if err != nil {
return fmt.Errorf("failed to get chain ID: %w", err)
}
signer := opcrypto.SignerFnFromBind(opcrypto.PrivateKeySignerFn(cfg.privateKeyECDSA, chainID))
chainDeployer := crypto.PubkeyToAddress(cfg.privateKeyECDSA.PublicKey)
bcaster, err := broadcaster.NewKeyedBroadcaster(broadcaster.KeyedBroadcasterOpts{
Logger: lgr,
ChainID: chainID,
Client: l1Client,
Signer: signer,
From: chainDeployer,
})
if err != nil {
return fmt.Errorf("failed to create broadcaster: %w", err)
}
host, err := env.DefaultForkedScriptHost(
ctx,
bcaster,
lgr,
chainDeployer,
artifactsFS,
l1RPC,
)
if err != nil {
return fmt.Errorf("failed to create script host: %w", err)
}
input := opcm.ManageDependenciesInput{
ChainId: cfg.ChainID.Big(),
SystemConfig: cfg.SystemConfig,
Remove: cfg.Remove,
}
msg := "adding dependencies"
if cfg.Remove {
msg = "removing dependencies"
}
lgr.Info(msg)
if err := opcm.ManageDependencies(host, input); err != nil {
return fmt.Errorf("error running manage dependencies script: %w", err)
}
if _, err := bcaster.Broadcast(ctx); err != nil {
return fmt.Errorf("failed to broadcast transactions: %w", err)
}
lgr.Info("success")
return nil
}
package manage
import (
"context"
"fmt"
"log/slog"
"math/big"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-chain-ops/devkeys"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/pipeline"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/standard"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/state"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/testutil"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum-optimism/optimism/op-service/testutils/anvil"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
)
func TestDependencies(t *testing.T) {
t.Parallel()
lgr := testlog.Logger(t, slog.LevelDebug)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
runner, err := anvil.New(
"",
lgr,
)
require.NoError(t, err)
require.NoError(t, runner.Start(ctx))
t.Cleanup(func() {
require.NoError(t, runner.Stop())
})
// Start by deploying a chain
l1ChainID := uint64(31337)
l1ChainIDBig := new(big.Int).SetUint64(l1ChainID)
l2ChainID := uint64(777)
l2ChainIDBig := new(big.Int).SetUint64(l2ChainID)
dk, err := devkeys.NewMnemonicDevKeys(devkeys.TestMnemonic)
require.NoError(t, err)
loc, _ := testutil.LocalArtifacts(t)
addrFor := func(role devkeys.Role) common.Address {
addr, err := dk.Address(role.Key(l1ChainIDBig))
require.NoError(t, err)
return addr
}
deployerPrivStr := "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
deployerPriv, err := crypto.HexToECDSA(deployerPrivStr)
require.NoError(t, err)
deployerAddr := crypto.PubkeyToAddress(deployerPriv.PublicKey)
intent := &state.Intent{
ConfigType: state.IntentConfigTypeCustom,
DeploymentStrategy: state.DeploymentStrategyLive,
L1ChainID: l1ChainID,
SuperchainRoles: &state.SuperchainRoles{
ProxyAdminOwner: addrFor(devkeys.L1ProxyAdminOwnerRole),
ProtocolVersionsOwner: addrFor(devkeys.SuperchainDeployerKey),
Guardian: addrFor(devkeys.SuperchainConfigGuardianKey),
},
FundDevAccounts: true,
UseInterop: true,
L1ContractsLocator: loc,
L2ContractsLocator: loc,
Chains: []*state.ChainIntent{
{
ID: common.BigToHash(l2ChainIDBig),
BaseFeeVaultRecipient: addrFor(devkeys.BaseFeeVaultRecipientRole),
L1FeeVaultRecipient: addrFor(devkeys.L1FeeVaultRecipientRole),
SequencerFeeVaultRecipient: addrFor(devkeys.SequencerFeeVaultRecipientRole),
Eip1559DenominatorCanyon: standard.Eip1559DenominatorCanyon,
Eip1559Denominator: standard.Eip1559Denominator,
Eip1559Elasticity: standard.Eip1559Elasticity,
Roles: state.ChainRoles{
L1ProxyAdminOwner: addrFor(devkeys.L2ProxyAdminOwnerRole),
L2ProxyAdminOwner: addrFor(devkeys.L2ProxyAdminOwnerRole),
// Set to deployer addr since it's prefunded in Anvil
SystemConfigOwner: deployerAddr,
UnsafeBlockSigner: addrFor(devkeys.SequencerP2PRole),
Batcher: addrFor(devkeys.BatcherRole),
Proposer: addrFor(devkeys.ProposerRole),
Challenger: addrFor(devkeys.ChallengerRole),
},
},
},
}
st := &state.State{
Version: 1,
}
opts := deployer.ApplyPipelineOpts{
L1RPCUrl: runner.RPCUrl(),
DeployerPrivateKey: deployerPriv,
Intent: intent,
State: st,
Logger: lgr,
StateWriter: pipeline.NoopStateWriter(),
}
require.NoError(t, deployer.ApplyPipeline(ctx, opts))
// Now we can test the Dependencies function
for _, remove := range []bool{true, false} {
t.Run(fmt.Sprintf("remove=%v", remove), func(t *testing.T) {
require.NoError(t, Dependencies(ctx, DependenciesConfig{
L1RPCUrl: runner.RPCUrl(),
PrivateKey: deployerPrivStr,
Logger: lgr,
ArtifactsLocator: loc,
ChainID: common.BigToHash(big.NewInt(1234)),
SystemConfig: st.Chains[0].SystemConfigProxyAddress,
Remove: remove,
privateKeyECDSA: nil,
}))
})
}
}
package manage
import (
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer"
"github.com/ethereum-optimism/optimism/op-deployer/pkg/deployer/bootstrap"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
"github.com/urfave/cli/v2"
)
const (
ChainIDFlagName = "chain-id"
SystemConfigFlagName = "system-config"
RemoveFlagName = "remove"
)
var (
ChainIDFlagManageDependencies = &cli.StringFlag{
Name: ChainIDFlagName,
Usage: "The chain ID to add or remove.",
EnvVars: deployer.PrefixEnvVar("CHAIN_ID"),
Required: true,
}
SystemConfigFlagManageDependencies = &cli.StringFlag{
Name: SystemConfigFlagName,
Usage: "The system config of the chain whose dependencies are being managed.",
EnvVars: deployer.PrefixEnvVar("SYSTEM_CONFIG"),
Required: true,
}
RemoveFlagManageDependencies = &cli.BoolFlag{
Name: RemoveFlagName,
Usage: "Remove the dependency instead of adding it.",
EnvVars: deployer.PrefixEnvVar("REMOVE"),
}
)
var Commands = []*cli.Command{
{
Name: "dependencies",
Usage: "Manage dependencies for a chain's interop set.",
Flags: cliapp.ProtectFlags([]cli.Flag{
deployer.L1RPCURLFlag,
deployer.PrivateKeyFlag,
bootstrap.ArtifactsLocatorFlag,
ChainIDFlagManageDependencies,
SystemConfigFlagManageDependencies,
RemoveFlagManageDependencies,
}),
Action: DependenciesCLI,
},
}
package opcm
import (
"math/big"
"github.com/ethereum-optimism/optimism/op-chain-ops/script"
"github.com/ethereum/go-ethereum/common"
)
type ManageDependenciesInput struct {
ChainId *big.Int
SystemConfig common.Address
Remove bool
}
func ManageDependencies(
host *script.Host,
input ManageDependenciesInput,
) error {
return RunScriptVoid[ManageDependenciesInput](host, input, "ManageDependencies.s.sol", "ManageDependencies")
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { Script } from "forge-std/Script.sol";
import { BaseDeployIO } from "scripts/deploy/BaseDeployIO.sol";
import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol";
contract ManageDependenciesInput is BaseDeployIO {
uint256 internal _chainId;
ISystemConfigInterop _systemConfig;
bool internal _remove;
// Setter for uint256 type
function set(bytes4 _sel, uint256 _value) public {
if (_sel == this.chainId.selector) _chainId = _value;
else revert("ManageDependenciesInput: unknown selector");
}
// Setter for address type
function set(bytes4 _sel, address _addr) public {
require(_addr != address(0), "ManageDependenciesInput: cannot set zero address");
if (_sel == this.systemConfig.selector) _systemConfig = ISystemConfigInterop(_addr);
else revert("ManageDependenciesInput: unknown selector");
}
// Setter for bool type
function set(bytes4 _sel, bool _value) public {
if (_sel == this.remove.selector) _remove = _value;
else revert("ManageDependenciesInput: unknown selector");
}
// Getters
function chainId() public view returns (uint256) {
require(_chainId > 0, "ManageDependenciesInput: not set");
return _chainId;
}
function systemConfig() public view returns (ISystemConfigInterop) {
require(address(_systemConfig) != address(0), "ManageDependenciesInput: not set");
return _systemConfig;
}
function remove() public view returns (bool) {
return _remove;
}
}
contract ManageDependencies is Script {
function run(ManageDependenciesInput _input) public {
bool remove = _input.remove();
uint256 chainId = _input.chainId();
ISystemConfigInterop systemConfig = _input.systemConfig();
// Call the appropriate function based on the remove flag
vm.broadcast(msg.sender);
if (remove) {
systemConfig.removeDependency(chainId);
} else {
systemConfig.addDependency(chainId);
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import { Test } from "forge-std/Test.sol";
import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol";
import { ManageDependencies, ManageDependenciesInput } from "scripts/deploy/ManageDependencies.s.sol";
contract ManageDependencies_Test is Test {
ManageDependencies script;
ManageDependenciesInput input;
address mockSystemConfig;
uint256 testChainId;
event DependencyAdded(uint256 indexed chainId);
event DependencyRemoved(uint256 indexed chainId);
function setUp() public {
script = new ManageDependencies();
input = new ManageDependenciesInput();
mockSystemConfig = makeAddr("systemConfig");
testChainId = 123;
vm.etch(mockSystemConfig, hex"01");
}
function test_run_add_succeeds() public {
input.set(input.systemConfig.selector, mockSystemConfig);
input.set(input.chainId.selector, testChainId);
input.set(input.remove.selector, false);
// Expect the addDependency call
vm.mockCall(mockSystemConfig, abi.encodeCall(ISystemConfigInterop.addDependency, testChainId), bytes(""));
script.run(input);
}
function test_run_remove_succeeds() public {
input.set(input.systemConfig.selector, mockSystemConfig);
input.set(input.chainId.selector, testChainId);
input.set(input.remove.selector, true);
vm.mockCall(mockSystemConfig, abi.encodeCall(ISystemConfigInterop.removeDependency, testChainId), bytes(""));
script.run(input);
}
}
contract ManageDependenciesInput_Test is Test {
ManageDependenciesInput input;
function setUp() public {
input = new ManageDependenciesInput();
}
function test_getters_whenNotSet_reverts() public {
vm.expectRevert("ManageDependenciesInput: not set");
input.chainId();
vm.expectRevert("ManageDependenciesInput: not set");
input.systemConfig();
// remove() doesn't revert when not set, returns false
assertFalse(input.remove());
}
function test_set_succeeds() public {
address systemConfig = makeAddr("systemConfig");
uint256 chainId = 123;
bool remove = true;
vm.etch(systemConfig, hex"01");
input.set(input.systemConfig.selector, systemConfig);
input.set(input.chainId.selector, chainId);
input.set(input.remove.selector, remove);
assertEq(address(input.systemConfig()), systemConfig);
assertEq(input.chainId(), chainId);
assertTrue(input.remove());
}
function test_set_withZeroAddress_reverts() public {
vm.expectRevert("ManageDependenciesInput: cannot set zero address");
input.set(input.systemConfig.selector, address(0));
}
function test_set_withInvalidSelector_reverts() public {
vm.expectRevert("ManageDependenciesInput: unknown selector");
input.set(bytes4(0xdeadbeef), makeAddr("test"));
vm.expectRevert("ManageDependenciesInput: unknown selector");
input.set(bytes4(0xdeadbeef), uint256(1));
vm.expectRevert("ManageDependenciesInput: unknown selector");
input.set(bytes4(0xdeadbeef), true);
}
}
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