• Matthew Slipper's avatar
    Add op-deployer proof-of-concept (#11804) · e9b80ee1
    Matthew Slipper authored
    This PR adds a proof-of-concept for `op-deployer`, a CLI tool that allows declarative management of live OP Stack chains. This POC supports initializing the declarative chain config (called an "intent") and deploying the Superchain smart contracts using the OP Stack Manager.
    
    An example intent for a Sepolia chain looks like this:
    
    ```toml
    l1ChainID = 11155111
    useFaultProofs = true
    useAltDA = false
    fundDevAccounts = true
    contractArtifactsURL = "file:///Users/matthewslipper/dev/optimism/packages/contracts-bedrock/forge-artifacts"
    
    [superchainRoles]
      proxyAdminOwner = "0xb9cdf788704088a4c0191d045c151fcbe2db14a4"
      protocolVersionsOwner = "0xb910764be39c84d572ff17713c615b5bfd7df650"
      guardian = "0x8c7e4a51acb17719d225bd17598b8a94b46c8767"
    ```
    
    When deployed, it produces a state file that looks like this:
    
    ```json
    {
      "version": 1,
      "appliedIntent": {
        "l1ChainID": 11155111,
        "superchainRoles": {
          "proxyAdminOwner": "0xb9cdf788704088a4c0191d045c151fcbe2db14a4",
          "protocolVersionsOwner": "0xb910764be39c84d572ff17713c615b5bfd7df650",
          "guardian": "0x8c7e4a51acb17719d225bd17598b8a94b46c8767"
        },
        "useFaultProofs": true,
        "useAltDA": false,
        "fundDevAccounts": true,
        "contractArtifactsURL": "file:///Users/matthewslipper/dev/optimism/packages/contracts-bedrock/forge-artifacts",
        "chains": null
      },
      "superchainDeployment": {
        "proxyAdminAddress": "0x54a6088c04a7782e69b5031579a1973a9e3c1a8c",
        "superchainConfigProxyAddress": "0xc969afc4799a9350f9f05b60748bc62f2829b03a",
        "superchainConfigImplAddress": "0x08426b74350e7cba5b52be4909c542d28b6b3962",
        "protocolVersionsProxyAddress": "0x212a023892803c7570eb317c77672c8391bf3dde",
        "protocolVersionsImplAddress": "0x2633ac74edb7ae1f1b5656e042285015f9ee477d"
      }
    }
    ```
    
    To use `op-deployer`, run `op-deployer init --dev --l1-chain-id <chain-id>`. This will initialize a deployment intent using the development keys in the repo. Then, run `op-deployer apply --l1-rpc-url <l1-rpc> --private-key <deployer-private-key>` to apply the deployment.
    
    - The contracts deployment is performed by the local Go/Forge tooling.
    - Upgrades of the contracts (i.e. modifying them after deploying the contracts afresh) is not currently supported. This will be supported in the future.
    - The rest of the pipeline (i.e., deploying L2s and generating genesis files) is not included in this PR to keep it smaller and allow us to get buy-in on the fundamental concepts behind `op-deployer` before further implementation.
    e9b80ee1
json_test.go 3.68 KB
package jsonutil

import (
	"encoding/json"
	"os"
	"path/filepath"
	"testing"

	"github.com/ethereum-optimism/optimism/op-service/ioutil"
	"github.com/stretchr/testify/require"
)

func TestRoundTripJSON(t *testing.T) {
	dir := t.TempDir()
	file := filepath.Join(dir, "test.json")
	data := &jsonTestData{A: "yay", B: 3}
	err := WriteJSON(data, ioutil.ToAtomicFile(file, 0o755))
	require.NoError(t, err)

	// Confirm the file is uncompressed
	fileContent, err := os.ReadFile(file)
	require.NoError(t, err)
	err = json.Unmarshal(fileContent, &jsonTestData{})
	require.NoError(t, err)

	var result *jsonTestData
	result, err = LoadJSON[jsonTestData](file)
	require.NoError(t, err)
	require.EqualValues(t, data, result)
}

func TestRoundTripJSONWithGzip(t *testing.T) {
	dir := t.TempDir()
	file := filepath.Join(dir, "test.json.gz")
	data := &jsonTestData{A: "yay", B: 3}
	err := WriteJSON(data, ioutil.ToAtomicFile(file, 0o755))
	require.NoError(t, err)

	// Confirm the file isn't raw JSON
	fileContent, err := os.ReadFile(file)
	require.NoError(t, err)
	err = json.Unmarshal(fileContent, &jsonTestData{})
	require.Error(t, err, "should not be able to decode without decompressing")

	var result *jsonTestData
	result, err = LoadJSON[jsonTestData](file)
	require.NoError(t, err)
	require.EqualValues(t, data, result)
}

func TestLoadJSONWithExtraDataAppended(t *testing.T) {
	data := &jsonTestData{A: "yay", B: 3}

	cases := []struct {
		name      string
		extraData func() ([]byte, error)
	}{
		{
			name: "duplicate json object",
			extraData: func() ([]byte, error) {
				return json.Marshal(data)
			},
		},
		{
			name: "duplicate comma-separated json object",
			extraData: func() ([]byte, error) {
				data, err := json.Marshal(data)
				if err != nil {
					return nil, err
				}
				return append([]byte(","), data...), nil
			},
		},
		{
			name: "additional characters",
			extraData: func() ([]byte, error) {
				return []byte("some text"), nil
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			dir := t.TempDir()
			file := filepath.Join(dir, "test.json")
			extraData, err := tc.extraData()
			require.NoError(t, err)

			// Write primary json payload + extra data to the file
			err = WriteJSON(data, ioutil.ToAtomicFile(file, 0o755))
			require.NoError(t, err)
			err = appendDataToFile(file, extraData)
			require.NoError(t, err)

			var result *jsonTestData
			result, err = LoadJSON[jsonTestData](file)
			require.ErrorContains(t, err, "unexpected trailing data")
			require.Nil(t, result)
		})
	}
}

func TestLoadJSONWithTrailingWhitespace(t *testing.T) {
	cases := []struct {
		name      string
		extraData []byte
	}{
		{
			name:      "space",
			extraData: []byte(" "),
		},
		{
			name:      "tab",
			extraData: []byte("\t"),
		},
		{
			name:      "new line",
			extraData: []byte("\n"),
		},
		{
			name:      "multiple chars",
			extraData: []byte(" \t\n"),
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			dir := t.TempDir()
			file := filepath.Join(dir, "test.json")
			data := &jsonTestData{A: "yay", B: 3}

			// Write primary json payload + extra data to the file
			err := WriteJSON(data, ioutil.ToAtomicFile(file, 0o755))
			require.NoError(t, err)
			err = appendDataToFile(file, tc.extraData)
			require.NoError(t, err)

			var result *jsonTestData
			result, err = LoadJSON[jsonTestData](file)
			require.NoError(t, err)
			require.EqualValues(t, data, result)
		})
	}
}

type jsonTestData struct {
	A string `json:"a"`
	B int    `json:"b"`
}

func appendDataToFile(outputPath string, data []byte) error {
	file, err := os.OpenFile(outputPath, os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer file.Close()

	_, err = file.Write(data)
	return err
}