From 96c21ec59cebdcad4546c32b0e5dc77379468e9c Mon Sep 17 00:00:00 2001 From: protolambda Date: Wed, 28 Sep 2022 14:53:43 +0200 Subject: [PATCH] op-e2e: e2eutils package for new action testing setup --- op-e2e/e2eutils/secrets.go | 160 ++++++++++++++++++++++++ op-e2e/e2eutils/setup.go | 228 ++++++++++++++++++++++++++++++++++ op-e2e/e2eutils/setup_test.go | 37 ++++++ op-e2e/e2eutils/testing.go | 26 ++++ op-e2e/go.mod | 1 + op-e2e/go.sum | 2 + 6 files changed, 454 insertions(+) create mode 100644 op-e2e/e2eutils/secrets.go create mode 100644 op-e2e/e2eutils/setup.go create mode 100644 op-e2e/e2eutils/setup_test.go create mode 100644 op-e2e/e2eutils/testing.go diff --git a/op-e2e/e2eutils/secrets.go b/op-e2e/e2eutils/secrets.go new file mode 100644 index 0000000000000..268b576aca43f --- /dev/null +++ b/op-e2e/e2eutils/secrets.go @@ -0,0 +1,160 @@ +package e2eutils + +import ( + "crypto/ecdsa" + "fmt" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + hdwallet "github.com/miguelmota/go-ethereum-hdwallet" +) + +// DefaultMnemonicConfig is the default mnemonic used in testing. +// We prefer a mnemonic rather than direct private keys to make it easier +// to export all testing keys in external tooling for use during debugging. +var DefaultMnemonicConfig = &MnemonicConfig{ + Mnemonic: "test test test test test test test test test test test junk", + Deployer: "m/44'/60'/0'/0/1", + // clique signer: removed, use engine API instead + Proposer: "m/44'/60'/0'/0/3", + Batcher: "m/44'/60'/0'/0/4", + SequencerP2P: "m/44'/60'/0'/0/5", + Alice: "m/44'/60'/0'/0/6", + Bob: "m/44'/60'/0'/0/7", + Mallory: "m/44'/60'/0'/0/8", +} + +// MnemonicConfig configures the private keys for testing purposes. +type MnemonicConfig struct { + Mnemonic string + + Deployer string + + // rollup actors + Proposer string + Batcher string + SequencerP2P string + + // prefunded L1/L2 accounts for testing + Alice string + Bob string + Mallory string +} + +// Secrets computes the private keys for all mnemonic paths, +// which can then be kept around for fast precomputed private key access. +func (m *MnemonicConfig) Secrets() (*Secrets, error) { + wallet, err := hdwallet.NewFromMnemonic(m.Mnemonic) + if err != nil { + return nil, fmt.Errorf("failed to create wallet: %w", err) + } + account := func(path string) accounts.Account { + return accounts.Account{URL: accounts.URL{Path: path}} + } + + deployer, err := wallet.PrivateKey(account(m.Deployer)) + if err != nil { + return nil, err + } + proposer, err := wallet.PrivateKey(account(m.Proposer)) + if err != nil { + return nil, err + } + batcher, err := wallet.PrivateKey(account(m.Batcher)) + if err != nil { + return nil, err + } + sequencerP2P, err := wallet.PrivateKey(account(m.SequencerP2P)) + if err != nil { + return nil, err + } + alice, err := wallet.PrivateKey(account(m.Alice)) + if err != nil { + return nil, err + } + bob, err := wallet.PrivateKey(account(m.Bob)) + if err != nil { + return nil, err + } + mallory, err := wallet.PrivateKey(account(m.Mallory)) + if err != nil { + return nil, err + } + + return &Secrets{ + Deployer: deployer, + Proposer: proposer, + Batcher: batcher, + SequencerP2P: sequencerP2P, + Alice: alice, + Bob: bob, + Mallory: mallory, + }, nil +} + +// Secrets bundles secp256k1 private keys for all common rollup actors for testing purposes. +type Secrets struct { + Deployer *ecdsa.PrivateKey + + // rollup actors + Proposer *ecdsa.PrivateKey + Batcher *ecdsa.PrivateKey + SequencerP2P *ecdsa.PrivateKey + + // prefunded L1/L2 accounts for testing + Alice *ecdsa.PrivateKey + Bob *ecdsa.PrivateKey + Mallory *ecdsa.PrivateKey +} + +// EncodePrivKey encodes the given private key in 32 bytes +func EncodePrivKey(priv *ecdsa.PrivateKey) hexutil.Bytes { + privkey := make([]byte, 32) + blob := priv.D.Bytes() + copy(privkey[32-len(blob):], blob) + return privkey +} + +// Addresses computes the ethereum address of each account, +// which can then be kept around for fast precomputed address access. +func (s *Secrets) Addresses() *Addresses { + return &Addresses{ + Deployer: crypto.PubkeyToAddress(s.Deployer.PublicKey), + Proposer: crypto.PubkeyToAddress(s.Proposer.PublicKey), + Batcher: crypto.PubkeyToAddress(s.Batcher.PublicKey), + SequencerP2P: crypto.PubkeyToAddress(s.SequencerP2P.PublicKey), + Alice: crypto.PubkeyToAddress(s.Alice.PublicKey), + Bob: crypto.PubkeyToAddress(s.Bob.PublicKey), + Mallory: crypto.PubkeyToAddress(s.Mallory.PublicKey), + } +} + +// Addresses bundles the addresses for all common rollup addresses for testing purposes. +type Addresses struct { + Deployer common.Address + + // rollup actors + Proposer common.Address + Batcher common.Address + SequencerP2P common.Address + + // prefunded L1/L2 accounts for testing + Alice common.Address + Bob common.Address + Mallory common.Address +} + +func (a *Addresses) All() []common.Address { + return []common.Address{ + a.Batcher, + a.Deployer, + a.Proposer, + a.Batcher, + a.SequencerP2P, + a.Alice, + a.Bob, + a.Mallory, + } +} diff --git a/op-e2e/e2eutils/setup.go b/op-e2e/e2eutils/setup.go new file mode 100644 index 0000000000000..b3a63aa0c0b64 --- /dev/null +++ b/op-e2e/e2eutils/setup.go @@ -0,0 +1,228 @@ +package e2eutils + +import ( + "math/big" + "os" + "path" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/op-bindings/predeploys" + "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" + "github.com/ethereum-optimism/optimism/op-node/eth" + "github.com/ethereum-optimism/optimism/op-node/rollup" +) + +var testingJWTSecret = [32]byte{123} + +// WriteDefaultJWT writes a testing JWT to the temporary directory of the test and returns the path to the JWT file. +func WriteDefaultJWT(t TestingBase) string { + // Sadly the geth node config cannot load JWT secret from memory, it has to be a file + jwtPath := path.Join(t.TempDir(), "jwt_secret") + if err := os.WriteFile(jwtPath, []byte(hexutil.Encode(testingJWTSecret[:])), 0600); err != nil { + t.Fatalf("failed to prepare jwt file for geth: %v", err) + } + return jwtPath +} + +func uint642big(in uint64) *hexutil.Big { + return (*hexutil.Big)(new(big.Int).SetUint64(in)) +} + +// DeployParams bundles the deployment parameters to generate further testing inputs with. +type DeployParams struct { + DeployConfig *genesis.DeployConfig + MnemonicConfig *MnemonicConfig + Secrets *Secrets + Addresses *Addresses +} + +// TestParams parametrizes the most essential rollup configuration parameters +type TestParams struct { + MaxSequencerDrift uint64 + SequencerWindowSize uint64 + ChannelTimeout uint64 +} + +func MakeDeployParams(t require.TestingT, tp *TestParams) *DeployParams { + mnemonicCfg := DefaultMnemonicConfig + secrets, err := mnemonicCfg.Secrets() + require.NoError(t, err) + addresses := secrets.Addresses() + deployConfig := &genesis.DeployConfig{ + L1ChainID: 901, + L2ChainID: 902, + L2BlockTime: 2, + + MaxSequencerDrift: tp.MaxSequencerDrift, + SequencerWindowSize: tp.SequencerWindowSize, + ChannelTimeout: tp.ChannelTimeout, + P2PSequencerAddress: addresses.SequencerP2P, + OptimismL2FeeRecipient: common.Address{0: 0x42, 19: 0xf0}, // tbd + BatchInboxAddress: common.Address{0: 0x42, 19: 0xff}, // tbd + BatchSenderAddress: addresses.Batcher, + + L2OutputOracleSubmissionInterval: 6, + L2OutputOracleStartingTimestamp: -1, + L2OutputOracleProposer: addresses.Proposer, + L2OutputOracleOwner: common.Address{}, // tbd + + L1BlockTime: 15, + L1GenesisBlockNonce: 0, + CliqueSignerAddress: common.Address{}, // proof of stake, no clique + L1GenesisBlockTimestamp: hexutil.Uint64(time.Now().Unix()), + L1GenesisBlockGasLimit: 15_000_000, + L1GenesisBlockDifficulty: uint642big(1), + L1GenesisBlockMixHash: common.Hash{}, + L1GenesisBlockCoinbase: common.Address{}, + L1GenesisBlockNumber: 0, + L1GenesisBlockGasUsed: 0, + L1GenesisBlockParentHash: common.Hash{}, + L1GenesisBlockBaseFeePerGas: uint642big(1000_000_000), // 1 gwei + + L2GenesisBlockNonce: 0, + L2GenesisBlockExtraData: []byte{}, + L2GenesisBlockGasLimit: 15_000_000, + L2GenesisBlockDifficulty: uint642big(0), + L2GenesisBlockMixHash: common.Hash{}, + L2GenesisBlockCoinbase: common.Address{0: 0x42, 19: 0xf0}, // matching OptimismL2FeeRecipient + L2GenesisBlockNumber: 0, + L2GenesisBlockGasUsed: 0, + L2GenesisBlockParentHash: common.Hash{}, + L2GenesisBlockBaseFeePerGas: uint642big(1000_000_000), + + OptimismBaseFeeRecipient: common.Address{0: 0x42, 19: 0xf1}, // tbd + OptimismL1FeeRecipient: addresses.Batcher, + L2CrossDomainMessengerOwner: common.Address{0: 0x42, 19: 0xf2}, // tbd + GasPriceOracleOwner: common.Address{0: 0x42, 19: 0xf3}, // tbd + GasPriceOracleOverhead: 2100, + GasPriceOracleScalar: 1000_000, + GasPriceOracleDecimals: 6, + DeploymentWaitConfirmations: 1, + + EIP1559Elasticity: 10, + EIP1559Denominator: 50, + + FundDevAccounts: false, + } + return &DeployParams{ + DeployConfig: deployConfig, + MnemonicConfig: mnemonicCfg, + Secrets: secrets, + Addresses: addresses, + } +} + +// DeploymentsL1 captures the L1 addresses used in the deployment, +// commonly just the developer predeploys during testing, +// but later deployed contracts may be used in some tests too. +type DeploymentsL1 struct { + L1CrossDomainMessengerProxy common.Address + L1StandardBridgeProxy common.Address + L2OutputOracleProxy common.Address + OptimismPortalProxy common.Address +} + +// SetupData bundles the L1, L2, rollup and deployment configuration data: everything for a full test setup. +type SetupData struct { + L1Cfg *core.Genesis + L2Cfg *core.Genesis + RollupCfg *rollup.Config + DeploymentsL1 DeploymentsL1 +} + +// AllocParams defines genesis allocations to apply on top of the genesis generated by deploy parameters. +// These allocations override existing allocations per account, +// i.e. the allocations are merged with AllocParams having priority. +type AllocParams struct { + L1Alloc core.GenesisAlloc + L2Alloc core.GenesisAlloc + PrefundTestUsers bool +} + +var etherScalar = new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) + +// Ether converts a uint64 Ether amount into a *big.Int amount in wei units, for allocating test balances. +func Ether(v uint64) *big.Int { + return new(big.Int).Mul(new(big.Int).SetUint64(v), etherScalar) +} + +// Setup computes the testing setup configurations from deployment configuration and optional allocation parameters. +func Setup(t require.TestingT, deployParams *DeployParams, alloc *AllocParams) *SetupData { + deployConf := deployParams.DeployConfig + l1Genesis, err := genesis.BuildL1DeveloperGenesis(deployConf) + require.NoError(t, err, "failed to create l1 genesis") + if alloc.PrefundTestUsers { + for _, addr := range deployParams.Addresses.All() { + l1Genesis.Alloc[addr] = core.GenesisAccount{ + Balance: Ether(1e6), + } + } + } + for addr, val := range alloc.L1Alloc { + l1Genesis.Alloc[addr] = val + } + + l1Block := l1Genesis.ToBlock() + l2Addrs := &genesis.L2Addresses{ + ProxyAdmin: predeploys.DevProxyAdminAddr, + L1StandardBridgeProxy: predeploys.DevL1StandardBridgeAddr, + L1CrossDomainMessengerProxy: predeploys.DevL1CrossDomainMessengerAddr, + } + + l2Genesis, err := genesis.BuildL2DeveloperGenesis(deployConf, l1Block, l2Addrs) + require.NoError(t, err, "failed to create l2 genesis") + if alloc.PrefundTestUsers { + for _, addr := range deployParams.Addresses.All() { + l2Genesis.Alloc[addr] = core.GenesisAccount{ + Balance: Ether(1e6), + } + } + } + for addr, val := range alloc.L2Alloc { + l2Genesis.Alloc[addr] = val + } + + rollupCfg := &rollup.Config{ + Genesis: rollup.Genesis{ + L1: eth.BlockID{ + Hash: l1Block.Hash(), + Number: 0, + }, + L2: eth.BlockID{ + Hash: l2Genesis.ToBlock().Hash(), + Number: 0, + }, + L2Time: uint64(deployConf.L1GenesisBlockTimestamp), + }, + BlockTime: deployConf.L2BlockTime, + MaxSequencerDrift: deployConf.MaxSequencerDrift, + SeqWindowSize: deployConf.SequencerWindowSize, + ChannelTimeout: deployConf.ChannelTimeout, + L1ChainID: new(big.Int).SetUint64(deployConf.L1ChainID), + L2ChainID: new(big.Int).SetUint64(deployConf.L2ChainID), + P2PSequencerAddress: deployConf.P2PSequencerAddress, + FeeRecipientAddress: deployConf.OptimismL2FeeRecipient, + BatchInboxAddress: deployConf.BatchInboxAddress, + BatchSenderAddress: deployConf.BatchSenderAddress, + DepositContractAddress: predeploys.DevOptimismPortalAddr, + } + + deploymentsL1 := DeploymentsL1{ + L1CrossDomainMessengerProxy: predeploys.DevL1CrossDomainMessengerAddr, + L1StandardBridgeProxy: predeploys.DevL1StandardBridgeAddr, + L2OutputOracleProxy: predeploys.DevL2OutputOracleAddr, + OptimismPortalProxy: predeploys.DevOptimismPortalAddr, + } + + return &SetupData{ + L1Cfg: l1Genesis, + L2Cfg: l2Genesis, + RollupCfg: rollupCfg, + DeploymentsL1: deploymentsL1, + } +} diff --git a/op-e2e/e2eutils/setup_test.go b/op-e2e/e2eutils/setup_test.go new file mode 100644 index 0000000000000..78cbff32d9272 --- /dev/null +++ b/op-e2e/e2eutils/setup_test.go @@ -0,0 +1,37 @@ +package e2eutils + +import ( + "encoding/hex" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/op-bindings/predeploys" +) + +func TestWriteDefaultJWT(t *testing.T) { + jwtPath := WriteDefaultJWT(t) + data, err := os.ReadFile(jwtPath) + require.NoError(t, err) + require.Equal(t, "0x"+hex.EncodeToString(testingJWTSecret[:]), string(data)) +} + +func TestSetup(t *testing.T) { + tp := &TestParams{ + MaxSequencerDrift: 40, + SequencerWindowSize: 120, + ChannelTimeout: 120, + } + dp := MakeDeployParams(t, tp) + alloc := &AllocParams{PrefundTestUsers: true} + sd := Setup(t, dp, alloc) + require.Contains(t, sd.L1Cfg.Alloc, dp.Addresses.Alice) + require.Equal(t, sd.L1Cfg.Alloc[dp.Addresses.Alice].Balance, Ether(1e6)) + + require.Contains(t, sd.L2Cfg.Alloc, dp.Addresses.Alice) + require.Equal(t, sd.L2Cfg.Alloc[dp.Addresses.Alice].Balance, Ether(1e6)) + + require.Contains(t, sd.L1Cfg.Alloc, predeploys.DevOptimismPortalAddr) + require.Contains(t, sd.L2Cfg.Alloc, predeploys.L1BlockAddr) +} diff --git a/op-e2e/e2eutils/testing.go b/op-e2e/e2eutils/testing.go new file mode 100644 index 0000000000000..79a824f74a657 --- /dev/null +++ b/op-e2e/e2eutils/testing.go @@ -0,0 +1,26 @@ +package e2eutils + +// TestingBase is an interface used for standard Go testing. +// This interface is used for unit tests, benchmarks, and fuzz tests and also emulated in Hive. +// +// The Go testing.TB interface does not allow extensions by embedding the interface, so we repeat it here. +type TestingBase interface { + Cleanup(func()) + Error(args ...any) + Errorf(format string, args ...any) + Fail() + FailNow() + Failed() bool + Fatal(args ...any) + Fatalf(format string, args ...any) + Helper() + Log(args ...any) + Logf(format string, args ...any) + Name() string + Setenv(key, value string) + Skip(args ...any) + SkipNow() + Skipf(format string, args ...any) + Skipped() bool + TempDir() string +} diff --git a/op-e2e/go.mod b/op-e2e/go.mod index df9d1acfc9cbc..c553a749a1be1 100644 --- a/op-e2e/go.mod +++ b/op-e2e/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/ethereum-optimism/optimism/op-batcher v0.8.8 github.com/ethereum-optimism/optimism/op-bindings v0.8.8 + github.com/ethereum-optimism/optimism/op-chain-ops v0.8.8 github.com/ethereum-optimism/optimism/op-node v0.8.8 github.com/ethereum-optimism/optimism/op-proposer v0.8.8 github.com/ethereum-optimism/optimism/op-service v0.8.8 diff --git a/op-e2e/go.sum b/op-e2e/go.sum index 39e06f93c69e4..0aa9626f4492b 100644 --- a/op-e2e/go.sum +++ b/op-e2e/go.sum @@ -245,6 +245,8 @@ github.com/ethereum-optimism/optimism/op-batcher v0.8.8 h1:raKQUL3eQtWyJ+BNYPz7I github.com/ethereum-optimism/optimism/op-batcher v0.8.8/go.mod h1:RnDJ6ilMHfvNhKwlTt4uhxABH8dbQ8arXdZjCvUI+8A= github.com/ethereum-optimism/optimism/op-bindings v0.8.8 h1:HN625JI2VsRsli+U6GAXipg7lAKx8EOmGIovN79Az+I= github.com/ethereum-optimism/optimism/op-bindings v0.8.8/go.mod h1:pyTCbh2o/SY+5/AL2Qo5GgAao3Gtt9Ff6tfK9Pa9emM= +github.com/ethereum-optimism/optimism/op-chain-ops v0.8.8 h1:wNn80nhMW4srM/TvA785mikElpPJhEtTLBBuYFxjUCM= +github.com/ethereum-optimism/optimism/op-chain-ops v0.8.8/go.mod h1:l9YB+bMCfYZV6rjcC6XL0VHa+6hkgdVPioHwBL9u6r8= github.com/ethereum-optimism/optimism/op-node v0.8.8 h1:WuZF1RsZnB+u/wwWToVaC1lZWfGfgncLusUssA3Hm+o= github.com/ethereum-optimism/optimism/op-node v0.8.8/go.mod h1:O5nXPCx8vn9c/CHgBC7vP5utcanIQ4uhWKkJ3pAaaSQ= github.com/ethereum-optimism/optimism/op-proposer v0.8.8 h1:IV0P64q7RJ52yvLMBLAKyqeDOxdQPGHbhmIXGkMKG/8=