Skip to content

Commit

Permalink
op-deployer: Test for existing OPCM (#12257)
Browse files Browse the repository at this point in the history
* op-deployer: Test for existing OPCM

Adds a test for deployments against an existing OPCM. The test works by spinning up an Anvil instance and forking Sepolia. To run this test, you'll need to specify two env vars:

- `SEPOLIA_RPC_URL`: RPC URL for a Sepolia node.
- `ENABLE_ANVIL`: Set to `true` to enable the test.

In CI, the test uses our internal CI RPC nodes.

* goimports

* ensure streams close

* lint

* run anvil as part of unit not integration tests

* simplify

* remove foundry from kurtosis

* use auto mine

* mount artifacts

* redeploy OPCM

* comment
  • Loading branch information
mslipper authored Oct 3, 2024
1 parent 82cb8ff commit a83c375
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 15 deletions.
19 changes: 17 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -890,9 +890,14 @@ jobs:
module:
description: Go Module Name
type: string
uses_artifacts:
description: Uses contract artifacts
type: boolean
default: false
docker:
- image: <<pipeline.parameters.ci_builder_image>>
resource_class: xlarge
circleci_ip_ranges: true
steps:
- checkout
- restore_cache:
Expand All @@ -903,6 +908,10 @@ jobs:
keys:
- golang-build-cache-test-<<parameters.module>>-{{ checksum "go.sum" }}
- golang-build-cache-test-
- when:
condition: <<parameters.uses_artifacts>>
steps:
- attach_workspace: { at: "." }
- run:
name: Install components
command: |
Expand All @@ -914,7 +923,7 @@ jobs:
- run:
name: run tests
command: |
gotestsum --format=testname --junitfile=/tmp/test-results/<<parameters.module>>.xml --jsonfile=/tmp/testlogs/log.json \
ENABLE_ANVIL=true SEPOLIA_RPC_URL="https://ci-sepolia-l1.optimism.io" gotestsum --format=testname --junitfile=/tmp/test-results/<<parameters.module>>.xml --jsonfile=/tmp/testlogs/log.json \
-- -parallel=8 -coverpkg=github.com/ethereum-optimism/optimism/... -coverprofile=coverage.out ./...
working_directory: <<parameters.module>>
- save_cache:
Expand Down Expand Up @@ -1441,7 +1450,6 @@ workflows:
parameters:
module:
- op-batcher
- op-chain-ops
- op-node
- op-proposer
- op-challenger
Expand All @@ -1453,6 +1461,13 @@ workflows:
- go-test:
name: semver-natspec-tests
module: packages/contracts-bedrock/scripts/checks/semver-natspec
- go-test:
name: op-chain-ops-tests
module: op-chain-ops
uses_artifacts: true
requires:
- go-mod-download
- contracts-bedrock-build
- go-test-kurtosis:
name: op-chain-ops-integration
module: op-chain-ops
Expand Down
84 changes: 73 additions & 11 deletions op-chain-ops/deployer/integration_test/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import (
"log/slog"
"math/big"
"net/url"
"os"
"path"
"runtime"
"testing"
"time"

"github.com/ethereum-optimism/optimism/op-service/testutils/anvil"
crypto "github.com/ethereum/go-ethereum/crypto"

"github.com/ethereum-optimism/optimism/op-chain-ops/deployer"
"github.com/holiman/uint256"
Expand Down Expand Up @@ -65,11 +70,6 @@ func TestEndToEndApply(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

_, testFilename, _, ok := runtime.Caller(0)
require.Truef(t, ok, "failed to get test filename")
monorepoDir := path.Join(path.Dir(testFilename), "..", "..", "..")
artifactsDir := path.Join(monorepoDir, "packages", "contracts-bedrock", "forge-artifacts")

enclaveCtx := kurtosisutil.StartEnclave(t, ctx, lgr, "github.com/ethpandaops/ethereum-package", TestParams)

service, err := enclaveCtx.GetServiceContext("el-1-geth-lighthouse")
Expand All @@ -81,9 +81,6 @@ func TestEndToEndApply(t *testing.T) {
l1Client, err := ethclient.Dial(rpcURL)
require.NoError(t, err)

artifactsURL, err := url.Parse(fmt.Sprintf("file://%s", artifactsDir))
require.NoError(t, err)

depKey := new(deployerKey)
l1ChainID := big.NewInt(77799777)
dk, err := devkeys.NewMnemonicDevKeys(devkeys.TestMnemonic)
Expand All @@ -106,7 +103,7 @@ func TestEndToEndApply(t *testing.T) {
}

t.Run("initial chain", func(t *testing.T) {
intent, st := makeIntent(t, l1ChainID, artifactsURL, dk, id)
intent, st := makeIntent(t, l1ChainID, dk, id)

require.NoError(t, deployer.ApplyPipeline(
ctx,
Expand Down Expand Up @@ -149,7 +146,7 @@ func TestEndToEndApply(t *testing.T) {

t.Run("subsequent chain", func(t *testing.T) {
newID := uint256.NewInt(2)
intent, st := makeIntent(t, l1ChainID, artifactsURL, dk, newID)
intent, st := makeIntent(t, l1ChainID, dk, newID)
env.Workdir = t.TempDir()

require.NoError(t, deployer.ApplyPipeline(
Expand Down Expand Up @@ -182,10 +179,16 @@ func TestEndToEndApply(t *testing.T) {
func makeIntent(
t *testing.T,
l1ChainID *big.Int,
artifactsURL *url.URL,
dk *devkeys.MnemonicDevKeys,
l2ChainID *uint256.Int,
) (*state.Intent, *state.State) {
_, testFilename, _, ok := runtime.Caller(0)
require.Truef(t, ok, "failed to get test filename")
monorepoDir := path.Join(path.Dir(testFilename), "..", "..", "..")
artifactsDir := path.Join(monorepoDir, "packages", "contracts-bedrock", "forge-artifacts")
artifactsURL, err := url.Parse(fmt.Sprintf("file://%s", artifactsDir))
require.NoError(t, err)

addrFor := func(key devkeys.Key) common.Address {
addr, err := dk.Address(key)
require.NoError(t, err)
Expand Down Expand Up @@ -261,3 +264,62 @@ func validateOPChainDeployment(t *testing.T, ctx context.Context, l1Client *ethc
})
}
}

func TestApplyExistingOPCM(t *testing.T) {
anvil.Test(t)

forkRPCUrl := os.Getenv("SEPOLIA_RPC_URL")
if forkRPCUrl == "" {
t.Skip("no fork RPC URL provided")
}

lgr := testlog.Logger(t, slog.LevelDebug)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

runner, err := anvil.New(
forkRPCUrl,
lgr,
)
require.NoError(t, err)

require.NoError(t, runner.Start(ctx))
t.Cleanup(func() {
require.NoError(t, runner.Stop())
})

l1Client, err := ethclient.Dial(runner.RPCUrl())
require.NoError(t, err)

l1ChainID := big.NewInt(11155111)
dk, err := devkeys.NewMnemonicDevKeys(devkeys.TestMnemonic)
require.NoError(t, err)
// index 0 from Anvil's test set
priv, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
require.NoError(t, err)
signer := opcrypto.SignerFnFromBind(opcrypto.PrivateKeySignerFn(priv, l1ChainID))
deployerAddr := common.HexToAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")

l2ChainID := uint256.NewInt(1)

env := &pipeline.Env{
Workdir: t.TempDir(),
L1Client: l1Client,
Signer: signer,
Deployer: deployerAddr,
Logger: lgr,
}

intent, st := makeIntent(t, l1ChainID, dk, l2ChainID)
intent.ContractsRelease = "op-contracts/v1.6.0"

require.NoError(t, deployer.ApplyPipeline(
ctx,
env,
intent,
st,
))

validateOPChainDeployment(t, ctx, l1Client, st)
}
48 changes: 46 additions & 2 deletions op-chain-ops/deployer/opcm/standard.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"embed"
"fmt"

"github.com/BurntSushi/toml"

"github.com/ethereum-optimism/superchain-registry/superchain"
"github.com/ethereum/go-ethereum/common"
)
Expand All @@ -14,6 +16,36 @@ var StandardVersionsMainnetData string
//go:embed standard-versions-sepolia.toml
var StandardVersionsSepoliaData string

var StandardVersionsSepolia StandardVersions

var StandardVersionsMainnet StandardVersions

type StandardVersions struct {
Releases map[string]StandardVersionsReleases `toml:"releases"`
}

type StandardVersionsReleases struct {
OptimismPortal StandardVersionRelease `toml:"optimism_portal"`
SystemConfig StandardVersionRelease `toml:"system_config"`
AnchorStateRegistry StandardVersionRelease `toml:"anchor_state_registry"`
DelayedWETH StandardVersionRelease `toml:"delayed_weth"`
DisputeGameFactory StandardVersionRelease `toml:"dispute_game_factory"`
FaultDisputeGame StandardVersionRelease `toml:"fault_dispute_game"`
PermissionedDisputeGame StandardVersionRelease `toml:"permissioned_dispute_game"`
MIPS StandardVersionRelease `toml:"mips"`
PreimageOracle StandardVersionRelease `toml:"preimage_oracle"`
L1CrossDomainMessenger StandardVersionRelease `toml:"l1_cross_domain_messenger"`
L1ERC721Bridge StandardVersionRelease `toml:"l1_erc721_bridge"`
L1StandardBridge StandardVersionRelease `toml:"l1_standard_bridge"`
OptimismMintableERC20Factory StandardVersionRelease `toml:"optimism_mintable_erc20_factory"`
}

type StandardVersionRelease struct {
Version string `toml:"version"`
ImplementationAddress common.Address `toml:"implementation_address"`
Address common.Address `toml:"address"`
}

var _ embed.FS

func StandardVersionsFor(chainID uint64) (string, error) {
Expand Down Expand Up @@ -41,8 +73,8 @@ func SuperchainFor(chainID uint64) (*superchain.Superchain, error) {
func ManagerImplementationAddrFor(chainID uint64) (common.Address, error) {
switch chainID {
case 11155111:
// Generated using the bootstrap command on 09/26/2024.
return common.HexToAddress("0x0dc727671d5c08e4e41e8909983ebfa6f57aa0bf"), nil
// Generated using the bootstrap command on 10/02/2024.
return common.HexToAddress("0x0f29118caed0f72873701bcc079398c594b6f8e4"), nil
default:
return common.Address{}, fmt.Errorf("unsupported chain ID: %d", chainID)
}
Expand All @@ -60,3 +92,15 @@ func ManagerOwnerAddrFor(chainID uint64) (common.Address, error) {
return common.Address{}, fmt.Errorf("unsupported chain ID: %d", chainID)
}
}

func init() {
StandardVersionsMainnet = StandardVersions{}
if err := toml.Unmarshal([]byte(StandardVersionsMainnetData), &StandardVersionsMainnet); err != nil {
panic(err)
}

StandardVersionsSepolia = StandardVersions{}
if err := toml.Unmarshal([]byte(StandardVersionsSepoliaData), &StandardVersionsSepolia); err != nil {
panic(err)
}
}
108 changes: 108 additions & 0 deletions op-service/testutils/anvil/anvil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package anvil

import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"testing"

"github.com/ethereum/go-ethereum/log"
)

func Test(t *testing.T) {
if os.Getenv("ENABLE_ANVIL") == "" {
t.Skip("skipping Anvil test")
}
}

const AnvilPort = 31967

type Runner struct {
proc *exec.Cmd
stdout io.ReadCloser
stderr io.ReadCloser
logger log.Logger
startedCh chan struct{}
wg sync.WaitGroup
}

func New(l1RPCURL string, logger log.Logger) (*Runner, error) {
proc := exec.Command(
"anvil",
"--fork-url", l1RPCURL,
"--port",
strconv.Itoa(AnvilPort),
)
stdout, err := proc.StdoutPipe()
if err != nil {
return nil, err
}
stderr, err := proc.StderrPipe()
if err != nil {
return nil, err
}

return &Runner{
proc: proc,
stdout: stdout,
stderr: stderr,
logger: logger,
startedCh: make(chan struct{}, 1),
}, nil
}

func (r *Runner) Start(ctx context.Context) error {
if err := r.proc.Start(); err != nil {
return err
}

r.wg.Add(2)
go r.outputStream(r.stdout)
go r.outputStream(r.stderr)

select {
case <-r.startedCh:
return nil
case <-ctx.Done():
return ctx.Err()
}
}

func (r *Runner) Stop() error {
err := r.proc.Process.Signal(os.Interrupt)
if err != nil {
return err
}

// make sure the output streams close
defer r.wg.Wait()
return r.proc.Wait()
}

func (r *Runner) outputStream(stream io.ReadCloser) {
defer r.wg.Done()
scanner := bufio.NewScanner(stream)
listenLine := fmt.Sprintf("Listening on 127.0.0.1:%d", AnvilPort)
started := sync.OnceFunc(func() {
r.startedCh <- struct{}{}
})

for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, listenLine) {
started()
}

r.logger.Debug("[ANVIL] " + scanner.Text())
}
}

func (r *Runner) RPCUrl() string {
return fmt.Sprintf("http://localhost:%d", AnvilPort)
}

0 comments on commit a83c375

Please sign in to comment.