diff --git a/ops/cmd/import_devnet/main.go b/ops/cmd/import_devnet/main.go index ca78966c1..fb124bb8d 100644 --- a/ops/cmd/import_devnet/main.go +++ b/ops/cmd/import_devnet/main.go @@ -6,7 +6,7 @@ import ( "path" "path/filepath" - "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/state" "github.com/ethereum-optimism/superchain-registry/ops/internal/manage" "github.com/ethereum-optimism/superchain-registry/ops/internal/output" "github.com/ethereum-optimism/superchain-registry/ops/internal/paths" @@ -81,7 +81,7 @@ func action(cliCtx *cli.Context) error { return fmt.Errorf("failed to read manifest file: %w", err) } - st, err := deployer.ReadOpaqueStateFile(statePath) + st, err := state.ReadOpaqueStateFile(statePath) if err != nil { return fmt.Errorf("failed to read opaque state file: %w", err) } diff --git a/ops/internal/deployer/deployer.go b/ops/internal/deployer/deployer.go index 5f71ae0da..2d67e9097 100644 --- a/ops/internal/deployer/deployer.go +++ b/ops/internal/deployer/deployer.go @@ -14,6 +14,8 @@ import ( "github.com/BurntSushi/toml" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/opaque_map" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/state" "github.com/ethereum/go-ethereum/log" ) @@ -25,23 +27,23 @@ var contractVersions map[string]string type BinaryPicker interface { Path() string - Merger() StateMerger + Merger() state.StateMerger } type FixedBinaryPicker struct { binaryPath string - merger StateMerger + merger state.StateMerger } func (f *FixedBinaryPicker) Path() string { return f.binaryPath } -func (f *FixedBinaryPicker) Merger() StateMerger { +func (f *FixedBinaryPicker) Merger() state.StateMerger { return f.merger } -func WithFixedBinary(binaryPath string, merger StateMerger) *FixedBinaryPicker { +func WithFixedBinary(binaryPath string, merger state.StateMerger) *FixedBinaryPicker { return &FixedBinaryPicker{ binaryPath: binaryPath, merger: merger, @@ -64,7 +66,7 @@ func WithReleaseBinary(binDir string, l1ContractsRelease string) (*FixedBinaryPi binaryPath := VersionedBinaryPath(binDir, deployerVersion) - merger, err := GetStateMerger(deployerVersion) + merger, err := state.GetStateMerger(deployerVersion) if err != nil { return nil, fmt.Errorf("failed to get state merger: %w", err) } @@ -86,7 +88,7 @@ func init() { // then shelling out to that binary for various cli commands type OpDeployer struct { binaryPath string - merger StateMerger + merger state.StateMerger lgr log.Logger } @@ -110,7 +112,7 @@ func NewOpDeployer(lgr log.Logger, binaryPicker BinaryPicker) (*OpDeployer, erro // in the specified working directory. func (d *OpDeployer) setupStateAndIntent(inputStatePath, workdir string) error { // Read the state file - state, err := ReadOpaqueStateFile(inputStatePath) + state, err := state.ReadOpaqueStateFile(inputStatePath) if err != nil { return fmt.Errorf("failed to read state file: %w", err) } @@ -132,7 +134,7 @@ func (d *OpDeployer) setupStateAndIntent(inputStatePath, workdir string) error { d.lgr.Info("Wrote state to temporary file", "path", stateTempPath) // Write intent.toml in the temp directory - useInts(mergedIntent) + opaque_map.UseInts(mergedIntent) intentTOML, err := toml.Marshal(mergedIntent) if err != nil { return fmt.Errorf("failed to marshal intent to TOML: %w", err) @@ -176,7 +178,7 @@ func (d *OpDeployer) inspectCommand(workdir, chainId, subcommand string, result // GenerateStandardGenesis runs op-deployer binary to generate a genesis // - l1RpcUrl must match the state's L1 and is required by op-deployer, even though we aren't sending any txs -func (d *OpDeployer) GenerateStandardGenesis(statePath, chainId, l1RpcUrl string) (*OpaqueMap, error) { +func (d *OpDeployer) GenerateStandardGenesis(statePath, chainId, l1RpcUrl string) (*opaque_map.OpaqueMap, error) { workdir, err := d.copyStateFileToTempDir(statePath) if err != nil { return nil, fmt.Errorf("failed to copy state file to temporary directory: %w", err) @@ -203,7 +205,7 @@ func (d *OpDeployer) GenerateStandardGenesis(statePath, chainId, l1RpcUrl string } // Run `op-deployer inspect genesis` to read the expected genesis - var genesis OpaqueMap + var genesis opaque_map.OpaqueMap if err := d.inspectCommand(workdir, chainId, "genesis", &genesis); err != nil { return nil, err } @@ -218,7 +220,7 @@ func (d *OpDeployer) copyStateFileToTempDir(statePath string) (string, error) { return "", fmt.Errorf("failed to create temporary directory: %w", err) } - state, err := ReadOpaqueStateFile(statePath) + state, err := state.ReadOpaqueStateFile(statePath) if err != nil { return "", fmt.Errorf("failed to read state file: %w", err) } @@ -238,14 +240,14 @@ func (d *OpDeployer) copyStateFileToTempDir(statePath string) (string, error) { return workdir, nil } -func (d *OpDeployer) InspectGenesis(statePath, chainId string) (*OpaqueMap, error) { +func (d *OpDeployer) InspectGenesis(statePath, chainId string) (*opaque_map.OpaqueMap, error) { workdir, err := d.copyStateFileToTempDir(statePath) if err != nil { return nil, fmt.Errorf("failed to copy state file: %w", err) } defer os.RemoveAll(workdir) - var genesis OpaqueMap + var genesis opaque_map.OpaqueMap if err := d.inspectCommand(workdir, chainId, "genesis", &genesis); err != nil { return nil, err } diff --git a/ops/internal/deployer/deployer_test.go b/ops/internal/deployer/deployer_test.go index 2f201d4a7..513cef555 100644 --- a/ops/internal/deployer/deployer_test.go +++ b/ops/internal/deployer/deployer_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/state" "github.com/ethereum/go-ethereum/log" "github.com/stretchr/testify/require" ) @@ -17,35 +18,35 @@ func TestAutodetectBinary(t *testing.T) { tests := []struct { name string l1ContractsRelease string - merger StateMerger + merger state.StateMerger binPath string shouldError bool }{ { name: "op-contracts/v1.6.0", l1ContractsRelease: "tag://op-contracts/v1.6.0", - merger: MergeStateV1, + merger: state.MergeStateV1, binPath: "op-deployer_v0.0.14", shouldError: false, }, { name: "op-contracts/v2.0.0", l1ContractsRelease: "tag://op-contracts/v2.0.0", - merger: MergeStateV2, + merger: state.MergeStateV2, binPath: "op-deployer_v0.2.3", shouldError: false, }, { name: "op-contracts/v3.0.0", l1ContractsRelease: "tag://op-contracts/v3.0.0", - merger: MergeStateV3, + merger: state.MergeStateV3, binPath: "op-deployer_v0.3.2", shouldError: false, }, { name: "op-contracts/v4.0.0", l1ContractsRelease: "tag://op-contracts/v4.0.0", - merger: MergeStateV4, + merger: state.MergeStateV4, binPath: "op-deployer_v0.4.0-rc.3", shouldError: false, }, diff --git a/ops/internal/deployer/diff.go b/ops/internal/deployer/opaque_map/diff.go similarity index 98% rename from ops/internal/deployer/diff.go rename to ops/internal/deployer/opaque_map/diff.go index 3eafc8f4d..721f0a15e 100644 --- a/ops/internal/deployer/diff.go +++ b/ops/internal/deployer/opaque_map/diff.go @@ -1,4 +1,4 @@ -package deployer +package opaque_map import ( "fmt" diff --git a/ops/internal/deployer/diff_test.go b/ops/internal/deployer/opaque_map/diff_test.go similarity index 99% rename from ops/internal/deployer/diff_test.go rename to ops/internal/deployer/opaque_map/diff_test.go index 724417434..2ec228625 100644 --- a/ops/internal/deployer/diff_test.go +++ b/ops/internal/deployer/opaque_map/diff_test.go @@ -1,4 +1,4 @@ -package deployer +package opaque_map import ( "testing" diff --git a/ops/internal/deployer/opaque_map/opaque_map.go b/ops/internal/deployer/opaque_map/opaque_map.go new file mode 100644 index 000000000..4986d8fc0 --- /dev/null +++ b/ops/internal/deployer/opaque_map/opaque_map.go @@ -0,0 +1,29 @@ +package opaque_map + +type OpaqueMap map[string]any + +// UseInts converts all float64 values without fractional parts to int64 values in a map +// so that they are properly marshaled to TOML +func UseInts(m map[string]any) { + for k, v := range m { + switch val := v.(type) { + case float64: + // If the float has no fractional part, convert to int + if val == float64(int64(val)) { + m[k] = int64(val) + } + case map[string]any: + // Recursively process nested maps + UseInts(val) + case []any: + // Process arrays + for i, item := range val { + if fItem, ok := item.(float64); ok && fItem == float64(int64(fItem)) { + val[i] = int64(fItem) + } else if mapItem, ok := item.(map[string]any); ok { + UseInts(mapItem) + } + } + } + } +} diff --git a/ops/internal/deployer/state.go b/ops/internal/deployer/state.go deleted file mode 100644 index fa2f52db4..000000000 --- a/ops/internal/deployer/state.go +++ /dev/null @@ -1,458 +0,0 @@ -package deployer - -import ( - _ "embed" - "encoding/json" - "fmt" - "os" - "regexp" - "strconv" - "strings" - - "github.com/BurntSushi/toml" - "github.com/ethereum-optimism/superchain-registry/validation" - "github.com/ethereum/go-ethereum/superchain" - "github.com/hashicorp/go-multierror" - "github.com/tomwright/dasel" -) - -//go:embed configs/v1-state.json -var standardV1State []byte - -//go:embed configs/v1-intent.toml -var standardV1Intent []byte - -//go:embed configs/v2-state.json -var standardV2State []byte - -//go:embed configs/v2-intent.toml -var standardV2Intent []byte - -//go:embed configs/v3-state.json -var standardV3State []byte - -//go:embed configs/v3-intent.toml -var standardV3Intent []byte - -//go:embed configs/v4-state.json -var standardV4State []byte - -//go:embed configs/v4-intent.toml -var standardV4Intent []byte - -func ReadOpaqueStateFile(p string) (OpaqueState, error) { - f, err := os.Open(p) - if err != nil { - return nil, fmt.Errorf("failed to open JSON file: %w", err) - } - defer f.Close() - - var out OpaqueState - if err := json.NewDecoder(f).Decode(&out); err != nil { - return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) - } - return out, nil -} - -type StateMerger = func(state OpaqueState) (OpaqueMap, OpaqueState, error) - -func GetStateMerger(version string) (StateMerger, error) { - // Extract the version number using regex - re := regexp.MustCompile(`op-deployer/v\d+\.(\d+)\.\d+`) - match := re.FindStringSubmatch(version) - - if len(match) < 2 { - return nil, fmt.Errorf("invalid deployer version format: %s", version) - } - - // Get the middle version number - versionNum, err := strconv.Atoi(match[1]) - if err != nil { - return nil, fmt.Errorf("failed to parse version number: %w", err) - } - - // Return the appropriate merge function - switch versionNum { - case 0, 1: - return MergeStateV1, nil - case 2: - return MergeStateV2, nil - case 3: - return MergeStateV3, nil - case 4: - return MergeStateV4, nil - default: - return nil, fmt.Errorf("unsupported deployer version: %d", versionNum) - } -} - -func MergeStateV1(userState OpaqueState) (OpaqueMap, OpaqueState, error) { - l1ChainID, err := userState.ReadL1ChainID() - if err != nil { - return nil, nil, fmt.Errorf("failed to read L1 chain ID: %w", err) - } - stdIntent, err := StandardIntentV1(l1ChainID) - if err != nil { - return nil, nil, fmt.Errorf("failed to create standard intent: %w", err) - } - stdState, err := StandardStateV1(l1ChainID) - if err != nil { - return nil, nil, fmt.Errorf("failed to create standard state: %w", err) - } - return mergeStateV2(userState, stdIntent, stdState) -} - -func MergeStateV2(userState OpaqueState) (OpaqueMap, OpaqueState, error) { - l1ChainID, err := userState.ReadL1ChainID() - if err != nil { - return nil, nil, fmt.Errorf("failed to read L1 chain ID: %w", err) - } - stdIntent, err := StandardIntentV2(l1ChainID) - if err != nil { - return nil, nil, fmt.Errorf("failed to create standard intent: %w", err) - } - stdState, err := StandardStateV2(l1ChainID) - if err != nil { - return nil, nil, fmt.Errorf("failed to create standard state: %w", err) - } - return mergeStateV2(userState, stdIntent, stdState) -} - -func MergeStateV3(userState OpaqueState) (OpaqueMap, OpaqueState, error) { - l1ChainID, err := userState.ReadL1ChainID() - if err != nil { - return nil, nil, fmt.Errorf("failed to read L1 chain ID: %w", err) - } - stdIntent, err := StandardIntentV3(l1ChainID) - if err != nil { - return nil, nil, fmt.Errorf("failed to create standard intent: %w", err) - } - stdState, err := StandardStateV3(l1ChainID) - if err != nil { - return nil, nil, fmt.Errorf("failed to create standard state: %w", err) - } - // V2 is correct here. V3's state is the same as V2, except with a - // slightly different intent that contains operator fee fields. - return mergeStateV2(userState, stdIntent, stdState) -} - -func MergeStateV4(userState OpaqueState) (OpaqueMap, OpaqueState, error) { - l1ChainID, err := userState.ReadL1ChainID() - if err != nil { - return nil, nil, fmt.Errorf("failed to read L1 chain ID: %w", err) - } - stdIntent, err := StandardIntentV4(l1ChainID) - if err != nil { - return nil, nil, fmt.Errorf("failed to create standard intent: %w", err) - } - stdState, err := StandardStateV4(l1ChainID) - if err != nil { - return nil, nil, fmt.Errorf("failed to create standard state: %w", err) - } - return mergeStateV4(userState, stdIntent, stdState) -} - -func mergeStateV4(userState OpaqueState, stdIntent OpaqueMap, stdState OpaqueState) (OpaqueMap, OpaqueState, error) { - userStateNode := dasel.New(userState) - stdIntentNode := dasel.New(stdIntent) - stdStateNode := dasel.New(stdState) - - appliedIntentNode, err := userStateNode.Query("appliedIntent") - if err != nil { - return nil, nil, fmt.Errorf("failed to read applied intent: %w", err) - } - - // Helper function to aggregate copy errors - var copyErrs error - guard := func(err error) { - if err != nil { - copyErrs = multierror.Append(copyErrs, fmt.Errorf("failed to copy value: %w", err)) - } - } - - // First, set up the intent - guard(copyValue(appliedIntentNode, stdIntentNode, "l1ChainID")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].id")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].baseFeeVaultRecipient")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].l1FeeVaultRecipient")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].sequencerFeeVaultRecipient")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.l1ProxyAdminOwner")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.l2ProxyAdminOwner")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.systemConfigOwner")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.unsafeBlockSigner")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.batcher")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.proposer")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.challenger")) - - // Then, set up the state - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].id")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].OpChainProxyAdminImpl")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].AddressManagerImpl")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].L1Erc721BridgeProxy")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].SystemConfigProxy")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].OptimismMintableErc20FactoryProxy")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].L1StandardBridgeProxy")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].L1CrossDomainMessengerProxy")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].OptimismPortalProxy")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].DisputeGameFactoryProxy")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].AnchorStateRegistryProxy")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].FaultDisputeGameImpl")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].PermissionedDisputeGameImpl")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].DelayedWethPermissionedGameProxy")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].DelayedWethPermissionlessGameProxy")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].startBlock")) - - if copyErrs != nil { - return nil, nil, copyErrs - } - - intentResult, okIntent := stdIntentNode.InterfaceValue().(OpaqueMap) - if !okIntent { - return nil, nil, fmt.Errorf("internal error: synthesized intent is not OpaqueMapping, but %T", stdIntentNode.InterfaceValue()) - } - stateResult, okState := stdStateNode.InterfaceValue().(OpaqueState) - if !okState { - return nil, nil, fmt.Errorf("internal error: synthesized state is not OpaqueMapping, but %T", stdStateNode.InterfaceValue()) - } - - return intentResult, stateResult, nil -} - -func mergeStateV2(userState OpaqueState, stdIntent OpaqueMap, stdState OpaqueState) (OpaqueMap, OpaqueState, error) { - userStateNode := dasel.New(userState) - stdIntentNode := dasel.New(stdIntent) - stdStateNode := dasel.New(stdState) - - appliedIntentNode, err := userStateNode.Query("appliedIntent") - if err != nil { - return nil, nil, fmt.Errorf("failed to read applied intent: %w", err) - } - - // Helper function to aggregate copy errors - var copyErrs error - guard := func(err error) { - if err != nil { - copyErrs = multierror.Append(copyErrs, fmt.Errorf("failed to copy value: %w", err)) - } - } - - // First, set up the intent - guard(copyValue(appliedIntentNode, stdIntentNode, "l1ChainID")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].id")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].baseFeeVaultRecipient")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].l1FeeVaultRecipient")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].sequencerFeeVaultRecipient")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.l1ProxyAdminOwner")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.l2ProxyAdminOwner")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.systemConfigOwner")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.unsafeBlockSigner")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.batcher")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.proposer")) - guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.challenger")) - - // Then, set up the state - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].id")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].proxyAdminAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].addressManagerAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].l1ERC721BridgeProxyAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].systemConfigProxyAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].optimismMintableERC20FactoryProxyAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].l1StandardBridgeProxyAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].l1CrossDomainMessengerProxyAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].optimismPortalProxyAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].disputeGameFactoryProxyAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].anchorStateRegistryProxyAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].faultDisputeGameAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].permissionedDisputeGameAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].delayedWETHPermissionedGameProxyAddress")) - guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].startBlock")) - - if copyErrs != nil { - return nil, nil, copyErrs - } - - intentResult, okIntent := stdIntentNode.InterfaceValue().(OpaqueMap) - if !okIntent { - return nil, nil, fmt.Errorf("internal error: synthesized intent is not OpaqueMapping, but %T", stdIntentNode.InterfaceValue()) - } - stateResult, okState := stdStateNode.InterfaceValue().(OpaqueState) - if !okState { - return nil, nil, fmt.Errorf("internal error: synthesized state is not OpaqueMapping, but %T", stdStateNode.InterfaceValue()) - } - - return intentResult, stateResult, nil -} - -func StandardIntentV4(l1ChainID uint64) (OpaqueMap, error) { - return standardIntentV4(l1ChainID, standardV4Intent) -} - -func StandardIntentV3(l1ChainID uint64) (OpaqueMap, error) { - return standardIntentV2(l1ChainID, standardV3Intent) -} - -func StandardIntentV2(l1ChainID uint64) (OpaqueMap, error) { - return standardIntentV2(l1ChainID, standardV2Intent) -} - -func StandardIntentV1(l1ChainID uint64) (OpaqueMap, error) { - return standardIntentV1(l1ChainID, standardV1Intent) -} - -func standardIntentV4(l1ChainID uint64, data []byte) (OpaqueMap, error) { - intent := make(OpaqueMap) - if err := toml.Unmarshal(data, &intent); err != nil { - panic(err) - } - - var stdRoles validation.RolesConfig - switch l1ChainID { - case 1: - stdRoles = validation.StandardConfigRolesMainnet - case 11155111: - stdRoles = validation.StandardConfigRolesSepolia - default: - return nil, fmt.Errorf("unsupported L1 chain ID: %d", l1ChainID) - } - - root := dasel.New(intent) - mustPutString(root, "superchainRoles.SuperchainProxyAdminOwner", stdRoles.L1ProxyAdminOwner) - mustPutString(root, "superchainRoles.ProtocolVersionsOwner", stdRoles.ProtocolVersionsOwner) - mustPutString(root, "superchainRoles.SuperchainGuardian", stdRoles.Guardian) - - return intent, nil -} - -func standardIntentV2(l1ChainID uint64, data []byte) (OpaqueMap, error) { - intent := make(OpaqueMap) - if err := toml.Unmarshal(data, &intent); err != nil { - panic(err) - } - - var stdRoles validation.RolesConfig - switch l1ChainID { - case 1: - stdRoles = validation.StandardConfigRolesMainnet - case 11155111: - stdRoles = validation.StandardConfigRolesSepolia - default: - return nil, fmt.Errorf("unsupported L1 chain ID: %d", l1ChainID) - } - - root := dasel.New(intent) - mustPutString(root, "superchainRoles.proxyAdminOwner", stdRoles.L1ProxyAdminOwner) - mustPutString(root, "superchainRoles.protocolVersionsOwner", stdRoles.ProtocolVersionsOwner) - mustPutString(root, "superchainRoles.guardian", stdRoles.Guardian) - - return intent, nil -} - -// Add this type near the top of the file -type stringWrapper string - -func (s stringWrapper) String() string { - return string(s) -} - -func standardIntentV1(l1ChainID uint64, data []byte) (OpaqueMap, error) { - intent, err := standardIntentV2(l1ChainID, data) - if err != nil { - return nil, fmt.Errorf("failed to create standard intent: %w", err) - } - - root := dasel.New(intent) - // This is a hack to workaround an op-deployer bug where the protocolVersionsOwner is incorrectly - // set to the protocolVersionsImpl address. So we mirror that value here so we can pass the intent validation. - mustPutString(root, "superchainRoles.protocolVersionsOwner", stringWrapper("0x79ADD5713B383DAa0a138d3C4780C7A1804a8090")) - - return intent, nil -} - -func StandardStateV4(l1ChainID uint64) (OpaqueState, error) { - return standardState(l1ChainID, validation.Semver400, standardV4State) -} - -func StandardStateV3(l1ChainID uint64) (OpaqueState, error) { - return standardState(l1ChainID, validation.Semver300, standardV3State) -} - -func StandardStateV2(l1ChainID uint64) (OpaqueState, error) { - return standardState(l1ChainID, validation.Semver200, standardV2State) -} - -func StandardStateV1(l1ChainID uint64) (OpaqueState, error) { - return standardState(l1ChainID, validation.Semver180, standardV1State) -} - -func standardState(l1ChainID uint64, semver validation.Semver, data []byte) (OpaqueState, error) { - state := make(OpaqueState) - if err := json.Unmarshal(data, &state); err != nil { - panic(err) - } - - var stdVersions validation.Versions - var scNetwork string - switch l1ChainID { - case 1: - stdVersions = validation.StandardVersionsMainnet - scNetwork = "mainnet" - case 11155111: - stdVersions = validation.StandardVersionsSepolia - scNetwork = "sepolia" - default: - return nil, fmt.Errorf("unsupported L1 chain ID: %d", l1ChainID) - } - - stdVals, ok := stdVersions[semver] - if !ok { - return nil, fmt.Errorf("semver not found in stdVersions: %s", semver) - } - - sc, err := superchain.GetSuperchain(scNetwork) - if err != nil { - return nil, fmt.Errorf("failed to get superchain: %w", err) - } - - root := dasel.New(state) - mustPutLowerString(root, "superchainDeployment.superchainConfigProxyAddress", sc.SuperchainConfigAddr) - mustPutLowerString(root, "superchainDeployment.protocolVersionsProxyAddress", sc.ProtocolVersionsAddr) - if stdVals.OPContractsManager != nil && stdVals.OPContractsManager.Address != nil { - mustPutLowerString(root, "implementationsDeployment.opcmAddress", stdVals.OPContractsManager.Address) - } - mustPutLowerString(root, "implementationsDeployment.delayedWETHImplAddress", stdVals.DelayedWeth.ImplementationAddress) - mustPutLowerString(root, "implementationsDeployment.optimismPortalImplAddress", stdVals.OptimismPortal.ImplementationAddress) - mustPutLowerString(root, "implementationsDeployment.preimageOracleSingletonAddress", stdVals.PreimageOracle.Address) - mustPutLowerString(root, "implementationsDeployment.mipsSingletonAddress", stdVals.Mips.Address) - mustPutLowerString(root, "implementationsDeployment.systemConfigImplAddress", stdVals.SystemConfig.ImplementationAddress) - mustPutLowerString(root, "implementationsDeployment.l1CrossDomainMessengerImplAddress", stdVals.L1CrossDomainMessenger.ImplementationAddress) - mustPutLowerString(root, "implementationsDeployment.l1ERC721BridgeImplAddress", stdVals.L1ERC721Bridge.ImplementationAddress) - mustPutLowerString(root, "implementationsDeployment.l1StandardBridgeImplAddress", stdVals.L1StandardBridge.ImplementationAddress) - mustPutLowerString(root, "implementationsDeployment.optimismMintableERC20FactoryImplAddress", stdVals.OptimismMintableERC20Factory.ImplementationAddress) - mustPutLowerString(root, "implementationsDeployment.disputeGameFactoryImplAddress", stdVals.DisputeGameFactory.ImplementationAddress) - - return state, nil -} - -func mustPutString(node *dasel.Node, sel string, val fmt.Stringer) { - if err := putString(node, sel, val); err != nil { - panic(err) - } -} - -func mustPutLowerString(node *dasel.Node, sel string, val fmt.Stringer) { - if err := node.Put(sel, strings.ToLower(val.String())); err != nil { - panic(err) - } -} - -func putString(node *dasel.Node, sel string, val fmt.Stringer) error { - return node.Put(sel, val.String()) -} - -func copyValue(src *dasel.Node, dest *dasel.Node, sel string) error { - val, err := src.Query(sel) - if err != nil { - return fmt.Errorf("failed to read value: %w", err) - } - return dest.Put(sel, val.InterfaceValue()) -} diff --git a/ops/internal/deployer/configs/v1-intent.toml b/ops/internal/deployer/state/configs/v1-intent.toml similarity index 100% rename from ops/internal/deployer/configs/v1-intent.toml rename to ops/internal/deployer/state/configs/v1-intent.toml diff --git a/ops/internal/deployer/configs/v1-state.json b/ops/internal/deployer/state/configs/v1-state.json similarity index 100% rename from ops/internal/deployer/configs/v1-state.json rename to ops/internal/deployer/state/configs/v1-state.json diff --git a/ops/internal/deployer/configs/v2-intent.toml b/ops/internal/deployer/state/configs/v2-intent.toml similarity index 100% rename from ops/internal/deployer/configs/v2-intent.toml rename to ops/internal/deployer/state/configs/v2-intent.toml diff --git a/ops/internal/deployer/configs/v2-state.json b/ops/internal/deployer/state/configs/v2-state.json similarity index 100% rename from ops/internal/deployer/configs/v2-state.json rename to ops/internal/deployer/state/configs/v2-state.json diff --git a/ops/internal/deployer/configs/v3-intent.toml b/ops/internal/deployer/state/configs/v3-intent.toml similarity index 100% rename from ops/internal/deployer/configs/v3-intent.toml rename to ops/internal/deployer/state/configs/v3-intent.toml diff --git a/ops/internal/deployer/configs/v3-state.json b/ops/internal/deployer/state/configs/v3-state.json similarity index 100% rename from ops/internal/deployer/configs/v3-state.json rename to ops/internal/deployer/state/configs/v3-state.json diff --git a/ops/internal/deployer/configs/v4-intent.toml b/ops/internal/deployer/state/configs/v4-intent.toml similarity index 100% rename from ops/internal/deployer/configs/v4-intent.toml rename to ops/internal/deployer/state/configs/v4-intent.toml diff --git a/ops/internal/deployer/configs/v4-state.json b/ops/internal/deployer/state/configs/v4-state.json similarity index 100% rename from ops/internal/deployer/configs/v4-state.json rename to ops/internal/deployer/state/configs/v4-state.json diff --git a/ops/internal/deployer/opaque_map.go b/ops/internal/deployer/state/opaque_state.go similarity index 88% rename from ops/internal/deployer/opaque_map.go rename to ops/internal/deployer/state/opaque_state.go index 0e7ea7e17..890cb18c4 100644 --- a/ops/internal/deployer/opaque_map.go +++ b/ops/internal/deployer/state/opaque_state.go @@ -1,46 +1,18 @@ -package deployer +package state import ( "fmt" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/opaque_map" "github.com/ethereum/go-ethereum/common" "github.com/tomwright/dasel" ) -type ( - OpaqueMap map[string]any - OpaqueState OpaqueMap -) - -// useInts converts all float64 values without fractional parts to int64 values in a map -// so that they are properly marshaled to TOML -func useInts(m map[string]any) { - for k, v := range m { - switch val := v.(type) { - case float64: - // If the float has no fractional part, convert to int - if val == float64(int64(val)) { - m[k] = int64(val) - } - case map[string]any: - // Recursively process nested maps - useInts(val) - case []any: - // Process arrays - for i, item := range val { - if fItem, ok := item.(float64); ok && fItem == float64(int64(fItem)) { - val[i] = int64(fItem) - } else if mapItem, ok := item.(map[string]any); ok { - useInts(mapItem) - } - } - } - } -} +type OpaqueState opaque_map.OpaqueMap -// QueryOpaqueMap queries the OpaqueState for the given paths in order, +// QueryOpaqueState queries the OpaqueState for the given paths in order, // and returns the first successful result (and an error otherwise) -func QueryOpaqueMap[T any](om OpaqueState, paths ...string) (T, error) { +func QueryOpaqueState[T any](om OpaqueState, paths ...string) (T, error) { node := dasel.New(om) resultNode := new(dasel.Node) var err error @@ -65,12 +37,12 @@ func QueryOpaqueMap[T any](om OpaqueState, paths ...string) (T, error) { // queryString retrieves a string value from the given path func (om OpaqueState) queryString(paths ...string) (string, error) { - return QueryOpaqueMap[string](om, paths...) + return QueryOpaqueState[string](om, paths...) } // queryAddress retrieves an address from the given path, with an optional fallback path func (om OpaqueState) queryAddress(paths ...string) (common.Address, error) { - val, err := QueryOpaqueMap[string](om, paths...) + val, err := QueryOpaqueState[string](om, paths...) if err != nil { return common.Address{}, err } @@ -267,7 +239,7 @@ func (om OpaqueState) ReadBatchSubmitter(idx int) (common.Address, error) { } func (om OpaqueState) GetNumChains() (int, error) { - return QueryOpaqueMap[int](om, "appliedIntent.chains.[#]") + return QueryOpaqueState[int](om, "appliedIntent.chains.[#]") } func (om OpaqueState) GetChainID(idx int) (uint64, error) { diff --git a/ops/internal/deployer/state/state.go b/ops/internal/deployer/state/state.go new file mode 100644 index 000000000..16aab5f59 --- /dev/null +++ b/ops/internal/deployer/state/state.go @@ -0,0 +1,141 @@ +package state + +import ( + "encoding/json" + "fmt" + "os" + "regexp" + "strconv" + "strings" + + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/opaque_map" + "github.com/ethereum-optimism/superchain-registry/validation" + "github.com/ethereum/go-ethereum/superchain" + "github.com/tomwright/dasel" +) + +func ReadOpaqueStateFile(p string) (OpaqueState, error) { + f, err := os.Open(p) + if err != nil { + return nil, fmt.Errorf("failed to open JSON file: %w", err) + } + defer f.Close() + + var out OpaqueState + if err := json.NewDecoder(f).Decode(&out); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return out, nil +} + +type StateMerger = func(state OpaqueState) (opaque_map.OpaqueMap, OpaqueState, error) + +func GetStateMerger(version string) (StateMerger, error) { + // Extract the version number using regex + re := regexp.MustCompile(`op-deployer/v\d+\.(\d+)\.\d+`) + match := re.FindStringSubmatch(version) + + if len(match) < 2 { + return nil, fmt.Errorf("invalid deployer version format: %s", version) + } + + // Get the middle version number + versionNum, err := strconv.Atoi(match[1]) + if err != nil { + return nil, fmt.Errorf("failed to parse version number: %w", err) + } + + // Return the appropriate merge function + switch versionNum { + case 0, 1: + return MergeStateV1, nil + case 2: + return MergeStateV2, nil + case 3: + return MergeStateV3, nil + case 4: + return MergeStateV4, nil + default: + return nil, fmt.Errorf("unsupported deployer version: %d", versionNum) + } +} + +// Add this type near the top of the file +type stringWrapper string + +func (s stringWrapper) String() string { + return string(s) +} + +func standardState(l1ChainID uint64, semver validation.Semver, data []byte) (OpaqueState, error) { + state := make(OpaqueState) + if err := json.Unmarshal(data, &state); err != nil { + panic(err) + } + + var stdVersions validation.Versions + var scNetwork string + switch l1ChainID { + case 1: + stdVersions = validation.StandardVersionsMainnet + scNetwork = "mainnet" + case 11155111: + stdVersions = validation.StandardVersionsSepolia + scNetwork = "sepolia" + default: + return nil, fmt.Errorf("unsupported L1 chain ID: %d", l1ChainID) + } + + stdVals, ok := stdVersions[semver] + if !ok { + return nil, fmt.Errorf("semver not found in stdVersions: %s", semver) + } + + sc, err := superchain.GetSuperchain(scNetwork) + if err != nil { + return nil, fmt.Errorf("failed to get superchain: %w", err) + } + + root := dasel.New(state) + mustPutLowerString(root, "superchainDeployment.superchainConfigProxyAddress", sc.SuperchainConfigAddr) + mustPutLowerString(root, "superchainDeployment.protocolVersionsProxyAddress", sc.ProtocolVersionsAddr) + if stdVals.OPContractsManager != nil && stdVals.OPContractsManager.Address != nil { + mustPutLowerString(root, "implementationsDeployment.opcmAddress", stdVals.OPContractsManager.Address) + } + mustPutLowerString(root, "implementationsDeployment.delayedWETHImplAddress", stdVals.DelayedWeth.ImplementationAddress) + mustPutLowerString(root, "implementationsDeployment.optimismPortalImplAddress", stdVals.OptimismPortal.ImplementationAddress) + mustPutLowerString(root, "implementationsDeployment.preimageOracleSingletonAddress", stdVals.PreimageOracle.Address) + mustPutLowerString(root, "implementationsDeployment.mipsSingletonAddress", stdVals.Mips.Address) + mustPutLowerString(root, "implementationsDeployment.systemConfigImplAddress", stdVals.SystemConfig.ImplementationAddress) + mustPutLowerString(root, "implementationsDeployment.l1CrossDomainMessengerImplAddress", stdVals.L1CrossDomainMessenger.ImplementationAddress) + mustPutLowerString(root, "implementationsDeployment.l1ERC721BridgeImplAddress", stdVals.L1ERC721Bridge.ImplementationAddress) + mustPutLowerString(root, "implementationsDeployment.l1StandardBridgeImplAddress", stdVals.L1StandardBridge.ImplementationAddress) + mustPutLowerString(root, "implementationsDeployment.optimismMintableERC20FactoryImplAddress", stdVals.OptimismMintableERC20Factory.ImplementationAddress) + mustPutLowerString(root, "implementationsDeployment.disputeGameFactoryImplAddress", stdVals.DisputeGameFactory.ImplementationAddress) + + return state, nil +} + +func mustPutString(node *dasel.Node, sel string, val fmt.Stringer) { + if err := putString(node, sel, val); err != nil { + panic(err) + } +} + +func mustPutLowerString(node *dasel.Node, sel string, val fmt.Stringer) { + if err := node.Put(sel, strings.ToLower(val.String())); err != nil { + panic(err) + } +} + +func putString(node *dasel.Node, sel string, val fmt.Stringer) error { + return node.Put(sel, val.String()) +} + +func copyValue(src *dasel.Node, dest *dasel.Node, sel string) error { + val, err := src.Query(sel) + if err != nil { + return fmt.Errorf("failed to read value: %w", err) + } + return dest.Put(sel, val.InterfaceValue()) +} diff --git a/ops/internal/deployer/state_test.go b/ops/internal/deployer/state/state_test.go similarity index 86% rename from ops/internal/deployer/state_test.go rename to ops/internal/deployer/state/state_test.go index 8a17d5b37..026b8973a 100644 --- a/ops/internal/deployer/state_test.go +++ b/ops/internal/deployer/state/state_test.go @@ -1,4 +1,4 @@ -package deployer +package state import ( "encoding/json" @@ -6,13 +6,14 @@ import ( "os" "testing" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/opaque_map" "github.com/stretchr/testify/require" ) func TestMergeState(t *testing.T) { tests := []struct { Version int - Merger func(state OpaqueState) (OpaqueMap, OpaqueState, error) + Merger func(state OpaqueState) (opaque_map.OpaqueMap, OpaqueState, error) }{ {1, MergeStateV1}, {2, MergeStateV2}, diff --git a/ops/internal/deployer/testdata/v1-intent-output.json b/ops/internal/deployer/state/testdata/v1-intent-output.json similarity index 100% rename from ops/internal/deployer/testdata/v1-intent-output.json rename to ops/internal/deployer/state/testdata/v1-intent-output.json diff --git a/ops/internal/deployer/testdata/v1-state-input.json b/ops/internal/deployer/state/testdata/v1-state-input.json similarity index 100% rename from ops/internal/deployer/testdata/v1-state-input.json rename to ops/internal/deployer/state/testdata/v1-state-input.json diff --git a/ops/internal/deployer/testdata/v1-state-output.json b/ops/internal/deployer/state/testdata/v1-state-output.json similarity index 100% rename from ops/internal/deployer/testdata/v1-state-output.json rename to ops/internal/deployer/state/testdata/v1-state-output.json diff --git a/ops/internal/deployer/testdata/v2-intent-output.json b/ops/internal/deployer/state/testdata/v2-intent-output.json similarity index 100% rename from ops/internal/deployer/testdata/v2-intent-output.json rename to ops/internal/deployer/state/testdata/v2-intent-output.json diff --git a/ops/internal/deployer/testdata/v2-state-input.json b/ops/internal/deployer/state/testdata/v2-state-input.json similarity index 100% rename from ops/internal/deployer/testdata/v2-state-input.json rename to ops/internal/deployer/state/testdata/v2-state-input.json diff --git a/ops/internal/deployer/testdata/v2-state-output.json b/ops/internal/deployer/state/testdata/v2-state-output.json similarity index 100% rename from ops/internal/deployer/testdata/v2-state-output.json rename to ops/internal/deployer/state/testdata/v2-state-output.json diff --git a/ops/internal/deployer/testdata/v3-intent-output.json b/ops/internal/deployer/state/testdata/v3-intent-output.json similarity index 100% rename from ops/internal/deployer/testdata/v3-intent-output.json rename to ops/internal/deployer/state/testdata/v3-intent-output.json diff --git a/ops/internal/deployer/testdata/v3-state-input.json b/ops/internal/deployer/state/testdata/v3-state-input.json similarity index 100% rename from ops/internal/deployer/testdata/v3-state-input.json rename to ops/internal/deployer/state/testdata/v3-state-input.json diff --git a/ops/internal/deployer/testdata/v3-state-output.json b/ops/internal/deployer/state/testdata/v3-state-output.json similarity index 100% rename from ops/internal/deployer/testdata/v3-state-output.json rename to ops/internal/deployer/state/testdata/v3-state-output.json diff --git a/ops/internal/deployer/state/v1.go b/ops/internal/deployer/state/v1.go new file mode 100644 index 000000000..3a5c40fd1 --- /dev/null +++ b/ops/internal/deployer/state/v1.go @@ -0,0 +1,54 @@ +package state + +import ( + _ "embed" + "fmt" + + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/opaque_map" + "github.com/ethereum-optimism/superchain-registry/validation" + "github.com/tomwright/dasel" +) + +//go:embed configs/v1-state.json +var standardV1State []byte + +//go:embed configs/v1-intent.toml +var standardV1Intent []byte + +func MergeStateV1(userState OpaqueState) (opaque_map.OpaqueMap, OpaqueState, error) { + l1ChainID, err := userState.ReadL1ChainID() + if err != nil { + return nil, nil, fmt.Errorf("failed to read L1 chain ID: %w", err) + } + stdIntent, err := StandardIntentV1(l1ChainID) + if err != nil { + return nil, nil, fmt.Errorf("failed to create standard intent: %w", err) + } + stdState, err := StandardStateV1(l1ChainID) + if err != nil { + return nil, nil, fmt.Errorf("failed to create standard state: %w", err) + } + return mergeStateV2(userState, stdIntent, stdState) +} + +func StandardStateV1(l1ChainID uint64) (OpaqueState, error) { + return standardState(l1ChainID, validation.Semver180, standardV1State) +} + +func StandardIntentV1(l1ChainID uint64) (opaque_map.OpaqueMap, error) { + return standardIntentV1(l1ChainID, standardV1Intent) +} + +func standardIntentV1(l1ChainID uint64, data []byte) (opaque_map.OpaqueMap, error) { + intent, err := standardIntentV2(l1ChainID, data) + if err != nil { + return nil, fmt.Errorf("failed to create standard intent: %w", err) + } + + root := dasel.New(intent) + // This is a hack to workaround an op-deployer bug where the protocolVersionsOwner is incorrectly + // set to the protocolVersionsImpl address. So we mirror that value here so we can pass the intent validation. + mustPutString(root, "superchainRoles.protocolVersionsOwner", stringWrapper("0x79ADD5713B383DAa0a138d3C4780C7A1804a8090")) + + return intent, nil +} diff --git a/ops/internal/deployer/state/v2.go b/ops/internal/deployer/state/v2.go new file mode 100644 index 000000000..b7f3929b6 --- /dev/null +++ b/ops/internal/deployer/state/v2.go @@ -0,0 +1,131 @@ +package state + +import ( + _ "embed" + "fmt" + + "github.com/BurntSushi/toml" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/opaque_map" + "github.com/ethereum-optimism/superchain-registry/validation" + "github.com/hashicorp/go-multierror" + "github.com/tomwright/dasel" +) + +//go:embed configs/v2-state.json +var standardV2State []byte + +//go:embed configs/v2-intent.toml +var standardV2Intent []byte + +func MergeStateV2(userState OpaqueState) (opaque_map.OpaqueMap, OpaqueState, error) { + l1ChainID, err := userState.ReadL1ChainID() + if err != nil { + return nil, nil, fmt.Errorf("failed to read L1 chain ID: %w", err) + } + stdIntent, err := StandardIntentV2(l1ChainID) + if err != nil { + return nil, nil, fmt.Errorf("failed to create standard intent: %w", err) + } + stdState, err := StandardStateV2(l1ChainID) + if err != nil { + return nil, nil, fmt.Errorf("failed to create standard state: %w", err) + } + return mergeStateV2(userState, stdIntent, stdState) +} + +func mergeStateV2(userState OpaqueState, stdIntent opaque_map.OpaqueMap, stdState OpaqueState) (opaque_map.OpaqueMap, OpaqueState, error) { + userStateNode := dasel.New(userState) + stdIntentNode := dasel.New(stdIntent) + stdStateNode := dasel.New(stdState) + + appliedIntentNode, err := userStateNode.Query("appliedIntent") + if err != nil { + return nil, nil, fmt.Errorf("failed to read applied intent: %w", err) + } + + // Helper function to aggregate copy errors + var copyErrs error + guard := func(err error) { + if err != nil { + copyErrs = multierror.Append(copyErrs, fmt.Errorf("failed to copy value: %w", err)) + } + } + + // First, set up the intent + guard(copyValue(appliedIntentNode, stdIntentNode, "l1ChainID")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].id")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].baseFeeVaultRecipient")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].l1FeeVaultRecipient")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].sequencerFeeVaultRecipient")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.l1ProxyAdminOwner")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.l2ProxyAdminOwner")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.systemConfigOwner")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.unsafeBlockSigner")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.batcher")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.proposer")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.challenger")) + + // Then, set up the state + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].id")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].proxyAdminAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].addressManagerAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].l1ERC721BridgeProxyAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].systemConfigProxyAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].optimismMintableERC20FactoryProxyAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].l1StandardBridgeProxyAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].l1CrossDomainMessengerProxyAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].optimismPortalProxyAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].disputeGameFactoryProxyAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].anchorStateRegistryProxyAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].faultDisputeGameAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].permissionedDisputeGameAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].delayedWETHPermissionedGameProxyAddress")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].startBlock")) + + if copyErrs != nil { + return nil, nil, copyErrs + } + + intentResult, okIntent := stdIntentNode.InterfaceValue().(opaque_map.OpaqueMap) + if !okIntent { + return nil, nil, fmt.Errorf("internal error: synthesized intent is not OpaqueMapping, but %T", stdIntentNode.InterfaceValue()) + } + stateResult, okState := stdStateNode.InterfaceValue().(OpaqueState) + if !okState { + return nil, nil, fmt.Errorf("internal error: synthesized state is not OpaqueMapping, but %T", stdStateNode.InterfaceValue()) + } + + return intentResult, stateResult, nil +} + +func StandardStateV2(l1ChainID uint64) (OpaqueState, error) { + return standardState(l1ChainID, validation.Semver200, standardV2State) +} + +func StandardIntentV2(l1ChainID uint64) (opaque_map.OpaqueMap, error) { + return standardIntentV2(l1ChainID, standardV2Intent) +} + +func standardIntentV2(l1ChainID uint64, data []byte) (opaque_map.OpaqueMap, error) { + intent := make(opaque_map.OpaqueMap) + if err := toml.Unmarshal(data, &intent); err != nil { + panic(err) + } + + var stdRoles validation.RolesConfig + switch l1ChainID { + case 1: + stdRoles = validation.StandardConfigRolesMainnet + case 11155111: + stdRoles = validation.StandardConfigRolesSepolia + default: + return nil, fmt.Errorf("unsupported L1 chain ID: %d", l1ChainID) + } + + root := dasel.New(intent) + mustPutString(root, "superchainRoles.proxyAdminOwner", stdRoles.L1ProxyAdminOwner) + mustPutString(root, "superchainRoles.protocolVersionsOwner", stdRoles.ProtocolVersionsOwner) + mustPutString(root, "superchainRoles.guardian", stdRoles.Guardian) + + return intent, nil +} diff --git a/ops/internal/deployer/state/v3.go b/ops/internal/deployer/state/v3.go new file mode 100644 index 000000000..8241dd506 --- /dev/null +++ b/ops/internal/deployer/state/v3.go @@ -0,0 +1,41 @@ +package state + +import ( + _ "embed" + "fmt" + + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/opaque_map" + "github.com/ethereum-optimism/superchain-registry/validation" +) + +//go:embed configs/v3-state.json +var standardV3State []byte + +//go:embed configs/v3-intent.toml +var standardV3Intent []byte + +func MergeStateV3(userState OpaqueState) (opaque_map.OpaqueMap, OpaqueState, error) { + l1ChainID, err := userState.ReadL1ChainID() + if err != nil { + return nil, nil, fmt.Errorf("failed to read L1 chain ID: %w", err) + } + stdIntent, err := StandardIntentV3(l1ChainID) + if err != nil { + return nil, nil, fmt.Errorf("failed to create standard intent: %w", err) + } + stdState, err := StandardStateV3(l1ChainID) + if err != nil { + return nil, nil, fmt.Errorf("failed to create standard state: %w", err) + } + // V2 is correct here. V3's state is the same as V2, except with a + // slightly different intent that contains operator fee fields. + return mergeStateV2(userState, stdIntent, stdState) +} + +func StandardStateV3(l1ChainID uint64) (OpaqueState, error) { + return standardState(l1ChainID, validation.Semver300, standardV3State) +} + +func StandardIntentV3(l1ChainID uint64) (opaque_map.OpaqueMap, error) { + return standardIntentV2(l1ChainID, standardV3Intent) +} diff --git a/ops/internal/deployer/state/v4.go b/ops/internal/deployer/state/v4.go new file mode 100644 index 000000000..8db28f6ce --- /dev/null +++ b/ops/internal/deployer/state/v4.go @@ -0,0 +1,132 @@ +package state + +import ( + _ "embed" + "fmt" + + "github.com/BurntSushi/toml" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/opaque_map" + "github.com/ethereum-optimism/superchain-registry/validation" + "github.com/hashicorp/go-multierror" + "github.com/tomwright/dasel" +) + +//go:embed configs/v4-state.json +var standardV4State []byte + +//go:embed configs/v4-intent.toml +var standardV4Intent []byte + +func MergeStateV4(userState OpaqueState) (opaque_map.OpaqueMap, OpaqueState, error) { + l1ChainID, err := userState.ReadL1ChainID() + if err != nil { + return nil, nil, fmt.Errorf("failed to read L1 chain ID: %w", err) + } + stdIntent, err := StandardIntentV4(l1ChainID) + if err != nil { + return nil, nil, fmt.Errorf("failed to create standard intent: %w", err) + } + stdState, err := StandardStateV4(l1ChainID) + if err != nil { + return nil, nil, fmt.Errorf("failed to create standard state: %w", err) + } + return mergeStateV4(userState, stdIntent, stdState) +} + +func StandardStateV4(l1ChainID uint64) (OpaqueState, error) { + return standardState(l1ChainID, validation.Semver400, standardV4State) +} + +func mergeStateV4(userState OpaqueState, stdIntent opaque_map.OpaqueMap, stdState OpaqueState) (opaque_map.OpaqueMap, OpaqueState, error) { + userStateNode := dasel.New(userState) + stdIntentNode := dasel.New(stdIntent) + stdStateNode := dasel.New(stdState) + + appliedIntentNode, err := userStateNode.Query("appliedIntent") + if err != nil { + return nil, nil, fmt.Errorf("failed to read applied intent: %w", err) + } + + // Helper function to aggregate copy errors + var copyErrs error + guard := func(err error) { + if err != nil { + copyErrs = multierror.Append(copyErrs, fmt.Errorf("failed to copy value: %w", err)) + } + } + + // First, set up the intent + guard(copyValue(appliedIntentNode, stdIntentNode, "l1ChainID")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].id")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].baseFeeVaultRecipient")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].l1FeeVaultRecipient")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].sequencerFeeVaultRecipient")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.l1ProxyAdminOwner")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.l2ProxyAdminOwner")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.systemConfigOwner")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.unsafeBlockSigner")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.batcher")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.proposer")) + guard(copyValue(appliedIntentNode, stdIntentNode, "chains.[0].roles.challenger")) + + // Then, set up the state + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].id")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].OpChainProxyAdminImpl")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].AddressManagerImpl")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].L1Erc721BridgeProxy")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].SystemConfigProxy")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].OptimismMintableErc20FactoryProxy")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].L1StandardBridgeProxy")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].L1CrossDomainMessengerProxy")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].OptimismPortalProxy")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].DisputeGameFactoryProxy")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].AnchorStateRegistryProxy")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].FaultDisputeGameImpl")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].PermissionedDisputeGameImpl")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].DelayedWethPermissionedGameProxy")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].DelayedWethPermissionlessGameProxy")) + guard(copyValue(userStateNode, stdStateNode, "opChainDeployments.[0].startBlock")) + + if copyErrs != nil { + return nil, nil, copyErrs + } + + intentResult, okIntent := stdIntentNode.InterfaceValue().(opaque_map.OpaqueMap) + if !okIntent { + return nil, nil, fmt.Errorf("internal error: synthesized intent is not OpaqueMapping, but %T", stdIntentNode.InterfaceValue()) + } + stateResult, okState := stdStateNode.InterfaceValue().(OpaqueState) + if !okState { + return nil, nil, fmt.Errorf("internal error: synthesized state is not OpaqueMapping, but %T", stdStateNode.InterfaceValue()) + } + + return intentResult, stateResult, nil +} + +func StandardIntentV4(l1ChainID uint64) (opaque_map.OpaqueMap, error) { + return standardIntentV4(l1ChainID, standardV4Intent) +} + +func standardIntentV4(l1ChainID uint64, data []byte) (opaque_map.OpaqueMap, error) { + intent := make(opaque_map.OpaqueMap) + if err := toml.Unmarshal(data, &intent); err != nil { + panic(err) + } + + var stdRoles validation.RolesConfig + switch l1ChainID { + case 1: + stdRoles = validation.StandardConfigRolesMainnet + case 11155111: + stdRoles = validation.StandardConfigRolesSepolia + default: + return nil, fmt.Errorf("unsupported L1 chain ID: %d", l1ChainID) + } + + root := dasel.New(intent) + mustPutString(root, "superchainRoles.SuperchainProxyAdminOwner", stdRoles.L1ProxyAdminOwner) + mustPutString(root, "superchainRoles.ProtocolVersionsOwner", stdRoles.ProtocolVersionsOwner) + mustPutString(root, "superchainRoles.SuperchainGuardian", stdRoles.Guardian) + + return intent, nil +} diff --git a/ops/internal/manage/generate.go b/ops/internal/manage/generate.go index 4016a482a..54091a890 100644 --- a/ops/internal/manage/generate.go +++ b/ops/internal/manage/generate.go @@ -10,6 +10,8 @@ import ( "github.com/ethereum-optimism/superchain-registry/ops/internal/config" "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/opaque_map" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/state" "github.com/ethereum-optimism/superchain-registry/ops/internal/output" "github.com/ethereum-optimism/superchain-registry/ops/internal/paths" "github.com/ethereum/go-ethereum/core" @@ -31,7 +33,7 @@ func GenerateChainArtifacts( opDeployerVersion string, opDeployerBinDir string, ) error { - st, err := deployer.ReadOpaqueStateFile(statePath) + st, err := state.ReadOpaqueStateFile(statePath) if err != nil { return fmt.Errorf("failed to read opaque state file: %w", err) } @@ -52,7 +54,7 @@ func GenerateChainArtifacts( } else { // Otherwise, use the specified op-deployer version. The correct state merger to use will be // autodetected based on the provided op-deployer version. - merger, err := deployer.GetStateMerger(opDeployerVersion) + merger, err := state.GetStateMerger(opDeployerVersion) if err != nil { return fmt.Errorf("failed to get state merger: %w", err) } @@ -121,7 +123,7 @@ func GenerateChainArtifacts( var ErrNotLossless = errors.New("conversion is not lossless, consider updating op-geth dependency") // Convert OpaqueMapping to core.Genesis -func opaqueToGenesis(opaque *deployer.OpaqueMap) (*core.Genesis, error) { +func opaqueToGenesis(opaque *opaque_map.OpaqueMap) (*core.Genesis, error) { // Step 1: Marshal the OpaqueMapping to JSON jsonData, err := json.MarshalIndent(opaque, "", " ") if err != nil { @@ -141,7 +143,7 @@ func opaqueToGenesis(opaque *deployer.OpaqueMap) (*core.Genesis, error) { return nil, fmt.Errorf("failed to marshal Genesis to JSON: %w", err) } - checkOpaque := new(deployer.OpaqueMap) + checkOpaque := new(opaque_map.OpaqueMap) if err := json.Unmarshal(jsonData2, checkOpaque); err != nil { return nil, fmt.Errorf("failed to unmarshal JSON to OpaqueMap: %w", err) } @@ -163,7 +165,7 @@ func opaqueToGenesis(opaque *deployer.OpaqueMap) (*core.Genesis, error) { } // containsAll checks if all the keys and values in b are present in a -func containsAll(a, b deployer.OpaqueMap) bool { +func containsAll(a, b opaque_map.OpaqueMap) bool { for k, bv := range b { av, ok := a[k] if !ok { diff --git a/ops/internal/manage/generate_test.go b/ops/internal/manage/generate_test.go index 9d95037de..8b4b2937b 100644 --- a/ops/internal/manage/generate_test.go +++ b/ops/internal/manage/generate_test.go @@ -4,14 +4,14 @@ import ( "path" "testing" - "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/opaque_map" "github.com/ethereum-optimism/superchain-registry/ops/internal/paths" "github.com/stretchr/testify/require" "github.com/tomwright/dasel" ) func TestOpaqueToGenesis(t *testing.T) { - om := new(deployer.OpaqueMap) + om := new(opaque_map.OpaqueMap) err := paths.ReadJSONFile(path.Join("testdata", "expected-genesis.json"), om) require.NoError(t, err) diff --git a/ops/internal/manage/staging.go b/ops/internal/manage/staging.go index 3626ffcf9..2de8a16a8 100644 --- a/ops/internal/manage/staging.go +++ b/ops/internal/manage/staging.go @@ -10,11 +10,12 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/superchain-registry/ops/internal/config" "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/state" "github.com/ethereum-optimism/superchain-registry/ops/internal/paths" "github.com/ethereum/go-ethereum/common" ) -func InflateChainConfig(opd *deployer.OpDeployer, st deployer.OpaqueState, statePath string, idx int) (*config.StagedChain, error) { +func InflateChainConfig(opd *deployer.OpDeployer, st state.OpaqueState, statePath string, idx int) (*config.StagedChain, error) { chainId, err := st.ReadL2ChainId(idx) if err != nil { return nil, fmt.Errorf("failed to read chain ID: %w", err) @@ -180,7 +181,7 @@ var ( ErrNoStagedSuperchainDefinition = errors.New("no staged superchain definition found") ) -func InflateSuperchainDefinition(name string, st deployer.OpaqueState) (*config.SuperchainDefinition, error) { +func InflateSuperchainDefinition(name string, st state.OpaqueState) (*config.SuperchainDefinition, error) { protocolVersionsProxyAddress, err := st.ReadProtocolVersionsProxy() if err != nil { return nil, fmt.Errorf("failed to read protocol versions proxy address: %w", err) @@ -253,7 +254,7 @@ func StagedSuperchainDefinition(rootP string) (*config.SuperchainDefinition, err return sM, err } -func GetRolesFromState(st deployer.OpaqueState, idx int) (config.Roles, error) { +func GetRolesFromState(st state.OpaqueState, idx int) (config.Roles, error) { roles := config.Roles{} systemConfigOwner, err := st.ReadSystemConfigOwner(idx) @@ -301,7 +302,7 @@ func GetRolesFromState(st deployer.OpaqueState, idx int) (config.Roles, error) { return roles, nil } -func GetContractAddressesFromState(st deployer.OpaqueState, idx int) (config.Addresses, error) { +func GetContractAddressesFromState(st state.OpaqueState, idx int) (config.Addresses, error) { var addresses config.Addresses var err error @@ -387,7 +388,7 @@ func GetContractAddressesFromState(st deployer.OpaqueState, idx int) (config.Add } // ExtractInteropDepSet reads the interop dependency set from state and converts it to config.Interop -func ExtractInteropDepSet(st deployer.OpaqueState) (*config.Interop, error) { +func ExtractInteropDepSet(st state.OpaqueState) (*config.Interop, error) { interopDepSet, err := st.ReadInteropDepSet() if err != nil { return nil, fmt.Errorf("failed to read interop dep set: %w", err) diff --git a/ops/internal/manage/staging_test.go b/ops/internal/manage/staging_test.go index 832c0fc53..af40b20d6 100644 --- a/ops/internal/manage/staging_test.go +++ b/ops/internal/manage/staging_test.go @@ -6,7 +6,7 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/superchain-registry/ops/internal/config" - "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/state" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/stretchr/testify/require" ) @@ -99,7 +99,7 @@ func TestExtractInteropDepSet(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - os := new(deployer.OpaqueState) + os := new(state.OpaqueState) err := json.Unmarshal([]byte(tt.stateDataJSON), os) require.NoError(t, err, "failed to unmarshal testdata") require.NotNil(t, os) diff --git a/ops/internal/report/l2.go b/ops/internal/report/l2.go index f4f7b7899..bacd48de0 100644 --- a/ops/internal/report/l2.go +++ b/ops/internal/report/l2.go @@ -7,6 +7,8 @@ import ( "strings" "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/opaque_map" + "github.com/ethereum-optimism/superchain-registry/ops/internal/deployer/state" "github.com/ethereum/go-ethereum/log" ) @@ -17,7 +19,7 @@ func ScanL2( l1RpcUrl string, deployerCacheDir string, ) (*L2Report, error) { - st, err := deployer.ReadOpaqueStateFile(statePath) + st, err := state.ReadOpaqueStateFile(statePath) if err != nil { return nil, fmt.Errorf("failed to read opaque state file: %w", err) } @@ -60,7 +62,7 @@ func ScanL2( var report L2Report report.Release = tagValue - genesisDiffs := deployer.DiffOpaqueMaps("genesis", *originalGenesis, *standardGenesis) + genesisDiffs := opaque_map.DiffOpaqueMaps("genesis", *originalGenesis, *standardGenesis) report.GenesisDiffs = genesisDiffs return &report, nil