From f4b76404d85929aa21db5d8dbd1fef9bf823ecff Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 9 Apr 2025 18:11:25 -0400 Subject: [PATCH 01/14] feat: add script for opcm verification --- .../verify-bytecode/bindings/opcm200.go | 1149 ++++++++++++++ .../scripts/verify/verify-bytecode/main.go | 1334 +++++++++++++---- .../verify/verify-bytecode/main_test.go | 10 +- 3 files changed, 2222 insertions(+), 271 deletions(-) create mode 100644 packages/contracts-bedrock/scripts/verify/verify-bytecode/bindings/opcm200.go diff --git a/packages/contracts-bedrock/scripts/verify/verify-bytecode/bindings/opcm200.go b/packages/contracts-bedrock/scripts/verify/verify-bytecode/bindings/opcm200.go new file mode 100644 index 00000000000..b31af018561 --- /dev/null +++ b/packages/contracts-bedrock/scripts/verify/verify-bytecode/bindings/opcm200.go @@ -0,0 +1,1149 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription +) + +// OPContractsManagerAddGameInput is an auto generated low-level Go binding around an user-defined struct. +type OPContractsManagerAddGameInput struct { + SaltMixer string + SystemConfig common.Address + ProxyAdmin common.Address + DelayedWETH common.Address + DisputeGameType uint32 + DisputeAbsolutePrestate [32]byte + DisputeMaxGameDepth *big.Int + DisputeSplitDepth *big.Int + DisputeClockExtension uint64 + DisputeMaxClockDuration uint64 + InitialBond *big.Int + Vm common.Address + Permissioned bool +} + +// OPContractsManagerAddGameOutput is an auto generated low-level Go binding around an user-defined struct. +type OPContractsManagerAddGameOutput struct { + DelayedWETH common.Address + FaultDisputeGame common.Address +} + +// OPContractsManagerBlueprints is an auto generated low-level Go binding around an user-defined struct. +type OPContractsManagerBlueprints struct { + AddressManager common.Address + Proxy common.Address + ProxyAdmin common.Address + L1ChugSplashProxy common.Address + ResolvedDelegateProxy common.Address + PermissionedDisputeGame1 common.Address + PermissionedDisputeGame2 common.Address + PermissionlessDisputeGame1 common.Address + PermissionlessDisputeGame2 common.Address +} + +// OPContractsManagerDeployInput is an auto generated low-level Go binding around an user-defined struct. +type OPContractsManagerDeployInput struct { + Roles OPContractsManagerRoles + BasefeeScalar uint32 + BlobBasefeeScalar uint32 + L2ChainId *big.Int + StartingAnchorRoot []byte + SaltMixer string + GasLimit uint64 + DisputeGameType uint32 + DisputeAbsolutePrestate [32]byte + DisputeMaxGameDepth *big.Int + DisputeSplitDepth *big.Int + DisputeClockExtension uint64 + DisputeMaxClockDuration uint64 +} + +// OPContractsManagerDeployOutput is an auto generated low-level Go binding around an user-defined struct. +type OPContractsManagerDeployOutput struct { + OpChainProxyAdmin common.Address + AddressManager common.Address + L1ERC721BridgeProxy common.Address + SystemConfigProxy common.Address + OptimismMintableERC20FactoryProxy common.Address + L1StandardBridgeProxy common.Address + L1CrossDomainMessengerProxy common.Address + OptimismPortalProxy common.Address + DisputeGameFactoryProxy common.Address + AnchorStateRegistryProxy common.Address + FaultDisputeGame common.Address + PermissionedDisputeGame common.Address + DelayedWETHPermissionedGameProxy common.Address + DelayedWETHPermissionlessGameProxy common.Address +} + +// OPContractsManagerImplementations is an auto generated low-level Go binding around an user-defined struct. +type OPContractsManagerImplementations struct { + SuperchainConfigImpl common.Address + ProtocolVersionsImpl common.Address + L1ERC721BridgeImpl common.Address + OptimismPortalImpl common.Address + SystemConfigImpl common.Address + OptimismMintableERC20FactoryImpl common.Address + L1CrossDomainMessengerImpl common.Address + L1StandardBridgeImpl common.Address + DisputeGameFactoryImpl common.Address + AnchorStateRegistryImpl common.Address + DelayedWETHImpl common.Address + MipsImpl common.Address +} + +// OPContractsManagerOpChainConfig is an auto generated low-level Go binding around an user-defined struct. +type OPContractsManagerOpChainConfig struct { + SystemConfigProxy common.Address + ProxyAdmin common.Address + AbsolutePrestate [32]byte +} + +// OPContractsManagerRoles is an auto generated low-level Go binding around an user-defined struct. +type OPContractsManagerRoles struct { + OpChainProxyAdminOwner common.Address + SystemConfigOwner common.Address + Batcher common.Address + UnsafeBlockSigner common.Address + Proposer common.Address + Challenger common.Address +} + +// Opcm200MetaData contains all meta data concerning the Opcm200 contract. +var Opcm200MetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"contractISuperchainConfig\",\"name\":\"_superchainConfig\",\"type\":\"address\"},{\"internalType\":\"contractIProtocolVersions\",\"name\":\"_protocolVersions\",\"type\":\"address\"},{\"internalType\":\"contractIProxyAdmin\",\"name\":\"_superchainProxyAdmin\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"_l1ContractsRelease\",\"type\":\"string\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"addressManager\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"proxy\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"proxyAdmin\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1ChugSplashProxy\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"resolvedDelegateProxy\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionedDisputeGame1\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionedDisputeGame2\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionlessDisputeGame1\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionlessDisputeGame2\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.Blueprints\",\"name\":\"_blueprints\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"superchainConfigImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"protocolVersionsImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1ERC721BridgeImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optimismPortalImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"systemConfigImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optimismMintableERC20FactoryImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1CrossDomainMessengerImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1StandardBridgeImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"disputeGameFactoryImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"anchorStateRegistryImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"delayedWETHImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"mipsImpl\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.Implementations\",\"name\":\"_implementations\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"_upgradeController\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"saltMixer\",\"type\":\"string\"},{\"internalType\":\"contractISystemConfig\",\"name\":\"systemConfig\",\"type\":\"address\"},{\"internalType\":\"contractIProxyAdmin\",\"name\":\"proxyAdmin\",\"type\":\"address\"},{\"internalType\":\"contractIDelayedWETH\",\"name\":\"delayedWETH\",\"type\":\"address\"},{\"internalType\":\"GameType\",\"name\":\"disputeGameType\",\"type\":\"uint32\"},{\"internalType\":\"Claim\",\"name\":\"disputeAbsolutePrestate\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"disputeMaxGameDepth\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"disputeSplitDepth\",\"type\":\"uint256\"},{\"internalType\":\"Duration\",\"name\":\"disputeClockExtension\",\"type\":\"uint64\"},{\"internalType\":\"Duration\",\"name\":\"disputeMaxClockDuration\",\"type\":\"uint64\"},{\"internalType\":\"uint256\",\"name\":\"initialBond\",\"type\":\"uint256\"},{\"internalType\":\"contractIBigStepper\",\"name\":\"vm\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"permissioned\",\"type\":\"bool\"}],\"internalType\":\"structOPContractsManager.AddGameInput[]\",\"name\":\"_gameConfigs\",\"type\":\"tuple[]\"}],\"name\":\"addGameType\",\"outputs\":[{\"components\":[{\"internalType\":\"contractIDelayedWETH\",\"name\":\"delayedWETH\",\"type\":\"address\"},{\"internalType\":\"contractIFaultDisputeGame\",\"name\":\"faultDisputeGame\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.AddGameOutput[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"blueprints\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"addressManager\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"proxy\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"proxyAdmin\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1ChugSplashProxy\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"resolvedDelegateProxy\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionedDisputeGame1\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionedDisputeGame2\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionlessDisputeGame1\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionlessDisputeGame2\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.Blueprints\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_l2ChainId\",\"type\":\"uint256\"}],\"name\":\"chainIdToBatchInboxAddress\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"opChainProxyAdminOwner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"systemConfigOwner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"batcher\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"unsafeBlockSigner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"proposer\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"challenger\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.Roles\",\"name\":\"roles\",\"type\":\"tuple\"},{\"internalType\":\"uint32\",\"name\":\"basefeeScalar\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"blobBasefeeScalar\",\"type\":\"uint32\"},{\"internalType\":\"uint256\",\"name\":\"l2ChainId\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"startingAnchorRoot\",\"type\":\"bytes\"},{\"internalType\":\"string\",\"name\":\"saltMixer\",\"type\":\"string\"},{\"internalType\":\"uint64\",\"name\":\"gasLimit\",\"type\":\"uint64\"},{\"internalType\":\"GameType\",\"name\":\"disputeGameType\",\"type\":\"uint32\"},{\"internalType\":\"Claim\",\"name\":\"disputeAbsolutePrestate\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"disputeMaxGameDepth\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"disputeSplitDepth\",\"type\":\"uint256\"},{\"internalType\":\"Duration\",\"name\":\"disputeClockExtension\",\"type\":\"uint64\"},{\"internalType\":\"Duration\",\"name\":\"disputeMaxClockDuration\",\"type\":\"uint64\"}],\"internalType\":\"structOPContractsManager.DeployInput\",\"name\":\"_input\",\"type\":\"tuple\"}],\"name\":\"deploy\",\"outputs\":[{\"components\":[{\"internalType\":\"contractIProxyAdmin\",\"name\":\"opChainProxyAdmin\",\"type\":\"address\"},{\"internalType\":\"contractIAddressManager\",\"name\":\"addressManager\",\"type\":\"address\"},{\"internalType\":\"contractIL1ERC721Bridge\",\"name\":\"l1ERC721BridgeProxy\",\"type\":\"address\"},{\"internalType\":\"contractISystemConfig\",\"name\":\"systemConfigProxy\",\"type\":\"address\"},{\"internalType\":\"contractIOptimismMintableERC20Factory\",\"name\":\"optimismMintableERC20FactoryProxy\",\"type\":\"address\"},{\"internalType\":\"contractIL1StandardBridge\",\"name\":\"l1StandardBridgeProxy\",\"type\":\"address\"},{\"internalType\":\"contractIL1CrossDomainMessenger\",\"name\":\"l1CrossDomainMessengerProxy\",\"type\":\"address\"},{\"internalType\":\"contractIOptimismPortal2\",\"name\":\"optimismPortalProxy\",\"type\":\"address\"},{\"internalType\":\"contractIDisputeGameFactory\",\"name\":\"disputeGameFactoryProxy\",\"type\":\"address\"},{\"internalType\":\"contractIAnchorStateRegistry\",\"name\":\"anchorStateRegistryProxy\",\"type\":\"address\"},{\"internalType\":\"contractIFaultDisputeGame\",\"name\":\"faultDisputeGame\",\"type\":\"address\"},{\"internalType\":\"contractIPermissionedDisputeGame\",\"name\":\"permissionedDisputeGame\",\"type\":\"address\"},{\"internalType\":\"contractIDelayedWETH\",\"name\":\"delayedWETHPermissionedGameProxy\",\"type\":\"address\"},{\"internalType\":\"contractIDelayedWETH\",\"name\":\"delayedWETHPermissionlessGameProxy\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.DeployOutput\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"implementations\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"superchainConfigImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"protocolVersionsImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1ERC721BridgeImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optimismPortalImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"systemConfigImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optimismMintableERC20FactoryImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1CrossDomainMessengerImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1StandardBridgeImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"disputeGameFactoryImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"anchorStateRegistryImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"delayedWETHImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"mipsImpl\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.Implementations\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"isRC\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"l1ContractsRelease\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"protocolVersions\",\"outputs\":[{\"internalType\":\"contractIProtocolVersions\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bool\",\"name\":\"_isRC\",\"type\":\"bool\"}],\"name\":\"setRC\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"superchainConfig\",\"outputs\":[{\"internalType\":\"contractISuperchainConfig\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"superchainProxyAdmin\",\"outputs\":[{\"internalType\":\"contractIProxyAdmin\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"contractISystemConfig\",\"name\":\"systemConfigProxy\",\"type\":\"address\"},{\"internalType\":\"contractIProxyAdmin\",\"name\":\"proxyAdmin\",\"type\":\"address\"},{\"internalType\":\"Claim\",\"name\":\"absolutePrestate\",\"type\":\"bytes32\"}],\"internalType\":\"structOPContractsManager.OpChainConfig[]\",\"name\":\"_opChainConfigs\",\"type\":\"tuple[]\"}],\"name\":\"upgrade\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"upgradeController\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"version\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"l2ChainId\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"deployer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"deployOutput\",\"type\":\"bytes\"}],\"name\":\"Deployed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"l2ChainId\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"GameType\",\"name\":\"gameType\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"contractIDisputeGame\",\"name\":\"newDisputeGame\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"contractIDisputeGame\",\"name\":\"oldDisputeGame\",\"type\":\"address\"}],\"name\":\"GameTypeAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"l2ChainId\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"contractISystemConfig\",\"name\":\"systemConfig\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"upgrader\",\"type\":\"address\"}],\"name\":\"Upgraded\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"who\",\"type\":\"address\"}],\"name\":\"AddressHasNoCode\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"who\",\"type\":\"address\"}],\"name\":\"AddressNotFound\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"AlreadyReleased\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"BytesArrayTooLong\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DeploymentFailed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyInitcode\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"IdentityPrecompileCallFailed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidChainId\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidGameConfigs\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"role\",\"type\":\"string\"}],\"name\":\"InvalidRoleAddress\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidStartingAnchorRoot\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"LatestReleaseNotSet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NotABlueprint\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyDelegatecall\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyUpgradeController\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"PrestateNotSet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ReservedBitsSet\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"contractISystemConfig\",\"name\":\"systemConfig\",\"type\":\"address\"}],\"name\":\"SuperchainConfigMismatch\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SuperchainProxyAdminMismatch\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"UnexpectedPreambleData\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"UnsupportedERCVersion\",\"type\":\"error\"}]", +} + +// Opcm200ABI is the input ABI used to generate the binding from. +// Deprecated: Use Opcm200MetaData.ABI instead. +var Opcm200ABI = Opcm200MetaData.ABI + +// Opcm200 is an auto generated Go binding around an Ethereum contract. +type Opcm200 struct { + Opcm200Caller // Read-only binding to the contract + Opcm200Transactor // Write-only binding to the contract + Opcm200Filterer // Log filterer for contract events +} + +// Opcm200Caller is an auto generated read-only Go binding around an Ethereum contract. +type Opcm200Caller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Opcm200Transactor is an auto generated write-only Go binding around an Ethereum contract. +type Opcm200Transactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Opcm200Filterer is an auto generated log filtering Go binding around an Ethereum contract events. +type Opcm200Filterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Opcm200Session is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type Opcm200Session struct { + Contract *Opcm200 // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// Opcm200CallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type Opcm200CallerSession struct { + Contract *Opcm200Caller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// Opcm200TransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type Opcm200TransactorSession struct { + Contract *Opcm200Transactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// Opcm200Raw is an auto generated low-level Go binding around an Ethereum contract. +type Opcm200Raw struct { + Contract *Opcm200 // Generic contract binding to access the raw methods on +} + +// Opcm200CallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type Opcm200CallerRaw struct { + Contract *Opcm200Caller // Generic read-only contract binding to access the raw methods on +} + +// Opcm200TransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type Opcm200TransactorRaw struct { + Contract *Opcm200Transactor // Generic write-only contract binding to access the raw methods on +} + +// NewOpcm200 creates a new instance of Opcm200, bound to a specific deployed contract. +func NewOpcm200(address common.Address, backend bind.ContractBackend) (*Opcm200, error) { + contract, err := bindOpcm200(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Opcm200{Opcm200Caller: Opcm200Caller{contract: contract}, Opcm200Transactor: Opcm200Transactor{contract: contract}, Opcm200Filterer: Opcm200Filterer{contract: contract}}, nil +} + +// NewOpcm200Caller creates a new read-only instance of Opcm200, bound to a specific deployed contract. +func NewOpcm200Caller(address common.Address, caller bind.ContractCaller) (*Opcm200Caller, error) { + contract, err := bindOpcm200(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &Opcm200Caller{contract: contract}, nil +} + +// NewOpcm200Transactor creates a new write-only instance of Opcm200, bound to a specific deployed contract. +func NewOpcm200Transactor(address common.Address, transactor bind.ContractTransactor) (*Opcm200Transactor, error) { + contract, err := bindOpcm200(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &Opcm200Transactor{contract: contract}, nil +} + +// NewOpcm200Filterer creates a new log filterer instance of Opcm200, bound to a specific deployed contract. +func NewOpcm200Filterer(address common.Address, filterer bind.ContractFilterer) (*Opcm200Filterer, error) { + contract, err := bindOpcm200(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &Opcm200Filterer{contract: contract}, nil +} + +// bindOpcm200 binds a generic wrapper to an already deployed contract. +func bindOpcm200(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(Opcm200ABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Opcm200 *Opcm200Raw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Opcm200.Contract.Opcm200Caller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Opcm200 *Opcm200Raw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Opcm200.Contract.Opcm200Transactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Opcm200 *Opcm200Raw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Opcm200.Contract.Opcm200Transactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Opcm200 *Opcm200CallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Opcm200.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Opcm200 *Opcm200TransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Opcm200.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Opcm200 *Opcm200TransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Opcm200.Contract.contract.Transact(opts, method, params...) +} + +// Blueprints is a free data retrieval call binding the contract method 0xb51f9c2b. +// +// Solidity: function blueprints() view returns((address,address,address,address,address,address,address,address,address)) +func (_Opcm200 *Opcm200Caller) Blueprints(opts *bind.CallOpts) (OPContractsManagerBlueprints, error) { + var out []interface{} + err := _Opcm200.contract.Call(opts, &out, "blueprints") + + if err != nil { + return *new(OPContractsManagerBlueprints), err + } + + out0 := *abi.ConvertType(out[0], new(OPContractsManagerBlueprints)).(*OPContractsManagerBlueprints) + + return out0, err + +} + +// Blueprints is a free data retrieval call binding the contract method 0xb51f9c2b. +// +// Solidity: function blueprints() view returns((address,address,address,address,address,address,address,address,address)) +func (_Opcm200 *Opcm200Session) Blueprints() (OPContractsManagerBlueprints, error) { + return _Opcm200.Contract.Blueprints(&_Opcm200.CallOpts) +} + +// Blueprints is a free data retrieval call binding the contract method 0xb51f9c2b. +// +// Solidity: function blueprints() view returns((address,address,address,address,address,address,address,address,address)) +func (_Opcm200 *Opcm200CallerSession) Blueprints() (OPContractsManagerBlueprints, error) { + return _Opcm200.Contract.Blueprints(&_Opcm200.CallOpts) +} + +// ChainIdToBatchInboxAddress is a free data retrieval call binding the contract method 0x318b1b80. +// +// Solidity: function chainIdToBatchInboxAddress(uint256 _l2ChainId) pure returns(address) +func (_Opcm200 *Opcm200Caller) ChainIdToBatchInboxAddress(opts *bind.CallOpts, _l2ChainId *big.Int) (common.Address, error) { + var out []interface{} + err := _Opcm200.contract.Call(opts, &out, "chainIdToBatchInboxAddress", _l2ChainId) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// ChainIdToBatchInboxAddress is a free data retrieval call binding the contract method 0x318b1b80. +// +// Solidity: function chainIdToBatchInboxAddress(uint256 _l2ChainId) pure returns(address) +func (_Opcm200 *Opcm200Session) ChainIdToBatchInboxAddress(_l2ChainId *big.Int) (common.Address, error) { + return _Opcm200.Contract.ChainIdToBatchInboxAddress(&_Opcm200.CallOpts, _l2ChainId) +} + +// ChainIdToBatchInboxAddress is a free data retrieval call binding the contract method 0x318b1b80. +// +// Solidity: function chainIdToBatchInboxAddress(uint256 _l2ChainId) pure returns(address) +func (_Opcm200 *Opcm200CallerSession) ChainIdToBatchInboxAddress(_l2ChainId *big.Int) (common.Address, error) { + return _Opcm200.Contract.ChainIdToBatchInboxAddress(&_Opcm200.CallOpts, _l2ChainId) +} + +// Implementations is a free data retrieval call binding the contract method 0x30e9012c. +// +// Solidity: function implementations() view returns((address,address,address,address,address,address,address,address,address,address,address,address)) +func (_Opcm200 *Opcm200Caller) Implementations(opts *bind.CallOpts) (OPContractsManagerImplementations, error) { + var out []interface{} + err := _Opcm200.contract.Call(opts, &out, "implementations") + + if err != nil { + return *new(OPContractsManagerImplementations), err + } + + out0 := *abi.ConvertType(out[0], new(OPContractsManagerImplementations)).(*OPContractsManagerImplementations) + + return out0, err + +} + +// Implementations is a free data retrieval call binding the contract method 0x30e9012c. +// +// Solidity: function implementations() view returns((address,address,address,address,address,address,address,address,address,address,address,address)) +func (_Opcm200 *Opcm200Session) Implementations() (OPContractsManagerImplementations, error) { + return _Opcm200.Contract.Implementations(&_Opcm200.CallOpts) +} + +// Implementations is a free data retrieval call binding the contract method 0x30e9012c. +// +// Solidity: function implementations() view returns((address,address,address,address,address,address,address,address,address,address,address,address)) +func (_Opcm200 *Opcm200CallerSession) Implementations() (OPContractsManagerImplementations, error) { + return _Opcm200.Contract.Implementations(&_Opcm200.CallOpts) +} + +// IsRC is a free data retrieval call binding the contract method 0xf179c48d. +// +// Solidity: function isRC() view returns(bool) +func (_Opcm200 *Opcm200Caller) IsRC(opts *bind.CallOpts) (bool, error) { + var out []interface{} + err := _Opcm200.contract.Call(opts, &out, "isRC") + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsRC is a free data retrieval call binding the contract method 0xf179c48d. +// +// Solidity: function isRC() view returns(bool) +func (_Opcm200 *Opcm200Session) IsRC() (bool, error) { + return _Opcm200.Contract.IsRC(&_Opcm200.CallOpts) +} + +// IsRC is a free data retrieval call binding the contract method 0xf179c48d. +// +// Solidity: function isRC() view returns(bool) +func (_Opcm200 *Opcm200CallerSession) IsRC() (bool, error) { + return _Opcm200.Contract.IsRC(&_Opcm200.CallOpts) +} + +// L1ContractsRelease is a free data retrieval call binding the contract method 0x35cb2e9b. +// +// Solidity: function l1ContractsRelease() view returns(string) +func (_Opcm200 *Opcm200Caller) L1ContractsRelease(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _Opcm200.contract.Call(opts, &out, "l1ContractsRelease") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// L1ContractsRelease is a free data retrieval call binding the contract method 0x35cb2e9b. +// +// Solidity: function l1ContractsRelease() view returns(string) +func (_Opcm200 *Opcm200Session) L1ContractsRelease() (string, error) { + return _Opcm200.Contract.L1ContractsRelease(&_Opcm200.CallOpts) +} + +// L1ContractsRelease is a free data retrieval call binding the contract method 0x35cb2e9b. +// +// Solidity: function l1ContractsRelease() view returns(string) +func (_Opcm200 *Opcm200CallerSession) L1ContractsRelease() (string, error) { + return _Opcm200.Contract.L1ContractsRelease(&_Opcm200.CallOpts) +} + +// ProtocolVersions is a free data retrieval call binding the contract method 0x6624856a. +// +// Solidity: function protocolVersions() view returns(address) +func (_Opcm200 *Opcm200Caller) ProtocolVersions(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Opcm200.contract.Call(opts, &out, "protocolVersions") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// ProtocolVersions is a free data retrieval call binding the contract method 0x6624856a. +// +// Solidity: function protocolVersions() view returns(address) +func (_Opcm200 *Opcm200Session) ProtocolVersions() (common.Address, error) { + return _Opcm200.Contract.ProtocolVersions(&_Opcm200.CallOpts) +} + +// ProtocolVersions is a free data retrieval call binding the contract method 0x6624856a. +// +// Solidity: function protocolVersions() view returns(address) +func (_Opcm200 *Opcm200CallerSession) ProtocolVersions() (common.Address, error) { + return _Opcm200.Contract.ProtocolVersions(&_Opcm200.CallOpts) +} + +// SuperchainConfig is a free data retrieval call binding the contract method 0x35e80ab3. +// +// Solidity: function superchainConfig() view returns(address) +func (_Opcm200 *Opcm200Caller) SuperchainConfig(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Opcm200.contract.Call(opts, &out, "superchainConfig") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// SuperchainConfig is a free data retrieval call binding the contract method 0x35e80ab3. +// +// Solidity: function superchainConfig() view returns(address) +func (_Opcm200 *Opcm200Session) SuperchainConfig() (common.Address, error) { + return _Opcm200.Contract.SuperchainConfig(&_Opcm200.CallOpts) +} + +// SuperchainConfig is a free data retrieval call binding the contract method 0x35e80ab3. +// +// Solidity: function superchainConfig() view returns(address) +func (_Opcm200 *Opcm200CallerSession) SuperchainConfig() (common.Address, error) { + return _Opcm200.Contract.SuperchainConfig(&_Opcm200.CallOpts) +} + +// SuperchainProxyAdmin is a free data retrieval call binding the contract method 0x2b96b839. +// +// Solidity: function superchainProxyAdmin() view returns(address) +func (_Opcm200 *Opcm200Caller) SuperchainProxyAdmin(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Opcm200.contract.Call(opts, &out, "superchainProxyAdmin") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// SuperchainProxyAdmin is a free data retrieval call binding the contract method 0x2b96b839. +// +// Solidity: function superchainProxyAdmin() view returns(address) +func (_Opcm200 *Opcm200Session) SuperchainProxyAdmin() (common.Address, error) { + return _Opcm200.Contract.SuperchainProxyAdmin(&_Opcm200.CallOpts) +} + +// SuperchainProxyAdmin is a free data retrieval call binding the contract method 0x2b96b839. +// +// Solidity: function superchainProxyAdmin() view returns(address) +func (_Opcm200 *Opcm200CallerSession) SuperchainProxyAdmin() (common.Address, error) { + return _Opcm200.Contract.SuperchainProxyAdmin(&_Opcm200.CallOpts) +} + +// UpgradeController is a free data retrieval call binding the contract method 0x87543ef6. +// +// Solidity: function upgradeController() view returns(address) +func (_Opcm200 *Opcm200Caller) UpgradeController(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Opcm200.contract.Call(opts, &out, "upgradeController") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// UpgradeController is a free data retrieval call binding the contract method 0x87543ef6. +// +// Solidity: function upgradeController() view returns(address) +func (_Opcm200 *Opcm200Session) UpgradeController() (common.Address, error) { + return _Opcm200.Contract.UpgradeController(&_Opcm200.CallOpts) +} + +// UpgradeController is a free data retrieval call binding the contract method 0x87543ef6. +// +// Solidity: function upgradeController() view returns(address) +func (_Opcm200 *Opcm200CallerSession) UpgradeController() (common.Address, error) { + return _Opcm200.Contract.UpgradeController(&_Opcm200.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() pure returns(string) +func (_Opcm200 *Opcm200Caller) Version(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _Opcm200.contract.Call(opts, &out, "version") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() pure returns(string) +func (_Opcm200 *Opcm200Session) Version() (string, error) { + return _Opcm200.Contract.Version(&_Opcm200.CallOpts) +} + +// Version is a free data retrieval call binding the contract method 0x54fd4d50. +// +// Solidity: function version() pure returns(string) +func (_Opcm200 *Opcm200CallerSession) Version() (string, error) { + return _Opcm200.Contract.Version(&_Opcm200.CallOpts) +} + +// AddGameType is a paid mutator transaction binding the contract method 0x1661a2e9. +// +// Solidity: function addGameType((string,address,address,address,uint32,bytes32,uint256,uint256,uint64,uint64,uint256,address,bool)[] _gameConfigs) returns((address,address)[]) +func (_Opcm200 *Opcm200Transactor) AddGameType(opts *bind.TransactOpts, _gameConfigs []OPContractsManagerAddGameInput) (*types.Transaction, error) { + return _Opcm200.contract.Transact(opts, "addGameType", _gameConfigs) +} + +// AddGameType is a paid mutator transaction binding the contract method 0x1661a2e9. +// +// Solidity: function addGameType((string,address,address,address,uint32,bytes32,uint256,uint256,uint64,uint64,uint256,address,bool)[] _gameConfigs) returns((address,address)[]) +func (_Opcm200 *Opcm200Session) AddGameType(_gameConfigs []OPContractsManagerAddGameInput) (*types.Transaction, error) { + return _Opcm200.Contract.AddGameType(&_Opcm200.TransactOpts, _gameConfigs) +} + +// AddGameType is a paid mutator transaction binding the contract method 0x1661a2e9. +// +// Solidity: function addGameType((string,address,address,address,uint32,bytes32,uint256,uint256,uint64,uint64,uint256,address,bool)[] _gameConfigs) returns((address,address)[]) +func (_Opcm200 *Opcm200TransactorSession) AddGameType(_gameConfigs []OPContractsManagerAddGameInput) (*types.Transaction, error) { + return _Opcm200.Contract.AddGameType(&_Opcm200.TransactOpts, _gameConfigs) +} + +// Deploy is a paid mutator transaction binding the contract method 0x613e827b. +// +// Solidity: function deploy(((address,address,address,address,address,address),uint32,uint32,uint256,bytes,string,uint64,uint32,bytes32,uint256,uint256,uint64,uint64) _input) returns((address,address,address,address,address,address,address,address,address,address,address,address,address,address)) +func (_Opcm200 *Opcm200Transactor) Deploy(opts *bind.TransactOpts, _input OPContractsManagerDeployInput) (*types.Transaction, error) { + return _Opcm200.contract.Transact(opts, "deploy", _input) +} + +// Deploy is a paid mutator transaction binding the contract method 0x613e827b. +// +// Solidity: function deploy(((address,address,address,address,address,address),uint32,uint32,uint256,bytes,string,uint64,uint32,bytes32,uint256,uint256,uint64,uint64) _input) returns((address,address,address,address,address,address,address,address,address,address,address,address,address,address)) +func (_Opcm200 *Opcm200Session) Deploy(_input OPContractsManagerDeployInput) (*types.Transaction, error) { + return _Opcm200.Contract.Deploy(&_Opcm200.TransactOpts, _input) +} + +// Deploy is a paid mutator transaction binding the contract method 0x613e827b. +// +// Solidity: function deploy(((address,address,address,address,address,address),uint32,uint32,uint256,bytes,string,uint64,uint32,bytes32,uint256,uint256,uint64,uint64) _input) returns((address,address,address,address,address,address,address,address,address,address,address,address,address,address)) +func (_Opcm200 *Opcm200TransactorSession) Deploy(_input OPContractsManagerDeployInput) (*types.Transaction, error) { + return _Opcm200.Contract.Deploy(&_Opcm200.TransactOpts, _input) +} + +// SetRC is a paid mutator transaction binding the contract method 0x6ccdfe11. +// +// Solidity: function setRC(bool _isRC) returns() +func (_Opcm200 *Opcm200Transactor) SetRC(opts *bind.TransactOpts, _isRC bool) (*types.Transaction, error) { + return _Opcm200.contract.Transact(opts, "setRC", _isRC) +} + +// SetRC is a paid mutator transaction binding the contract method 0x6ccdfe11. +// +// Solidity: function setRC(bool _isRC) returns() +func (_Opcm200 *Opcm200Session) SetRC(_isRC bool) (*types.Transaction, error) { + return _Opcm200.Contract.SetRC(&_Opcm200.TransactOpts, _isRC) +} + +// SetRC is a paid mutator transaction binding the contract method 0x6ccdfe11. +// +// Solidity: function setRC(bool _isRC) returns() +func (_Opcm200 *Opcm200TransactorSession) SetRC(_isRC bool) (*types.Transaction, error) { + return _Opcm200.Contract.SetRC(&_Opcm200.TransactOpts, _isRC) +} + +// Upgrade is a paid mutator transaction binding the contract method 0xff2dd5a1. +// +// Solidity: function upgrade((address,address,bytes32)[] _opChainConfigs) returns() +func (_Opcm200 *Opcm200Transactor) Upgrade(opts *bind.TransactOpts, _opChainConfigs []OPContractsManagerOpChainConfig) (*types.Transaction, error) { + return _Opcm200.contract.Transact(opts, "upgrade", _opChainConfigs) +} + +// Upgrade is a paid mutator transaction binding the contract method 0xff2dd5a1. +// +// Solidity: function upgrade((address,address,bytes32)[] _opChainConfigs) returns() +func (_Opcm200 *Opcm200Session) Upgrade(_opChainConfigs []OPContractsManagerOpChainConfig) (*types.Transaction, error) { + return _Opcm200.Contract.Upgrade(&_Opcm200.TransactOpts, _opChainConfigs) +} + +// Upgrade is a paid mutator transaction binding the contract method 0xff2dd5a1. +// +// Solidity: function upgrade((address,address,bytes32)[] _opChainConfigs) returns() +func (_Opcm200 *Opcm200TransactorSession) Upgrade(_opChainConfigs []OPContractsManagerOpChainConfig) (*types.Transaction, error) { + return _Opcm200.Contract.Upgrade(&_Opcm200.TransactOpts, _opChainConfigs) +} + +// Opcm200DeployedIterator is returned from FilterDeployed and is used to iterate over the raw logs and unpacked data for Deployed events raised by the Opcm200 contract. +type Opcm200DeployedIterator struct { + Event *Opcm200Deployed // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *Opcm200DeployedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(Opcm200Deployed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(Opcm200Deployed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *Opcm200DeployedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *Opcm200DeployedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// Opcm200Deployed represents a Deployed event raised by the Opcm200 contract. +type Opcm200Deployed struct { + L2ChainId *big.Int + Deployer common.Address + DeployOutput []byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDeployed is a free log retrieval operation binding the contract event 0xb40fb1137b92aa97efb20f29c17d36c5947aac681c3315ba854b0232f8349542. +// +// Solidity: event Deployed(uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput) +func (_Opcm200 *Opcm200Filterer) FilterDeployed(opts *bind.FilterOpts, l2ChainId []*big.Int, deployer []common.Address) (*Opcm200DeployedIterator, error) { + + var l2ChainIdRule []interface{} + for _, l2ChainIdItem := range l2ChainId { + l2ChainIdRule = append(l2ChainIdRule, l2ChainIdItem) + } + var deployerRule []interface{} + for _, deployerItem := range deployer { + deployerRule = append(deployerRule, deployerItem) + } + + logs, sub, err := _Opcm200.contract.FilterLogs(opts, "Deployed", l2ChainIdRule, deployerRule) + if err != nil { + return nil, err + } + return &Opcm200DeployedIterator{contract: _Opcm200.contract, event: "Deployed", logs: logs, sub: sub}, nil +} + +// WatchDeployed is a free log subscription operation binding the contract event 0xb40fb1137b92aa97efb20f29c17d36c5947aac681c3315ba854b0232f8349542. +// +// Solidity: event Deployed(uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput) +func (_Opcm200 *Opcm200Filterer) WatchDeployed(opts *bind.WatchOpts, sink chan<- *Opcm200Deployed, l2ChainId []*big.Int, deployer []common.Address) (event.Subscription, error) { + + var l2ChainIdRule []interface{} + for _, l2ChainIdItem := range l2ChainId { + l2ChainIdRule = append(l2ChainIdRule, l2ChainIdItem) + } + var deployerRule []interface{} + for _, deployerItem := range deployer { + deployerRule = append(deployerRule, deployerItem) + } + + logs, sub, err := _Opcm200.contract.WatchLogs(opts, "Deployed", l2ChainIdRule, deployerRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(Opcm200Deployed) + if err := _Opcm200.contract.UnpackLog(event, "Deployed", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseDeployed is a log parse operation binding the contract event 0xb40fb1137b92aa97efb20f29c17d36c5947aac681c3315ba854b0232f8349542. +// +// Solidity: event Deployed(uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput) +func (_Opcm200 *Opcm200Filterer) ParseDeployed(log types.Log) (*Opcm200Deployed, error) { + event := new(Opcm200Deployed) + if err := _Opcm200.contract.UnpackLog(event, "Deployed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// Opcm200GameTypeAddedIterator is returned from FilterGameTypeAdded and is used to iterate over the raw logs and unpacked data for GameTypeAdded events raised by the Opcm200 contract. +type Opcm200GameTypeAddedIterator struct { + Event *Opcm200GameTypeAdded // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *Opcm200GameTypeAddedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(Opcm200GameTypeAdded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(Opcm200GameTypeAdded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *Opcm200GameTypeAddedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *Opcm200GameTypeAddedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// Opcm200GameTypeAdded represents a GameTypeAdded event raised by the Opcm200 contract. +type Opcm200GameTypeAdded struct { + L2ChainId *big.Int + GameType uint32 + NewDisputeGame common.Address + OldDisputeGame common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterGameTypeAdded is a free log retrieval operation binding the contract event 0x4b8d2d3f00ea4ebab553d99606c8aea67fd4deb9ef0abee0e7c4b246c59a0e0f. +// +// Solidity: event GameTypeAdded(uint256 indexed l2ChainId, uint32 indexed gameType, address newDisputeGame, address oldDisputeGame) +func (_Opcm200 *Opcm200Filterer) FilterGameTypeAdded(opts *bind.FilterOpts, l2ChainId []*big.Int, gameType []uint32) (*Opcm200GameTypeAddedIterator, error) { + + var l2ChainIdRule []interface{} + for _, l2ChainIdItem := range l2ChainId { + l2ChainIdRule = append(l2ChainIdRule, l2ChainIdItem) + } + var gameTypeRule []interface{} + for _, gameTypeItem := range gameType { + gameTypeRule = append(gameTypeRule, gameTypeItem) + } + + logs, sub, err := _Opcm200.contract.FilterLogs(opts, "GameTypeAdded", l2ChainIdRule, gameTypeRule) + if err != nil { + return nil, err + } + return &Opcm200GameTypeAddedIterator{contract: _Opcm200.contract, event: "GameTypeAdded", logs: logs, sub: sub}, nil +} + +// WatchGameTypeAdded is a free log subscription operation binding the contract event 0x4b8d2d3f00ea4ebab553d99606c8aea67fd4deb9ef0abee0e7c4b246c59a0e0f. +// +// Solidity: event GameTypeAdded(uint256 indexed l2ChainId, uint32 indexed gameType, address newDisputeGame, address oldDisputeGame) +func (_Opcm200 *Opcm200Filterer) WatchGameTypeAdded(opts *bind.WatchOpts, sink chan<- *Opcm200GameTypeAdded, l2ChainId []*big.Int, gameType []uint32) (event.Subscription, error) { + + var l2ChainIdRule []interface{} + for _, l2ChainIdItem := range l2ChainId { + l2ChainIdRule = append(l2ChainIdRule, l2ChainIdItem) + } + var gameTypeRule []interface{} + for _, gameTypeItem := range gameType { + gameTypeRule = append(gameTypeRule, gameTypeItem) + } + + logs, sub, err := _Opcm200.contract.WatchLogs(opts, "GameTypeAdded", l2ChainIdRule, gameTypeRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(Opcm200GameTypeAdded) + if err := _Opcm200.contract.UnpackLog(event, "GameTypeAdded", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseGameTypeAdded is a log parse operation binding the contract event 0x4b8d2d3f00ea4ebab553d99606c8aea67fd4deb9ef0abee0e7c4b246c59a0e0f. +// +// Solidity: event GameTypeAdded(uint256 indexed l2ChainId, uint32 indexed gameType, address newDisputeGame, address oldDisputeGame) +func (_Opcm200 *Opcm200Filterer) ParseGameTypeAdded(log types.Log) (*Opcm200GameTypeAdded, error) { + event := new(Opcm200GameTypeAdded) + if err := _Opcm200.contract.UnpackLog(event, "GameTypeAdded", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// Opcm200UpgradedIterator is returned from FilterUpgraded and is used to iterate over the raw logs and unpacked data for Upgraded events raised by the Opcm200 contract. +type Opcm200UpgradedIterator struct { + Event *Opcm200Upgraded // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *Opcm200UpgradedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(Opcm200Upgraded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(Opcm200Upgraded) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *Opcm200UpgradedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *Opcm200UpgradedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// Opcm200Upgraded represents a Upgraded event raised by the Opcm200 contract. +type Opcm200Upgraded struct { + L2ChainId *big.Int + SystemConfig common.Address + Upgrader common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterUpgraded is a free log retrieval operation binding the contract event 0x78bc67b9bf548ef6410becd31a3e10b9ea6c255974ef6b4530728b431df30030. +// +// Solidity: event Upgraded(uint256 indexed l2ChainId, address indexed systemConfig, address indexed upgrader) +func (_Opcm200 *Opcm200Filterer) FilterUpgraded(opts *bind.FilterOpts, l2ChainId []*big.Int, systemConfig []common.Address, upgrader []common.Address) (*Opcm200UpgradedIterator, error) { + + var l2ChainIdRule []interface{} + for _, l2ChainIdItem := range l2ChainId { + l2ChainIdRule = append(l2ChainIdRule, l2ChainIdItem) + } + var systemConfigRule []interface{} + for _, systemConfigItem := range systemConfig { + systemConfigRule = append(systemConfigRule, systemConfigItem) + } + var upgraderRule []interface{} + for _, upgraderItem := range upgrader { + upgraderRule = append(upgraderRule, upgraderItem) + } + + logs, sub, err := _Opcm200.contract.FilterLogs(opts, "Upgraded", l2ChainIdRule, systemConfigRule, upgraderRule) + if err != nil { + return nil, err + } + return &Opcm200UpgradedIterator{contract: _Opcm200.contract, event: "Upgraded", logs: logs, sub: sub}, nil +} + +// WatchUpgraded is a free log subscription operation binding the contract event 0x78bc67b9bf548ef6410becd31a3e10b9ea6c255974ef6b4530728b431df30030. +// +// Solidity: event Upgraded(uint256 indexed l2ChainId, address indexed systemConfig, address indexed upgrader) +func (_Opcm200 *Opcm200Filterer) WatchUpgraded(opts *bind.WatchOpts, sink chan<- *Opcm200Upgraded, l2ChainId []*big.Int, systemConfig []common.Address, upgrader []common.Address) (event.Subscription, error) { + + var l2ChainIdRule []interface{} + for _, l2ChainIdItem := range l2ChainId { + l2ChainIdRule = append(l2ChainIdRule, l2ChainIdItem) + } + var systemConfigRule []interface{} + for _, systemConfigItem := range systemConfig { + systemConfigRule = append(systemConfigRule, systemConfigItem) + } + var upgraderRule []interface{} + for _, upgraderItem := range upgrader { + upgraderRule = append(upgraderRule, upgraderItem) + } + + logs, sub, err := _Opcm200.contract.WatchLogs(opts, "Upgraded", l2ChainIdRule, systemConfigRule, upgraderRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(Opcm200Upgraded) + if err := _Opcm200.contract.UnpackLog(event, "Upgraded", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseUpgraded is a log parse operation binding the contract event 0x78bc67b9bf548ef6410becd31a3e10b9ea6c255974ef6b4530728b431df30030. +// +// Solidity: event Upgraded(uint256 indexed l2ChainId, address indexed systemConfig, address indexed upgrader) +func (_Opcm200 *Opcm200Filterer) ParseUpgraded(log types.Log) (*Opcm200Upgraded, error) { + event := new(Opcm200Upgraded) + if err := _Opcm200.contract.UnpackLog(event, "Upgraded", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go b/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go index 3119cd9fcb7..c85dcbbba97 100644 --- a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go +++ b/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go @@ -1,321 +1,1013 @@ +// Package main implements a CLI tool to verify deployed Ethereum contract bytecode +// against local build artifacts. It supports verifying single contracts, blueprints +// (ERC-5202), and the contracts managed by an OPContractsManager instance. package main import ( "context" "encoding/hex" "encoding/json" - "flag" + "errors" "fmt" "os" + "path/filepath" + "reflect" + "regexp" "strconv" "strings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/fatih/color" + "github.com/urfave/cli/v2" + + "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/verify/verify-bytecode/bindings" ) -// ImmutableReference represents an immutable reference in the contract bytecode +// ImmutableReference represents a single location within expected contract bytecode +// where an immutable variable is injected. The actual value is populated during comparison. type ImmutableReference struct { - Offset int - Length int - Value string + Offset int // Byte offset where the immutable value starts. + Length int // Length of the immutable value in bytes. + Value string // Actual value found at this location in the deployed bytecode (hex encoded). } -// BytecodeDifference represents a difference between expected and actual bytecode +// BytecodeDifference represents a contiguous block of differing bytes found during comparison. type BytecodeDifference struct { - Start int - Length int - Expected string - Actual string - InImmutable bool - ImmutableName string + Start int // Byte offset where the difference begins. + Length int // Length of the differing block in bytes. + Expected string // Expected bytes (hex encoded). + Actual string // Actual bytes found onchain (hex encoded). + InImmutable bool // True if this difference falls within a known immutable reference range. + ImmutableName string // Name of the immutable variable if InImmutable is true. } -// currentDiff is a helper struct for tracking differences during comparison +// currentDiff is a temporary helper struct used internally by findDifferences +// to track an ongoing sequence of differing bytes during the comparison loop. type currentDiff struct { - Start int - Expected []string - Actual []string - InImmutable bool - ImmutableName string + Start int // Starting byte offset of the current difference block. + Expected []string // Accumulated expected hex bytes in the current block. + Actual []string // Accumulated actual hex bytes in the current block. + InImmutable bool // True if the current block is within an immutable reference range. + ImmutableName string // Name of the immutable variable if InImmutable is true. +} + +// defaultArtifactsDir is the default directory name expected to contain forge build artifacts. +const defaultArtifactsDir = "forge-artifacts" + +// defaultOPCMArtifactFilename is the default path relative to the artifacts directory +// for the OPContractsManager contract artifact. +const defaultOPCMArtifactFilename = "OPContractsManager.sol/OPContractsManager.json" + +// blueprintPreamble is the ERC-5202 preamble (0xFE71) followed by version (00). +const blueprintPreamble = "0xFE7100" + +// maxInitCodeSize defines the maximum size in bytes for the init code (creation code) +// that can be stored in a single blueprint slot, according to the split blueprint standard. +// (24576 - 3 byte preamble). +const maxInitCodeSize = 24573 + +// implementationArtifactOverrides maps specific field names from the OPCM Implementations struct +// (as defined in the Go bindings) to their corresponding artifact file paths (relative to artifacts-dir). +// This is used ONLY when the default naming convention (FieldName ending in "Impl" -> "BaseName.sol/BaseName.json") +// does not apply. +var implementationArtifactOverrides = map[string]string{ + "OptimismPortalImpl": "OptimismPortal2.sol/OptimismPortal2.json", } +// blueprintArtifactOverrides maps specific field names from the OPCM Blueprints struct +// (as defined in the Go bindings) to their corresponding artifact file paths (relative to artifacts-dir). +// This is used ONLY when the default naming convention (FieldName -> "FieldName.sol/FieldName.json" +// after removing trailing digits) does not apply. +var blueprintArtifactOverrides = map[string]string{ + "Proxy": "Proxy.sol/Proxy.json", + "PermissionlessDisputeGame1": "FaultDisputeGame.sol/FaultDisputeGame.json", + "PermissionlessDisputeGame2": "FaultDisputeGame.sol/FaultDisputeGame.json", + "PermissionedDisputeGame1": "PermissionedDisputeGame.sol/PermissionedDisputeGame.json", + "PermissionedDisputeGame2": "PermissionedDisputeGame.sol/PermissionedDisputeGame.json", +} + +// trailingDigitsRegex is used to remove trailing digits (e.g., '1' or '2') from blueprint field names +// to infer the base contract name for finding artifacts (e.g., "PermissionedDisputeGame1" -> "PermissionedDisputeGame"). +var trailingDigitsRegex = regexp.MustCompile(`\d+$`) + +// main sets up the CLI application using urfave/cli/v2 and defines the available commands. func main() { - // Parse command line arguments - address := flag.String("address", "", "Contract address to check") - artifactPath := flag.String("artifact", "", "Path to the contract artifact JSON file") - rpcURL := flag.String("rpc", "", "RPC URL for the network") - flag.Parse() - - if *rpcURL == "" { - color.Red("Error: RPC URL is required") - flag.Usage() + app := &cli.App{ + Name: "verify-bytecode", + Usage: "Verify onchain contract bytecode against build artifacts", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "rpc", + Usage: "RPC URL for the network", + Required: true, + EnvVars: []string{"ETH_RPC_URL"}, + }, + &cli.StringFlag{ + Name: "artifacts-dir", + Usage: "Base directory containing the forge compilation artifacts", + Value: defaultArtifactsDir, + EnvVars: []string{"ARTIFACTS_DIR"}, + }, + }, + Commands: []*cli.Command{ + { + Name: "single", + Usage: "Verify a single contract (compares deployed bytecode)", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "address", + Usage: "Contract address to check", + Required: true, + }, + &cli.StringFlag{ + Name: "artifact", + Usage: "Path to the contract artifact JSON file (can be absolute or relative to artifacts-dir)", + Required: true, + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Print detailed immutable diff information even on success", + Value: false, + }, + }, + Action: func(c *cli.Context) error { + rpcURL := c.String("rpc") + artifactsDir := c.String("artifacts-dir") + address := c.String("address") + artifactPath := c.String("artifact") + verbose := c.Bool("verbose") + + // Resolve artifact path relative to artifacts-dir if not absolute + if !filepath.IsAbs(artifactPath) { + cwd, _ := os.Getwd() + baseDir := artifactsDir + if !filepath.IsAbs(baseDir) { + baseDir = filepath.Join(cwd, baseDir) + } + artifactPath = filepath.Join(baseDir, artifactPath) + } + + color.Cyan("Comparing contract at %s with artifact %s", address, artifactPath) + err := verifyDeployedContract(address, artifactPath, rpcURL, verbose) + if err != nil { + // Error is already printed within verifyDeployedContract or its callees + return cli.Exit("", 1) // Indicate failure to the shell + } + // Success message is printed within verifyDeployedContract + return nil + }, + }, + { + Name: "opcm", + Usage: "Verify OPContractsManager and its managed implementations and blueprints", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "opcm-address", + Usage: "OPContractsManager contract address", + Required: true, + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Print detailed immutable diff information even on success", + Value: false, + }, + }, + Action: func(c *cli.Context) error { + rpcURL := c.String("rpc") + artifactsDir := c.String("artifacts-dir") + opcmAddress := c.String("opcm-address") + verbose := c.Bool("verbose") + + // Resolve base directory for artifacts + cwd, _ := os.Getwd() + baseDir := artifactsDir + if !filepath.IsAbs(baseDir) { + baseDir = filepath.Join(cwd, baseDir) + } + opcmArtifactPath := filepath.Join(baseDir, defaultOPCMArtifactFilename) + + err := runOPCMVerification(opcmAddress, opcmArtifactPath, rpcURL, baseDir, verbose) + if err != nil { + // Error is already printed within runOPCMVerification or its callees + return cli.Exit("", 1) // Indicate failure to the shell + } + // Success message is printed within runOPCMVerification + return nil + }, + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + // Errors should be printed by the Action handlers or cli library itself. + // Exit with non-zero status if Run returns an error. os.Exit(1) } +} - color.Cyan("Comparing contract at %s with artifact %s", *address, *artifactPath) +// runOPCMVerification orchestrates the verification process for the OPContractsManager (OPCM) +// and all the implementation and blueprint contracts it references. +// It first verifies the OPCM contract itself, then queries the OPCM for implementation +// and blueprint addresses, resolves their artifact paths, and calls the appropriate +// verification function (verifyDeployedContract, verifyBlueprint, or verifySplitBlueprint) for each. +// It aggregates errors encountered during the process. +func runOPCMVerification(opcmAddressHex, opcmArtifactPath, rpcURL, artifactsBaseDir string, verbose bool) error { + var combinedErr error + opcmAddress := common.HexToAddress(opcmAddressHex) + + // --- Verify OPContractsManager itself --- + color.Yellow("--- Verifying OPContractsManager ---") + err := verifyDeployedContract(opcmAddressHex, opcmArtifactPath, rpcURL, verbose) + if err != nil { + err = fmt.Errorf("failed to verify OPContractsManager contract: %w", err) + color.Red("Error: %v", err) + combinedErr = errors.Join(combinedErr, err) + // Continue verification even if OPCM fails, but report overall failure later. + } - // Load the artifact - artifact, err := loadArtifact(*artifactPath) + // --- Set up Ethereum client and OPCM caller --- + client, err := ethclient.Dial(rpcURL) if err != nil { - color.Red("Error loading artifact: %v", err) - os.Exit(1) + err = fmt.Errorf("failed to connect to RPC at %s: %w", rpcURL, err) + color.Red("Error: %v", err) + return errors.Join(combinedErr, err) // Cannot proceed without client } + defer client.Close() - // Get expected bytecode from artifact - expectedBytecode, err := getDeployedBytecode(artifact) + opcmCaller, err := bindings.NewOpcm200Caller(opcmAddress, client) if err != nil { + err = fmt.Errorf("failed to bind Opcm200 caller to address %s: %w", opcmAddressHex, err) color.Red("Error: %v", err) - os.Exit(1) + return errors.Join(combinedErr, err) // Cannot proceed without caller } - // Get immutable references - immutableRefs, err := getImmutableReferences(artifact) + // --- Verify Implementations --- + color.Yellow("\n--- Verifying Implementations ---") + implementationsResult, err := opcmCaller.Implementations(nil) if err != nil { + err = fmt.Errorf("failed to call implementations() on OPCM contract %s: %w", opcmAddressHex, err) color.Red("Error: %v", err) - os.Exit(1) + combinedErr = errors.Join(combinedErr, err) + } else { + if verbose { + color.Green("✓ Successfully retrieved implementation addresses.") + } + implValue := reflect.ValueOf(implementationsResult) + implType := implValue.Type() + + // Iterate through the fields of the Implementations struct + for i := 0; i < implValue.NumField(); i++ { + fieldName := implType.Field(i).Name + fieldValue := implValue.Field(i).Interface().(common.Address) + implAddressStr := fieldValue.Hex() + + // Skip zero addresses + if fieldValue == (common.Address{}) { + if verbose { + color.Yellow(" Skipping zero address for implementation: %s", fieldName) + } + continue + } + + // Determine the artifact path for this implementation + var relativePath string + var ok bool + if relativePath, ok = implementationArtifactOverrides[fieldName]; !ok { + // Apply default naming convention if no override exists + if strings.HasSuffix(fieldName, "Impl") { + baseName := strings.TrimSuffix(fieldName, "Impl") + relativePath = filepath.Join(fmt.Sprintf("%s.sol", baseName), fmt.Sprintf("%s.json", baseName)) + } else { + // Error if convention doesn't apply and no override exists + err = fmt.Errorf("cannot infer artifact path for implementation field '%s' (doesn't end in Impl) and no override exists", fieldName) + color.Red("Error: %v", err) + combinedErr = errors.Join(combinedErr, err) + continue // Skip verification for this implementation + } + } + + artifactPath := filepath.Join(artifactsBaseDir, relativePath) + // Verify the implementation contract + err := verifyDeployedContract(implAddressStr, artifactPath, rpcURL, verbose) + if err != nil { + // Combine errors for overall reporting + combinedErr = errors.Join(combinedErr, fmt.Errorf("implementation %s (%s): %w", fieldName, implAddressStr, err)) + } + } } - // Get actual bytecode from the network - actualBytecode, err := getOnchainBytecode(*address, *rpcURL) + // --- Verify Blueprints --- + color.Yellow("\n--- Verifying Blueprints ---") + blueprintsResult, err := opcmCaller.Blueprints(nil) if err != nil { + err = fmt.Errorf("failed to call blueprints() on OPCM contract %s: %w", opcmAddressHex, err) color.Red("Error: %v", err) - os.Exit(1) + combinedErr = errors.Join(combinedErr, err) + } else { + if verbose { + color.Green("✓ Successfully retrieved blueprint addresses.") + } + blueprintValue := reflect.ValueOf(blueprintsResult) + blueprintType := blueprintValue.Type() + // Store blueprint fields for easy lookup (needed for split blueprint check) + blueprintFields := make(map[string]common.Address) + processedPart2 := make(map[string]bool) // Track part 2 blueprints already handled + + for i := 0; i < blueprintValue.NumField(); i++ { + fieldName := blueprintType.Field(i).Name + fieldValue := blueprintValue.Field(i).Interface().(common.Address) + blueprintFields[fieldName] = fieldValue + } + + // Iterate through the fields of the Blueprints struct again for verification + for i := 0; i < blueprintValue.NumField(); i++ { + fieldName := blueprintType.Field(i).Name + fieldValue := blueprintValue.Field(i).Interface().(common.Address) + blueprintAddressStr := fieldValue.Hex() + + // Skip if this field was already processed as part 2 of a split blueprint + if processedPart2[fieldName] { + continue + } + + // Skip zero addresses + if fieldValue == (common.Address{}) { + if verbose { + color.Yellow(" Skipping zero address for blueprint: %s", fieldName) + } + continue + } + + // Determine the artifact path for this blueprint's target contract + var relativePath string + var baseName string // Base contract name inferred from field or override + var ok bool + if relativePath, ok = blueprintArtifactOverrides[fieldName]; !ok { + // Apply default naming convention if no override exists + baseName = trailingDigitsRegex.ReplaceAllString(fieldName, "") + if baseName == "" { + err = fmt.Errorf("cannot infer artifact path for blueprint field '%s' (empty after removing digits) and no override exists", fieldName) + color.Red("Error: %v", err) + combinedErr = errors.Join(combinedErr, err) + continue // Skip verification for this blueprint + } + relativePath = filepath.Join(fmt.Sprintf("%s.sol", baseName), fmt.Sprintf("%s.json", baseName)) + } else { + // If override exists, try to infer baseName from it, otherwise use field name + parts := strings.Split(filepath.ToSlash(relativePath), "/") + if len(parts) == 2 && strings.HasSuffix(parts[0], ".sol") && strings.HasSuffix(parts[1], ".json") { + baseName = strings.TrimSuffix(parts[1], ".json") + } else { + // Fallback if override path doesn't match expected pattern + baseName = trailingDigitsRegex.ReplaceAllString(fieldName, "") + } + } + + artifactPath := filepath.Join(artifactsBaseDir, relativePath) + + // Check if this is part 1 of a split blueprint + if strings.HasSuffix(fieldName, "1") { + part2FieldName := strings.TrimSuffix(fieldName, "1") + "2" + if part2Addr, exists := blueprintFields[part2FieldName]; exists && part2Addr != (common.Address{}) { + // If corresponding part 2 exists and is non-zero, verify as split blueprint + err := verifySplitBlueprint(fieldName, blueprintAddressStr, part2FieldName, part2Addr.Hex(), artifactPath, rpcURL, verbose) + if err != nil { + combinedErr = errors.Join(combinedErr, fmt.Errorf("split blueprint %s/%s: %w", fieldName, part2FieldName, err)) + } + processedPart2[part2FieldName] = true // Mark part 2 as handled + continue // Move to next field + } + // Warn if part 1 exists but part 2 doesn't (or is zero address) + color.Yellow("Warning: Found blueprint %s ending in '1' but no valid corresponding '%s' found. Verifying as single blueprint.", fieldName, part2FieldName) + } + + // Verify as a standard (single) blueprint + err := verifyBlueprint(fieldName, blueprintAddressStr, artifactPath, rpcURL, verbose) + if err != nil { + combinedErr = errors.Join(combinedErr, fmt.Errorf("blueprint %s (%s): %w", fieldName, blueprintAddressStr, err)) + } + } + } + + // Return combined error if any verification step failed + if combinedErr != nil { + return fmt.Errorf("one or more OPCM verification steps failed") } - // Find differences + return nil +} + +// verifyDeployedContract performs bytecode verification for a standard deployed contract. +// It loads the artifact, extracts the expected deployed bytecode and immutable references, +// fetches the actual bytecode from the chain, compares them, and prints the results. +// It handles differences within immutable variable locations specifically. +func verifyDeployedContract(address, artifactPath, rpcURL string, verbose bool) error { + fmt.Println() // Add spacing for readability + contractName := strings.TrimSuffix(filepath.Base(artifactPath), ".json") + color.Cyan("Verifying %s (%s)", contractName, common.HexToAddress(address).String()) + + // Load artifact JSON + artifact, err := loadArtifact(artifactPath) + if err != nil { + color.Red(" Error loading artifact: %v", err) + return err + } + + // Extract expected deployed bytecode from artifact + expectedBytecode, err := getDeployedBytecode(artifact) + if err != nil { + color.Red(" Error getting deployed bytecode from %s: %v", artifactPath, err) + return err + } + + // Extract immutable reference locations from artifact + immutableRefs, err := getImmutableReferences(artifact) + if err != nil { + // Log error but continue comparison, treating differences as code errors + color.Red(" Error getting immutable references from %s: %v", artifactPath, err) + immutableRefs = nil // Ensure it's nil so findDifferences doesn't use partial data + } + + // Fetch actual bytecode from the blockchain + actualBytecode, err := getOnchainBytecode(address, rpcURL) + if err != nil { + color.Red(" Error getting onchain bytecode for %s: %v", address, err) + return err + } + + // Compare expected and actual bytecode, considering immutables differences, err := findDifferences(expectedBytecode, actualBytecode, immutableRefs) if err != nil { - color.Red("Error: %v", err) - os.Exit(1) + color.Red(" Error comparing bytecode for %s: %v", address, err) + return err } - // Print results - printDifferences(differences, immutableRefs) + // Print the comparison results (summary, errors, immutable details) + printDifferences(differences, immutableRefs, verbose) - // Exit with error code if there are non-immutable differences + // Determine if verification failed due to non-immutable differences + hasCodeDifferences := false for _, diff := range differences { if !diff.InImmutable { - os.Exit(1) + hasCodeDifferences = true + break } } - fmt.Println() - color.Green("✓ Contract bytecode matches the artifact (accounting for immutable references).") + if hasCodeDifferences { + color.Red(" ✗ Verification FAILED for %s: Found unexpected differences in code.", address) + return fmt.Errorf("bytecode mismatch for %s", address) + } else { + successMsg := fmt.Sprintf(" ✓ Verification successful") + if len(differences) > 0 { + successMsg += " (differences only in immutables)" + } else { + successMsg += " (exact match)" + } + color.Green(successMsg) + } + + return nil +} + +// verifyBlueprint performs bytecode verification for an ERC-5202 blueprint contract. +// It loads the target contract's artifact, extracts its creation code (initcode), +// prepends the ERC-5202 preamble, fetches the blueprint's bytecode from the chain, +// compares them, and prints the results. Immutable references are not considered for blueprints. +func verifyBlueprint(fieldName, address, targetArtifactPath, rpcURL string, verbose bool) error { + fmt.Println() // Add spacing for readability + targetContractName := strings.TrimSuffix(filepath.Base(targetArtifactPath), ".json") + color.Cyan("Verifying blueprint %s (for %s) at %s", fieldName, targetContractName, common.HexToAddress(address).String()) + + // Load the artifact of the contract the blueprint creates + artifact, err := loadArtifact(targetArtifactPath) + if err != nil { + color.Red(" Error loading target artifact: %v", err) + return err + } + + // Extract the creation code (initcode) from the artifact + creationCode, err := getCreationBytecode(artifact) + if err != nil { + color.Red(" Error getting creation code from %s: %v", targetArtifactPath, err) + return err + } + + // Construct the expected blueprint bytecode (Preamble + Creation Code) + expectedBlueprintBytecode := blueprintPreamble + strings.TrimPrefix(creationCode, "0x") + + // Fetch the actual bytecode stored at the blueprint address + actualBytecode, err := getOnchainBytecode(address, rpcURL) + if err != nil { + color.Red(" Error getting onchain bytecode for blueprint %s: %v", address, err) + // Attempt comparison even if fetch failed, might compare against "0x" + cmpErr := compareBlueprintCode(address, fieldName, expectedBlueprintBytecode, actualBytecode, verbose) + return errors.Join(err, cmpErr) // Return both fetch and compare errors if any + } + + // Compare expected blueprint bytecode with actual onchain bytecode + return compareBlueprintCode(address, fieldName, expectedBlueprintBytecode, actualBytecode, verbose) +} + +// verifySplitBlueprint verifies a blueprint that has been split into two parts due to size limits. +// It loads the target contract's artifact, extracts the full creation code, splits it into two parts +// based on maxInitCodeSize, prepends the preamble to each part, fetches the bytecode for both +// blueprint addresses (part 1 and part 2), and compares each part individually. +func verifySplitBlueprint(fieldName1, address1, fieldName2, address2, targetArtifactPath, rpcURL string, verbose bool) error { + fmt.Println() // Add spacing for readability + targetContractName := strings.TrimSuffix(filepath.Base(targetArtifactPath), ".json") + color.Cyan("Verifying split blueprint %s/%s (for %s)", + fieldName1, fieldName2, targetContractName) + color.Cyan(" Part 1 Address: %s", common.HexToAddress(address1).String()) + color.Cyan(" Part 2 Address: %s", common.HexToAddress(address2).String()) + + // Load the artifact of the contract the blueprint creates + artifact, err := loadArtifact(targetArtifactPath) + if err != nil { + color.Red(" Error loading target artifact: %v", err) + return err + } + + // Extract the full creation code (initcode) from the artifact + fullCreationCodeHex, err := getCreationBytecode(artifact) + if err != nil { + color.Red(" Error getting full creation code from %s: %v", targetArtifactPath, err) + return err + } + fullCreationCodeHex = strings.TrimPrefix(fullCreationCodeHex, "0x") + + // Decode the creation code from hex to bytes + fullCreationCodeBytes, err := hex.DecodeString(fullCreationCodeHex) + if err != nil { + return fmt.Errorf("failed to decode creation code from artifact %s: %w", targetArtifactPath, err) + } + + // Split the creation code into two parts + part1Bytes := fullCreationCodeBytes + var part2Bytes []byte + if len(fullCreationCodeBytes) > maxInitCodeSize { + part1Bytes = fullCreationCodeBytes[:maxInitCodeSize] + part2Bytes = fullCreationCodeBytes[maxInitCodeSize:] + } else { + // Warn if it was expected to be split but wasn't large enough + color.Yellow(" Warning: Expected split blueprint %s/%s, but total initcode size (%d bytes) <= max size (%d bytes)", + fieldName1, fieldName2, len(fullCreationCodeBytes), maxInitCodeSize) + part2Bytes = []byte{} // Part 2 should be empty in this case + } + + // Construct expected bytecode for each part (Preamble + Part Code) + expectedBytecode1 := blueprintPreamble + hex.EncodeToString(part1Bytes) + expectedBytecode2 := blueprintPreamble + hex.EncodeToString(part2Bytes) + + // Fetch actual bytecode for both blueprint addresses + actualBytecode1, err1 := getOnchainBytecode(address1, rpcURL) + actualBytecode2, err2 := getOnchainBytecode(address2, rpcURL) + + // Verify Part 1 + color.Cyan(" Verifying part 1 (%s)", fieldName1) + errPart1 := compareBlueprintCode(address1, fieldName1, expectedBytecode1, actualBytecode1, verbose) + if err1 != nil { + // Combine fetch error with comparison error if any + errPart1 = errors.Join(errPart1, fmt.Errorf("failed to get onchain bytecode for %s: %w", address1, err1)) + color.Red(" Error getting onchain bytecode for part 1: %v", err1) + } + + // Verify Part 2 + color.Cyan(" Verifying part 2 (%s)", fieldName2) + errPart2 := compareBlueprintCode(address2, fieldName2, expectedBytecode2, actualBytecode2, verbose) + if err2 != nil { + // Combine fetch error with comparison error if any + errPart2 = errors.Join(errPart2, fmt.Errorf("failed to get onchain bytecode for %s: %w", address2, err2)) + color.Red(" Error getting onchain bytecode for part 2: %v", err2) + } + + // Return combined errors from both parts + return errors.Join(errPart1, errPart2) } +// compareBlueprintCode performs the direct bytecode comparison for a single blueprint part. +// It compares the expected bytecode (preamble + creation code fragment) with the actual +// bytecode fetched from the chain for the given blueprint address. It prints success or failure messages. +// Immutable references are ignored in this comparison. +func compareBlueprintCode(address, fieldName, expectedBytecode, actualBytecode string, verbose bool) error { + expectedClean := strings.ToLower(strings.TrimPrefix(expectedBytecode, "0x")) + actualClean := strings.ToLower(strings.TrimPrefix(actualBytecode, "0x")) + + // Compare the expected and actual bytecode + if expectedClean == actualClean { + color.Green(" ✓ Verification successful (exact match)") + return nil + } else { + color.Red(" ✗ Verification FAILED: Bytecode mismatch for blueprint %s (%s)", fieldName, address) + // Use findDifferences to show where the mismatch occurs, ignoring immutables (nil map) + differences, diffErr := findDifferences(expectedBytecode, actualBytecode, nil) + if diffErr == nil && len(differences) > 0 { + // Print the first block of differences found + diff := differences[0] + endPos := diff.Start + diff.Length - 1 + color.Red(" Difference found at byte %d-%d (%d bytes):", diff.Start, endPos, diff.Length) + + // Limit printed length for readability + maxLen := 50 // Print up to 50 hex chars (25 bytes) + expectedPrint := diff.Expected + actualPrint := diff.Actual + if len(expectedPrint) > maxLen { + expectedPrint = expectedPrint[:maxLen] + "..." + } + if len(actualPrint) > maxLen { + actualPrint = actualPrint[:maxLen] + "..." + } + fmt.Printf(" Expected: 0x%s\n", expectedPrint) + fmt.Printf(" Actual: 0x%s\n", actualPrint) + + // If verbose, show full expected/actual (truncated) + if verbose { + maxLenTotal := 100 + expectedSuffix := "" + actualSuffix := "" + expectedTotalPrint := expectedBytecode + actualTotalPrint := actualBytecode + if len(expectedTotalPrint) > maxLenTotal { + expectedTotalPrint = expectedTotalPrint[:maxLenTotal] + expectedSuffix = "..." + } + if len(actualTotalPrint) > maxLenTotal { + actualTotalPrint = actualTotalPrint[:maxLenTotal] + actualSuffix = "..." + } + fmt.Printf(" Expected Full (start): %s%s\n", expectedTotalPrint, expectedSuffix) + fmt.Printf(" Actual Full (start): %s%s\n", actualTotalPrint, actualSuffix) + } + + } else { + // Fallback if findDifferences fails or finds no diffs (e.g., length mismatch only) + color.Red(" Comparison failed, unable to generate detailed diff: %v", diffErr) + fmt.Printf(" Expected Length: %d bytes\n", len(expectedClean)/2) + fmt.Printf(" Actual Length: %d bytes\n", len(actualClean)/2) + if verbose { + maxLenTotal := 100 + expectedSuffix := "" + actualSuffix := "" + expectedTotalPrint := expectedBytecode + actualTotalPrint := actualBytecode + if len(expectedTotalPrint) > maxLenTotal { + expectedTotalPrint = expectedTotalPrint[:maxLenTotal] + expectedSuffix = "..." + } + if len(actualTotalPrint) > maxLenTotal { + actualTotalPrint = actualTotalPrint[:maxLenTotal] + actualSuffix = "..." + } + fmt.Printf(" Expected (start): %s%s\n", expectedTotalPrint, expectedSuffix) + fmt.Printf(" Actual (start): %s%s\n", actualTotalPrint, actualSuffix) + } + } + return fmt.Errorf("blueprint bytecode mismatch for %s (%s)", fieldName, address) + } +} + +// --- Artifact and Bytecode Handling Helpers --- + +// loadArtifact reads and parses a JSON artifact file from the given path. func loadArtifact(path string) (map[string]any, error) { if path == "" { return nil, fmt.Errorf("artifact path is required") } + // Check if file exists first for a clearer error message + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, fmt.Errorf("artifact file not found: %s", path) + } else if err != nil { + return nil, fmt.Errorf("error checking artifact file %s: %w", path, err) + } data, err := os.ReadFile(path) if err != nil { - return nil, fmt.Errorf("failed to read artifact file: %w", err) + return nil, fmt.Errorf("failed to read artifact file %s: %w", path, err) } var artifact map[string]any if err := json.Unmarshal(data, &artifact); err != nil { - return nil, fmt.Errorf("failed to parse JSON: %w", err) + return nil, fmt.Errorf("failed to parse JSON from %s: %w", path, err) } return artifact, nil } +// getDeployedBytecode extracts the deployed bytecode string (runtime code) from a parsed artifact. +// It prioritizes `deployedBytecode.object` > `deployedBytecode` (string) > `bytecode.object` > `bytecode` (string). func getDeployedBytecode(artifact map[string]any) (string, error) { - // Check for Forge/Foundry artifact format - if deployedBytecode, ok := artifact["deployedBytecode"].(map[string]any); ok { - if object, ok := deployedBytecode["object"].(string); ok { + // Try deployedBytecode.object first + if deployedBytecodeMap, ok := artifact["deployedBytecode"].(map[string]any); ok { + if object, ok := deployedBytecodeMap["object"].(string); ok && object != "" && object != "0x" { return object, nil } } - - // Check for standard artifact formats - if deployedBytecode, ok := artifact["deployedBytecode"].(string); ok { - return deployedBytecode, nil + // Try deployedBytecode (string) second + if deployedBytecodeStr, ok := artifact["deployedBytecode"].(string); ok && deployedBytecodeStr != "" && deployedBytecodeStr != "0x" { + return deployedBytecodeStr, nil } - - // Check for bytecode field - if bytecode, ok := artifact["bytecode"].(map[string]any); ok { - if object, ok := bytecode["object"].(string); ok { + // Try bytecode.object third (fallback, sometimes used for libraries or older artifacts) + if bytecodeMap, ok := artifact["bytecode"].(map[string]any); ok { + if object, ok := bytecodeMap["object"].(string); ok && object != "" && object != "0x" { + color.Yellow(" Warning: Using bytecode.object as deployed bytecode (deployedBytecode field missing/empty).") return object, nil } - } else if bytecode, ok := artifact["bytecode"].(string); ok { - return bytecode, nil } + // Try bytecode (string) fourth (fallback) + if bytecodeStr, ok := artifact["bytecode"].(string); ok && bytecodeStr != "" && bytecodeStr != "0x" { + color.Yellow(" Warning: Using bytecode string as deployed bytecode (deployedBytecode field missing/empty).") + return bytecodeStr, nil + } + return "", fmt.Errorf("could not find non-empty deployedBytecode or bytecode in artifact") +} - return "", fmt.Errorf("could not find deployedBytecode in artifact") +// getCreationBytecode extracts the creation bytecode string (initcode) from a parsed artifact. +// It prioritizes `bytecode.object` > `bytecode` (string). +func getCreationBytecode(artifact map[string]any) (string, error) { + // Try bytecode.object first + if bytecodeMap, ok := artifact["bytecode"].(map[string]any); ok { + if object, ok := bytecodeMap["object"].(string); ok && object != "" && object != "0x" { + return object, nil + } + } + // Try bytecode (string) second + if bytecodeStr, ok := artifact["bytecode"].(string); ok && bytecodeStr != "" && bytecodeStr != "0x" { + return bytecodeStr, nil + } + return "", fmt.Errorf("could not find non-empty bytecode.object or bytecode string in artifact") } +// getVariableNameFromAST attempts to find the human-readable variable name corresponding +// to an immutable reference ID by searching the contract's AST (Abstract Syntax Tree) +// included in the artifact. Falls back to returning the original ID if not found. func getVariableNameFromAST(artifact map[string]any, varID string) string { - // Remove any prefix from the ID (sometimes IDs are prefixed with a path) + // Sometimes IDs have prefixes like "t_string_storage:", remove them. cleanID := varID if strings.Contains(varID, ":") { parts := strings.Split(varID, ":") cleanID = parts[len(parts)-1] } - // Try to convert to int + // Convert the numeric part of the ID to an integer for matching AST node IDs. idInt, err := strconv.Atoi(cleanID) if err != nil { + // If conversion fails, return the original ID as the name. + color.Yellow(" Warning: Could not parse integer ID from immutable reference '%s'. Using original ID.", varID) return varID } - // Try to find the AST node + // Search the AST recursively. if ast, ok := artifact["ast"].(map[string]any); ok { - // Recursively search for the node with matching ID name := findNodeName(ast, idInt) if name != "" { - return name + return name // Found the name. } } - // Fallback to using the ID if we can't find the name + // If AST is missing or name not found, return the original ID. + color.Yellow(" Warning: Could not find variable name for immutable ID %s (numeric: %d) in AST. Using original ID.", varID, idInt) return varID } +// findNodeName is a recursive helper function to search the AST (represented as nested maps/slices) +// for a node with a specific `id` and return its associated `name`. func findNodeName(node any, targetID int) string { switch n := node.(type) { case map[string]any: - // Check if this is the node we're looking for - if id, ok := n["id"].(float64); ok && int(id) == targetID { - if name, ok := n["name"].(string); ok { + // Check if the current node has the target ID. + if idFloat, ok := n["id"].(float64); ok && int(idFloat) == targetID { + // If ID matches, try to find the name in 'name' or 'attributes.name'. + if name, ok := n["name"].(string); ok && name != "" { return name } + if attributes, ok := n["attributes"].(map[string]any); ok { + if name, ok := attributes["name"].(string); ok && name != "" { + return name + } + } + // Optionally log the node type if ID matched but name wasn't found. + // if nodeType, ok := n["nodeType"].(string); ok { + // fmt.Printf("Debug: Found node %d, type %s, but no name\n", targetID, nodeType) + // } } - // Recursively search in all child nodes + // Recursively search child nodes (values in the map). for _, value := range n { - result := findNodeName(value, targetID) - if result != "" { - return result + switch v := value.(type) { + case map[string]any, []any: // Only recurse into nested maps or slices. + result := findNodeName(v, targetID) + if result != "" { + return result // Found in child node. + } } } case []any: - // Search in list items + // Recursively search items in the slice. for _, item := range n { - result := findNodeName(item, targetID) - if result != "" { - return result + switch i := item.(type) { + case map[string]any, []any: // Only recurse into nested maps or slices. + result := findNodeName(i, targetID) + if result != "" { + return result // Found in slice item. + } } } } - return "" + return "" // Not found in this branch. } +// getImmutableReferences parses the `immutableReferences` section of a contract artifact +// (preferring the one under `deployedBytecode`) and constructs a map where keys are +// variable names (resolved via AST) and values are slices of `ImmutableReference` structs +// indicating the location(s) of each immutable variable in the bytecode. func getImmutableReferences(artifact map[string]any) (map[string][]ImmutableReference, error) { references := make(map[string][]ImmutableReference) + var immutableRefsData any - var immutableRefs map[string]any - - // Handle Forge/Foundry artifact format + // Prefer immutable references from deployedBytecode section if available. if deployedBytecode, ok := artifact["deployedBytecode"].(map[string]any); ok { - if refs, ok := deployedBytecode["immutableReferences"].(map[string]any); ok { - immutableRefs = refs + if refs, ok := deployedBytecode["immutableReferences"]; ok { + immutableRefsData = refs + } + } + + // Fallback to top-level immutableReferences if not found under deployedBytecode. + if immutableRefsData == nil { + if refs, ok := artifact["immutableReferences"]; ok { + immutableRefsData = refs } else { - return references, nil // No immutable references found + // No immutable references found, return empty map. + return references, nil } - } else if refs, ok := artifact["immutableReferences"].(map[string]any); ok { - // Handle standard artifact format - immutableRefs = refs - } else { - return references, nil // No immutable references found } - // Process the references - for varID, refs := range immutableRefs { - // Get the variable name from AST + // Ensure the data is in the expected map[string]any format. + immutableRefsMap, ok := immutableRefsData.(map[string]any) + if !ok { + if immutableRefsData != nil { + // Warn about unexpected format but return empty map. + color.Yellow(" Warning: Unexpected type for immutableReferences data: %T. Skipping.", immutableRefsData) + } + return references, nil // Return empty map if format is wrong. + } + + // Iterate over each variable ID found in the immutable references map. + for varID, refs := range immutableRefsMap { + // Resolve the human-readable variable name from the AST. varName := getVariableNameFromAST(artifact, varID) - references[varName] = []ImmutableReference{} + references[varName] = []ImmutableReference{} // Initialize slice for this variable. + // Ensure the references for this variable are in the expected []any format. refsList, ok := refs.([]any) if !ok { - continue + color.Yellow(" Warning: Expected list for immutable references of variable '%s' (ID: %s), got %T. Skipping.", varName, varID, refs) + continue // Skip this variable if format is wrong. } - for _, ref := range refsList { + // Iterate over each reference location for the current variable. + for refIdx, ref := range refsList { var start, length int + validFormat := false - // Handle different formats of immutable references + // Try parsing as map[string]any {"start": N, "length": M} if refMap, ok := ref.(map[string]any); ok { - if startVal, ok := refMap["start"].(float64); ok { + startVal, startOk := refMap["start"].(float64) + lengthVal, lengthOk := refMap["length"].(float64) + if startOk && lengthOk { start = int(startVal) - } - if lengthVal, ok := refMap["length"].(float64); ok { length = int(lengthVal) + validFormat = true + } else { + color.Yellow(" Warning: Missing or invalid 'start'/'length' in map-style immutable reference %d for variable '%s'. Skipping.", refIdx, varName) + continue } - } else if refArray, ok := ref.([]any); ok && len(refArray) >= 2 { - // Some formats use [start, length] array - if startVal, ok := refArray[0].(float64); ok { - start = int(startVal) - } - if lengthVal, ok := refArray[1].(float64); ok { - length = int(lengthVal) + } + + // Try parsing as []any {N, M} if map parsing failed + if !validFormat { + if refSlice, ok := ref.([]any); ok && len(refSlice) == 2 { + startVal, startOk := refSlice[0].(float64) + lengthVal, lengthOk := refSlice[1].(float64) + if startOk && lengthOk { + start = int(startVal) + length = int(lengthVal) + validFormat = true + } else { + color.Yellow(" Warning: Invalid numeric types in slice-style immutable reference %d for variable '%s'. Skipping.", refIdx, varName) + continue + } } - } else { - color.Yellow("Warning: Unrecognized immutable reference format: %v", ref) + } + + // If neither format matched, issue warning and skip + if !validFormat { + color.Yellow(" Warning: Unrecognized immutable reference format at index %d for variable '%s': %T. Skipping.", refIdx, varName, ref) + continue // Skip this specific reference location. + } + + // Basic validation for length. + if length <= 0 { + color.Yellow(" Warning: Invalid length %d in immutable reference %d for variable '%s'. Skipping reference.", length, refIdx, varName) continue } + // Add the valid reference location to the list for this variable. references[varName] = append(references[varName], ImmutableReference{ Offset: start, Length: length, - Value: "", + Value: "", // Value will be populated later during comparison. }) } + + // If, after processing, a variable has no valid references, remove it from the map. + if len(references[varName]) == 0 { + delete(references, varName) + } } return references, nil } +// getOnchainBytecode connects to the specified RPC URL and fetches the bytecode +// deployed at the given contract address. Returns the bytecode as a hex string ("0x...") +// or "0x" if no code exists at the address. func getOnchainBytecode(address string, rpcURL string) (string, error) { if address == "" { return "", fmt.Errorf("contract address is required") } + addr := common.HexToAddress(address) + // Dial the RPC endpoint. client, err := ethclient.Dial(rpcURL) if err != nil { return "", fmt.Errorf("failed to connect to RPC at %s: %w", rpcURL, err) } + defer client.Close() - code, err := client.CodeAt(context.Background(), common.HexToAddress(address), nil) + // Fetch the code. + code, err := client.CodeAt(context.Background(), addr, nil) if err != nil { return "", fmt.Errorf("failed to get code at address %s: %w", address, err) } + // Handle case where contract exists but has no code (e.g., EOA or destroyed contract). if len(code) == 0 { - return "", fmt.Errorf("no code found at address %s", address) + color.Yellow(" Warning: No bytecode found at address %s on chain.", address) + return "0x", nil // Return "0x" to represent empty code. } + // Return hex-encoded bytecode string. return "0x" + hex.EncodeToString(code), nil } +// --- Bytecode Comparison Logic --- + +// isInImmutableReference checks if a given byte position falls within the range +// of any known immutable variable reference. If it does, it returns true, +// the variable name, and a pointer to the specific ImmutableReference struct. func isInImmutableReference( position int, - immutableRefs map[string][]ImmutableReference, + immutableRefs map[string][]ImmutableReference, // Assumed to be non-nil if called ) (bool, string, *ImmutableReference) { + // Iterate through each variable and its reference locations. for varName, refs := range immutableRefs { - for i := range refs { - ref := &refs[i] - if ref.Offset <= position && position < ref.Offset+ref.Length { + // Check references in reverse order - might slightly optimize if overlaps exist, + // though true overlaps shouldn't occur in valid compiler output. + for i := len(refs) - 1; i >= 0; i-- { + ref := &refs[i] // Get pointer to modify Value later. + // Check if the position is within the [Offset, Offset + Length) range. + if ref.Length > 0 && ref.Offset <= position && position < ref.Offset+ref.Length { return true, varName, ref } } } + // Position does not fall within any immutable reference range. return false, "", nil } +// findDifferences compares the expected bytecode (from artifact) with the actual bytecode +// (from chain), byte by byte. It identifies contiguous blocks of differing bytes +// and returns them as a slice of BytecodeDifference structs. +// It uses the `immutableRefs` map to classify differences that occur within immutable +// variable locations and populates the `Value` field of the corresponding `ImmutableReference` structs. +// Handles cases where bytecode lengths differ. func findDifferences( expectedBytecode string, actualBytecode string, - immutableRefs map[string][]ImmutableReference, + immutableRefs map[string][]ImmutableReference, // Map can be nil for blueprint checks where immutables are ignored. ) ([]BytecodeDifference, error) { - // Remove '0x' prefix if present + // Normalize hex strings by removing "0x" prefix. expected := strings.TrimPrefix(expectedBytecode, "0x") actual := strings.TrimPrefix(actualBytecode, "0x") - // Convert to bytes for comparison + // Handle trivial case: both empty. + if len(expected) == 0 && len(actual) == 0 { + return []BytecodeDifference{}, nil + } + + // Decode hex strings into byte slices. expectedBytes, err := hex.DecodeString(expected) if err != nil { return nil, fmt.Errorf("failed to decode expected bytecode: %w", err) @@ -323,149 +1015,160 @@ func findDifferences( actualBytes, err := hex.DecodeString(actual) if err != nil { - return nil, fmt.Errorf("failed to decode actual bytecode: %w", err) + // Allow comparison to proceed if actual bytecode is empty or "0x". + if actual == "" || actual == "0x" { + actualBytes = []byte{} // Treat as empty byte slice. + } else { + return nil, fmt.Errorf("failed to decode actual bytecode: %w", err) + } } - // Check length differences + // Warn if lengths differ, as comparison beyond the shorter length will show differences. if len(expectedBytes) != len(actualBytes) { - color.Yellow("Warning: Bytecode length mismatch. Expected: %d, Actual: %d", + color.Yellow(" Warning: Bytecode length mismatch. Expected: %d bytes, Actual: %d bytes.", len(expectedBytes), len(actualBytes)) } - // Use the shorter length for comparison - compareLength := min(len(expectedBytes), len(actualBytes)) + // Determine the maximum length to iterate over. + maxLength := len(expectedBytes) + if len(actualBytes) > maxLength { + maxLength = len(actualBytes) + } - // Initialize all immutable reference values - for _, refs := range immutableRefs { - for i := range refs { - refs[i].Value = "" + // Reset collected values in immutableRefs before comparison. + if immutableRefs != nil { + for varName := range immutableRefs { + refs := immutableRefs[varName] + if refs != nil { + for i := range refs { + // Ensure Value is reset for each comparison run. + refs[i].Value = "" + } + } } } differences := []BytecodeDifference{} - var currDiff *currentDiff = nil + var currDiff *currentDiff = nil // Tracks the current contiguous difference block. + + // Iterate through each byte position up to the maximum length. + for i := 0; i < maxLength; i++ { + inImmutable := false + varName := "" + var ref *ImmutableReference = nil + // Check if this position is within an immutable reference, if applicable. + if immutableRefs != nil { + inImmutable, varName, ref = isInImmutableReference(i, immutableRefs) + } - for i := 0; i < compareLength; i++ { - inImmutable, varName, ref := isInImmutableReference(i, immutableRefs) + // Get expected and actual bytes/hex strings, handling out-of-bounds access. + var expectedByte byte = 0 + var actualByte byte = 0 + var expectedHex string = ".." // Placeholder for out-of-bounds bytes. + var actualHex string = ".." // Placeholder for out-of-bounds bytes. + + if i < len(expectedBytes) { + expectedByte = expectedBytes[i] + expectedHex = fmt.Sprintf("%02x", expectedByte) + } + if i < len(actualBytes) { + actualByte = actualBytes[i] + actualHex = fmt.Sprintf("%02x", actualByte) + } - // If we're in an immutable reference, collect the value - if inImmutable && ref != nil { - // Add this byte to the immutable value - ref.Value += fmt.Sprintf("%02x", actualBytes[i]) + bytesDiffer := expectedByte != actualByte - // If bytes differ and we're in an immutable reference, that's expected - if expectedBytes[i] != actualBytes[i] { + // --- State machine logic for handling differences --- + + if inImmutable { + // If inside an immutable, append the actual byte to its value regardless of diff. + if i < len(actualBytes) { + ref.Value += actualHex // Populate the value from actual bytecode. + } + + if bytesDiffer { + // Start or extend an immutable difference block. if currDiff == nil { - currDiff = ¤tDiff{ - Start: i, - Expected: []string{}, - Actual: []string{}, - InImmutable: true, - ImmutableName: varName, - } + // Start a new immutable diff block. + currDiff = ¤tDiff{Start: i, Expected: []string{expectedHex}, Actual: []string{actualHex}, InImmutable: true, ImmutableName: varName} } else if !currDiff.InImmutable { - // We were tracking a non-immutable diff, finish it and start a new one - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) - currDiff = ¤tDiff{ - Start: i, - Expected: []string{}, - Actual: []string{}, - InImmutable: true, - ImmutableName: varName, - } + // End previous code diff block, start new immutable diff block. + differences = append(differences, BytecodeDifference{currDiff.Start, len(currDiff.Expected), strings.Join(currDiff.Expected, ""), strings.Join(currDiff.Actual, ""), false, ""}) + currDiff = ¤tDiff{Start: i, Expected: []string{expectedHex}, Actual: []string{actualHex}, InImmutable: true, ImmutableName: varName} + } else if currDiff.ImmutableName != varName { + // End previous immutable diff block (different variable), start new one. + differences = append(differences, BytecodeDifference{currDiff.Start, len(currDiff.Expected), strings.Join(currDiff.Expected, ""), strings.Join(currDiff.Actual, ""), true, currDiff.ImmutableName}) + currDiff = ¤tDiff{Start: i, Expected: []string{expectedHex}, Actual: []string{actualHex}, InImmutable: true, ImmutableName: varName} + } else { + // Extend the current immutable diff block. + currDiff.Expected = append(currDiff.Expected, expectedHex) + currDiff.Actual = append(currDiff.Actual, actualHex) } - - currDiff.Expected = append(currDiff.Expected, fmt.Sprintf("%02x", expectedBytes[i])) - currDiff.Actual = append(currDiff.Actual, fmt.Sprintf("%02x", actualBytes[i])) - } else if currDiff != nil && currDiff.InImmutable { - // End of a difference section within an immutable reference - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) - currDiff = nil + } else { // Bytes match within an immutable range. + // If we were tracking an immutable diff, end it now. + if currDiff != nil && currDiff.InImmutable { + differences = append(differences, BytecodeDifference{currDiff.Start, len(currDiff.Expected), strings.Join(currDiff.Expected, ""), strings.Join(currDiff.Actual, ""), true, currDiff.ImmutableName}) + currDiff = nil // Reset tracker. + } + // Otherwise, do nothing (matching bytes in immutable range). } - } else { - // Not in an immutable reference - any difference is an error - if expectedBytes[i] != actualBytes[i] { + } else { // Not in an immutable reference range. + if bytesDiffer { + // Start or extend a code difference block. if currDiff == nil { - currDiff = ¤tDiff{ - Start: i, - Expected: []string{}, - Actual: []string{}, - InImmutable: false, - ImmutableName: "", - } + // Start a new code diff block. + currDiff = ¤tDiff{Start: i, Expected: []string{expectedHex}, Actual: []string{actualHex}, InImmutable: false, ImmutableName: ""} } else if currDiff.InImmutable { - // We were tracking an immutable diff, finish it and start a new one - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) - currDiff = ¤tDiff{ - Start: i, - Expected: []string{}, - Actual: []string{}, - InImmutable: false, - ImmutableName: "", + // End previous immutable diff block, start new code diff block. + differences = append(differences, BytecodeDifference{currDiff.Start, len(currDiff.Expected), strings.Join(currDiff.Expected, ""), strings.Join(currDiff.Actual, ""), true, currDiff.ImmutableName}) + currDiff = ¤tDiff{Start: i, Expected: []string{expectedHex}, Actual: []string{actualHex}, InImmutable: false, ImmutableName: ""} + } else { + // Extend the current code diff block. + currDiff.Expected = append(currDiff.Expected, expectedHex) + currDiff.Actual = append(currDiff.Actual, actualHex) + } + } else { // Bytes match outside an immutable range. + // If we were tracking any diff (code or immutable), end it now. + if currDiff != nil { + diffType := currDiff.InImmutable + immName := currDiff.ImmutableName + if !diffType { + immName = "" // Ensure name is empty for code diffs } + differences = append(differences, BytecodeDifference{currDiff.Start, len(currDiff.Expected), strings.Join(currDiff.Expected, ""), strings.Join(currDiff.Actual, ""), diffType, immName}) + currDiff = nil // Reset tracker. } - - currDiff.Expected = append(currDiff.Expected, fmt.Sprintf("%02x", expectedBytes[i])) - currDiff.Actual = append(currDiff.Actual, fmt.Sprintf("%02x", actualBytes[i])) - } else if currDiff != nil && !currDiff.InImmutable { - // End of a difference section outside immutable reference - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) - currDiff = nil + // Otherwise, do nothing (matching bytes in code range). } } - } + } // End of byte loop - // Don't forget the last difference if we reached the end + // If the loop finishes while tracking a difference, record the final block. if currDiff != nil { - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) + diffType := currDiff.InImmutable + immName := currDiff.ImmutableName + if !diffType { + immName = "" + } + differences = append(differences, BytecodeDifference{currDiff.Start, len(currDiff.Expected), strings.Join(currDiff.Expected, ""), strings.Join(currDiff.Actual, ""), diffType, immName}) } return differences, nil } +// printDifferences formats and prints the results of the bytecode comparison. +// It provides a summary, details of any code differences (errors), warnings about +// inconsistent immutable values, and (if verbose or errors exist) a detailed breakdown +// of the values found for each immutable variable. func printDifferences( differences []BytecodeDifference, - immutableRefs map[string][]ImmutableReference, + immutableRefs map[string][]ImmutableReference, // Map can be nil for blueprint checks. + verbose bool, ) { - // Separate immutable and non-immutable differences var nonImmutableDiffs []BytecodeDifference var immutableDiffs []BytecodeDifference + // Separate differences into code/unknown and immutable categories. for _, diff := range differences { if diff.InImmutable { immutableDiffs = append(immutableDiffs, diff) @@ -474,68 +1177,167 @@ func printDifferences( } } - // Print summary - color.Cyan("\n=== Bytecode Comparison Summary ===") - fmt.Printf("Total differences: %d\n", len(differences)) - fmt.Printf(" - In immutable references: %d\n", len(immutableDiffs)) - fmt.Printf(" - In code: %d\n", len(nonImmutableDiffs)) + hasCodeErrors := len(nonImmutableDiffs) > 0 + // Determine if detailed output sections should be printed. + shouldPrintDetails := hasCodeErrors || verbose + + // --- Print Summary --- + // Print summary only if details are needed or if there were any differences at all. + if shouldPrintDetails { + color.Cyan("\n --- Comparison Summary ---") + fmt.Printf(" Total difference blocks found: %d\n", len(differences)) + + // Print summary breakdown only if printing details. + if shouldPrintDetails { + // Immutable differences count. + immCount := len(immutableDiffs) + if immutableRefs == nil { // Note if immutables weren't checked (e.g., blueprints) + fmt.Printf(" - In immutable reference ranges: N/A (not checked)\n") + } else if immCount > 0 { + fmt.Printf(" - In immutable reference ranges: %d\n", immCount) + } else { + fmt.Printf(" - In immutable reference ranges: 0\n") + } + + // Code/unknown differences count (colored based on presence of errors). + if hasCodeErrors { + color.Set(color.FgRed) + fmt.Printf(" - In code / unknown areas: %d <<<< CODE MISMATCH\n", len(nonImmutableDiffs)) + color.Unset() + } else { + fmt.Printf(" - In code / unknown areas: 0\n") + color.Unset() + } + } + } else if len(differences) == 0 { + // If no differences and not verbose, print a simple confirmation. + // (The main success message is printed by the calling verify function) + // color.Green(" No differences found.") // Optional: Can be redundant. + } + // --- End Summary --- - // Print non-immutable differences (these are errors) - if len(nonImmutableDiffs) > 0 { - color.Red("\n=== Unexpected Differences in Code ===") + // --- Print Code Errors (Always print if they exist) --- + if hasCodeErrors { + color.Red("\n === CODE ERRORS: Unexpected Differences Found ===") for _, diff := range nonImmutableDiffs { - color.Red("Position %d-%d:", diff.Start, diff.Start+diff.Length-1) - fmt.Printf(" Expected: 0x%s\n", diff.Expected) - fmt.Printf(" Actual: 0x%s\n", diff.Actual) + endPos := diff.Start + diff.Length - 1 + color.Red(" Byte %d-%d (%d bytes):", diff.Start, endPos, diff.Length) + // Truncate long diffs for readability + maxLen := 64 // Show max 32 bytes + expectedPrint := diff.Expected + actualPrint := diff.Actual + if len(expectedPrint) > maxLen { + expectedPrint = expectedPrint[:maxLen] + "..." + } + if len(actualPrint) > maxLen { + actualPrint = actualPrint[:maxLen] + "..." + } + fmt.Printf(" Expected: 0x%s\n", expectedPrint) + fmt.Printf(" Actual: 0x%s\n", actualPrint) } - color.Red("\n⚠️ The contract bytecode does not match the artifact!") - } else { - color.Green("\n✓ No unexpected differences in code.") } + // --- End Code Errors --- - // Print immutable references - color.Cyan("\n=== Immutable References ===") - if len(immutableRefs) == 0 { - fmt.Println("No immutable references found in the artifact.") - } else { + // --- Check Immutable Consistency (Always run if immutables were checked) --- + var inconsistentImmutables []string + if immutableRefs != nil { // Only perform check if immutables were processed. for varName, refs := range immutableRefs { - color.Yellow("\n%s:", varName) - - // Check if all values for this variable are the same - allSameValue := true - var firstValue string - var nonEmptyValueFound bool - + if len(refs) <= 1 { + continue // Need at least two reference points to check consistency. + } + firstValue := "" + nonEmptyValueFound := false + inconsistent := false + // Iterate through all reference points for this variable. for _, ref := range refs { + // Only compare non-empty values derived from the actual bytecode. if ref.Value != "" { if !nonEmptyValueFound { + // Found the first non-empty value for comparison. firstValue = ref.Value nonEmptyValueFound = true } else if ref.Value != firstValue { - allSameValue = false - break + // Found a subsequent reference with a different non-empty value. + inconsistent = true + break // Inconsistency found, no need to check further refs for this var. } } } + if inconsistent { + inconsistentImmutables = append(inconsistentImmutables, varName) + } + } + + // Print a warning section *if* any inconsistencies were found. + if len(inconsistentImmutables) > 0 { + color.Red("\n === IMMUTABLE WARNING: Inconsistent Values Found ===") + color.Yellow(" This means the same immutable variable seems to have different values") + color.Yellow(" at different locations in the deployed bytecode. This is highly unusual.") + for _, varName := range inconsistentImmutables { + color.Red(" - Variable '%s' has differing values across its reference points.", varName) + } + } + } + // --- End Immutable Consistency Check --- + + // --- Print Full Immutable Details (Print only if verbose or code errors exist) --- + // Print only if: details are requested AND immutables were checked AND there are references found. + if shouldPrintDetails && immutableRefs != nil && len(immutableRefs) > 0 { + color.Cyan("\n === Immutable Reference Values (from Actual Bytecode) ===") + var varNames []string + for name := range immutableRefs { + varNames = append(varNames, name) + } + // Consider sorting varNames for consistent output: sort.Strings(varNames) + + for _, varName := range varNames { + refs := immutableRefs[varName] + if len(refs) == 0 { + continue // Should not happen if getImmutableReferences filters, but safeguard. + } + color.Yellow("\n Variable: %s", varName) + + allPopulatedValuesSame := true // Assume consistency until proven otherwise. + var firstPopulatedValue string = "" + nonEmptyValueFound := false // Track if *any* value was populated for this var. + hasMissingValue := false // Track if *any* ref slot had no value populated. - // Print each reference position on separate lines + // Print details for each reference location of the current variable. for i, ref := range refs { - fmt.Printf(" [%d] Offset: %d, Length: %d\n", i, ref.Offset, ref.Length) + fmt.Printf(" [%d] Artifact Location: Offset %d, Length %d bytes\n", i, ref.Offset, ref.Length) if ref.Value != "" { - fmt.Printf(" Value: 0x%s\n", ref.Value) + // Value was populated from actual bytecode at this location. + fmt.Printf(" Actual Value Found: 0x%s\n", ref.Value) + if !nonEmptyValueFound { + firstPopulatedValue = ref.Value + nonEmptyValueFound = true + } else if ref.Value != firstPopulatedValue { + // Mark inconsistency (warning already printed previously). + allPopulatedValuesSame = false + } } else { - fmt.Printf(" Value: (not modified)\n") + // No value was populated (likely actual bytecode was too short or comparison issue). + fmt.Printf(" Actual Value Found: (Not present or comparison mismatch at this location)\n") + hasMissingValue = true // Mark that at least one location had no value. } } - // Print consistency status + // Print a summary line for this variable's consistency within the details section. if nonEmptyValueFound { - if allSameValue { - color.Green(" ✓ All values for this variable are consistent.") + if allPopulatedValuesSame { + color.Green(" ✓ Consistency: All populated values for '%s' are identical.", varName) } else { - color.Red(" ⚠️ WARNING: Different values found for the same immutable variable!") + // The main warning was printed earlier, just add context here. + color.Red(" ! Consistency: Found differing values for '%s'. (See warning above)", varName) } + } else if hasMissingValue { + // Only report missing if *none* were populated but some locations existed. + color.Yellow(" - Consistency: No values populated for '%s' (check bytecode length/offsets).", varName) + } else if len(refs) > 0 { + // Edge case: Refs exist, but none have values and none were marked missing. Unlikely. + color.Yellow(" - Consistency: No values populated or missing for '%s'.", varName) } } } + // --- End Full Immutable Details --- } diff --git a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main_test.go b/packages/contracts-bedrock/scripts/verify/verify-bytecode/main_test.go index bc07b31a836..14b7963457d 100644 --- a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main_test.go +++ b/packages/contracts-bedrock/scripts/verify/verify-bytecode/main_test.go @@ -38,7 +38,7 @@ func TestLoadArtifact(t *testing.T) { // Test case 3: Non-existent file _, err = loadArtifact(filepath.Join(tempDir, "nonexistent.json")) assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to read artifact file") + assert.Contains(t, err.Error(), "artifact file not found", "Should correctly report non-existent file") // Test case 4: Invalid JSON err = os.WriteFile(artifactPath, []byte("invalid json"), 0644) @@ -492,7 +492,7 @@ func TestFindDifferences(t *testing.T) { expectedBytecode: "0x1234", actualBytecode: "0x123456", immutableRefs: map[string][]ImmutableReference{}, - wantDiffs: 0, // No differences in the common part + wantDiffs: 1, wantImmutable: 0, wantErr: false, }, @@ -588,13 +588,13 @@ func TestPrintDifferences(t *testing.T) { } // This should not panic - printDifferences(differences, immutableRefs) + printDifferences(differences, immutableRefs, false) // Test with empty differences - printDifferences([]BytecodeDifference{}, immutableRefs) + printDifferences([]BytecodeDifference{}, immutableRefs, false) // Test with empty immutable references - printDifferences(differences, map[string][]ImmutableReference{}) + printDifferences(differences, map[string][]ImmutableReference{}, false) } // Test handling of bytecode with and without 0x prefix From bf47df55cf6c8c8aa13883d43142e13c1596ba2a Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Thu, 10 Apr 2025 00:18:46 -0400 Subject: [PATCH 02/14] testing --- .../contracts-bedrock/scripts/verify/verify-bytecode/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go b/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go index c85dcbbba97..cdc340be3ab 100644 --- a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go +++ b/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go @@ -862,7 +862,7 @@ func getImmutableReferences(artifact map[string]any) (map[string][]ImmutableRefe // Ensure the references for this variable are in the expected []any format. refsList, ok := refs.([]any) if !ok { - color.Yellow(" Warning: Expected list for immutable references of variable '%s' (ID: %s), got %T. Skipping.", varName, varID, refs) + color.Yellow(" Warning: Expected list for immutable references of variable '%s' (ID: %s), got %T. Skipping.", varName, varID, refs) continue // Skip this variable if format is wrong. } From 562da079025118f8b086d9a27211122167c2fe74 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Thu, 10 Apr 2025 00:19:03 -0400 Subject: [PATCH 03/14] testing --- .../contracts-bedrock/scripts/verify/verify-bytecode/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go b/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go index cdc340be3ab..c85dcbbba97 100644 --- a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go +++ b/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go @@ -862,7 +862,7 @@ func getImmutableReferences(artifact map[string]any) (map[string][]ImmutableRefe // Ensure the references for this variable are in the expected []any format. refsList, ok := refs.([]any) if !ok { - color.Yellow(" Warning: Expected list for immutable references of variable '%s' (ID: %s), got %T. Skipping.", varName, varID, refs) + color.Yellow(" Warning: Expected list for immutable references of variable '%s' (ID: %s), got %T. Skipping.", varName, varID, refs) continue // Skip this variable if format is wrong. } From 0f023a16ab7ec2eee9d8b2b32b66fdb6b5d0ca0a Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Thu, 10 Apr 2025 11:33:35 -0400 Subject: [PATCH 04/14] feat: switch to separated verification and presentation --- op-chain-ops/solc/types.go | 16 +- .../verify-bytecode/bindings/opcm200.go | 0 .../scripts/checks/verify-bytecode/main.go | 1196 +++++++++++++++ .../verify-bytecode/main_test.go | 0 .../scripts/verify/verify-bytecode/main.go | 1343 ----------------- 5 files changed, 1208 insertions(+), 1347 deletions(-) rename packages/contracts-bedrock/scripts/{verify => checks}/verify-bytecode/bindings/opcm200.go (100%) create mode 100644 packages/contracts-bedrock/scripts/checks/verify-bytecode/main.go rename packages/contracts-bedrock/scripts/{verify => checks}/verify-bytecode/main_test.go (100%) delete mode 100644 packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go diff --git a/op-chain-ops/solc/types.go b/op-chain-ops/solc/types.go index a7d24d208da..6c0518e6af0 100644 --- a/op-chain-ops/solc/types.go +++ b/op-chain-ops/solc/types.go @@ -123,10 +123,11 @@ type CompilerOutputEvm struct { // Object must be a string because its not guaranteed to be // a hex string type CompilerOutputBytecode struct { - Object string `json:"object"` - Opcodes string `json:"opcodes"` - SourceMap string `json:"sourceMap"` - LinkReferences LinkReferences `json:"linkReferences"` + Object string `json:"object"` + Opcodes string `json:"opcodes"` + SourceMap string `json:"sourceMap"` + LinkReferences LinkReferences `json:"linkReferences"` + ImmutableReferences ImmutableReferences `json:"immutableReferences"` } type LinkReferences map[string]LinkReference @@ -137,6 +138,13 @@ type LinkReferenceOffset struct { Start uint `json:"start"` } +type ImmutableReferences map[string][]ImmutableReference + +type ImmutableReference struct { + Start uint `json:"start"` + Length uint `json:"end"` +} + type CompilerOutputSources map[string]CompilerOutputSource type CompilerOutputSource struct { diff --git a/packages/contracts-bedrock/scripts/verify/verify-bytecode/bindings/opcm200.go b/packages/contracts-bedrock/scripts/checks/verify-bytecode/bindings/opcm200.go similarity index 100% rename from packages/contracts-bedrock/scripts/verify/verify-bytecode/bindings/opcm200.go rename to packages/contracts-bedrock/scripts/checks/verify-bytecode/bindings/opcm200.go diff --git a/packages/contracts-bedrock/scripts/checks/verify-bytecode/main.go b/packages/contracts-bedrock/scripts/checks/verify-bytecode/main.go new file mode 100644 index 00000000000..042fd0ea6e9 --- /dev/null +++ b/packages/contracts-bedrock/scripts/checks/verify-bytecode/main.go @@ -0,0 +1,1196 @@ +// Package main implements a CLI tool to verify deployed Ethereum contract bytecode +// against local build artifacts. It supports verifying single contracts, blueprints +// (ERC-5202), and the contracts managed by an OPContractsManager instance. +package main + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + "regexp" + "strconv" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/fatih/color" + "github.com/urfave/cli/v2" + + "github.com/ethereum-optimism/optimism/op-chain-ops/solc" + ccom "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/common" + "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/verify-bytecode/bindings" +) + +// --- Data Structures --- + +// VerificationType indicates the kind of verification performed. +type VerificationType string + +const ( + DeployedContract VerificationType = "deployed contract" + Blueprint VerificationType = "blueprint" + SplitBlueprintPart1 VerificationType = "split blueprint part 1" + SplitBlueprintPart2 VerificationType = "split blueprint part 2" + OPContractsManager VerificationType = "OPContractsManager" + Implementation VerificationType = "implementation" + UnknownImplementation VerificationType = "unknown implementation" + UnknownBlueprint VerificationType = "unknown blueprint" +) + +// BytecodeDifference represents a contiguous block of differing bytes found during comparison. +// (Retained from original, still useful) +type BytecodeDifference struct { + Start int // Byte offset where the difference begins. + Length int // Length of the differing block in bytes. + Expected string // Expected bytes (hex encoded). + Actual string // Actual bytes found onchain (hex encoded). + InImmutable bool // True if this difference falls within a known immutable reference range. + ImmutableName string // Name of the immutable variable if InImmutable is true. +} + +// ImmutableValueInfo holds details about a specific immutable variable's location and the value found there. +type ImmutableValueInfo struct { + Name string // Human-readable name (best effort via AST) + Offset int // Byte offset where the immutable value starts. + Length int // Length of the immutable value in bytes. + Value string // Actual value found at this location in the deployed bytecode (hex encoded). +} + +// VerificationResult encapsulates the outcome of a single verification check. +type VerificationResult struct { + Type VerificationType // What kind of check was this? + ContractName string // User-friendly name (e.g., "L1CrossDomainMessenger") + FieldName string // Field name from OPCM struct if applicable (e.g., "L1CrossDomainMessengerImpl") + Address string // Primary address checked (hex) + AddressPart2 string // Address of part 2 for split blueprints (hex) + ArtifactPath string // Filesystem path to the artifact used + ProcessError error // Error during setup/fetching (RPC, file read, JSON parse, etc.) + Differences []BytecodeDifference // List of bytecode differences found + ImmutableInfos []ImmutableValueInfo // List of immutable values found in actual code (for DeployedContract type) + TargetContract string // For blueprints, the contract they deploy +} + +// ArtifactConfig holds configuration related to finding contract artifacts. +type ArtifactConfig struct { + ArtifactsDir string + ImplementationOverrides map[string]string + BlueprintOverrides map[string]string + DefaultOPCMArtifactName string // e.g., "OPContractsManager" +} + +// ContractArtifact holds the relevant data extracted from a single artifact JSON file. +type ContractArtifact struct { + ContractName string // Best guess at the contract's name from path/artifact + DeployedBytecode string // Hex string ("0x...") + CreationBytecode string // Hex string ("0x...") + ImmutableRefs map[string][]immutableLocation // Internal map: varName -> locations + RawAST map[string]any // Store raw AST for potential name lookup +} + +// immutableLocation is an internal helper struct used during artifact parsing and comparison. +type immutableLocation struct { + Offset int // Byte offset where the immutable value starts. + Length int // Length of the immutable value in bytes. + Value string // Populated during comparison by findDifferences logic +} + +// currentDiff is a temporary helper struct used internally by findDifferences logic +// (Retained from original) +type currentDiff struct { + Start int // Starting byte offset of the current difference block. + Expected []string // Accumulated expected hex bytes in the current block. + Actual []string // Accumulated actual hex bytes in the current block. + InImmutable bool // True if the current block is within an immutable reference range. + ImmutableName string // Name of the immutable variable if InImmutable is true. +} + +// --- Constants and Regex --- + +const defaultArtifactsDir = "forge-artifacts" +const defaultOPCMContractName = "OPContractsManager" +const blueprintPreamble = "0xFE7100" +const maxInitCodeSize = 24573 // 24 KiB - 3 byte preamble + +var trailingDigitsRegex = regexp.MustCompile(`\d+$`) + +// --- Main Application Setup (CLI) --- + +func main() { + // Default override maps (could be loaded from config file in future) + // These map OPCM struct field names to artifact file paths relative to artifacts-dir + implementationArtifactOverrides := map[string]string{ + "OptimismPortalImpl": "OptimismPortal2.sol/OptimismPortal2.json", + // Add other overrides if needed + } + blueprintArtifactOverrides := map[string]string{ + "PermissionlessDisputeGame1": "FaultDisputeGame.sol/FaultDisputeGame.json", + "PermissionlessDisputeGame2": "FaultDisputeGame.sol/FaultDisputeGame.json", + "PermissionedDisputeGame1": "PermissionedDisputeGame.sol/PermissionedDisputeGame.json", + "PermissionedDisputeGame2": "PermissionedDisputeGame.sol/PermissionedDisputeGame.json", + // Add other overrides if needed + } + + app := &cli.App{ + Name: "verify-bytecode", + Usage: "Verify onchain contract bytecode against local build artifacts", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "rpc", + Usage: "RPC URL for the network", + Required: true, + EnvVars: []string{"ETH_RPC_URL"}, + }, + &cli.StringFlag{ + Name: "artifacts-dir", + Usage: "Base directory containing the forge compilation artifacts", + Value: defaultArtifactsDir, + EnvVars: []string{"ARTIFACTS_DIR"}, + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Print detailed immutable diff information even on success", + Value: false, + }, + }, + Commands: []*cli.Command{ + { + Name: "single", + Usage: "Verify a single deployed contract", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "address", + Usage: "Contract address to check", + Required: true, + }, + &cli.StringFlag{ + Name: "artifact", + Usage: "Path to the contract artifact JSON file (can be absolute or relative to artifacts-dir)", + Required: true, + }, + }, + Action: func(c *cli.Context) error { + return runVerifySingle(c, implementationArtifactOverrides, blueprintArtifactOverrides) + }, + }, + { + Name: "opcm", + Usage: "Verify OPContractsManager and its managed implementations and blueprints", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "opcm-address", + Usage: "OPContractsManager contract address", + Required: true, + }, + }, + Action: func(c *cli.Context) error { + return runVerifyOPCM(c, implementationArtifactOverrides, blueprintArtifactOverrides) + }, + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + // Print top-level errors (e.g., CLI parsing) that aren't handled by printResults + color.Set(color.FgRed) + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + color.Unset() + os.Exit(1) + } +} + +// --- CLI Action Handlers --- + +func runVerifySingle(c *cli.Context, implOverrides, bpOverrides map[string]string) error { + rpcURL := c.String("rpc") + artifactsDir := c.String("artifacts-dir") + addressHex := c.String("address") + artifactPathArg := c.String("artifact") + verbose := c.Bool("verbose") // Inherited from global flags or command flags + + // Resolve artifact path + artifactPath, err := resolvePath(artifactPathArg, artifactsDir) + if err != nil { + // Print error directly as this is a setup failure before core logic runs + color.Red("Error resolving artifact path: %v", err) + return cli.Exit("", 1) + } + + // Create Ethereum client + client, err := ethclient.Dial(rpcURL) + if err != nil { + color.Red("Error connecting to RPC %s: %v", rpcURL, err) + return cli.Exit("", 1) + } + defer client.Close() + + // Perform verification + addr := common.HexToAddress(addressHex) + result := verifyDeployedContractLogic(client, artifactPath, addr) + + // Print results + printResults([]*VerificationResult{result}, verbose) + + // Determine exit code + exitCode := 0 + if result.ProcessError != nil || hasCodeDifferences(result) { + exitCode = 1 + } + return cli.Exit("", exitCode) +} + +func runVerifyOPCM(c *cli.Context, implOverrides, bpOverrides map[string]string) error { + rpcURL := c.String("rpc") + artifactsDir := c.String("artifacts-dir") + opcmAddressHex := c.String("opcm-address") + verbose := c.Bool("verbose") // Inherited from global flags or command flags + + // Resolve base artifact directory path + baseArtifactsDir, err := resolvePath("", artifactsDir) // Resolve artifactsDir itself + if err != nil { + color.Red("Error resolving artifacts directory path: %v", err) + return cli.Exit("", 1) + } + + // Create ArtifactConfig + config := ArtifactConfig{ + ArtifactsDir: baseArtifactsDir, + ImplementationOverrides: implOverrides, + BlueprintOverrides: bpOverrides, + DefaultOPCMArtifactName: defaultOPCMContractName, + } + + // Create Ethereum client + client, err := ethclient.Dial(rpcURL) + if err != nil { + color.Red("Error connecting to RPC %s: %v", rpcURL, err) + return cli.Exit("", 1) + } + defer client.Close() + + // Perform verification for OPCM and all its managed contracts + opcmAddr := common.HexToAddress(opcmAddressHex) + results := runOPCMVerificationLogic(client, opcmAddr, config) + + // Print results + printResults(results, verbose) + + // Determine exit code + exitCode := 0 + for _, result := range results { + if result.ProcessError != nil || hasCodeDifferences(result) { + exitCode = 1 + break + } + } + return cli.Exit("", exitCode) +} + +// --- Core Verification Logic (No Printing) --- + +// verifyDeployedContractLogic performs verification for a standard deployed contract. +func verifyDeployedContractLogic(client *ethclient.Client, artifactPath string, address common.Address) *VerificationResult { + result := &VerificationResult{ + Type: DeployedContract, + Address: address.Hex(), + ArtifactPath: artifactPath, + } + + // Set contract name from artifact path + result.ContractName = strings.TrimSuffix(filepath.Base(artifactPath), ".json") + + // 1. Load artifact + artifact, err := ccom.ReadForgeArtifact(artifactPath) + if err != nil { + result.ProcessError = fmt.Errorf("loading artifact: %w", err) + return result + } + + // 2. Get onchain bytecode + actualBytecode, err := getOnchainBytecode(client, address) + if err != nil { + result.ProcessError = fmt.Errorf("getting onchain bytecode: %w", err) + return result + } + + // 3. Compare bytecode + differences, immutables, err := compareBytecode(artifact, artifact.DeployedBytecode.Object, actualBytecode) + if err != nil { + result.ProcessError = fmt.Errorf("comparing bytecode: %w", err) + return result + } + result.Differences = differences + result.ImmutableInfos = immutables + + return result +} + +// verifyBlueprintLogic performs verification for a single ERC-5202 blueprint. +func verifyBlueprintLogic(client *ethclient.Client, targetArtifactPath string, blueprintAddress common.Address, blueprintFieldName string) *VerificationResult { + result := &VerificationResult{ + Type: Blueprint, + FieldName: blueprintFieldName, + Address: blueprintAddress.Hex(), + ArtifactPath: targetArtifactPath, // Path to the artifact of the contract *created* by the blueprint + } + + // Set contract name from artifact path + result.TargetContract = strings.TrimSuffix(filepath.Base(targetArtifactPath), ".json") + result.ContractName = fmt.Sprintf("Blueprint for %s", result.TargetContract) + + // 1. Load target artifact for creation code + artifact, err := ccom.ReadForgeArtifact(targetArtifactPath) + if err != nil { + result.ProcessError = fmt.Errorf("loading target artifact %s: %w", targetArtifactPath, err) + return result + } + + if artifact.Bytecode.Object == "" || artifact.Bytecode.Object == "0x" { + result.ProcessError = fmt.Errorf("no creation bytecode found in target artifact %s", targetArtifactPath) + return result + } + + // 2. Construct expected blueprint bytecode + expectedBlueprintBytecode := blueprintPreamble + strings.TrimPrefix(artifact.Bytecode.Object, "0x") + + // 3. Get actual blueprint bytecode from chain + actualBytecode, err := getOnchainBytecode(client, blueprintAddress) + if err != nil { + result.ProcessError = fmt.Errorf("getting onchain bytecode for blueprint %s: %w", blueprintAddress.Hex(), err) + // Try to compare anyway, maybe actualBytecode is "0x" which compare can handle + } + + // 4. Compare bytecode (no immutables for blueprints) + differences, _, err := compareBytecode(artifact, expectedBlueprintBytecode, actualBytecode) + if err != nil { + // Join comparison error with potential fetch error + result.ProcessError = errors.Join(result.ProcessError, fmt.Errorf("comparing blueprint bytecode: %w", err)) + return result + } + result.Differences = differences + + return result +} + +// verifySplitBlueprintLogic verifies a blueprint split into two parts. +func verifySplitBlueprintLogic(client *ethclient.Client, targetArtifactPath string, address1, address2 common.Address, fieldName1, fieldName2 string) (*VerificationResult, *VerificationResult) { + result1 := &VerificationResult{ + Type: SplitBlueprintPart1, + FieldName: fieldName1, + Address: address1.Hex(), + AddressPart2: address2.Hex(), // Include part 2 address in part 1 result for context + ArtifactPath: targetArtifactPath, + } + result2 := &VerificationResult{ + Type: SplitBlueprintPart2, + FieldName: fieldName2, + Address: address2.Hex(), + ArtifactPath: targetArtifactPath, + } + + // Set contract names from artifact path + targetName := strings.TrimSuffix(filepath.Base(targetArtifactPath), ".json") + result1.TargetContract = targetName + result2.TargetContract = targetName + result1.ContractName = fmt.Sprintf("Split BP 1/2 for %s", targetName) + result2.ContractName = fmt.Sprintf("Split BP 2/2 for %s", targetName) + + // 1. Load target artifact + artifact, err := ccom.ReadForgeArtifact(targetArtifactPath) + if err != nil { + err = fmt.Errorf("loading target artifact %s: %w", targetArtifactPath, err) + result1.ProcessError = err + result2.ProcessError = err + return result1, result2 + } + + if artifact.Bytecode.Object == "" || artifact.Bytecode.Object == "0x" { + err = fmt.Errorf("no creation bytecode found in target artifact %s", targetArtifactPath) + result1.ProcessError = err + result2.ProcessError = err + return result1, result2 + } + + // 2. Split creation code + fullCreationCodeHex := strings.TrimPrefix(artifact.Bytecode.Object, "0x") + fullCreationCodeBytes, err := hex.DecodeString(fullCreationCodeHex) + if err != nil { + err = fmt.Errorf("failed to decode creation code hex from %s: %w", targetArtifactPath, err) + result1.ProcessError = err + result2.ProcessError = err + return result1, result2 + } + + part1Bytes := fullCreationCodeBytes + var part2Bytes []byte + if len(fullCreationCodeBytes) > maxInitCodeSize { + part1Bytes = fullCreationCodeBytes[:maxInitCodeSize] + part2Bytes = fullCreationCodeBytes[maxInitCodeSize:] + } else { + // This case should ideally be handled by the caller (runOPCMVerificationLogic) + // If it gets here, treat part 2 as empty. + part2Bytes = []byte{} + } + + // 3. Construct expected bytecodes + expectedBytecode1 := blueprintPreamble + hex.EncodeToString(part1Bytes) + expectedBytecode2 := blueprintPreamble + hex.EncodeToString(part2Bytes) + + // 4. Fetch actual bytecodes + actualBytecode1, err1 := getOnchainBytecode(client, address1) + actualBytecode2, err2 := getOnchainBytecode(client, address2) + + if err1 != nil { + result1.ProcessError = fmt.Errorf("getting onchain code for part 1 (%s): %w", address1.Hex(), err1) + } + if err2 != nil { + result2.ProcessError = fmt.Errorf("getting onchain code for part 2 (%s): %w", address2.Hex(), err2) + } + + // 5. Compare Part 1 + if result1.ProcessError == nil { // Only compare if fetch (potentially) succeeded + diffs1, _, cmpErr1 := compareBytecode(artifact, expectedBytecode1, actualBytecode1) + if cmpErr1 != nil { + result1.ProcessError = errors.Join(result1.ProcessError, fmt.Errorf("comparing part 1 bytecode: %w", cmpErr1)) + } + result1.Differences = diffs1 + } + + // 6. Compare Part 2 + if result2.ProcessError == nil { // Only compare if fetch (potentially) succeeded + diffs2, _, cmpErr2 := compareBytecode(artifact, expectedBytecode2, actualBytecode2) + if cmpErr2 != nil { + result2.ProcessError = errors.Join(result2.ProcessError, fmt.Errorf("comparing part 2 bytecode: %w", cmpErr2)) + } + result2.Differences = diffs2 + } + + return result1, result2 +} + +// runOPCMVerificationLogic orchestrates verification for OPCM and its managed contracts. +func runOPCMVerificationLogic(client *ethclient.Client, opcmAddress common.Address, config ArtifactConfig) []*VerificationResult { + results := []*VerificationResult{} + + // --- Verify OPContractsManager itself --- + opcmArtifactBase := config.DefaultOPCMArtifactName // e.g., "OPContractsManager" + opcmArtifactRelative := filepath.Join(fmt.Sprintf("%s.sol", opcmArtifactBase), fmt.Sprintf("%s.json", opcmArtifactBase)) + opcmArtifactPath := filepath.Join(config.ArtifactsDir, opcmArtifactRelative) + + opcmResult := verifyDeployedContractLogic(client, opcmArtifactPath, opcmAddress) + opcmResult.Type = OPContractsManager // Override type + if opcmResult.ContractName == "" { // Ensure name is set + opcmResult.ContractName = config.DefaultOPCMArtifactName + } + results = append(results, opcmResult) + + // Cannot proceed if OPCM verification itself had a processing error (e.g., RPC down) + // or if the OPCM address has no code (can't call it). + if opcmResult.ProcessError != nil { + opcmResult.ProcessError = errors.Join(opcmResult.ProcessError, errors.New("cannot query implementations/blueprints due to OPCM verification error")) + return results + } + + // --- Set up OPCM caller --- + // Assuming OPCM verification passed or had only bytecode diffs, we can try to bind + opcmCaller, err := bindings.NewOpcm200Caller(opcmAddress, client) + if err != nil { + // Add a synthetic result to indicate this failure + results = append(results, &VerificationResult{ + Type: OPContractsManager, + ContractName: config.DefaultOPCMArtifactName, + Address: opcmAddress.Hex(), + ProcessError: fmt.Errorf("failed to bind OPCM caller: %w", err), + }) + return results // Cannot proceed without caller + } + + // --- Verify Implementations --- + implementationsResult, err := opcmCaller.Implementations(nil) + if err != nil { + results = append(results, &VerificationResult{ + Type: OPContractsManager, // Attributing error to OPCM interaction + ContractName: config.DefaultOPCMArtifactName, + Address: opcmAddress.Hex(), + ProcessError: fmt.Errorf("failed to call implementations() on OPCM: %w", err), + }) + } else { + implValue := reflect.ValueOf(implementationsResult) + implType := implValue.Type() + + for i := 0; i < implValue.NumField(); i++ { + fieldName := implType.Field(i).Name + fieldValue := implValue.Field(i).Interface().(common.Address) + + if fieldValue == (common.Address{}) { + continue // Skip zero addresses silently + } + + implAddressStr := fieldValue.Hex() + var relativePath string + var ok bool + + // Determine artifact path using overrides or convention + if relativePath, ok = config.ImplementationOverrides[fieldName]; !ok { + if strings.HasSuffix(fieldName, "Impl") { + baseName := strings.TrimSuffix(fieldName, "Impl") + relativePath = filepath.Join(fmt.Sprintf("%s.sol", baseName), fmt.Sprintf("%s.json", baseName)) + } else { + // Cannot determine path + results = append(results, &VerificationResult{ + Type: UnknownImplementation, + FieldName: fieldName, + ContractName: fmt.Sprintf("Unknown (%s)", fieldName), + Address: implAddressStr, + ProcessError: fmt.Errorf("cannot infer artifact path for implementation field '%s' (no override and doesn't end in 'Impl')", fieldName), + }) + continue + } + } + + artifactPath := filepath.Join(config.ArtifactsDir, relativePath) + implResult := verifyDeployedContractLogic(client, artifactPath, fieldValue) + implResult.Type = Implementation // Override type + implResult.FieldName = fieldName // Store the field name + if implResult.ContractName == "" { + implResult.ContractName = strings.TrimSuffix(filepath.Base(artifactPath), ".json") + } + results = append(results, implResult) + } + } + + // --- Verify Blueprints --- + blueprintsResult, err := opcmCaller.Blueprints(nil) + if err != nil { + results = append(results, &VerificationResult{ + Type: OPContractsManager, // Attributing error to OPCM interaction + ContractName: config.DefaultOPCMArtifactName, + Address: opcmAddress.Hex(), + ProcessError: fmt.Errorf("failed to call blueprints() on OPCM: %w", err), + }) + } else { + blueprintValue := reflect.ValueOf(blueprintsResult) + blueprintType := blueprintValue.Type() + blueprintFields := make(map[string]common.Address) + processedPart2 := make(map[string]bool) // Track part 2 blueprints already handled + + // First pass: collect all blueprint addresses + for i := 0; i < blueprintValue.NumField(); i++ { + fieldName := blueprintType.Field(i).Name + fieldValue := blueprintValue.Field(i).Interface().(common.Address) + blueprintFields[fieldName] = fieldValue + } + + // Second pass: verify each blueprint, handling splits + for i := 0; i < blueprintValue.NumField(); i++ { + fieldName := blueprintType.Field(i).Name + fieldValue := blueprintValue.Field(i).Interface().(common.Address) + + if processedPart2[fieldName] || fieldValue == (common.Address{}) { + continue // Skip zero addresses and already processed part 2s + } + + blueprintAddressStr := fieldValue.Hex() + var relativePath string + var baseName string + var ok bool + + // Determine artifact path for the *target* contract + if relativePath, ok = config.BlueprintOverrides[fieldName]; !ok { + baseName = trailingDigitsRegex.ReplaceAllString(fieldName, "") + if baseName == "" { + results = append(results, &VerificationResult{ + Type: UnknownBlueprint, + FieldName: fieldName, + ContractName: fmt.Sprintf("Unknown (%s)", fieldName), + Address: blueprintAddressStr, + ProcessError: fmt.Errorf("cannot infer artifact path for blueprint field '%s' (no override and empty base name)", fieldName), + }) + continue + } + relativePath = filepath.Join(fmt.Sprintf("%s.sol", baseName), fmt.Sprintf("%s.json", baseName)) + } else { + // Infer baseName from override path if possible, fallback to field name + parts := strings.Split(filepath.ToSlash(relativePath), "/") + if len(parts) == 2 && strings.HasSuffix(parts[0], ".sol") && strings.HasSuffix(parts[1], ".json") { + baseName = strings.TrimSuffix(parts[1], ".json") + } else { + baseName = trailingDigitsRegex.ReplaceAllString(fieldName, "") // Fallback + } + } + + targetArtifactPath := filepath.Join(config.ArtifactsDir, relativePath) + + // Check for split blueprint + if strings.HasSuffix(fieldName, "1") { + part2FieldName := strings.TrimSuffix(fieldName, "1") + "2" + if part2Addr, exists := blueprintFields[part2FieldName]; exists && part2Addr != (common.Address{}) { + // Verify as split blueprint + res1, res2 := verifySplitBlueprintLogic(client, targetArtifactPath, fieldValue, part2Addr, fieldName, part2FieldName) + results = append(results, res1, res2) + processedPart2[part2FieldName] = true // Mark part 2 as handled + continue // Move to next field + } + // If part 1 exists but part 2 doesn't, fall through to verify as single blueprint (warning printed later) + } + + // Verify as a standard (single) blueprint + bpResult := verifyBlueprintLogic(client, targetArtifactPath, fieldValue, fieldName) + results = append(results, bpResult) + } + } + + return results +} + +// --- Blockchain Interaction Helpers --- + +// getOnchainBytecode fetches bytecode from the chain. Returns hex string or error. +func getOnchainBytecode(client *ethclient.Client, address common.Address) (string, error) { + if client == nil { + return "", errors.New("ethereum client is nil") + } + code, err := client.CodeAt(context.Background(), address, nil) + if err != nil { + return "", err + } + if len(code) == 0 { + return "0x", errors.New("no code found at address") + } + return "0x" + hex.EncodeToString(code), nil +} + +// --- Bytecode Comparison Logic --- + +// compareBytecode compares expected and actual bytecode, handling immutables. +// It uses the artifact to find immutable names via the AST. +func compareBytecode( + artifact *solc.ForgeArtifact, // Changed input: Full artifact + expectedBytecodeHex string, + actualBytecodeHex string, +) ([]BytecodeDifference, []ImmutableValueInfo, error) { + + // --- Input Validation and Decoding --- + expectedClean := strings.TrimPrefix(expectedBytecodeHex, "0x") + actualClean := strings.TrimPrefix(actualBytecodeHex, "0x") + + if len(expectedClean)%2 != 0 { + return nil, nil, fmt.Errorf("invalid expected bytecode hex length: %d", len(expectedClean)) + } + // Allow empty or odd length for actual if it came from chain (e.g., "0x")? + // For now, strict check on actual too. If empty actual is valid, adjust here. + if actualClean != "" && len(actualClean)%2 != 0 { + // return nil, nil, fmt.Errorf("invalid actual bytecode hex length: %d", len(actualClean)) + // Or treat as empty if needed: actualClean = "" + } + + expectedBytes, err := hex.DecodeString(expectedClean) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode expected bytecode: %w", err) + } + + actualBytes, err := hex.DecodeString(actualClean) + if err != nil { + // Allow comparison if actual is empty/invalid hex from chain (treat as empty) + if actualClean == "" { + actualBytes = []byte{} + } else { + return nil, nil, fmt.Errorf("failed to decode actual bytecode '%s': %w", actualBytecodeHex, err) + } + } + + // --- Precompute Immutable Locations --- + type immutableByteInfo struct { + Name string + // Length int // We don't strictly need Length here anymore + } + // Map byte offset -> info about the immutable variable at that offset + immutableBytes := make(map[int]immutableByteInfo) + + if artifact != nil && artifact.DeployedBytecode.ImmutableReferences != nil { + for refKey, locations := range artifact.DeployedBytecode.ImmutableReferences { + // Get the human-readable name using the AST + name := GetImmutableName(artifact, refKey) + if name == "" { + name = fmt.Sprintf("immutable(id:%s)", refKey) // Fallback name + } + + info := immutableByteInfo{Name: name} + for _, loc := range locations { + start := int(loc.Start) + length := int(loc.Length) + if length <= 0 { + continue // Skip invalid length + } + + // Mark each byte within this location + for i := 0; i < length; i++ { + offset := start + i + if existing, exists := immutableBytes[offset]; exists { + // Warning if overlapping, but allow overwriting (last one wins?) + if existing.Name != info.Name { + fmt.Fprintf(os.Stderr, "Warning: Overlapping immutable reference at offset %d. Prev: '%s', New: '%s'\n", + offset, existing.Name, info.Name) + } + } + immutableBytes[offset] = info + } + } + } + } + + // --- Compare Byte by Byte --- + differences := []BytecodeDifference{} + var currDiff *currentDiff = nil // Tracks the current block of differences + maxLength := max(len(expectedBytes), len(actualBytes)) + + for i := 0; i < maxLength; i++ { + // Determine immutable status for the current byte offset + inImmutableRange := false + immName := "" + if info, ok := immutableBytes[i]; ok { + inImmutableRange = true + immName = info.Name + } + + // Get bytes and hex representations, handling out-of-bounds access + var expectedByte, actualByte byte + var expectedHex, actualHex string = "..", ".." // Use ".." for out-of-bounds + + if i < len(expectedBytes) { + expectedByte = expectedBytes[i] + expectedHex = fmt.Sprintf("%02x", expectedByte) + } + if i < len(actualBytes) { + actualByte = actualBytes[i] + actualHex = fmt.Sprintf("%02x", actualByte) + } + + bytesDiffer := expectedByte != actualByte + + // --- State machine logic for tracking differences --- + if bytesDiffer { + if currDiff == nil { + // Start a new difference block + currDiff = ¤tDiff{ + Start: i, + Expected: []string{expectedHex}, + Actual: []string{actualHex}, + InImmutable: inImmutableRange, + ImmutableName: immName, + } + } else if currDiff.InImmutable != inImmutableRange || (inImmutableRange && currDiff.ImmutableName != immName) { + // End the previous block because immutable status or name changed, then start a new one + differences = append(differences, BytecodeDifference{ + Start: currDiff.Start, + Length: len(currDiff.Expected), + Expected: strings.Join(currDiff.Expected, ""), + Actual: strings.Join(currDiff.Actual, ""), + InImmutable: currDiff.InImmutable, + ImmutableName: currDiff.ImmutableName, + }) + // Start new diff block + currDiff = ¤tDiff{ + Start: i, + Expected: []string{expectedHex}, + Actual: []string{actualHex}, + InImmutable: inImmutableRange, + ImmutableName: immName, + } + } else { + // Extend the current difference block (same immutable status/name) + currDiff.Expected = append(currDiff.Expected, expectedHex) + currDiff.Actual = append(currDiff.Actual, actualHex) + } + } else { // Bytes match + if currDiff != nil { + // End the current difference block as the mismatch ended + differences = append(differences, BytecodeDifference{ + Start: currDiff.Start, + Length: len(currDiff.Expected), + Expected: strings.Join(currDiff.Expected, ""), + Actual: strings.Join(currDiff.Actual, ""), + InImmutable: currDiff.InImmutable, + ImmutableName: currDiff.ImmutableName, + }) + currDiff = nil // Reset tracker + } + // No action needed if bytes match and not in a diff block + } + } // End of byte loop + + // Record the final difference block if the loop ended while in a diff + if currDiff != nil { + differences = append(differences, BytecodeDifference{ + Start: currDiff.Start, + Length: len(currDiff.Expected), + Expected: strings.Join(currDiff.Expected, ""), + Actual: strings.Join(currDiff.Actual, ""), + InImmutable: currDiff.InImmutable, + ImmutableName: currDiff.ImmutableName, + }) + } + + // --- Collect Populated Immutable Values --- + immutableValues := []ImmutableValueInfo{} + if artifact != nil && artifact.DeployedBytecode.ImmutableReferences != nil { + // Iterate through the defined locations in the artifact + for refKey, locations := range artifact.DeployedBytecode.ImmutableReferences { + name := GetImmutableName(artifact, refKey) + if name == "" { + name = fmt.Sprintf("immutable(id:%s)", refKey) // Fallback + } + + for _, loc := range locations { + start := int(loc.Start) + length := int(loc.Length) + if length <= 0 { + continue // Skip invalid locations + } + + // Extract the actual value directly from actualBytes based on this location + var actualValueHex string + upperBound := start + length + if start >= 0 && upperBound <= len(actualBytes) { + // Safely extract the slice + actualValueBytes := actualBytes[start:upperBound] + actualValueHex = "0x" + hex.EncodeToString(actualValueBytes) + } else { + // Handle cases where the location is partly or fully out of bounds + actualValueHex = "0x" // Represent out-of-bounds or partial data as empty hex + if start < len(actualBytes) && start >= 0 { + // Partial overlap: extract what's available + partialBytes := actualBytes[start:] // Extract from start to end of actualBytes + actualValueHex = "0x" + hex.EncodeToString(partialBytes) + " (incomplete)" + } else { + // Location is completely outside actual bytecode + actualValueHex = "(out of bounds)" + } + // Add warning? + // fmt.Fprintf(os.Stderr, "Warning: Immutable '%s' location [%d:%d] out of bounds for actual bytecode length %d\n", name, start, upperBound, len(actualBytes)) + } + + immutableValues = append(immutableValues, ImmutableValueInfo{ + Name: name, + Offset: start, + Length: length, + Value: actualValueHex, + }) + } + } + } + + // Optionally add a warning/error if lengths didn't match, but still return results + if len(expectedBytes) != len(actualBytes) { + // Could potentially add a specific difference entry for the length mismatch + // fmt.Fprintf(os.Stderr, "Warning: Bytecode length mismatch. Expected: %d, Actual: %d\n", len(expectedBytes), len(actualBytes)) + } + + return differences, immutableValues, nil +} + +// --- Presentation Layer (Printing) --- + +// printResults formats and prints the outcomes of verification checks. +func printResults(results []*VerificationResult, verbose bool) { + overallSuccess := true // Track if any *code* mismatches occurred + + for i, result := range results { + if i > 0 { + fmt.Println() // Add spacing between results + } + + // --- Print Header --- + printResultHeader(result) + + // --- Handle and Print Process Errors --- + if result.ProcessError != nil { + color.Red(" ERROR during verification: %v", result.ProcessError) + overallSuccess = false + continue // Skip comparison details if processing failed severely + } + + // --- Analyze Differences --- + codeDiffs, immDiffs := categorizeDifferences(result) + + // --- Print Status and Details --- + if len(codeDiffs) > 0 { + overallSuccess = false + color.Red(" ✗ Verification FAILED: Found unexpected differences in code.") + printCodeDifferences(codeDiffs) + // Optionally print immutable info if verbose AND failed (for deployed contracts) + if verbose && (result.Type == DeployedContract || result.Type == Implementation || result.Type == OPContractsManager) && len(result.ImmutableInfos) > 0 { + printImmutableDetails(result.ImmutableInfos, immDiffs, true) // Pass true for includeConsistencyWarning + } + } else if len(immDiffs) > 0 && (result.Type == DeployedContract || result.Type == Implementation || result.Type == OPContractsManager) { + color.Green(" ✓ Verification successful (differences only in known immutable locations).") + if verbose { + printImmutableDetails(result.ImmutableInfos, immDiffs, true) + } + } else { + // Exact match (or blueprint match where immutables aren't checked) + color.Green(" ✓ Verification successful (exact match).") + // If verbose, still show immutable values even on exact match for deployed contracts + if verbose && (result.Type == DeployedContract || result.Type == Implementation || result.Type == OPContractsManager) && len(result.ImmutableInfos) > 0 { + printImmutableDetails(result.ImmutableInfos, nil, false) // Pass nil diffs, no consistency warning needed + } + } + } // End loop through results + + // --- Print Overall Summary Footer --- + fmt.Println("\n----------------------------------------") + if overallSuccess { + color.Green("Overall Result: Verification PASSED.") + fmt.Println("All checked items match expected bytecode (or differ only in known immutable locations).") + } else { + color.Red("Overall Result: Verification FAILED.") + fmt.Println("One or more items had unexpected code differences or processing errors.") + } + fmt.Println("----------------------------------------") +} + +// printResultHeader prints the title section for a single result. +func printResultHeader(result *VerificationResult) { + title := "" + switch result.Type { + case OPContractsManager: + title = fmt.Sprintf("Verifying %s: %s", result.Type, result.Address) + case Implementation: + title = fmt.Sprintf("Verifying %s (%s): %s", result.FieldName, result.ContractName, result.Address) + case DeployedContract: + title = fmt.Sprintf("Verifying %s: %s (%s)", result.Type, result.ContractName, result.Address) + case Blueprint: + title = fmt.Sprintf("Verifying %s (%s for %s): %s", result.Type, result.FieldName, result.TargetContract, result.Address) + case SplitBlueprintPart1: + title = fmt.Sprintf("Verifying %s (%s for %s): %s", result.Type, result.FieldName, result.TargetContract, result.Address) + case SplitBlueprintPart2: + title = fmt.Sprintf("Verifying %s (%s for %s): %s", result.Type, result.FieldName, result.TargetContract, result.Address) + default: + title = fmt.Sprintf("Verifying %s (%s): %s", result.Type, result.ContractName, result.Address) + } + + color.Cyan(title) + fmt.Printf(" Artifact: %s\n", result.ArtifactPath) +} + +// categorizeDifferences separates differences into code/unknown and immutable. +func categorizeDifferences(result *VerificationResult) (codeDiffs, immutableDiffs []BytecodeDifference) { + isDeployed := result.Type == DeployedContract || result.Type == Implementation || result.Type == OPContractsManager + for _, diff := range result.Differences { + if isDeployed && diff.InImmutable { + immutableDiffs = append(immutableDiffs, diff) + } else { + codeDiffs = append(codeDiffs, diff) + } + } + return codeDiffs, immutableDiffs +} + +// hasCodeDifferences checks if a result has any non-immutable differences. +func hasCodeDifferences(result *VerificationResult) bool { + codeDiffs, _ := categorizeDifferences(result) + return len(codeDiffs) > 0 +} + +// printCodeDifferences formats and prints code/unknown differences. +func printCodeDifferences(diffs []BytecodeDifference) { + color.Set(color.FgRed) + fmt.Println(" --- Code Differences Found ---") + color.Unset() + for _, diff := range diffs { + endPos := diff.Start + diff.Length - 1 + color.Red(" Byte %d-%d (%d bytes):", diff.Start, endPos, diff.Length) + // Add truncation logic + fmt.Printf(" Expected: 0x%s\n", maybeTruncate(diff.Expected, 64)) + fmt.Printf(" Actual: 0x%s\n", maybeTruncate(diff.Actual, 64)) + } +} + +// printImmutableDetails formats and prints immutable variable info. +func printImmutableDetails(infos []ImmutableValueInfo, diffs []BytecodeDifference, includeConsistencyWarning bool) { + if len(infos) == 0 { + // Don't print header if there's nothing to show + // Or print a "No immutable variables found" message? + // fmt.Println(" --- Immutable Reference Details ---") + // fmt.Println(" (No immutable variables defined or found in bytecode)") + return + } + + color.Cyan(" --- Immutable Reference Details (Values from Actual Bytecode) ---") + + // Group infos by name for consistency check + infosByName := make(map[string][]ImmutableValueInfo) + var names []string + for _, info := range infos { + if _, exists := infosByName[info.Name]; !exists { + names = append(names, info.Name) // Keep order of first appearance + } + infosByName[info.Name] = append(infosByName[info.Name], info) + } + // sort.Strings(names) // Optional: sort names alphabetically + + for _, name := range names { + locations := infosByName[name] + color.Yellow(" Variable: %s", name) + inconsistent := false + firstValue := "" + populatedCount := 0 + + for i, loc := range locations { + fmt.Printf(" [%d] Location: Offset %d, Length %d bytes\n", i, loc.Offset, loc.Length) + if loc.Value != "" && loc.Value != "0x" { // Check Value field from ImmutableValueInfo + fmt.Printf(" Actual Value: %s\n", loc.Value) + if populatedCount == 0 { + firstValue = loc.Value + } else if loc.Value != firstValue { + inconsistent = true + } + populatedCount++ + } else { + // This case should be less common now if infos only includes populated values + fmt.Printf(" Actual Value: (Not populated - check comparison logic or bytecode length)\n") + } + } + // Print consistency summary for this variable if needed + if includeConsistencyWarning && inconsistent { + color.Red(" ! Consistency WARNING: Found differing values for '%s' across its locations.", name) + } else if populatedCount > 1 { + // color.Green(" ✓ Consistency: All populated values for '%s' are identical.", name) + } else if populatedCount == 0 && len(locations) > 0 { + // color.Yellow(" - Consistency: No values populated for '%s'.", name) + } + } + + // Optionally print raw immutable differences if any occurred + if len(diffs) > 0 { + color.Set(color.FgYellow) + fmt.Println("\n --- Differences Within Immutable Ranges ---") + color.Unset() + for _, diff := range diffs { + endPos := diff.Start + diff.Length - 1 + color.Yellow(" Byte %d-%d (%d bytes) in '%s':", diff.Start, endPos, diff.Length, diff.ImmutableName) + fmt.Printf(" Expected: 0x%s\n", maybeTruncate(diff.Expected, 64)) + fmt.Printf(" Actual: 0x%s\n", maybeTruncate(diff.Actual, 64)) + } + } +} + +// maybeTruncate shortens a string if it exceeds maxLen. +func maybeTruncate(s string, maxLen int) string { + if len(s) > maxLen && maxLen > 3 { + return s[:maxLen-3] + "..." + } + return s +} + +// --- Utility Helpers --- + +// resolvePath resolves a potentially relative path against a base directory. +func resolvePath(path, baseDir string) (string, error) { + if filepath.IsAbs(path) { + return path, nil + } + cwd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get current working directory: %w", err) + } + absBaseDir := baseDir + if !filepath.IsAbs(absBaseDir) { + absBaseDir = filepath.Join(cwd, absBaseDir) + } + return filepath.Join(absBaseDir, path), nil +} + +// max returns the greater of two integers. +func max(a, b int) int { + if a > b { + return a + } + return b +} + +// --- AST Traversal Helper for Immutable Names --- + +// GetImmutableName finds the human-readable name of an immutable variable within a ForgeArtifact's AST, +// given the reference key from the artifact's ImmutableReferences map. +// The refKey is usually a string representation of the variable's AST node ID (e.g., "36"). +func GetImmutableName(artifact *solc.ForgeArtifact, refKey string) string { + if artifact == nil { + fmt.Fprintln(os.Stderr, "Warning: Cannot get immutable name, artifact is nil") + return "" + } + + // Extract the numeric ID from the key. + // Handles formats like "36" or "t_int256:36". + parts := strings.Split(refKey, ":") + idStr := parts[len(parts)-1] // Take the last part after splitting by ':' + + numericID, err := strconv.Atoi(idStr) + if err != nil { + // If the key itself wasn't purely numeric and splitting didn't help, try parsing the whole key. + numericID, err = strconv.Atoi(refKey) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: Could not parse numeric ID from immutable reference key '%s': %v\n", refKey, err) + return "" // Cannot parse numeric ID, cannot search AST + } + } + + // Search the AST nodes recursively + return findAstNodeNameByID(artifact.Ast.Nodes, numericID) +} + +// findAstNodeNameByID recursively searches a slice of AST nodes for a node with the target ID +// and returns its name. +func findAstNodeNameByID(nodes []solc.AstNode, targetID int) string { + for _, node := range nodes { + // Check if the current node matches the target ID + if node.Id == targetID { + // Ensure the node is a variable declaration, as IDs can be reused for other node types + if node.NodeType == "VariableDeclaration" && node.Name != "" { + return node.Name // Found the name + } + // If the ID matches but it's not a VariableDeclaration or has no name, + // we might still find the right node deeper, so we don't return early. + // However, typically the ID in immutable references points directly to the VariableDeclaration. + } + + // Recursively search within nested nodes + // Common places for nested declarations or structures: + // 1. Direct children (`node.Nodes`) - Covers ContractDefinition, StructDefinition, etc. + if len(node.Nodes) > 0 { + if name := findAstNodeNameByID(node.Nodes, targetID); name != "" { + return name + } + } + + // 2. Function bodies (`node.Body.Statements`) + if node.Body != nil && len(node.Body.Statements) > 0 { + // Note: Immutables are state variables, usually not declared inside function bodies, + // but searching here for completeness doesn't hurt. + if name := findAstNodeNameByID(node.Body.Statements, targetID); name != "" { + return name + } + } + + // 3. Blocks within control structures (If, For, While - less likely for immutables) + if node.TrueBody != nil && len(node.TrueBody.Statements) > 0 { + if name := findAstNodeNameByID(node.TrueBody.Statements, targetID); name != "" { + return name + } + } + if node.FalseBody != nil && len(node.FalseBody.Statements) > 0 { + if name := findAstNodeNameByID(node.FalseBody.Statements, targetID); name != "" { + return name + } + } + + // We don't need to search ParameterLists (`node.Parameters`, `node.ReturnParameters`) + // as immutables cannot be parameters. + } + + return "" // Not found in this slice or its children +} diff --git a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main_test.go b/packages/contracts-bedrock/scripts/checks/verify-bytecode/main_test.go similarity index 100% rename from packages/contracts-bedrock/scripts/verify/verify-bytecode/main_test.go rename to packages/contracts-bedrock/scripts/checks/verify-bytecode/main_test.go diff --git a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go b/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go deleted file mode 100644 index c85dcbbba97..00000000000 --- a/packages/contracts-bedrock/scripts/verify/verify-bytecode/main.go +++ /dev/null @@ -1,1343 +0,0 @@ -// Package main implements a CLI tool to verify deployed Ethereum contract bytecode -// against local build artifacts. It supports verifying single contracts, blueprints -// (ERC-5202), and the contracts managed by an OPContractsManager instance. -package main - -import ( - "context" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "reflect" - "regexp" - "strconv" - "strings" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/fatih/color" - "github.com/urfave/cli/v2" - - "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/verify/verify-bytecode/bindings" -) - -// ImmutableReference represents a single location within expected contract bytecode -// where an immutable variable is injected. The actual value is populated during comparison. -type ImmutableReference struct { - Offset int // Byte offset where the immutable value starts. - Length int // Length of the immutable value in bytes. - Value string // Actual value found at this location in the deployed bytecode (hex encoded). -} - -// BytecodeDifference represents a contiguous block of differing bytes found during comparison. -type BytecodeDifference struct { - Start int // Byte offset where the difference begins. - Length int // Length of the differing block in bytes. - Expected string // Expected bytes (hex encoded). - Actual string // Actual bytes found onchain (hex encoded). - InImmutable bool // True if this difference falls within a known immutable reference range. - ImmutableName string // Name of the immutable variable if InImmutable is true. -} - -// currentDiff is a temporary helper struct used internally by findDifferences -// to track an ongoing sequence of differing bytes during the comparison loop. -type currentDiff struct { - Start int // Starting byte offset of the current difference block. - Expected []string // Accumulated expected hex bytes in the current block. - Actual []string // Accumulated actual hex bytes in the current block. - InImmutable bool // True if the current block is within an immutable reference range. - ImmutableName string // Name of the immutable variable if InImmutable is true. -} - -// defaultArtifactsDir is the default directory name expected to contain forge build artifacts. -const defaultArtifactsDir = "forge-artifacts" - -// defaultOPCMArtifactFilename is the default path relative to the artifacts directory -// for the OPContractsManager contract artifact. -const defaultOPCMArtifactFilename = "OPContractsManager.sol/OPContractsManager.json" - -// blueprintPreamble is the ERC-5202 preamble (0xFE71) followed by version (00). -const blueprintPreamble = "0xFE7100" - -// maxInitCodeSize defines the maximum size in bytes for the init code (creation code) -// that can be stored in a single blueprint slot, according to the split blueprint standard. -// (24576 - 3 byte preamble). -const maxInitCodeSize = 24573 - -// implementationArtifactOverrides maps specific field names from the OPCM Implementations struct -// (as defined in the Go bindings) to their corresponding artifact file paths (relative to artifacts-dir). -// This is used ONLY when the default naming convention (FieldName ending in "Impl" -> "BaseName.sol/BaseName.json") -// does not apply. -var implementationArtifactOverrides = map[string]string{ - "OptimismPortalImpl": "OptimismPortal2.sol/OptimismPortal2.json", -} - -// blueprintArtifactOverrides maps specific field names from the OPCM Blueprints struct -// (as defined in the Go bindings) to their corresponding artifact file paths (relative to artifacts-dir). -// This is used ONLY when the default naming convention (FieldName -> "FieldName.sol/FieldName.json" -// after removing trailing digits) does not apply. -var blueprintArtifactOverrides = map[string]string{ - "Proxy": "Proxy.sol/Proxy.json", - "PermissionlessDisputeGame1": "FaultDisputeGame.sol/FaultDisputeGame.json", - "PermissionlessDisputeGame2": "FaultDisputeGame.sol/FaultDisputeGame.json", - "PermissionedDisputeGame1": "PermissionedDisputeGame.sol/PermissionedDisputeGame.json", - "PermissionedDisputeGame2": "PermissionedDisputeGame.sol/PermissionedDisputeGame.json", -} - -// trailingDigitsRegex is used to remove trailing digits (e.g., '1' or '2') from blueprint field names -// to infer the base contract name for finding artifacts (e.g., "PermissionedDisputeGame1" -> "PermissionedDisputeGame"). -var trailingDigitsRegex = regexp.MustCompile(`\d+$`) - -// main sets up the CLI application using urfave/cli/v2 and defines the available commands. -func main() { - app := &cli.App{ - Name: "verify-bytecode", - Usage: "Verify onchain contract bytecode against build artifacts", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "rpc", - Usage: "RPC URL for the network", - Required: true, - EnvVars: []string{"ETH_RPC_URL"}, - }, - &cli.StringFlag{ - Name: "artifacts-dir", - Usage: "Base directory containing the forge compilation artifacts", - Value: defaultArtifactsDir, - EnvVars: []string{"ARTIFACTS_DIR"}, - }, - }, - Commands: []*cli.Command{ - { - Name: "single", - Usage: "Verify a single contract (compares deployed bytecode)", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "address", - Usage: "Contract address to check", - Required: true, - }, - &cli.StringFlag{ - Name: "artifact", - Usage: "Path to the contract artifact JSON file (can be absolute or relative to artifacts-dir)", - Required: true, - }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Print detailed immutable diff information even on success", - Value: false, - }, - }, - Action: func(c *cli.Context) error { - rpcURL := c.String("rpc") - artifactsDir := c.String("artifacts-dir") - address := c.String("address") - artifactPath := c.String("artifact") - verbose := c.Bool("verbose") - - // Resolve artifact path relative to artifacts-dir if not absolute - if !filepath.IsAbs(artifactPath) { - cwd, _ := os.Getwd() - baseDir := artifactsDir - if !filepath.IsAbs(baseDir) { - baseDir = filepath.Join(cwd, baseDir) - } - artifactPath = filepath.Join(baseDir, artifactPath) - } - - color.Cyan("Comparing contract at %s with artifact %s", address, artifactPath) - err := verifyDeployedContract(address, artifactPath, rpcURL, verbose) - if err != nil { - // Error is already printed within verifyDeployedContract or its callees - return cli.Exit("", 1) // Indicate failure to the shell - } - // Success message is printed within verifyDeployedContract - return nil - }, - }, - { - Name: "opcm", - Usage: "Verify OPContractsManager and its managed implementations and blueprints", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "opcm-address", - Usage: "OPContractsManager contract address", - Required: true, - }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Print detailed immutable diff information even on success", - Value: false, - }, - }, - Action: func(c *cli.Context) error { - rpcURL := c.String("rpc") - artifactsDir := c.String("artifacts-dir") - opcmAddress := c.String("opcm-address") - verbose := c.Bool("verbose") - - // Resolve base directory for artifacts - cwd, _ := os.Getwd() - baseDir := artifactsDir - if !filepath.IsAbs(baseDir) { - baseDir = filepath.Join(cwd, baseDir) - } - opcmArtifactPath := filepath.Join(baseDir, defaultOPCMArtifactFilename) - - err := runOPCMVerification(opcmAddress, opcmArtifactPath, rpcURL, baseDir, verbose) - if err != nil { - // Error is already printed within runOPCMVerification or its callees - return cli.Exit("", 1) // Indicate failure to the shell - } - // Success message is printed within runOPCMVerification - return nil - }, - }, - }, - } - - err := app.Run(os.Args) - if err != nil { - // Errors should be printed by the Action handlers or cli library itself. - // Exit with non-zero status if Run returns an error. - os.Exit(1) - } -} - -// runOPCMVerification orchestrates the verification process for the OPContractsManager (OPCM) -// and all the implementation and blueprint contracts it references. -// It first verifies the OPCM contract itself, then queries the OPCM for implementation -// and blueprint addresses, resolves their artifact paths, and calls the appropriate -// verification function (verifyDeployedContract, verifyBlueprint, or verifySplitBlueprint) for each. -// It aggregates errors encountered during the process. -func runOPCMVerification(opcmAddressHex, opcmArtifactPath, rpcURL, artifactsBaseDir string, verbose bool) error { - var combinedErr error - opcmAddress := common.HexToAddress(opcmAddressHex) - - // --- Verify OPContractsManager itself --- - color.Yellow("--- Verifying OPContractsManager ---") - err := verifyDeployedContract(opcmAddressHex, opcmArtifactPath, rpcURL, verbose) - if err != nil { - err = fmt.Errorf("failed to verify OPContractsManager contract: %w", err) - color.Red("Error: %v", err) - combinedErr = errors.Join(combinedErr, err) - // Continue verification even if OPCM fails, but report overall failure later. - } - - // --- Set up Ethereum client and OPCM caller --- - client, err := ethclient.Dial(rpcURL) - if err != nil { - err = fmt.Errorf("failed to connect to RPC at %s: %w", rpcURL, err) - color.Red("Error: %v", err) - return errors.Join(combinedErr, err) // Cannot proceed without client - } - defer client.Close() - - opcmCaller, err := bindings.NewOpcm200Caller(opcmAddress, client) - if err != nil { - err = fmt.Errorf("failed to bind Opcm200 caller to address %s: %w", opcmAddressHex, err) - color.Red("Error: %v", err) - return errors.Join(combinedErr, err) // Cannot proceed without caller - } - - // --- Verify Implementations --- - color.Yellow("\n--- Verifying Implementations ---") - implementationsResult, err := opcmCaller.Implementations(nil) - if err != nil { - err = fmt.Errorf("failed to call implementations() on OPCM contract %s: %w", opcmAddressHex, err) - color.Red("Error: %v", err) - combinedErr = errors.Join(combinedErr, err) - } else { - if verbose { - color.Green("✓ Successfully retrieved implementation addresses.") - } - implValue := reflect.ValueOf(implementationsResult) - implType := implValue.Type() - - // Iterate through the fields of the Implementations struct - for i := 0; i < implValue.NumField(); i++ { - fieldName := implType.Field(i).Name - fieldValue := implValue.Field(i).Interface().(common.Address) - implAddressStr := fieldValue.Hex() - - // Skip zero addresses - if fieldValue == (common.Address{}) { - if verbose { - color.Yellow(" Skipping zero address for implementation: %s", fieldName) - } - continue - } - - // Determine the artifact path for this implementation - var relativePath string - var ok bool - if relativePath, ok = implementationArtifactOverrides[fieldName]; !ok { - // Apply default naming convention if no override exists - if strings.HasSuffix(fieldName, "Impl") { - baseName := strings.TrimSuffix(fieldName, "Impl") - relativePath = filepath.Join(fmt.Sprintf("%s.sol", baseName), fmt.Sprintf("%s.json", baseName)) - } else { - // Error if convention doesn't apply and no override exists - err = fmt.Errorf("cannot infer artifact path for implementation field '%s' (doesn't end in Impl) and no override exists", fieldName) - color.Red("Error: %v", err) - combinedErr = errors.Join(combinedErr, err) - continue // Skip verification for this implementation - } - } - - artifactPath := filepath.Join(artifactsBaseDir, relativePath) - // Verify the implementation contract - err := verifyDeployedContract(implAddressStr, artifactPath, rpcURL, verbose) - if err != nil { - // Combine errors for overall reporting - combinedErr = errors.Join(combinedErr, fmt.Errorf("implementation %s (%s): %w", fieldName, implAddressStr, err)) - } - } - } - - // --- Verify Blueprints --- - color.Yellow("\n--- Verifying Blueprints ---") - blueprintsResult, err := opcmCaller.Blueprints(nil) - if err != nil { - err = fmt.Errorf("failed to call blueprints() on OPCM contract %s: %w", opcmAddressHex, err) - color.Red("Error: %v", err) - combinedErr = errors.Join(combinedErr, err) - } else { - if verbose { - color.Green("✓ Successfully retrieved blueprint addresses.") - } - blueprintValue := reflect.ValueOf(blueprintsResult) - blueprintType := blueprintValue.Type() - // Store blueprint fields for easy lookup (needed for split blueprint check) - blueprintFields := make(map[string]common.Address) - processedPart2 := make(map[string]bool) // Track part 2 blueprints already handled - - for i := 0; i < blueprintValue.NumField(); i++ { - fieldName := blueprintType.Field(i).Name - fieldValue := blueprintValue.Field(i).Interface().(common.Address) - blueprintFields[fieldName] = fieldValue - } - - // Iterate through the fields of the Blueprints struct again for verification - for i := 0; i < blueprintValue.NumField(); i++ { - fieldName := blueprintType.Field(i).Name - fieldValue := blueprintValue.Field(i).Interface().(common.Address) - blueprintAddressStr := fieldValue.Hex() - - // Skip if this field was already processed as part 2 of a split blueprint - if processedPart2[fieldName] { - continue - } - - // Skip zero addresses - if fieldValue == (common.Address{}) { - if verbose { - color.Yellow(" Skipping zero address for blueprint: %s", fieldName) - } - continue - } - - // Determine the artifact path for this blueprint's target contract - var relativePath string - var baseName string // Base contract name inferred from field or override - var ok bool - if relativePath, ok = blueprintArtifactOverrides[fieldName]; !ok { - // Apply default naming convention if no override exists - baseName = trailingDigitsRegex.ReplaceAllString(fieldName, "") - if baseName == "" { - err = fmt.Errorf("cannot infer artifact path for blueprint field '%s' (empty after removing digits) and no override exists", fieldName) - color.Red("Error: %v", err) - combinedErr = errors.Join(combinedErr, err) - continue // Skip verification for this blueprint - } - relativePath = filepath.Join(fmt.Sprintf("%s.sol", baseName), fmt.Sprintf("%s.json", baseName)) - } else { - // If override exists, try to infer baseName from it, otherwise use field name - parts := strings.Split(filepath.ToSlash(relativePath), "/") - if len(parts) == 2 && strings.HasSuffix(parts[0], ".sol") && strings.HasSuffix(parts[1], ".json") { - baseName = strings.TrimSuffix(parts[1], ".json") - } else { - // Fallback if override path doesn't match expected pattern - baseName = trailingDigitsRegex.ReplaceAllString(fieldName, "") - } - } - - artifactPath := filepath.Join(artifactsBaseDir, relativePath) - - // Check if this is part 1 of a split blueprint - if strings.HasSuffix(fieldName, "1") { - part2FieldName := strings.TrimSuffix(fieldName, "1") + "2" - if part2Addr, exists := blueprintFields[part2FieldName]; exists && part2Addr != (common.Address{}) { - // If corresponding part 2 exists and is non-zero, verify as split blueprint - err := verifySplitBlueprint(fieldName, blueprintAddressStr, part2FieldName, part2Addr.Hex(), artifactPath, rpcURL, verbose) - if err != nil { - combinedErr = errors.Join(combinedErr, fmt.Errorf("split blueprint %s/%s: %w", fieldName, part2FieldName, err)) - } - processedPart2[part2FieldName] = true // Mark part 2 as handled - continue // Move to next field - } - // Warn if part 1 exists but part 2 doesn't (or is zero address) - color.Yellow("Warning: Found blueprint %s ending in '1' but no valid corresponding '%s' found. Verifying as single blueprint.", fieldName, part2FieldName) - } - - // Verify as a standard (single) blueprint - err := verifyBlueprint(fieldName, blueprintAddressStr, artifactPath, rpcURL, verbose) - if err != nil { - combinedErr = errors.Join(combinedErr, fmt.Errorf("blueprint %s (%s): %w", fieldName, blueprintAddressStr, err)) - } - } - } - - // Return combined error if any verification step failed - if combinedErr != nil { - return fmt.Errorf("one or more OPCM verification steps failed") - } - - return nil -} - -// verifyDeployedContract performs bytecode verification for a standard deployed contract. -// It loads the artifact, extracts the expected deployed bytecode and immutable references, -// fetches the actual bytecode from the chain, compares them, and prints the results. -// It handles differences within immutable variable locations specifically. -func verifyDeployedContract(address, artifactPath, rpcURL string, verbose bool) error { - fmt.Println() // Add spacing for readability - contractName := strings.TrimSuffix(filepath.Base(artifactPath), ".json") - color.Cyan("Verifying %s (%s)", contractName, common.HexToAddress(address).String()) - - // Load artifact JSON - artifact, err := loadArtifact(artifactPath) - if err != nil { - color.Red(" Error loading artifact: %v", err) - return err - } - - // Extract expected deployed bytecode from artifact - expectedBytecode, err := getDeployedBytecode(artifact) - if err != nil { - color.Red(" Error getting deployed bytecode from %s: %v", artifactPath, err) - return err - } - - // Extract immutable reference locations from artifact - immutableRefs, err := getImmutableReferences(artifact) - if err != nil { - // Log error but continue comparison, treating differences as code errors - color.Red(" Error getting immutable references from %s: %v", artifactPath, err) - immutableRefs = nil // Ensure it's nil so findDifferences doesn't use partial data - } - - // Fetch actual bytecode from the blockchain - actualBytecode, err := getOnchainBytecode(address, rpcURL) - if err != nil { - color.Red(" Error getting onchain bytecode for %s: %v", address, err) - return err - } - - // Compare expected and actual bytecode, considering immutables - differences, err := findDifferences(expectedBytecode, actualBytecode, immutableRefs) - if err != nil { - color.Red(" Error comparing bytecode for %s: %v", address, err) - return err - } - - // Print the comparison results (summary, errors, immutable details) - printDifferences(differences, immutableRefs, verbose) - - // Determine if verification failed due to non-immutable differences - hasCodeDifferences := false - for _, diff := range differences { - if !diff.InImmutable { - hasCodeDifferences = true - break - } - } - - if hasCodeDifferences { - color.Red(" ✗ Verification FAILED for %s: Found unexpected differences in code.", address) - return fmt.Errorf("bytecode mismatch for %s", address) - } else { - successMsg := fmt.Sprintf(" ✓ Verification successful") - if len(differences) > 0 { - successMsg += " (differences only in immutables)" - } else { - successMsg += " (exact match)" - } - color.Green(successMsg) - } - - return nil -} - -// verifyBlueprint performs bytecode verification for an ERC-5202 blueprint contract. -// It loads the target contract's artifact, extracts its creation code (initcode), -// prepends the ERC-5202 preamble, fetches the blueprint's bytecode from the chain, -// compares them, and prints the results. Immutable references are not considered for blueprints. -func verifyBlueprint(fieldName, address, targetArtifactPath, rpcURL string, verbose bool) error { - fmt.Println() // Add spacing for readability - targetContractName := strings.TrimSuffix(filepath.Base(targetArtifactPath), ".json") - color.Cyan("Verifying blueprint %s (for %s) at %s", fieldName, targetContractName, common.HexToAddress(address).String()) - - // Load the artifact of the contract the blueprint creates - artifact, err := loadArtifact(targetArtifactPath) - if err != nil { - color.Red(" Error loading target artifact: %v", err) - return err - } - - // Extract the creation code (initcode) from the artifact - creationCode, err := getCreationBytecode(artifact) - if err != nil { - color.Red(" Error getting creation code from %s: %v", targetArtifactPath, err) - return err - } - - // Construct the expected blueprint bytecode (Preamble + Creation Code) - expectedBlueprintBytecode := blueprintPreamble + strings.TrimPrefix(creationCode, "0x") - - // Fetch the actual bytecode stored at the blueprint address - actualBytecode, err := getOnchainBytecode(address, rpcURL) - if err != nil { - color.Red(" Error getting onchain bytecode for blueprint %s: %v", address, err) - // Attempt comparison even if fetch failed, might compare against "0x" - cmpErr := compareBlueprintCode(address, fieldName, expectedBlueprintBytecode, actualBytecode, verbose) - return errors.Join(err, cmpErr) // Return both fetch and compare errors if any - } - - // Compare expected blueprint bytecode with actual onchain bytecode - return compareBlueprintCode(address, fieldName, expectedBlueprintBytecode, actualBytecode, verbose) -} - -// verifySplitBlueprint verifies a blueprint that has been split into two parts due to size limits. -// It loads the target contract's artifact, extracts the full creation code, splits it into two parts -// based on maxInitCodeSize, prepends the preamble to each part, fetches the bytecode for both -// blueprint addresses (part 1 and part 2), and compares each part individually. -func verifySplitBlueprint(fieldName1, address1, fieldName2, address2, targetArtifactPath, rpcURL string, verbose bool) error { - fmt.Println() // Add spacing for readability - targetContractName := strings.TrimSuffix(filepath.Base(targetArtifactPath), ".json") - color.Cyan("Verifying split blueprint %s/%s (for %s)", - fieldName1, fieldName2, targetContractName) - color.Cyan(" Part 1 Address: %s", common.HexToAddress(address1).String()) - color.Cyan(" Part 2 Address: %s", common.HexToAddress(address2).String()) - - // Load the artifact of the contract the blueprint creates - artifact, err := loadArtifact(targetArtifactPath) - if err != nil { - color.Red(" Error loading target artifact: %v", err) - return err - } - - // Extract the full creation code (initcode) from the artifact - fullCreationCodeHex, err := getCreationBytecode(artifact) - if err != nil { - color.Red(" Error getting full creation code from %s: %v", targetArtifactPath, err) - return err - } - fullCreationCodeHex = strings.TrimPrefix(fullCreationCodeHex, "0x") - - // Decode the creation code from hex to bytes - fullCreationCodeBytes, err := hex.DecodeString(fullCreationCodeHex) - if err != nil { - return fmt.Errorf("failed to decode creation code from artifact %s: %w", targetArtifactPath, err) - } - - // Split the creation code into two parts - part1Bytes := fullCreationCodeBytes - var part2Bytes []byte - if len(fullCreationCodeBytes) > maxInitCodeSize { - part1Bytes = fullCreationCodeBytes[:maxInitCodeSize] - part2Bytes = fullCreationCodeBytes[maxInitCodeSize:] - } else { - // Warn if it was expected to be split but wasn't large enough - color.Yellow(" Warning: Expected split blueprint %s/%s, but total initcode size (%d bytes) <= max size (%d bytes)", - fieldName1, fieldName2, len(fullCreationCodeBytes), maxInitCodeSize) - part2Bytes = []byte{} // Part 2 should be empty in this case - } - - // Construct expected bytecode for each part (Preamble + Part Code) - expectedBytecode1 := blueprintPreamble + hex.EncodeToString(part1Bytes) - expectedBytecode2 := blueprintPreamble + hex.EncodeToString(part2Bytes) - - // Fetch actual bytecode for both blueprint addresses - actualBytecode1, err1 := getOnchainBytecode(address1, rpcURL) - actualBytecode2, err2 := getOnchainBytecode(address2, rpcURL) - - // Verify Part 1 - color.Cyan(" Verifying part 1 (%s)", fieldName1) - errPart1 := compareBlueprintCode(address1, fieldName1, expectedBytecode1, actualBytecode1, verbose) - if err1 != nil { - // Combine fetch error with comparison error if any - errPart1 = errors.Join(errPart1, fmt.Errorf("failed to get onchain bytecode for %s: %w", address1, err1)) - color.Red(" Error getting onchain bytecode for part 1: %v", err1) - } - - // Verify Part 2 - color.Cyan(" Verifying part 2 (%s)", fieldName2) - errPart2 := compareBlueprintCode(address2, fieldName2, expectedBytecode2, actualBytecode2, verbose) - if err2 != nil { - // Combine fetch error with comparison error if any - errPart2 = errors.Join(errPart2, fmt.Errorf("failed to get onchain bytecode for %s: %w", address2, err2)) - color.Red(" Error getting onchain bytecode for part 2: %v", err2) - } - - // Return combined errors from both parts - return errors.Join(errPart1, errPart2) -} - -// compareBlueprintCode performs the direct bytecode comparison for a single blueprint part. -// It compares the expected bytecode (preamble + creation code fragment) with the actual -// bytecode fetched from the chain for the given blueprint address. It prints success or failure messages. -// Immutable references are ignored in this comparison. -func compareBlueprintCode(address, fieldName, expectedBytecode, actualBytecode string, verbose bool) error { - expectedClean := strings.ToLower(strings.TrimPrefix(expectedBytecode, "0x")) - actualClean := strings.ToLower(strings.TrimPrefix(actualBytecode, "0x")) - - // Compare the expected and actual bytecode - if expectedClean == actualClean { - color.Green(" ✓ Verification successful (exact match)") - return nil - } else { - color.Red(" ✗ Verification FAILED: Bytecode mismatch for blueprint %s (%s)", fieldName, address) - // Use findDifferences to show where the mismatch occurs, ignoring immutables (nil map) - differences, diffErr := findDifferences(expectedBytecode, actualBytecode, nil) - if diffErr == nil && len(differences) > 0 { - // Print the first block of differences found - diff := differences[0] - endPos := diff.Start + diff.Length - 1 - color.Red(" Difference found at byte %d-%d (%d bytes):", diff.Start, endPos, diff.Length) - - // Limit printed length for readability - maxLen := 50 // Print up to 50 hex chars (25 bytes) - expectedPrint := diff.Expected - actualPrint := diff.Actual - if len(expectedPrint) > maxLen { - expectedPrint = expectedPrint[:maxLen] + "..." - } - if len(actualPrint) > maxLen { - actualPrint = actualPrint[:maxLen] + "..." - } - fmt.Printf(" Expected: 0x%s\n", expectedPrint) - fmt.Printf(" Actual: 0x%s\n", actualPrint) - - // If verbose, show full expected/actual (truncated) - if verbose { - maxLenTotal := 100 - expectedSuffix := "" - actualSuffix := "" - expectedTotalPrint := expectedBytecode - actualTotalPrint := actualBytecode - if len(expectedTotalPrint) > maxLenTotal { - expectedTotalPrint = expectedTotalPrint[:maxLenTotal] - expectedSuffix = "..." - } - if len(actualTotalPrint) > maxLenTotal { - actualTotalPrint = actualTotalPrint[:maxLenTotal] - actualSuffix = "..." - } - fmt.Printf(" Expected Full (start): %s%s\n", expectedTotalPrint, expectedSuffix) - fmt.Printf(" Actual Full (start): %s%s\n", actualTotalPrint, actualSuffix) - } - - } else { - // Fallback if findDifferences fails or finds no diffs (e.g., length mismatch only) - color.Red(" Comparison failed, unable to generate detailed diff: %v", diffErr) - fmt.Printf(" Expected Length: %d bytes\n", len(expectedClean)/2) - fmt.Printf(" Actual Length: %d bytes\n", len(actualClean)/2) - if verbose { - maxLenTotal := 100 - expectedSuffix := "" - actualSuffix := "" - expectedTotalPrint := expectedBytecode - actualTotalPrint := actualBytecode - if len(expectedTotalPrint) > maxLenTotal { - expectedTotalPrint = expectedTotalPrint[:maxLenTotal] - expectedSuffix = "..." - } - if len(actualTotalPrint) > maxLenTotal { - actualTotalPrint = actualTotalPrint[:maxLenTotal] - actualSuffix = "..." - } - fmt.Printf(" Expected (start): %s%s\n", expectedTotalPrint, expectedSuffix) - fmt.Printf(" Actual (start): %s%s\n", actualTotalPrint, actualSuffix) - } - } - return fmt.Errorf("blueprint bytecode mismatch for %s (%s)", fieldName, address) - } -} - -// --- Artifact and Bytecode Handling Helpers --- - -// loadArtifact reads and parses a JSON artifact file from the given path. -func loadArtifact(path string) (map[string]any, error) { - if path == "" { - return nil, fmt.Errorf("artifact path is required") - } - // Check if file exists first for a clearer error message - if _, err := os.Stat(path); os.IsNotExist(err) { - return nil, fmt.Errorf("artifact file not found: %s", path) - } else if err != nil { - return nil, fmt.Errorf("error checking artifact file %s: %w", path, err) - } - - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("failed to read artifact file %s: %w", path, err) - } - - var artifact map[string]any - if err := json.Unmarshal(data, &artifact); err != nil { - return nil, fmt.Errorf("failed to parse JSON from %s: %w", path, err) - } - - return artifact, nil -} - -// getDeployedBytecode extracts the deployed bytecode string (runtime code) from a parsed artifact. -// It prioritizes `deployedBytecode.object` > `deployedBytecode` (string) > `bytecode.object` > `bytecode` (string). -func getDeployedBytecode(artifact map[string]any) (string, error) { - // Try deployedBytecode.object first - if deployedBytecodeMap, ok := artifact["deployedBytecode"].(map[string]any); ok { - if object, ok := deployedBytecodeMap["object"].(string); ok && object != "" && object != "0x" { - return object, nil - } - } - // Try deployedBytecode (string) second - if deployedBytecodeStr, ok := artifact["deployedBytecode"].(string); ok && deployedBytecodeStr != "" && deployedBytecodeStr != "0x" { - return deployedBytecodeStr, nil - } - // Try bytecode.object third (fallback, sometimes used for libraries or older artifacts) - if bytecodeMap, ok := artifact["bytecode"].(map[string]any); ok { - if object, ok := bytecodeMap["object"].(string); ok && object != "" && object != "0x" { - color.Yellow(" Warning: Using bytecode.object as deployed bytecode (deployedBytecode field missing/empty).") - return object, nil - } - } - // Try bytecode (string) fourth (fallback) - if bytecodeStr, ok := artifact["bytecode"].(string); ok && bytecodeStr != "" && bytecodeStr != "0x" { - color.Yellow(" Warning: Using bytecode string as deployed bytecode (deployedBytecode field missing/empty).") - return bytecodeStr, nil - } - return "", fmt.Errorf("could not find non-empty deployedBytecode or bytecode in artifact") -} - -// getCreationBytecode extracts the creation bytecode string (initcode) from a parsed artifact. -// It prioritizes `bytecode.object` > `bytecode` (string). -func getCreationBytecode(artifact map[string]any) (string, error) { - // Try bytecode.object first - if bytecodeMap, ok := artifact["bytecode"].(map[string]any); ok { - if object, ok := bytecodeMap["object"].(string); ok && object != "" && object != "0x" { - return object, nil - } - } - // Try bytecode (string) second - if bytecodeStr, ok := artifact["bytecode"].(string); ok && bytecodeStr != "" && bytecodeStr != "0x" { - return bytecodeStr, nil - } - return "", fmt.Errorf("could not find non-empty bytecode.object or bytecode string in artifact") -} - -// getVariableNameFromAST attempts to find the human-readable variable name corresponding -// to an immutable reference ID by searching the contract's AST (Abstract Syntax Tree) -// included in the artifact. Falls back to returning the original ID if not found. -func getVariableNameFromAST(artifact map[string]any, varID string) string { - // Sometimes IDs have prefixes like "t_string_storage:", remove them. - cleanID := varID - if strings.Contains(varID, ":") { - parts := strings.Split(varID, ":") - cleanID = parts[len(parts)-1] - } - - // Convert the numeric part of the ID to an integer for matching AST node IDs. - idInt, err := strconv.Atoi(cleanID) - if err != nil { - // If conversion fails, return the original ID as the name. - color.Yellow(" Warning: Could not parse integer ID from immutable reference '%s'. Using original ID.", varID) - return varID - } - - // Search the AST recursively. - if ast, ok := artifact["ast"].(map[string]any); ok { - name := findNodeName(ast, idInt) - if name != "" { - return name // Found the name. - } - } - - // If AST is missing or name not found, return the original ID. - color.Yellow(" Warning: Could not find variable name for immutable ID %s (numeric: %d) in AST. Using original ID.", varID, idInt) - return varID -} - -// findNodeName is a recursive helper function to search the AST (represented as nested maps/slices) -// for a node with a specific `id` and return its associated `name`. -func findNodeName(node any, targetID int) string { - switch n := node.(type) { - case map[string]any: - // Check if the current node has the target ID. - if idFloat, ok := n["id"].(float64); ok && int(idFloat) == targetID { - // If ID matches, try to find the name in 'name' or 'attributes.name'. - if name, ok := n["name"].(string); ok && name != "" { - return name - } - if attributes, ok := n["attributes"].(map[string]any); ok { - if name, ok := attributes["name"].(string); ok && name != "" { - return name - } - } - // Optionally log the node type if ID matched but name wasn't found. - // if nodeType, ok := n["nodeType"].(string); ok { - // fmt.Printf("Debug: Found node %d, type %s, but no name\n", targetID, nodeType) - // } - } - - // Recursively search child nodes (values in the map). - for _, value := range n { - switch v := value.(type) { - case map[string]any, []any: // Only recurse into nested maps or slices. - result := findNodeName(v, targetID) - if result != "" { - return result // Found in child node. - } - } - } - case []any: - // Recursively search items in the slice. - for _, item := range n { - switch i := item.(type) { - case map[string]any, []any: // Only recurse into nested maps or slices. - result := findNodeName(i, targetID) - if result != "" { - return result // Found in slice item. - } - } - } - } - return "" // Not found in this branch. -} - -// getImmutableReferences parses the `immutableReferences` section of a contract artifact -// (preferring the one under `deployedBytecode`) and constructs a map where keys are -// variable names (resolved via AST) and values are slices of `ImmutableReference` structs -// indicating the location(s) of each immutable variable in the bytecode. -func getImmutableReferences(artifact map[string]any) (map[string][]ImmutableReference, error) { - references := make(map[string][]ImmutableReference) - var immutableRefsData any - - // Prefer immutable references from deployedBytecode section if available. - if deployedBytecode, ok := artifact["deployedBytecode"].(map[string]any); ok { - if refs, ok := deployedBytecode["immutableReferences"]; ok { - immutableRefsData = refs - } - } - - // Fallback to top-level immutableReferences if not found under deployedBytecode. - if immutableRefsData == nil { - if refs, ok := artifact["immutableReferences"]; ok { - immutableRefsData = refs - } else { - // No immutable references found, return empty map. - return references, nil - } - } - - // Ensure the data is in the expected map[string]any format. - immutableRefsMap, ok := immutableRefsData.(map[string]any) - if !ok { - if immutableRefsData != nil { - // Warn about unexpected format but return empty map. - color.Yellow(" Warning: Unexpected type for immutableReferences data: %T. Skipping.", immutableRefsData) - } - return references, nil // Return empty map if format is wrong. - } - - // Iterate over each variable ID found in the immutable references map. - for varID, refs := range immutableRefsMap { - // Resolve the human-readable variable name from the AST. - varName := getVariableNameFromAST(artifact, varID) - references[varName] = []ImmutableReference{} // Initialize slice for this variable. - - // Ensure the references for this variable are in the expected []any format. - refsList, ok := refs.([]any) - if !ok { - color.Yellow(" Warning: Expected list for immutable references of variable '%s' (ID: %s), got %T. Skipping.", varName, varID, refs) - continue // Skip this variable if format is wrong. - } - - // Iterate over each reference location for the current variable. - for refIdx, ref := range refsList { - var start, length int - validFormat := false - - // Try parsing as map[string]any {"start": N, "length": M} - if refMap, ok := ref.(map[string]any); ok { - startVal, startOk := refMap["start"].(float64) - lengthVal, lengthOk := refMap["length"].(float64) - if startOk && lengthOk { - start = int(startVal) - length = int(lengthVal) - validFormat = true - } else { - color.Yellow(" Warning: Missing or invalid 'start'/'length' in map-style immutable reference %d for variable '%s'. Skipping.", refIdx, varName) - continue - } - } - - // Try parsing as []any {N, M} if map parsing failed - if !validFormat { - if refSlice, ok := ref.([]any); ok && len(refSlice) == 2 { - startVal, startOk := refSlice[0].(float64) - lengthVal, lengthOk := refSlice[1].(float64) - if startOk && lengthOk { - start = int(startVal) - length = int(lengthVal) - validFormat = true - } else { - color.Yellow(" Warning: Invalid numeric types in slice-style immutable reference %d for variable '%s'. Skipping.", refIdx, varName) - continue - } - } - } - - // If neither format matched, issue warning and skip - if !validFormat { - color.Yellow(" Warning: Unrecognized immutable reference format at index %d for variable '%s': %T. Skipping.", refIdx, varName, ref) - continue // Skip this specific reference location. - } - - // Basic validation for length. - if length <= 0 { - color.Yellow(" Warning: Invalid length %d in immutable reference %d for variable '%s'. Skipping reference.", length, refIdx, varName) - continue - } - - // Add the valid reference location to the list for this variable. - references[varName] = append(references[varName], ImmutableReference{ - Offset: start, - Length: length, - Value: "", // Value will be populated later during comparison. - }) - } - - // If, after processing, a variable has no valid references, remove it from the map. - if len(references[varName]) == 0 { - delete(references, varName) - } - } - - return references, nil -} - -// getOnchainBytecode connects to the specified RPC URL and fetches the bytecode -// deployed at the given contract address. Returns the bytecode as a hex string ("0x...") -// or "0x" if no code exists at the address. -func getOnchainBytecode(address string, rpcURL string) (string, error) { - if address == "" { - return "", fmt.Errorf("contract address is required") - } - addr := common.HexToAddress(address) - - // Dial the RPC endpoint. - client, err := ethclient.Dial(rpcURL) - if err != nil { - return "", fmt.Errorf("failed to connect to RPC at %s: %w", rpcURL, err) - } - defer client.Close() - - // Fetch the code. - code, err := client.CodeAt(context.Background(), addr, nil) - if err != nil { - return "", fmt.Errorf("failed to get code at address %s: %w", address, err) - } - - // Handle case where contract exists but has no code (e.g., EOA or destroyed contract). - if len(code) == 0 { - color.Yellow(" Warning: No bytecode found at address %s on chain.", address) - return "0x", nil // Return "0x" to represent empty code. - } - - // Return hex-encoded bytecode string. - return "0x" + hex.EncodeToString(code), nil -} - -// --- Bytecode Comparison Logic --- - -// isInImmutableReference checks if a given byte position falls within the range -// of any known immutable variable reference. If it does, it returns true, -// the variable name, and a pointer to the specific ImmutableReference struct. -func isInImmutableReference( - position int, - immutableRefs map[string][]ImmutableReference, // Assumed to be non-nil if called -) (bool, string, *ImmutableReference) { - // Iterate through each variable and its reference locations. - for varName, refs := range immutableRefs { - // Check references in reverse order - might slightly optimize if overlaps exist, - // though true overlaps shouldn't occur in valid compiler output. - for i := len(refs) - 1; i >= 0; i-- { - ref := &refs[i] // Get pointer to modify Value later. - // Check if the position is within the [Offset, Offset + Length) range. - if ref.Length > 0 && ref.Offset <= position && position < ref.Offset+ref.Length { - return true, varName, ref - } - } - } - // Position does not fall within any immutable reference range. - return false, "", nil -} - -// findDifferences compares the expected bytecode (from artifact) with the actual bytecode -// (from chain), byte by byte. It identifies contiguous blocks of differing bytes -// and returns them as a slice of BytecodeDifference structs. -// It uses the `immutableRefs` map to classify differences that occur within immutable -// variable locations and populates the `Value` field of the corresponding `ImmutableReference` structs. -// Handles cases where bytecode lengths differ. -func findDifferences( - expectedBytecode string, - actualBytecode string, - immutableRefs map[string][]ImmutableReference, // Map can be nil for blueprint checks where immutables are ignored. -) ([]BytecodeDifference, error) { - // Normalize hex strings by removing "0x" prefix. - expected := strings.TrimPrefix(expectedBytecode, "0x") - actual := strings.TrimPrefix(actualBytecode, "0x") - - // Handle trivial case: both empty. - if len(expected) == 0 && len(actual) == 0 { - return []BytecodeDifference{}, nil - } - - // Decode hex strings into byte slices. - expectedBytes, err := hex.DecodeString(expected) - if err != nil { - return nil, fmt.Errorf("failed to decode expected bytecode: %w", err) - } - - actualBytes, err := hex.DecodeString(actual) - if err != nil { - // Allow comparison to proceed if actual bytecode is empty or "0x". - if actual == "" || actual == "0x" { - actualBytes = []byte{} // Treat as empty byte slice. - } else { - return nil, fmt.Errorf("failed to decode actual bytecode: %w", err) - } - } - - // Warn if lengths differ, as comparison beyond the shorter length will show differences. - if len(expectedBytes) != len(actualBytes) { - color.Yellow(" Warning: Bytecode length mismatch. Expected: %d bytes, Actual: %d bytes.", - len(expectedBytes), len(actualBytes)) - } - - // Determine the maximum length to iterate over. - maxLength := len(expectedBytes) - if len(actualBytes) > maxLength { - maxLength = len(actualBytes) - } - - // Reset collected values in immutableRefs before comparison. - if immutableRefs != nil { - for varName := range immutableRefs { - refs := immutableRefs[varName] - if refs != nil { - for i := range refs { - // Ensure Value is reset for each comparison run. - refs[i].Value = "" - } - } - } - } - - differences := []BytecodeDifference{} - var currDiff *currentDiff = nil // Tracks the current contiguous difference block. - - // Iterate through each byte position up to the maximum length. - for i := 0; i < maxLength; i++ { - inImmutable := false - varName := "" - var ref *ImmutableReference = nil - // Check if this position is within an immutable reference, if applicable. - if immutableRefs != nil { - inImmutable, varName, ref = isInImmutableReference(i, immutableRefs) - } - - // Get expected and actual bytes/hex strings, handling out-of-bounds access. - var expectedByte byte = 0 - var actualByte byte = 0 - var expectedHex string = ".." // Placeholder for out-of-bounds bytes. - var actualHex string = ".." // Placeholder for out-of-bounds bytes. - - if i < len(expectedBytes) { - expectedByte = expectedBytes[i] - expectedHex = fmt.Sprintf("%02x", expectedByte) - } - if i < len(actualBytes) { - actualByte = actualBytes[i] - actualHex = fmt.Sprintf("%02x", actualByte) - } - - bytesDiffer := expectedByte != actualByte - - // --- State machine logic for handling differences --- - - if inImmutable { - // If inside an immutable, append the actual byte to its value regardless of diff. - if i < len(actualBytes) { - ref.Value += actualHex // Populate the value from actual bytecode. - } - - if bytesDiffer { - // Start or extend an immutable difference block. - if currDiff == nil { - // Start a new immutable diff block. - currDiff = ¤tDiff{Start: i, Expected: []string{expectedHex}, Actual: []string{actualHex}, InImmutable: true, ImmutableName: varName} - } else if !currDiff.InImmutable { - // End previous code diff block, start new immutable diff block. - differences = append(differences, BytecodeDifference{currDiff.Start, len(currDiff.Expected), strings.Join(currDiff.Expected, ""), strings.Join(currDiff.Actual, ""), false, ""}) - currDiff = ¤tDiff{Start: i, Expected: []string{expectedHex}, Actual: []string{actualHex}, InImmutable: true, ImmutableName: varName} - } else if currDiff.ImmutableName != varName { - // End previous immutable diff block (different variable), start new one. - differences = append(differences, BytecodeDifference{currDiff.Start, len(currDiff.Expected), strings.Join(currDiff.Expected, ""), strings.Join(currDiff.Actual, ""), true, currDiff.ImmutableName}) - currDiff = ¤tDiff{Start: i, Expected: []string{expectedHex}, Actual: []string{actualHex}, InImmutable: true, ImmutableName: varName} - } else { - // Extend the current immutable diff block. - currDiff.Expected = append(currDiff.Expected, expectedHex) - currDiff.Actual = append(currDiff.Actual, actualHex) - } - } else { // Bytes match within an immutable range. - // If we were tracking an immutable diff, end it now. - if currDiff != nil && currDiff.InImmutable { - differences = append(differences, BytecodeDifference{currDiff.Start, len(currDiff.Expected), strings.Join(currDiff.Expected, ""), strings.Join(currDiff.Actual, ""), true, currDiff.ImmutableName}) - currDiff = nil // Reset tracker. - } - // Otherwise, do nothing (matching bytes in immutable range). - } - } else { // Not in an immutable reference range. - if bytesDiffer { - // Start or extend a code difference block. - if currDiff == nil { - // Start a new code diff block. - currDiff = ¤tDiff{Start: i, Expected: []string{expectedHex}, Actual: []string{actualHex}, InImmutable: false, ImmutableName: ""} - } else if currDiff.InImmutable { - // End previous immutable diff block, start new code diff block. - differences = append(differences, BytecodeDifference{currDiff.Start, len(currDiff.Expected), strings.Join(currDiff.Expected, ""), strings.Join(currDiff.Actual, ""), true, currDiff.ImmutableName}) - currDiff = ¤tDiff{Start: i, Expected: []string{expectedHex}, Actual: []string{actualHex}, InImmutable: false, ImmutableName: ""} - } else { - // Extend the current code diff block. - currDiff.Expected = append(currDiff.Expected, expectedHex) - currDiff.Actual = append(currDiff.Actual, actualHex) - } - } else { // Bytes match outside an immutable range. - // If we were tracking any diff (code or immutable), end it now. - if currDiff != nil { - diffType := currDiff.InImmutable - immName := currDiff.ImmutableName - if !diffType { - immName = "" // Ensure name is empty for code diffs - } - differences = append(differences, BytecodeDifference{currDiff.Start, len(currDiff.Expected), strings.Join(currDiff.Expected, ""), strings.Join(currDiff.Actual, ""), diffType, immName}) - currDiff = nil // Reset tracker. - } - // Otherwise, do nothing (matching bytes in code range). - } - } - } // End of byte loop - - // If the loop finishes while tracking a difference, record the final block. - if currDiff != nil { - diffType := currDiff.InImmutable - immName := currDiff.ImmutableName - if !diffType { - immName = "" - } - differences = append(differences, BytecodeDifference{currDiff.Start, len(currDiff.Expected), strings.Join(currDiff.Expected, ""), strings.Join(currDiff.Actual, ""), diffType, immName}) - } - - return differences, nil -} - -// printDifferences formats and prints the results of the bytecode comparison. -// It provides a summary, details of any code differences (errors), warnings about -// inconsistent immutable values, and (if verbose or errors exist) a detailed breakdown -// of the values found for each immutable variable. -func printDifferences( - differences []BytecodeDifference, - immutableRefs map[string][]ImmutableReference, // Map can be nil for blueprint checks. - verbose bool, -) { - var nonImmutableDiffs []BytecodeDifference - var immutableDiffs []BytecodeDifference - - // Separate differences into code/unknown and immutable categories. - for _, diff := range differences { - if diff.InImmutable { - immutableDiffs = append(immutableDiffs, diff) - } else { - nonImmutableDiffs = append(nonImmutableDiffs, diff) - } - } - - hasCodeErrors := len(nonImmutableDiffs) > 0 - // Determine if detailed output sections should be printed. - shouldPrintDetails := hasCodeErrors || verbose - - // --- Print Summary --- - // Print summary only if details are needed or if there were any differences at all. - if shouldPrintDetails { - color.Cyan("\n --- Comparison Summary ---") - fmt.Printf(" Total difference blocks found: %d\n", len(differences)) - - // Print summary breakdown only if printing details. - if shouldPrintDetails { - // Immutable differences count. - immCount := len(immutableDiffs) - if immutableRefs == nil { // Note if immutables weren't checked (e.g., blueprints) - fmt.Printf(" - In immutable reference ranges: N/A (not checked)\n") - } else if immCount > 0 { - fmt.Printf(" - In immutable reference ranges: %d\n", immCount) - } else { - fmt.Printf(" - In immutable reference ranges: 0\n") - } - - // Code/unknown differences count (colored based on presence of errors). - if hasCodeErrors { - color.Set(color.FgRed) - fmt.Printf(" - In code / unknown areas: %d <<<< CODE MISMATCH\n", len(nonImmutableDiffs)) - color.Unset() - } else { - fmt.Printf(" - In code / unknown areas: 0\n") - color.Unset() - } - } - } else if len(differences) == 0 { - // If no differences and not verbose, print a simple confirmation. - // (The main success message is printed by the calling verify function) - // color.Green(" No differences found.") // Optional: Can be redundant. - } - // --- End Summary --- - - // --- Print Code Errors (Always print if they exist) --- - if hasCodeErrors { - color.Red("\n === CODE ERRORS: Unexpected Differences Found ===") - for _, diff := range nonImmutableDiffs { - endPos := diff.Start + diff.Length - 1 - color.Red(" Byte %d-%d (%d bytes):", diff.Start, endPos, diff.Length) - // Truncate long diffs for readability - maxLen := 64 // Show max 32 bytes - expectedPrint := diff.Expected - actualPrint := diff.Actual - if len(expectedPrint) > maxLen { - expectedPrint = expectedPrint[:maxLen] + "..." - } - if len(actualPrint) > maxLen { - actualPrint = actualPrint[:maxLen] + "..." - } - fmt.Printf(" Expected: 0x%s\n", expectedPrint) - fmt.Printf(" Actual: 0x%s\n", actualPrint) - } - } - // --- End Code Errors --- - - // --- Check Immutable Consistency (Always run if immutables were checked) --- - var inconsistentImmutables []string - if immutableRefs != nil { // Only perform check if immutables were processed. - for varName, refs := range immutableRefs { - if len(refs) <= 1 { - continue // Need at least two reference points to check consistency. - } - firstValue := "" - nonEmptyValueFound := false - inconsistent := false - // Iterate through all reference points for this variable. - for _, ref := range refs { - // Only compare non-empty values derived from the actual bytecode. - if ref.Value != "" { - if !nonEmptyValueFound { - // Found the first non-empty value for comparison. - firstValue = ref.Value - nonEmptyValueFound = true - } else if ref.Value != firstValue { - // Found a subsequent reference with a different non-empty value. - inconsistent = true - break // Inconsistency found, no need to check further refs for this var. - } - } - } - if inconsistent { - inconsistentImmutables = append(inconsistentImmutables, varName) - } - } - - // Print a warning section *if* any inconsistencies were found. - if len(inconsistentImmutables) > 0 { - color.Red("\n === IMMUTABLE WARNING: Inconsistent Values Found ===") - color.Yellow(" This means the same immutable variable seems to have different values") - color.Yellow(" at different locations in the deployed bytecode. This is highly unusual.") - for _, varName := range inconsistentImmutables { - color.Red(" - Variable '%s' has differing values across its reference points.", varName) - } - } - } - // --- End Immutable Consistency Check --- - - // --- Print Full Immutable Details (Print only if verbose or code errors exist) --- - // Print only if: details are requested AND immutables were checked AND there are references found. - if shouldPrintDetails && immutableRefs != nil && len(immutableRefs) > 0 { - color.Cyan("\n === Immutable Reference Values (from Actual Bytecode) ===") - var varNames []string - for name := range immutableRefs { - varNames = append(varNames, name) - } - // Consider sorting varNames for consistent output: sort.Strings(varNames) - - for _, varName := range varNames { - refs := immutableRefs[varName] - if len(refs) == 0 { - continue // Should not happen if getImmutableReferences filters, but safeguard. - } - color.Yellow("\n Variable: %s", varName) - - allPopulatedValuesSame := true // Assume consistency until proven otherwise. - var firstPopulatedValue string = "" - nonEmptyValueFound := false // Track if *any* value was populated for this var. - hasMissingValue := false // Track if *any* ref slot had no value populated. - - // Print details for each reference location of the current variable. - for i, ref := range refs { - fmt.Printf(" [%d] Artifact Location: Offset %d, Length %d bytes\n", i, ref.Offset, ref.Length) - if ref.Value != "" { - // Value was populated from actual bytecode at this location. - fmt.Printf(" Actual Value Found: 0x%s\n", ref.Value) - if !nonEmptyValueFound { - firstPopulatedValue = ref.Value - nonEmptyValueFound = true - } else if ref.Value != firstPopulatedValue { - // Mark inconsistency (warning already printed previously). - allPopulatedValuesSame = false - } - } else { - // No value was populated (likely actual bytecode was too short or comparison issue). - fmt.Printf(" Actual Value Found: (Not present or comparison mismatch at this location)\n") - hasMissingValue = true // Mark that at least one location had no value. - } - } - - // Print a summary line for this variable's consistency within the details section. - if nonEmptyValueFound { - if allPopulatedValuesSame { - color.Green(" ✓ Consistency: All populated values for '%s' are identical.", varName) - } else { - // The main warning was printed earlier, just add context here. - color.Red(" ! Consistency: Found differing values for '%s'. (See warning above)", varName) - } - } else if hasMissingValue { - // Only report missing if *none* were populated but some locations existed. - color.Yellow(" - Consistency: No values populated for '%s' (check bytecode length/offsets).", varName) - } else if len(refs) > 0 { - // Edge case: Refs exist, but none have values and none were marked missing. Unlikely. - color.Yellow(" - Consistency: No values populated or missing for '%s'.", varName) - } - } - } - // --- End Full Immutable Details --- -} From ae15dc8c85b19b2197148f46120710784c48ffd9 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Mon, 14 Apr 2025 12:00:13 -0400 Subject: [PATCH 05/14] feat: opcm verification for v200 --- op-chain-ops/solc/types.go | 2 +- .../scripts/checks/verify-bytecode/main.go | 543 ++++--- .../checks/verify-bytecode/main_test.go | 1388 +++++++++++------ 3 files changed, 1192 insertions(+), 741 deletions(-) diff --git a/op-chain-ops/solc/types.go b/op-chain-ops/solc/types.go index 6c0518e6af0..301a8e1b9cd 100644 --- a/op-chain-ops/solc/types.go +++ b/op-chain-ops/solc/types.go @@ -142,7 +142,7 @@ type ImmutableReferences map[string][]ImmutableReference type ImmutableReference struct { Start uint `json:"start"` - Length uint `json:"end"` + Length uint `json:"length"` } type CompilerOutputSources map[string]CompilerOutputSource diff --git a/packages/contracts-bedrock/scripts/checks/verify-bytecode/main.go b/packages/contracts-bedrock/scripts/checks/verify-bytecode/main.go index 042fd0ea6e9..f8d199c3b18 100644 --- a/packages/contracts-bedrock/scripts/checks/verify-bytecode/main.go +++ b/packages/contracts-bedrock/scripts/checks/verify-bytecode/main.go @@ -25,11 +25,10 @@ import ( "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/verify-bytecode/bindings" ) -// --- Data Structures --- - // VerificationType indicates the kind of verification performed. type VerificationType string +// VerificationType constants. const ( DeployedContract VerificationType = "deployed contract" Blueprint VerificationType = "blueprint" @@ -42,7 +41,6 @@ const ( ) // BytecodeDifference represents a contiguous block of differing bytes found during comparison. -// (Retained from original, still useful) type BytecodeDifference struct { Start int // Byte offset where the difference begins. Length int // Length of the differing block in bytes. @@ -62,16 +60,16 @@ type ImmutableValueInfo struct { // VerificationResult encapsulates the outcome of a single verification check. type VerificationResult struct { - Type VerificationType // What kind of check was this? - ContractName string // User-friendly name (e.g., "L1CrossDomainMessenger") - FieldName string // Field name from OPCM struct if applicable (e.g., "L1CrossDomainMessengerImpl") - Address string // Primary address checked (hex) - AddressPart2 string // Address of part 2 for split blueprints (hex) - ArtifactPath string // Filesystem path to the artifact used - ProcessError error // Error during setup/fetching (RPC, file read, JSON parse, etc.) - Differences []BytecodeDifference // List of bytecode differences found - ImmutableInfos []ImmutableValueInfo // List of immutable values found in actual code (for DeployedContract type) - TargetContract string // For blueprints, the contract they deploy + Type VerificationType + ContractName string + FieldName string + Address string + AddressPart2 string + ArtifactPath string + ProcessError error + Differences []BytecodeDifference + ImmutableInfos []ImmutableValueInfo + TargetContract string } // ArtifactConfig holds configuration related to finding contract artifacts. @@ -79,48 +77,49 @@ type ArtifactConfig struct { ArtifactsDir string ImplementationOverrides map[string]string BlueprintOverrides map[string]string - DefaultOPCMArtifactName string // e.g., "OPContractsManager" + DefaultOPCMArtifactName string } // ContractArtifact holds the relevant data extracted from a single artifact JSON file. type ContractArtifact struct { - ContractName string // Best guess at the contract's name from path/artifact - DeployedBytecode string // Hex string ("0x...") - CreationBytecode string // Hex string ("0x...") - ImmutableRefs map[string][]immutableLocation // Internal map: varName -> locations - RawAST map[string]any // Store raw AST for potential name lookup + ContractName string + DeployedBytecode string + CreationBytecode string + ImmutableRefs map[string][]immutableLocation + RawAST map[string]any } // immutableLocation is an internal helper struct used during artifact parsing and comparison. type immutableLocation struct { - Offset int // Byte offset where the immutable value starts. - Length int // Length of the immutable value in bytes. - Value string // Populated during comparison by findDifferences logic + Offset int + Length int + Value string } // currentDiff is a temporary helper struct used internally by findDifferences logic -// (Retained from original) type currentDiff struct { - Start int // Starting byte offset of the current difference block. - Expected []string // Accumulated expected hex bytes in the current block. - Actual []string // Accumulated actual hex bytes in the current block. - InImmutable bool // True if the current block is within an immutable reference range. - ImmutableName string // Name of the immutable variable if InImmutable is true. + Start int + Expected []string + Actual []string + InImmutable bool + ImmutableName string } -// --- Constants and Regex --- - +// Defaults and constants. const defaultArtifactsDir = "forge-artifacts" const defaultOPCMContractName = "OPContractsManager" const blueprintPreamble = "0xFE7100" const maxInitCodeSize = 24573 // 24 KiB - 3 byte preamble +// trailingDigitsRegex matches the last sequence of digits in a string. var trailingDigitsRegex = regexp.MustCompile(`\d+$`) -// --- Main Application Setup (CLI) --- +// Function variable for dependency injection / mocking in tests +var getOnchainBytecodeImpl = getOnchainBytecode +// main is the entrypoint for the verify-bytecode CLI tool. func main() { - // Default override maps (could be loaded from config file in future) + // Default override maps // These map OPCM struct field names to artifact file paths relative to artifacts-dir implementationArtifactOverrides := map[string]string{ "OptimismPortalImpl": "OptimismPortal2.sol/OptimismPortal2.json", @@ -137,25 +136,6 @@ func main() { app := &cli.App{ Name: "verify-bytecode", Usage: "Verify onchain contract bytecode against local build artifacts", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "rpc", - Usage: "RPC URL for the network", - Required: true, - EnvVars: []string{"ETH_RPC_URL"}, - }, - &cli.StringFlag{ - Name: "artifacts-dir", - Usage: "Base directory containing the forge compilation artifacts", - Value: defaultArtifactsDir, - EnvVars: []string{"ARTIFACTS_DIR"}, - }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Print detailed immutable diff information even on success", - Value: false, - }, - }, Commands: []*cli.Command{ { Name: "single", @@ -171,6 +151,23 @@ func main() { Usage: "Path to the contract artifact JSON file (can be absolute or relative to artifacts-dir)", Required: true, }, + &cli.StringFlag{ + Name: "rpc-url", + Usage: "RPC URL for the network", + Required: true, + EnvVars: []string{"ETH_RPC_URL"}, + }, + &cli.StringFlag{ + Name: "artifacts-dir", + Usage: "Base directory containing the forge compilation artifacts", + Value: defaultArtifactsDir, + EnvVars: []string{"ARTIFACTS_DIR"}, + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Print detailed immutable diff information even on success", + Value: false, + }, }, Action: func(c *cli.Context) error { return runVerifySingle(c, implementationArtifactOverrides, blueprintArtifactOverrides) @@ -185,6 +182,23 @@ func main() { Usage: "OPContractsManager contract address", Required: true, }, + &cli.StringFlag{ + Name: "rpc-url", + Usage: "RPC URL for the network", + Required: true, + EnvVars: []string{"ETH_RPC_URL"}, + }, + &cli.StringFlag{ + Name: "artifacts-dir", + Usage: "Base directory containing the forge compilation artifacts", + Value: defaultArtifactsDir, + EnvVars: []string{"ARTIFACTS_DIR"}, + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Print detailed immutable diff information even on success", + Value: false, + }, }, Action: func(c *cli.Context) error { return runVerifyOPCM(c, implementationArtifactOverrides, blueprintArtifactOverrides) @@ -195,7 +209,6 @@ func main() { err := app.Run(os.Args) if err != nil { - // Print top-level errors (e.g., CLI parsing) that aren't handled by printResults color.Set(color.FgRed) fmt.Fprintf(os.Stderr, "Error: %v\n", err) color.Unset() @@ -203,14 +216,13 @@ func main() { } } -// --- CLI Action Handlers --- - +// runVerifySingle performs verification for a single deployed contract. func runVerifySingle(c *cli.Context, implOverrides, bpOverrides map[string]string) error { - rpcURL := c.String("rpc") + rpcURL := c.String("rpc-url") artifactsDir := c.String("artifacts-dir") addressHex := c.String("address") artifactPathArg := c.String("artifact") - verbose := c.Bool("verbose") // Inherited from global flags or command flags + verbose := c.Bool("verbose") // Resolve artifact path artifactPath, err := resolvePath(artifactPathArg, artifactsDir) @@ -228,9 +240,28 @@ func runVerifySingle(c *cli.Context, implOverrides, bpOverrides map[string]strin } defer client.Close() + // Load artifact + artifact, err := ccom.ReadForgeArtifact(artifactPath) + if err != nil { + // Handle artifact loading error before verification + contractName := strings.TrimSuffix(filepath.Base(artifactPath), ".json") // Best effort name from path + color.Red("Error loading artifact %s: %v", artifactPath, err) + printResults([]*VerificationResult{{ + Type: DeployedContract, + Address: addressHex, + ArtifactPath: artifactPath, + ContractName: contractName, + ProcessError: fmt.Errorf("loading artifact: %w", err), + }}, verbose) + return cli.Exit("", 1) // Exit with error code 1 if artifact fails to load + } + // Derive name from path after successful load + contractName := strings.TrimSuffix(filepath.Base(artifactPath), ".json") + // Perform verification addr := common.HexToAddress(addressHex) - result := verifyDeployedContractLogic(client, artifactPath, addr) + // Pass loaded artifact, derived name, and original path + result := verifyDeployedContractLogic(client, artifact, contractName, artifactPath, addr) // Print results printResults([]*VerificationResult{result}, verbose) @@ -244,10 +275,10 @@ func runVerifySingle(c *cli.Context, implOverrides, bpOverrides map[string]strin } func runVerifyOPCM(c *cli.Context, implOverrides, bpOverrides map[string]string) error { - rpcURL := c.String("rpc") + rpcURL := c.String("rpc-url") artifactsDir := c.String("artifacts-dir") opcmAddressHex := c.String("opcm-address") - verbose := c.Bool("verbose") // Inherited from global flags or command flags + verbose := c.Bool("verbose") // Resolve base artifact directory path baseArtifactsDir, err := resolvePath("", artifactsDir) // Resolve artifactsDir itself @@ -290,35 +321,25 @@ func runVerifyOPCM(c *cli.Context, implOverrides, bpOverrides map[string]string) return cli.Exit("", exitCode) } -// --- Core Verification Logic (No Printing) --- - // verifyDeployedContractLogic performs verification for a standard deployed contract. -func verifyDeployedContractLogic(client *ethclient.Client, artifactPath string, address common.Address) *VerificationResult { +// It now accepts a pre-loaded artifact and contract name. +func verifyDeployedContractLogic(client *ethclient.Client, artifact *solc.ForgeArtifact, contractName string, artifactPath string, address common.Address) *VerificationResult { result := &VerificationResult{ Type: DeployedContract, Address: address.Hex(), - ArtifactPath: artifactPath, - } - - // Set contract name from artifact path - result.ContractName = strings.TrimSuffix(filepath.Base(artifactPath), ".json") - - // 1. Load artifact - artifact, err := ccom.ReadForgeArtifact(artifactPath) - if err != nil { - result.ProcessError = fmt.Errorf("loading artifact: %w", err) - return result + ArtifactPath: artifactPath, // Still store the path for reporting + ContractName: contractName, // Use passed-in name } - // 2. Get onchain bytecode - actualBytecode, err := getOnchainBytecode(client, address) + // Get onchain bytecode + actualBytecode, err := getOnchainBytecodeImpl(client, address) if err != nil { result.ProcessError = fmt.Errorf("getting onchain bytecode: %w", err) return result } - // 3. Compare bytecode - differences, immutables, err := compareBytecode(artifact, artifact.DeployedBytecode.Object, actualBytecode) + // Compare bytecode + differences, immutables, err := compareBytecode(artifact, true, artifact.DeployedBytecode.Object, actualBytecode) if err != nil { result.ProcessError = fmt.Errorf("comparing bytecode: %w", err) return result @@ -330,44 +351,36 @@ func verifyDeployedContractLogic(client *ethclient.Client, artifactPath string, } // verifyBlueprintLogic performs verification for a single ERC-5202 blueprint. -func verifyBlueprintLogic(client *ethclient.Client, targetArtifactPath string, blueprintAddress common.Address, blueprintFieldName string) *VerificationResult { +// It now accepts a pre-loaded artifact and target contract name. +func verifyBlueprintLogic(client *ethclient.Client, artifact *solc.ForgeArtifact, targetContractName string, targetArtifactPath string, blueprintAddress common.Address, blueprintFieldName string) *VerificationResult { result := &VerificationResult{ - Type: Blueprint, - FieldName: blueprintFieldName, - Address: blueprintAddress.Hex(), - ArtifactPath: targetArtifactPath, // Path to the artifact of the contract *created* by the blueprint - } - - // Set contract name from artifact path - result.TargetContract = strings.TrimSuffix(filepath.Base(targetArtifactPath), ".json") - result.ContractName = fmt.Sprintf("Blueprint for %s", result.TargetContract) - - // 1. Load target artifact for creation code - artifact, err := ccom.ReadForgeArtifact(targetArtifactPath) - if err != nil { - result.ProcessError = fmt.Errorf("loading target artifact %s: %w", targetArtifactPath, err) - return result + Type: Blueprint, + FieldName: blueprintFieldName, + Address: blueprintAddress.Hex(), + ArtifactPath: targetArtifactPath, // Path to the artifact of the contract *created* by the blueprint + TargetContract: targetContractName, // Use passed-in name + ContractName: fmt.Sprintf("Blueprint for %s", targetContractName), // Use passed-in name } + // Error if no creation bytecode found if artifact.Bytecode.Object == "" || artifact.Bytecode.Object == "0x" { result.ProcessError = fmt.Errorf("no creation bytecode found in target artifact %s", targetArtifactPath) return result } - // 2. Construct expected blueprint bytecode + // Construct expected blueprint bytecode expectedBlueprintBytecode := blueprintPreamble + strings.TrimPrefix(artifact.Bytecode.Object, "0x") - // 3. Get actual blueprint bytecode from chain - actualBytecode, err := getOnchainBytecode(client, blueprintAddress) + // Get actual blueprint bytecode from chain + actualBytecode, err := getOnchainBytecodeImpl(client, blueprintAddress) if err != nil { result.ProcessError = fmt.Errorf("getting onchain bytecode for blueprint %s: %w", blueprintAddress.Hex(), err) - // Try to compare anyway, maybe actualBytecode is "0x" which compare can handle + return result } - // 4. Compare bytecode (no immutables for blueprints) - differences, _, err := compareBytecode(artifact, expectedBlueprintBytecode, actualBytecode) + // Compare bytecode (no immutables for blueprints) + differences, _, err := compareBytecode(artifact, false, expectedBlueprintBytecode, actualBytecode) if err != nil { - // Join comparison error with potential fetch error result.ProcessError = errors.Join(result.ProcessError, fmt.Errorf("comparing blueprint bytecode: %w", err)) return result } @@ -377,45 +390,35 @@ func verifyBlueprintLogic(client *ethclient.Client, targetArtifactPath string, b } // verifySplitBlueprintLogic verifies a blueprint split into two parts. -func verifySplitBlueprintLogic(client *ethclient.Client, targetArtifactPath string, address1, address2 common.Address, fieldName1, fieldName2 string) (*VerificationResult, *VerificationResult) { +// It now accepts a pre-loaded artifact and target contract name. +func verifySplitBlueprintLogic(client *ethclient.Client, artifact *solc.ForgeArtifact, targetContractName string, targetArtifactPath string, address1, address2 common.Address, fieldName1, fieldName2 string) (*VerificationResult, *VerificationResult) { result1 := &VerificationResult{ - Type: SplitBlueprintPart1, - FieldName: fieldName1, - Address: address1.Hex(), - AddressPart2: address2.Hex(), // Include part 2 address in part 1 result for context - ArtifactPath: targetArtifactPath, + Type: SplitBlueprintPart1, + FieldName: fieldName1, + Address: address1.Hex(), + AddressPart2: address2.Hex(), + ArtifactPath: targetArtifactPath, + TargetContract: targetContractName, // Use passed-in name + ContractName: fmt.Sprintf("Split BP 1/2 for %s", targetContractName), // Use passed-in name } result2 := &VerificationResult{ - Type: SplitBlueprintPart2, - FieldName: fieldName2, - Address: address2.Hex(), - ArtifactPath: targetArtifactPath, - } - - // Set contract names from artifact path - targetName := strings.TrimSuffix(filepath.Base(targetArtifactPath), ".json") - result1.TargetContract = targetName - result2.TargetContract = targetName - result1.ContractName = fmt.Sprintf("Split BP 1/2 for %s", targetName) - result2.ContractName = fmt.Sprintf("Split BP 2/2 for %s", targetName) - - // 1. Load target artifact - artifact, err := ccom.ReadForgeArtifact(targetArtifactPath) - if err != nil { - err = fmt.Errorf("loading target artifact %s: %w", targetArtifactPath, err) - result1.ProcessError = err - result2.ProcessError = err - return result1, result2 + Type: SplitBlueprintPart2, + FieldName: fieldName2, + Address: address2.Hex(), + ArtifactPath: targetArtifactPath, + TargetContract: targetContractName, // Use passed-in name + ContractName: fmt.Sprintf("Split BP 2/2 for %s", targetContractName), // Use passed-in name } + // Error if no creation bytecode found if artifact.Bytecode.Object == "" || artifact.Bytecode.Object == "0x" { - err = fmt.Errorf("no creation bytecode found in target artifact %s", targetArtifactPath) + err := fmt.Errorf("no creation bytecode found in target artifact %s", targetArtifactPath) result1.ProcessError = err result2.ProcessError = err return result1, result2 } - // 2. Split creation code + // Split creation code fullCreationCodeHex := strings.TrimPrefix(artifact.Bytecode.Object, "0x") fullCreationCodeBytes, err := hex.DecodeString(fullCreationCodeHex) if err != nil { @@ -425,6 +428,7 @@ func verifySplitBlueprintLogic(client *ethclient.Client, targetArtifactPath stri return result1, result2 } + // Split up creation code part1Bytes := fullCreationCodeBytes var part2Bytes []byte if len(fullCreationCodeBytes) > maxInitCodeSize { @@ -436,33 +440,34 @@ func verifySplitBlueprintLogic(client *ethclient.Client, targetArtifactPath stri part2Bytes = []byte{} } - // 3. Construct expected bytecodes + // Construct expected bytecodes expectedBytecode1 := blueprintPreamble + hex.EncodeToString(part1Bytes) expectedBytecode2 := blueprintPreamble + hex.EncodeToString(part2Bytes) - // 4. Fetch actual bytecodes - actualBytecode1, err1 := getOnchainBytecode(client, address1) - actualBytecode2, err2 := getOnchainBytecode(client, address2) - + // Fetch actual bytecode for address 1 + actualBytecode1, err1 := getOnchainBytecodeImpl(client, address1) if err1 != nil { result1.ProcessError = fmt.Errorf("getting onchain code for part 1 (%s): %w", address1.Hex(), err1) } + + // Fetch actual bytecode for address 2 + actualBytecode2, err2 := getOnchainBytecodeImpl(client, address2) if err2 != nil { result2.ProcessError = fmt.Errorf("getting onchain code for part 2 (%s): %w", address2.Hex(), err2) } - // 5. Compare Part 1 - if result1.ProcessError == nil { // Only compare if fetch (potentially) succeeded - diffs1, _, cmpErr1 := compareBytecode(artifact, expectedBytecode1, actualBytecode1) + // Compare Part 1 + if result1.ProcessError == nil { + diffs1, _, cmpErr1 := compareBytecode(artifact, false, expectedBytecode1, actualBytecode1) if cmpErr1 != nil { result1.ProcessError = errors.Join(result1.ProcessError, fmt.Errorf("comparing part 1 bytecode: %w", cmpErr1)) } result1.Differences = diffs1 } - // 6. Compare Part 2 - if result2.ProcessError == nil { // Only compare if fetch (potentially) succeeded - diffs2, _, cmpErr2 := compareBytecode(artifact, expectedBytecode2, actualBytecode2) + // Compare Part 2 + if result2.ProcessError == nil { + diffs2, _, cmpErr2 := compareBytecode(artifact, false, expectedBytecode2, actualBytecode2) if cmpErr2 != nil { result2.ProcessError = errors.Join(result2.ProcessError, fmt.Errorf("comparing part 2 bytecode: %w", cmpErr2)) } @@ -476,16 +481,36 @@ func verifySplitBlueprintLogic(client *ethclient.Client, targetArtifactPath stri func runOPCMVerificationLogic(client *ethclient.Client, opcmAddress common.Address, config ArtifactConfig) []*VerificationResult { results := []*VerificationResult{} - // --- Verify OPContractsManager itself --- - opcmArtifactBase := config.DefaultOPCMArtifactName // e.g., "OPContractsManager" + // Verify OPCM itself + opcmArtifactBase := config.DefaultOPCMArtifactName opcmArtifactRelative := filepath.Join(fmt.Sprintf("%s.sol", opcmArtifactBase), fmt.Sprintf("%s.json", opcmArtifactBase)) opcmArtifactPath := filepath.Join(config.ArtifactsDir, opcmArtifactRelative) - opcmResult := verifyDeployedContractLogic(client, opcmArtifactPath, opcmAddress) - opcmResult.Type = OPContractsManager // Override type - if opcmResult.ContractName == "" { // Ensure name is set - opcmResult.ContractName = config.DefaultOPCMArtifactName + // Load OPCM artifact first + opcmArtifact, err := ccom.ReadForgeArtifact(opcmArtifactPath) + // Derive name from path before potentially erroring out + opcmContractName := config.DefaultOPCMArtifactName // Use default name as fallback + // Attempt to refine name from path + if nameFromPath := strings.TrimSuffix(filepath.Base(opcmArtifactPath), ".json"); nameFromPath != "" { + opcmContractName = nameFromPath + } + if err != nil { + // If OPCM artifact fails to load, create an error result and cannot proceed + results = append(results, &VerificationResult{ + Type: OPContractsManager, + Address: opcmAddress.Hex(), + ArtifactPath: opcmArtifactPath, + ContractName: opcmContractName, // Use derived name + ProcessError: fmt.Errorf("loading OPCM artifact %s: %w", opcmArtifactPath, err), + }) + return results } + // Name is already derived above + + // Verify OPCM itself using the loaded artifact + opcmResult := verifyDeployedContractLogic(client, opcmArtifact, opcmContractName, opcmArtifactPath, opcmAddress) + opcmResult.Type = OPContractsManager // Override type + // ContractName is already set correctly by verifyDeployedContractLogic results = append(results, opcmResult) // Cannot proceed if OPCM verification itself had a processing error (e.g., RPC down) @@ -495,7 +520,6 @@ func runOPCMVerificationLogic(client *ethclient.Client, opcmAddress common.Addre return results } - // --- Set up OPCM caller --- // Assuming OPCM verification passed or had only bytecode diffs, we can try to bind opcmCaller, err := bindings.NewOpcm200Caller(opcmAddress, client) if err != nil { @@ -509,7 +533,7 @@ func runOPCMVerificationLogic(client *ethclient.Client, opcmAddress common.Addre return results // Cannot proceed without caller } - // --- Verify Implementations --- + // Verify implementations implementationsResult, err := opcmCaller.Implementations(nil) if err != nil { results = append(results, &VerificationResult{ @@ -553,17 +577,35 @@ func runOPCMVerificationLogic(client *ethclient.Client, opcmAddress common.Addre } artifactPath := filepath.Join(config.ArtifactsDir, relativePath) - implResult := verifyDeployedContractLogic(client, artifactPath, fieldValue) + + // Load implementation artifact + implArtifact, err := ccom.ReadForgeArtifact(artifactPath) + // Derive name from path before potentially erroring out + implContractName := strings.TrimSuffix(filepath.Base(artifactPath), ".json") + if err != nil { + // Handle artifact loading error for this implementation + results = append(results, &VerificationResult{ + Type: Implementation, + FieldName: fieldName, + ContractName: implContractName, // Use derived name + Address: implAddressStr, + ArtifactPath: artifactPath, + ProcessError: fmt.Errorf("loading implementation artifact %s: %w", artifactPath, err), + }) + continue // Skip to next implementation + } + // Name is already derived + + // Verify implementation using loaded artifact + implResult := verifyDeployedContractLogic(client, implArtifact, implContractName, artifactPath, fieldValue) implResult.Type = Implementation // Override type implResult.FieldName = fieldName // Store the field name - if implResult.ContractName == "" { - implResult.ContractName = strings.TrimSuffix(filepath.Base(artifactPath), ".json") - } + // ContractName is already set correctly results = append(results, implResult) } } - // --- Verify Blueprints --- + // Verify blueprints blueprintsResult, err := opcmCaller.Blueprints(nil) if err != nil { results = append(results, &VerificationResult{ @@ -625,21 +667,52 @@ func runOPCMVerificationLogic(client *ethclient.Client, opcmAddress common.Addre targetArtifactPath := filepath.Join(config.ArtifactsDir, relativePath) + // Load target artifact (used for both single and split blueprints) + targetArtifact, err := ccom.ReadForgeArtifact(targetArtifactPath) + // Derive target contract name from baseName determined earlier + // baseName was derived from field name or override path + targetContractName := baseName + if err != nil { + // Handle artifact loading error + errResult := VerificationResult{ + Type: UnknownBlueprint, // Or SplitBlueprintPart1 if applicable + FieldName: fieldName, + // Use baseName for the failed contract's name if possible + ContractName: fmt.Sprintf("Unknown (%s - loading failed)", targetContractName), // Indicate loading failure + Address: blueprintAddressStr, + ArtifactPath: targetArtifactPath, + ProcessError: fmt.Errorf("loading target artifact %s: %w", targetArtifactPath, err), + } + results = append(results, &errResult) + // If it was potentially a split, maybe add a placeholder for part 2? Less clear. + // For now, just report the loading error once. + continue // Skip to next blueprint field + } + // Name is already derived + // Check for split blueprint if strings.HasSuffix(fieldName, "1") { part2FieldName := strings.TrimSuffix(fieldName, "1") + "2" if part2Addr, exists := blueprintFields[part2FieldName]; exists && part2Addr != (common.Address{}) { - // Verify as split blueprint - res1, res2 := verifySplitBlueprintLogic(client, targetArtifactPath, fieldValue, part2Addr, fieldName, part2FieldName) + // Verify as split blueprint using loaded artifact + res1, res2 := verifySplitBlueprintLogic(client, targetArtifact, targetContractName, targetArtifactPath, fieldValue, part2Addr, fieldName, part2FieldName) results = append(results, res1, res2) processedPart2[part2FieldName] = true // Mark part 2 as handled continue // Move to next field + } else { + // Error out + results = append(results, &VerificationResult{ + Type: UnknownBlueprint, + FieldName: fieldName, + ContractName: fmt.Sprintf("Unknown (%s)", fieldName), + Address: blueprintAddressStr, + ProcessError: fmt.Errorf("split blueprint part 2 not found for %s", fieldName), + }) } - // If part 1 exists but part 2 doesn't, fall through to verify as single blueprint (warning printed later) } - // Verify as a standard (single) blueprint - bpResult := verifyBlueprintLogic(client, targetArtifactPath, fieldValue, fieldName) + // Verify as a standard (single) blueprint using loaded artifact + bpResult := verifyBlueprintLogic(client, targetArtifact, targetContractName, targetArtifactPath, fieldValue, fieldName) results = append(results, bpResult) } } @@ -647,8 +720,6 @@ func runOPCMVerificationLogic(client *ethclient.Client, opcmAddress common.Addre return results } -// --- Blockchain Interaction Helpers --- - // getOnchainBytecode fetches bytecode from the chain. Returns hex string or error. func getOnchainBytecode(client *ethclient.Client, address common.Address) (string, error) { if client == nil { @@ -664,17 +735,15 @@ func getOnchainBytecode(client *ethclient.Client, address common.Address) (strin return "0x" + hex.EncodeToString(code), nil } -// --- Bytecode Comparison Logic --- - // compareBytecode compares expected and actual bytecode, handling immutables. // It uses the artifact to find immutable names via the AST. func compareBytecode( - artifact *solc.ForgeArtifact, // Changed input: Full artifact + artifact *solc.ForgeArtifact, + checkImmutables bool, expectedBytecodeHex string, actualBytecodeHex string, ) ([]BytecodeDifference, []ImmutableValueInfo, error) { - - // --- Input Validation and Decoding --- + // Input validation and decoding expectedClean := strings.TrimPrefix(expectedBytecodeHex, "0x") actualClean := strings.TrimPrefix(actualBytecodeHex, "0x") @@ -703,18 +772,20 @@ func compareBytecode( } } - // --- Precompute Immutable Locations --- + // Error if bytecode lengths don't match + if len(expectedBytes) != len(actualBytes) { + return nil, nil, fmt.Errorf("bytecode length mismatch, expected: %d, actual: %d", len(expectedBytes), len(actualBytes)) + } + + // Precompute immutable locations type immutableByteInfo struct { Name string - // Length int // We don't strictly need Length here anymore } - // Map byte offset -> info about the immutable variable at that offset immutableBytes := make(map[int]immutableByteInfo) if artifact != nil && artifact.DeployedBytecode.ImmutableReferences != nil { for refKey, locations := range artifact.DeployedBytecode.ImmutableReferences { - // Get the human-readable name using the AST - name := GetImmutableName(artifact, refKey) + name := getImmutableName(artifact, refKey) if name == "" { name = fmt.Sprintf("immutable(id:%s)", refKey) // Fallback name } @@ -727,11 +798,10 @@ func compareBytecode( continue // Skip invalid length } - // Mark each byte within this location - for i := 0; i < length; i++ { - offset := start + i + // Mark each byte within this location using 'j' + for j := 0; j < length; j++ { + offset := start + j if existing, exists := immutableBytes[offset]; exists { - // Warning if overlapping, but allow overwriting (last one wins?) if existing.Name != info.Name { fmt.Fprintf(os.Stderr, "Warning: Overlapping immutable reference at offset %d. Prev: '%s', New: '%s'\n", offset, existing.Name, info.Name) @@ -743,7 +813,7 @@ func compareBytecode( } } - // --- Compare Byte by Byte --- + // Compare byte by byte differences := []BytecodeDifference{} var currDiff *currentDiff = nil // Tracks the current block of differences maxLength := max(len(expectedBytes), len(actualBytes)) @@ -772,7 +842,7 @@ func compareBytecode( bytesDiffer := expectedByte != actualByte - // --- State machine logic for tracking differences --- + // State machine logic for tracking differences if bytesDiffer { if currDiff == nil { // Start a new difference block @@ -821,7 +891,7 @@ func compareBytecode( } // No action needed if bytes match and not in a diff block } - } // End of byte loop + } // Record the final difference block if the loop ended while in a diff if currDiff != nil { @@ -835,12 +905,12 @@ func compareBytecode( }) } - // --- Collect Populated Immutable Values --- + // Collect and return immutable differences immutableValues := []ImmutableValueInfo{} - if artifact != nil && artifact.DeployedBytecode.ImmutableReferences != nil { + if checkImmutables && artifact != nil && artifact.DeployedBytecode.ImmutableReferences != nil { // Iterate through the defined locations in the artifact for refKey, locations := range artifact.DeployedBytecode.ImmutableReferences { - name := GetImmutableName(artifact, refKey) + name := getImmutableName(artifact, refKey) if name == "" { name = fmt.Sprintf("immutable(id:%s)", refKey) // Fallback } @@ -849,7 +919,11 @@ func compareBytecode( start := int(loc.Start) length := int(loc.Length) if length <= 0 { - continue // Skip invalid locations + // Invalid length - return an error + return nil, nil, fmt.Errorf( + "immutable '%s' location (offset %d, length %d) has invalid length %d", + name, start, length, length, + ) } // Extract the actual value directly from actualBytes based on this location @@ -860,18 +934,11 @@ func compareBytecode( actualValueBytes := actualBytes[start:upperBound] actualValueHex = "0x" + hex.EncodeToString(actualValueBytes) } else { - // Handle cases where the location is partly or fully out of bounds - actualValueHex = "0x" // Represent out-of-bounds or partial data as empty hex - if start < len(actualBytes) && start >= 0 { - // Partial overlap: extract what's available - partialBytes := actualBytes[start:] // Extract from start to end of actualBytes - actualValueHex = "0x" + hex.EncodeToString(partialBytes) + " (incomplete)" - } else { - // Location is completely outside actual bytecode - actualValueHex = "(out of bounds)" - } - // Add warning? - // fmt.Fprintf(os.Stderr, "Warning: Immutable '%s' location [%d:%d] out of bounds for actual bytecode length %d\n", name, start, upperBound, len(actualBytes)) + // Fundamental mismatch - return an error + return nil, nil, fmt.Errorf( + "immutable '%s' location (offset %d, length %d) is out of bounds for actual bytecode length %d", + name, start, length, len(actualBytes), + ) } immutableValues = append(immutableValues, ImmutableValueInfo{ @@ -884,45 +951,38 @@ func compareBytecode( } } - // Optionally add a warning/error if lengths didn't match, but still return results - if len(expectedBytes) != len(actualBytes) { - // Could potentially add a specific difference entry for the length mismatch - // fmt.Fprintf(os.Stderr, "Warning: Bytecode length mismatch. Expected: %d, Actual: %d\n", len(expectedBytes), len(actualBytes)) - } - return differences, immutableValues, nil } -// --- Presentation Layer (Printing) --- - // printResults formats and prints the outcomes of verification checks. func printResults(results []*VerificationResult, verbose bool) { - overallSuccess := true // Track if any *code* mismatches occurred + // Track if any *code* mismatches occurred + overallSuccess := true for i, result := range results { if i > 0 { - fmt.Println() // Add spacing between results + // Add spacing between results + fmt.Println() } - // --- Print Header --- + // Print header printResultHeader(result) - // --- Handle and Print Process Errors --- + // Handle and print process errors if result.ProcessError != nil { color.Red(" ERROR during verification: %v", result.ProcessError) overallSuccess = false - continue // Skip comparison details if processing failed severely + continue } - // --- Analyze Differences --- + // Analyze differences codeDiffs, immDiffs := categorizeDifferences(result) - // --- Print Status and Details --- + // Print status and details if len(codeDiffs) > 0 { overallSuccess = false color.Red(" ✗ Verification FAILED: Found unexpected differences in code.") printCodeDifferences(codeDiffs) - // Optionally print immutable info if verbose AND failed (for deployed contracts) if verbose && (result.Type == DeployedContract || result.Type == Implementation || result.Type == OPContractsManager) && len(result.ImmutableInfos) > 0 { printImmutableDetails(result.ImmutableInfos, immDiffs, true) // Pass true for includeConsistencyWarning } @@ -934,23 +994,19 @@ func printResults(results []*VerificationResult, verbose bool) { } else { // Exact match (or blueprint match where immutables aren't checked) color.Green(" ✓ Verification successful (exact match).") - // If verbose, still show immutable values even on exact match for deployed contracts if verbose && (result.Type == DeployedContract || result.Type == Implementation || result.Type == OPContractsManager) && len(result.ImmutableInfos) > 0 { - printImmutableDetails(result.ImmutableInfos, nil, false) // Pass nil diffs, no consistency warning needed + printImmutableDetails(result.ImmutableInfos, nil, verbose) } } - } // End loop through results + } - // --- Print Overall Summary Footer --- - fmt.Println("\n----------------------------------------") + // Print overall summary + fmt.Println() if overallSuccess { - color.Green("Overall Result: Verification PASSED.") - fmt.Println("All checked items match expected bytecode (or differ only in known immutable locations).") + color.Green("OK") } else { - color.Red("Overall Result: Verification FAILED.") - fmt.Println("One or more items had unexpected code differences or processing errors.") + color.Red("FAILED") } - fmt.Println("----------------------------------------") } // printResultHeader prints the title section for a single result. @@ -980,6 +1036,8 @@ func printResultHeader(result *VerificationResult) { // categorizeDifferences separates differences into code/unknown and immutable. func categorizeDifferences(result *VerificationResult) (codeDiffs, immutableDiffs []BytecodeDifference) { isDeployed := result.Type == DeployedContract || result.Type == Implementation || result.Type == OPContractsManager + codeDiffs = make([]BytecodeDifference, 0) + immutableDiffs = make([]BytecodeDifference, 0) for _, diff := range result.Differences { if isDeployed && diff.InImmutable { immutableDiffs = append(immutableDiffs, diff) @@ -1004,23 +1062,18 @@ func printCodeDifferences(diffs []BytecodeDifference) { for _, diff := range diffs { endPos := diff.Start + diff.Length - 1 color.Red(" Byte %d-%d (%d bytes):", diff.Start, endPos, diff.Length) - // Add truncation logic fmt.Printf(" Expected: 0x%s\n", maybeTruncate(diff.Expected, 64)) fmt.Printf(" Actual: 0x%s\n", maybeTruncate(diff.Actual, 64)) } } // printImmutableDetails formats and prints immutable variable info. -func printImmutableDetails(infos []ImmutableValueInfo, diffs []BytecodeDifference, includeConsistencyWarning bool) { +func printImmutableDetails(infos []ImmutableValueInfo, diffs []BytecodeDifference, verbose bool) { if len(infos) == 0 { - // Don't print header if there's nothing to show - // Or print a "No immutable variables found" message? - // fmt.Println(" --- Immutable Reference Details ---") - // fmt.Println(" (No immutable variables defined or found in bytecode)") return } - color.Cyan(" --- Immutable Reference Details (Values from Actual Bytecode) ---") + color.Cyan(" --- Immutable Reference Details ---") // Group infos by name for consistency check infosByName := make(map[string][]ImmutableValueInfo) @@ -1031,7 +1084,6 @@ func printImmutableDetails(infos []ImmutableValueInfo, diffs []BytecodeDifferenc } infosByName[info.Name] = append(infosByName[info.Name], info) } - // sort.Strings(names) // Optional: sort names alphabetically for _, name := range names { locations := infosByName[name] @@ -1042,7 +1094,7 @@ func printImmutableDetails(infos []ImmutableValueInfo, diffs []BytecodeDifferenc for i, loc := range locations { fmt.Printf(" [%d] Location: Offset %d, Length %d bytes\n", i, loc.Offset, loc.Length) - if loc.Value != "" && loc.Value != "0x" { // Check Value field from ImmutableValueInfo + if loc.Value != "" && loc.Value != "0x" { fmt.Printf(" Actual Value: %s\n", loc.Value) if populatedCount == 0 { firstValue = loc.Value @@ -1051,30 +1103,16 @@ func printImmutableDetails(infos []ImmutableValueInfo, diffs []BytecodeDifferenc } populatedCount++ } else { - // This case should be less common now if infos only includes populated values fmt.Printf(" Actual Value: (Not populated - check comparison logic or bytecode length)\n") } } - // Print consistency summary for this variable if needed - if includeConsistencyWarning && inconsistent { - color.Red(" ! Consistency WARNING: Found differing values for '%s' across its locations.", name) - } else if populatedCount > 1 { - // color.Green(" ✓ Consistency: All populated values for '%s' are identical.", name) - } else if populatedCount == 0 && len(locations) > 0 { - // color.Yellow(" - Consistency: No values populated for '%s'.", name) - } - } - // Optionally print raw immutable differences if any occurred - if len(diffs) > 0 { - color.Set(color.FgYellow) - fmt.Println("\n --- Differences Within Immutable Ranges ---") - color.Unset() - for _, diff := range diffs { - endPos := diff.Start + diff.Length - 1 - color.Yellow(" Byte %d-%d (%d bytes) in '%s':", diff.Start, endPos, diff.Length, diff.ImmutableName) - fmt.Printf(" Expected: 0x%s\n", maybeTruncate(diff.Expected, 64)) - fmt.Printf(" Actual: 0x%s\n", maybeTruncate(diff.Actual, 64)) + if inconsistent { + color.Red(" ! Consistency WARNING: Found differing values for '%s' across its locations.", name) + } else if verbose && populatedCount > 1 { + color.Green(" ✓ Consistency: All populated values for '%s' are identical.", name) + } else if verbose && populatedCount == 0 && len(locations) > 0 { + color.Yellow(" - Consistency: No values populated for '%s'.", name) } } } @@ -1087,8 +1125,6 @@ func maybeTruncate(s string, maxLen int) string { return s } -// --- Utility Helpers --- - // resolvePath resolves a potentially relative path against a base directory. func resolvePath(path, baseDir string) (string, error) { if filepath.IsAbs(path) { @@ -1113,12 +1149,10 @@ func max(a, b int) int { return b } -// --- AST Traversal Helper for Immutable Names --- - -// GetImmutableName finds the human-readable name of an immutable variable within a ForgeArtifact's AST, +// getImmutableName finds the human-readable name of an immutable variable within a ForgeArtifact's AST, // given the reference key from the artifact's ImmutableReferences map. // The refKey is usually a string representation of the variable's AST node ID (e.g., "36"). -func GetImmutableName(artifact *solc.ForgeArtifact, refKey string) string { +func getImmutableName(artifact *solc.ForgeArtifact, refKey string) string { if artifact == nil { fmt.Fprintln(os.Stderr, "Warning: Cannot get immutable name, artifact is nil") return "" @@ -1187,9 +1221,6 @@ func findAstNodeNameByID(nodes []solc.AstNode, targetID int) string { return name } } - - // We don't need to search ParameterLists (`node.Parameters`, `node.ReturnParameters`) - // as immutables cannot be parameters. } return "" // Not found in this slice or its children diff --git a/packages/contracts-bedrock/scripts/checks/verify-bytecode/main_test.go b/packages/contracts-bedrock/scripts/checks/verify-bytecode/main_test.go index 14b7963457d..348b9472817 100644 --- a/packages/contracts-bedrock/scripts/checks/verify-bytecode/main_test.go +++ b/packages/contracts-bedrock/scripts/checks/verify-bytecode/main_test.go @@ -1,646 +1,1066 @@ package main import ( - "encoding/json" - "os" - "path/filepath" + "fmt" + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/ethereum-optimism/optimism/op-chain-ops/solc" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" ) -func TestLoadArtifact(t *testing.T) { - // Create a temporary artifact file - tempDir := t.TempDir() - artifactPath := filepath.Join(tempDir, "artifact.json") +// Helper to create a basic ForgeArtifact for testing +func newTestArtifact(opts ...func(*solc.ForgeArtifact)) *solc.ForgeArtifact { + artifact := &solc.ForgeArtifact{ + Abi: solc.AbiType{}, // Usually not needed for bytecode verification tests + Bytecode: solc.CompilerOutputBytecode{Object: "0x"}, + DeployedBytecode: solc.CompilerOutputBytecode{Object: "0x"}, + Ast: solc.Ast{Nodes: []solc.AstNode{}}, + } + for _, opt := range opts { + opt(artifact) + } + return artifact +} - // Test case 1: Valid artifact - validArtifact := map[string]interface{}{ - "deployedBytecode": map[string]interface{}{ - "object": "0x1234", +// Option to set deployed bytecode +func withDeployedBytecode(code string) func(*solc.ForgeArtifact) { + return func(a *solc.ForgeArtifact) { + a.DeployedBytecode.Object = code + } +} + +// Option to set creation bytecode +func withCreationBytecode(code string) func(*solc.ForgeArtifact) { + return func(a *solc.ForgeArtifact) { + a.Bytecode.Object = code + } +} + +// Option to add immutable references +func withImmutableRefs(refs map[string][]solc.ImmutableReference) func(*solc.ForgeArtifact) { + return func(a *solc.ForgeArtifact) { + if a.DeployedBytecode.ImmutableReferences == nil { + a.DeployedBytecode.ImmutableReferences = make(map[string][]solc.ImmutableReference) + } + for k, v := range refs { + a.DeployedBytecode.ImmutableReferences[k] = v + } + } +} + +// Option to add AST nodes +func withAstNodes(nodes []solc.AstNode) func(*solc.ForgeArtifact) { + return func(a *solc.ForgeArtifact) { + a.Ast.Nodes = nodes + } +} + +// Helper to create AST nodes for immutable tests +func createTestAstNodes() []solc.AstNode { + return []solc.AstNode{ + { // Contract Definition + Id: 10, + NodeType: "ContractDefinition", + Name: "MyContract", + Nodes: []solc.AstNode{ + { // State Variable 1 (immutable) + Id: 5, + NodeType: "VariableDeclaration", + Name: "IMMUTABLE_VAR_1", + StateVariable: true, + Mutability: "immutable", + Constant: false, + TypeDescriptions: &solc.AstTypeDescriptions{TypeString: "uint256"}, + }, + { // State Variable 2 (regular) + Id: 6, + NodeType: "VariableDeclaration", + Name: "regularVar", + StateVariable: true, + TypeDescriptions: &solc.AstTypeDescriptions{TypeString: "bool"}, + }, + { // Function Definition + Id: 8, + NodeType: "FunctionDefinition", + Name: "doSomething", + Body: &solc.AstBlock{ + NodeType: "Block", + Id: 9, + Statements: []solc.AstNode{ + { // Local Variable (shouldn't be found by ID 5) + Id: 7, + NodeType: "ExpressionStatement", + Src: "placeholder;", + }, + }, + }, + }, + { // State Variable 3 (immutable, nested struct type not important for name lookup) + Id: 15, + NodeType: "VariableDeclaration", + Name: "IMMUTABLE_VAR_2", + StateVariable: true, + Mutability: "immutable", + TypeDescriptions: &solc.AstTypeDescriptions{TypeString: "struct MyStruct"}, + }, + }, + }, + { // Another top-level node (e.g., ImportDirective, ErrorDefinition) + Id: 11, + NodeType: "ImportDirective", + }, + { // Struct Definition (containing a node with ID 5, but wrong type) + Id: 12, + NodeType: "StructDefinition", + Name: "MyStruct", + Nodes: []solc.AstNode{ + { + Id: 5, // Duplicate ID, but wrong node type + NodeType: "MemberAccess", + Name: "", // Not a declaration name + TypeDescriptions: &solc.AstTypeDescriptions{TypeString: "uint"}, + }, + }, }, } - artifactJSON, err := json.Marshal(validArtifact) +} + +func TestGetImmutableName(t *testing.T) { + astNodes := createTestAstNodes() + artifact := newTestArtifact(withAstNodes(astNodes)) + + tests := []struct { + name string + refKey string + artifact *solc.ForgeArtifact + wantName string + }{ + { + name: "Valid ID simple", + refKey: "5", + artifact: artifact, + wantName: "IMMUTABLE_VAR_1", + }, + { + name: "Valid ID with type prefix", + refKey: "t_struct:MyStruct:15", + artifact: artifact, + wantName: "IMMUTABLE_VAR_2", + }, + { + name: "ID exists but not VariableDeclaration", + refKey: "7", + artifact: artifact, + wantName: "", + }, + { + name: "ID not found", + refKey: "999", + artifact: artifact, + wantName: "", + }, + { + name: "Invalid refKey format", + refKey: "invalid-key", + artifact: artifact, + wantName: "", + }, + { + name: "Nil artifact", + refKey: "5", + artifact: nil, + wantName: "", + }, + { + name: "Artifact with no AST", + refKey: "5", + artifact: newTestArtifact(), + wantName: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Note: We don't directly test findAstNodeNameByID as it's an internal helper. + // Its behavior is tested via getImmutableName. + gotName := getImmutableName(tt.artifact, tt.refKey) + assert.Equal(t, tt.wantName, gotName) + }) + } +} + +func TestCompareBytecode_ExactMatch(t *testing.T) { + artifact := newTestArtifact() // No immutables needed + expected := "0x12345678" + actual := "0x12345678" + + diffs, infos, err := compareBytecode(artifact, true, expected, actual) require.NoError(t, err) - err = os.WriteFile(artifactPath, artifactJSON, 0644) + assert.Empty(t, diffs, "Should be no differences") + assert.Empty(t, infos, "Should be no immutable info") +} + +func TestCompareBytecode_SimpleMismatch(t *testing.T) { + artifact := newTestArtifact() + expected := "0x12345678" + actual := "0x1234ff78" // Mismatch at byte 2 (0-indexed) + + diffs, infos, err := compareBytecode(artifact, true, expected, actual) require.NoError(t, err) + assert.Empty(t, infos) + require.Len(t, diffs, 1) + + diff := diffs[0] + assert.Equal(t, 2, diff.Start) + assert.Equal(t, 1, diff.Length) + assert.Equal(t, "56", diff.Expected) + assert.Equal(t, "ff", diff.Actual) + assert.False(t, diff.InImmutable) + assert.Equal(t, "", diff.ImmutableName) +} + +func TestCompareBytecode_DifferentLengths(t *testing.T) { + artifact := newTestArtifact() + expected := "0x12345678" + actualShort := "0x123456" + actualLong := "0x1234567890" + + _, _, err := compareBytecode(artifact, true, expected, actualShort) + require.Error(t, err) + assert.Contains(t, err.Error(), "bytecode length mismatch") + + _, _, err = compareBytecode(artifact, true, expected, actualLong) + require.Error(t, err) + assert.Contains(t, err.Error(), "bytecode length mismatch") - artifact, err := loadArtifact(artifactPath) + _, _, err = compareBytecode(artifact, true, actualShort, expected) + require.Error(t, err) + assert.Contains(t, err.Error(), "bytecode length mismatch") +} + +func TestCompareBytecode_PrefixHandling(t *testing.T) { + artifact := newTestArtifact() + expected := "0x1234" + actualNoPrefix := "1234" + actualWithPrefix := "0x1234" + + // Expected has prefix, actual does not + diffs, infos, err := compareBytecode(artifact, true, expected, actualNoPrefix) require.NoError(t, err) - assert.Equal(t, "0x1234", artifact["deployedBytecode"].(map[string]interface{})["object"]) + assert.Empty(t, diffs) + assert.Empty(t, infos) - // Test case 2: Empty path - _, err = loadArtifact("") - assert.Error(t, err) - assert.Contains(t, err.Error(), "artifact path is required") + // Expected does not have prefix, actual does + diffs, infos, err = compareBytecode(artifact, true, actualNoPrefix, actualWithPrefix) + require.NoError(t, err) + assert.Empty(t, diffs) + assert.Empty(t, infos) +} - // Test case 3: Non-existent file - _, err = loadArtifact(filepath.Join(tempDir, "nonexistent.json")) - assert.Error(t, err) - assert.Contains(t, err.Error(), "artifact file not found", "Should correctly report non-existent file") +func TestCompareBytecode_EmptyBytecode(t *testing.T) { + artifact := newTestArtifact() + + // Both empty with prefix + diffs, infos, err := compareBytecode(artifact, true, "0x", "0x") + require.NoError(t, err) + assert.Empty(t, diffs) + assert.Empty(t, infos) - // Test case 4: Invalid JSON - err = os.WriteFile(artifactPath, []byte("invalid json"), 0644) + // Both empty without prefix + diffs, infos, err = compareBytecode(artifact, true, "", "") require.NoError(t, err) - _, err = loadArtifact(artifactPath) - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to parse JSON") + assert.Empty(t, diffs) + assert.Empty(t, infos) + + // One empty, one not (should fail length check) + _, _, err = compareBytecode(artifact, true, "0x12", "0x") + require.Error(t, err) + assert.Contains(t, err.Error(), "bytecode length mismatch") + + _, _, err = compareBytecode(artifact, true, "", "12") + require.Error(t, err) + assert.Contains(t, err.Error(), "bytecode length mismatch") +} + +func TestCompareBytecode_InvalidHex(t *testing.T) { + artifact := newTestArtifact() + valid := "0x1234" + invalid := "0x123G" // Invalid character 'G' + oddLen := "0x123" // Odd length + + _, _, err := compareBytecode(artifact, true, invalid, valid) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to decode expected bytecode") + + _, _, err = compareBytecode(artifact, true, valid, invalid) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to decode actual bytecode") + + _, _, err = compareBytecode(artifact, true, oddLen, valid) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid expected bytecode hex length") + + _, _, err = compareBytecode(artifact, true, valid, oddLen) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to decode actual bytecode") + assert.Contains(t, err.Error(), "odd length hex string") // Specific error from hex pkg } -func TestGetDeployedBytecode(t *testing.T) { +func TestCompareBytecode_Immutables(t *testing.T) { tests := []struct { - name string - artifact map[string]interface{} - want string - wantErr bool + name string + expected string + actual string + checkImmut bool + immutables solc.ImmutableReferences + wantDiffs []BytecodeDifference + wantInfos []ImmutableValueInfo + wantErr bool + wantErrMsg string }{ { - name: "Forge/Foundry format", - artifact: map[string]interface{}{ - "deployedBytecode": map[string]interface{}{ - "object": "0x1234", + name: "Match", + expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", + actual: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", + checkImmut: true, + immutables: solc.ImmutableReferences{ + "5": { + {Start: 2, Length: 3}, + }, + "15": { + {Start: 8, Length: 2}, + {Start: 12, Length: 1}, }, }, - want: "0x1234", + wantDiffs: []BytecodeDifference{}, + wantInfos: []ImmutableValueInfo{ + {Name: "IMMUTABLE_VAR_1", Offset: 2, Length: 3, Value: "0xaaaa00"}, + {Name: "IMMUTABLE_VAR_2", Offset: 8, Length: 2, Value: "0xbbbb"}, + {Name: "IMMUTABLE_VAR_2", Offset: 12, Length: 1, Value: "0xcc"}, + }, wantErr: false, }, { - name: "Standard format with string", - artifact: map[string]interface{}{ - "deployedBytecode": "0x5678", + name: "Diff inside immutable only (checkImmut=false)", + expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", + actual: "0xAAAAAAAA112233BBBBBBBBCCCCDDDDDD", + checkImmut: false, + immutables: solc.ImmutableReferences{}, + wantDiffs: []BytecodeDifference{ + {Start: 4, Length: 3, Expected: "000000", Actual: "112233", InImmutable: false, ImmutableName: ""}, + }, + wantInfos: []ImmutableValueInfo{}, + wantErr: false, + }, + { + name: "Diff outside immutable only", + expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", + actual: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDFF", + checkImmut: true, + immutables: solc.ImmutableReferences{ + "5": { + {Start: 2, Length: 3}, + }, + "15": { + {Start: 8, Length: 2}, + {Start: 12, Length: 1}, + }, + }, + wantDiffs: []BytecodeDifference{ + {Start: 15, Length: 1, Expected: "dd", Actual: "ff", InImmutable: false, ImmutableName: ""}, + }, + wantInfos: []ImmutableValueInfo{ + {Name: "IMMUTABLE_VAR_1", Offset: 2, Length: 3, Value: "0xaaaa00"}, + {Name: "IMMUTABLE_VAR_2", Offset: 8, Length: 2, Value: "0xbbbb"}, + {Name: "IMMUTABLE_VAR_2", Offset: 12, Length: 1, Value: "0xcc"}, }, - want: "0x5678", wantErr: false, }, { - name: "Bytecode object format", - artifact: map[string]interface{}{ - "bytecode": map[string]interface{}{ - "object": "0xabcd", + name: "Diffs inside and outside immutable", + expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", + actual: "0xAAAA1122330000BBBBBBBBCCCCDDDDFF", + checkImmut: true, + immutables: solc.ImmutableReferences{ + "5": { + {Start: 2, Length: 3}, }, + "15": { + {Start: 8, Length: 2}, + {Start: 12, Length: 1}, + }, + }, + wantDiffs: []BytecodeDifference{ + {Start: 2, Length: 3, Expected: "aaaa00", Actual: "112233", InImmutable: true, ImmutableName: "IMMUTABLE_VAR_1"}, + {Start: 15, Length: 1, Expected: "dd", Actual: "ff", InImmutable: false, ImmutableName: ""}, + }, + wantInfos: []ImmutableValueInfo{ + {Name: "IMMUTABLE_VAR_1", Offset: 2, Length: 3, Value: "0x112233"}, + {Name: "IMMUTABLE_VAR_2", Offset: 8, Length: 2, Value: "0xbbbb"}, + {Name: "IMMUTABLE_VAR_2", Offset: 12, Length: 1, Value: "0xcc"}, }, - want: "0xabcd", wantErr: false, }, { - name: "Bytecode string format", - artifact: map[string]interface{}{ - "bytecode": "0xef01", + name: "Diff spanning immutable boundary", + expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", + actual: "0xAAAAAAAA112200BBBBBBBBCCCCDDDDDD", + checkImmut: true, + immutables: solc.ImmutableReferences{ + "5": { + {Start: 2, Length: 3}, + }, + "15": { + {Start: 8, Length: 2}, + {Start: 12, Length: 1}, + }, + }, + wantDiffs: []BytecodeDifference{ + {Start: 4, Length: 1, Expected: "00", Actual: "11", InImmutable: true, ImmutableName: "IMMUTABLE_VAR_1"}, + {Start: 5, Length: 1, Expected: "00", Actual: "22", InImmutable: false, ImmutableName: ""}, + }, + wantInfos: []ImmutableValueInfo{ + {Name: "IMMUTABLE_VAR_1", Offset: 2, Length: 3, Value: "0xaaaa11"}, + {Name: "IMMUTABLE_VAR_2", Offset: 8, Length: 2, Value: "0xbbbb"}, + {Name: "IMMUTABLE_VAR_2", Offset: 12, Length: 1, Value: "0xcc"}, }, - want: "0xef01", wantErr: false, }, { - name: "No bytecode", - artifact: map[string]interface{}{}, - want: "", - wantErr: true, + name: "Immutable ref out of bounds for actual bytecode", + expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", + actual: "0xAAAAAAAA000000BBBBBBBBCCCC", + checkImmut: true, + immutables: solc.ImmutableReferences{ + "5": { + {Start: 2, Length: 3}, + }, + "15": { + {Start: 8, Length: 2}, + {Start: 12, Length: 1}, + }, + }, + wantDiffs: nil, + wantInfos: nil, + wantErr: true, + wantErrMsg: "bytecode length mismatch", + }, + { + name: "Immutable ref has invalid length (zero)", + expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", + actual: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", + checkImmut: true, + immutables: solc.ImmutableReferences{ + "5": { + {Start: 2, Length: 0}, + }, + }, + wantDiffs: nil, + wantInfos: nil, + wantErr: true, + wantErrMsg: "immutable 'IMMUTABLE_VAR_1' location (offset 2, length 0) has invalid length 0", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := getDeployedBytecode(tt.artifact) + astNodes := createTestAstNodes() + art := newTestArtifact(withAstNodes(astNodes), withImmutableRefs(tt.immutables)) + diffs, infos, err := compareBytecode(art, tt.checkImmut, tt.expected, tt.actual) + if tt.wantErr { - assert.Error(t, err) + require.Error(t, err) + if tt.wantErrMsg != "" { + assert.Contains(t, err.Error(), tt.wantErrMsg) + } } else { - assert.NoError(t, err) - assert.Equal(t, tt.want, got) + require.NoError(t, err) + assert.Equal(t, tt.wantDiffs, diffs, "Differences mismatch") + assert.ElementsMatch(t, tt.wantInfos, infos, "Immutable infos mismatch") } }) } } -func TestGetVariableNameFromAST(t *testing.T) { +func TestCompareBytecode_DifferenceGrouping(t *testing.T) { + artifact := newTestArtifact() + expected := "0x112233445566" + actual := "0x11aabbcc5566" // Differs @ 1,2,3 (0x223344 -> 0xaabbcc) + + diffs, infos, err := compareBytecode(artifact, true, expected, actual) + require.NoError(t, err) + assert.Empty(t, infos) + require.Len(t, diffs, 1, "Differences should be grouped") + + diff := diffs[0] + assert.Equal(t, 1, diff.Start) + assert.Equal(t, 3, diff.Length) + assert.Equal(t, "223344", diff.Expected) + assert.Equal(t, "aabbcc", diff.Actual) + assert.False(t, diff.InImmutable) +} + +func TestCategorizeDifferences(t *testing.T) { tests := []struct { - name string - artifact map[string]interface{} - varID string - want string + name string + resultType VerificationType + allDiffs []BytecodeDifference + wantCodeDiffs []BytecodeDifference + wantImmDiffs []BytecodeDifference + wantHasCode bool }{ { - name: "Find variable by ID", - artifact: map[string]interface{}{ - "ast": map[string]interface{}{ - "nodes": []interface{}{ - map[string]interface{}{ - "id": float64(123), - "name": "testVar", - }, - }, - }, - }, - varID: "123", - want: "testVar", + name: "No diffs", + resultType: DeployedContract, + allDiffs: []BytecodeDifference{}, + wantCodeDiffs: []BytecodeDifference{}, + wantImmDiffs: []BytecodeDifference{}, + wantHasCode: false, }, { - name: "Find variable with path prefix", - artifact: map[string]interface{}{ - "ast": map[string]interface{}{ - "nodes": []interface{}{ - map[string]interface{}{ - "id": float64(456), - "name": "prefixedVar", - }, - }, - }, + name: "Only code diffs", + resultType: DeployedContract, + allDiffs: []BytecodeDifference{ + {Start: 1, Length: 1, Expected: "aa", Actual: "bb", InImmutable: false}, + {Start: 5, Length: 2, Expected: "cccc", Actual: "dddd", InImmutable: false}, + }, + wantCodeDiffs: []BytecodeDifference{ + {Start: 1, Length: 1, Expected: "aa", Actual: "bb", InImmutable: false}, + {Start: 5, Length: 2, Expected: "cccc", Actual: "dddd", InImmutable: false}, }, - varID: "path:to:456", - want: "prefixedVar", + wantImmDiffs: []BytecodeDifference{}, + wantHasCode: true, }, { - name: "Variable not found", - artifact: map[string]interface{}{ - "ast": map[string]interface{}{ - "nodes": []interface{}{ - map[string]interface{}{ - "id": float64(789), - "name": "otherVar", - }, - }, - }, + name: "Only immutable diffs (DeployedContract)", + resultType: DeployedContract, + allDiffs: []BytecodeDifference{ + {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, + {Start: 20, Length: 1, Expected: "ee", Actual: "ff", InImmutable: true, ImmutableName: "VarB"}, + }, + wantCodeDiffs: []BytecodeDifference{}, + wantImmDiffs: []BytecodeDifference{ + {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, + {Start: 20, Length: 1, Expected: "ee", Actual: "ff", InImmutable: true, ImmutableName: "VarB"}, }, - varID: "999", - want: "999", // Returns the ID if not found + wantHasCode: false, }, { - name: "Non-numeric ID", - artifact: map[string]interface{}{ - "ast": map[string]interface{}{ - "nodes": []interface{}{}, - }, + name: "Mixed diffs (DeployedContract)", + resultType: DeployedContract, + allDiffs: []BytecodeDifference{ + {Start: 1, Length: 1, Expected: "aa", Actual: "bb", InImmutable: false}, + {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, + {Start: 5, Length: 2, Expected: "cccc", Actual: "dddd", InImmutable: false}, + {Start: 20, Length: 1, Expected: "ee", Actual: "ff", InImmutable: true, ImmutableName: "VarB"}, + }, + wantCodeDiffs: []BytecodeDifference{ + {Start: 1, Length: 1, Expected: "aa", Actual: "bb", InImmutable: false}, + {Start: 5, Length: 2, Expected: "cccc", Actual: "dddd", InImmutable: false}, }, - varID: "abc", - want: "abc", // Returns the ID if not numeric + wantImmDiffs: []BytecodeDifference{ + {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, + {Start: 20, Length: 1, Expected: "ee", Actual: "ff", InImmutable: true, ImmutableName: "VarB"}, + }, + wantHasCode: true, }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := getVariableNameFromAST(tt.artifact, tt.varID) - assert.Equal(t, tt.want, got) - }) - } -} - -func TestFindNodeName(t *testing.T) { - tests := []struct { - name string - node interface{} - targetID int - want string - }{ { - name: "Find node in map", - node: map[string]interface{}{ - "id": float64(123), - "name": "testNode", + name: "Only immutable diffs (Implementation)", + resultType: Implementation, // Also checks immutables + allDiffs: []BytecodeDifference{ + {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, + }, + wantCodeDiffs: []BytecodeDifference{}, + wantImmDiffs: []BytecodeDifference{ + {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, }, - targetID: 123, - want: "testNode", + wantHasCode: false, }, { - name: "Find node in nested map", - node: map[string]interface{}{ - "child": map[string]interface{}{ - "id": float64(456), - "name": "nestedNode", - }, + name: "Only immutable diffs (OPContractsManager)", + resultType: OPContractsManager, // Also checks immutables + allDiffs: []BytecodeDifference{ + {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, + }, + wantCodeDiffs: []BytecodeDifference{}, + wantImmDiffs: []BytecodeDifference{ + {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, }, - targetID: 456, - want: "nestedNode", + wantHasCode: false, }, { - name: "Find node in array", - node: map[string]interface{}{ - "children": []interface{}{ - map[string]interface{}{ - "id": float64(789), - "name": "arrayNode", - }, - }, + name: "Only immutable diffs (Blueprint)", + resultType: Blueprint, // Does NOT check immutables + allDiffs: []BytecodeDifference{ + {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, }, - targetID: 789, - want: "arrayNode", + wantCodeDiffs: []BytecodeDifference{ + {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, + }, // Immutable diffs treated as code diffs + wantImmDiffs: []BytecodeDifference{}, + wantHasCode: true, }, { - name: "Node not found", - node: map[string]interface{}{ - "id": float64(111), - "name": "wrongNode", + name: "Mixed diffs (SplitBlueprintPart1)", + resultType: SplitBlueprintPart1, // Does NOT check immutables + allDiffs: []BytecodeDifference{ + {Start: 1, Length: 1, Expected: "aa", Actual: "bb", InImmutable: false}, + {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, }, - targetID: 999, - want: "", + wantCodeDiffs: []BytecodeDifference{ + {Start: 1, Length: 1, Expected: "aa", Actual: "bb", InImmutable: false}, + {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, + }, // Immutable diffs treated as code diffs + wantImmDiffs: []BytecodeDifference{}, + wantHasCode: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := findNodeName(tt.node, tt.targetID) - assert.Equal(t, tt.want, got) + result := &VerificationResult{ + Type: tt.resultType, + Differences: tt.allDiffs, + } + gotCodeDiffs, gotImmDiffs := categorizeDifferences(result) + assert.Equal(t, tt.wantCodeDiffs, gotCodeDiffs, "Code differences mismatch") + assert.Equal(t, tt.wantImmDiffs, gotImmDiffs, "Immutable differences mismatch") + + // Test hasCodeDifferences as well + assert.Equal(t, tt.wantHasCode, hasCodeDifferences(result), "hasCodeDifferences mismatch") }) } } -func TestGetImmutableReferences(t *testing.T) { +// --- Tests for verify*Logic functions --- +// Note: These tests focus on the orchestration logic, assuming dependencies like +// getOnchainBytecode, ccom.ReadForgeArtifact, and compareBytecode work correctly (or are mocked implicitly). +// They primarily check the structure and fields of the returned VerificationResult. + +// Mock implementations (replace with actual mocking library if needed) +var mockBytecodeStore = make(map[string]string) +var mockArtifactStore = make(map[string]*solc.ForgeArtifact) +var mockReadArtifactError error +var mockGetBytecodeError error + +// setupMocks resets mock state and installs mock implementations for the current test. +func setupMocks(t *testing.T) { + t.Helper() + + mockBytecodeStore = make(map[string]string) + mockArtifactStore = make(map[string]*solc.ForgeArtifact) // Although ReadForgeArtifact isn't directly mocked here now + mockReadArtifactError = nil + mockGetBytecodeError = nil + + // Keep track of the original implementation + originalGetBytecode := getOnchainBytecodeImpl + + // Define the mock implementation + getOnchainBytecodeImpl = func(client *ethclient.Client, addr common.Address) (string, error) { + if mockGetBytecodeError != nil { + // Check if the error is specific to this address (optional enhancement) + // For now, any error applies globally. + return "", mockGetBytecodeError + } + code, ok := mockBytecodeStore[addr.Hex()] + if !ok { + // Return 0x for unknown addresses to simulate no code found, common case + return "0x", fmt.Errorf("no code found at address (mock)") + } + return code, nil + } + + // Use t.Cleanup to restore the original implementation after the test + t.Cleanup(func() { + getOnchainBytecodeImpl = originalGetBytecode + }) +} + +func TestVerifyDeployedContractLogic(t *testing.T) { + artifactPath := "/mock/MyContract.json" // Path is still used for metadata + contractName := "MyContract" + address := common.HexToAddress("0x1234567890123456789012345678901234567890") + expectedCode := "0x6080604052348015600f57600080fd5b50604051602080606f8339810160405280600a5f5260005f60005f5151f3fe" + actualCodeMatch := expectedCode + actualCodeMismatch := "0x6080604052348015600f57600080fd5b50604051602080606f8339810160405280ffff5f5260005f60005f5151f3fe" // Mismatch '600a' -> 'ffff' (same length) + tests := []struct { - name string - artifact map[string]interface{} - want map[string][]ImmutableReference - wantLen int + name string + artifact *solc.ForgeArtifact + mockSetup func() + wantErr bool + wantErrContent string + wantDiffs bool + wantImmutables bool }{ { - name: "Forge/Foundry format", - artifact: map[string]interface{}{ - "deployedBytecode": map[string]interface{}{ - "immutableReferences": map[string]interface{}{ - "123": []interface{}{ - map[string]interface{}{ - "start": float64(10), - "length": float64(32), - }, - }, - }, - }, - "ast": map[string]interface{}{ - "nodes": []interface{}{ - map[string]interface{}{ - "id": float64(123), - "name": "testVar", - }, - }, - }, + name: "Match", + artifact: newTestArtifact(withDeployedBytecode(expectedCode)), + mockSetup: func() { + mockBytecodeStore[address.Hex()] = actualCodeMatch }, - want: map[string][]ImmutableReference{ - "testVar": { - { - Offset: 10, - Length: 32, - Value: "", - }, - }, + wantErr: false, + wantDiffs: false, + wantImmutables: false, + }, + { + name: "Mismatch", + artifact: newTestArtifact(withDeployedBytecode(expectedCode)), + mockSetup: func() { + mockBytecodeStore[address.Hex()] = actualCodeMismatch }, - wantLen: 1, + wantErr: false, + wantDiffs: true, + wantImmutables: false, }, { - name: "Standard format", - artifact: map[string]interface{}{ - "immutableReferences": map[string]interface{}{ - "456": []interface{}{ - []interface{}{float64(20), float64(16)}, - }, - }, - "ast": map[string]interface{}{ - "nodes": []interface{}{ - map[string]interface{}{ - "id": float64(456), - "name": "anotherVar", - }, - }, - }, + name: "Get bytecode error", + artifact: newTestArtifact(withDeployedBytecode(expectedCode)), + mockSetup: func() { + mockGetBytecodeError = fmt.Errorf("rpc is down") }, - want: map[string][]ImmutableReference{ - "anotherVar": { - { - Offset: 20, - Length: 16, - Value: "", - }, - }, + wantErr: true, + wantErrContent: "getting onchain bytecode: rpc is down", + }, + { + name: "No code at address", + artifact: newTestArtifact(withDeployedBytecode(expectedCode)), + mockSetup: func() { + // No entry in mockBytecodeStore triggers the mock's error }, - wantLen: 1, + wantErr: true, + wantErrContent: "no code found at address (mock)", }, { - name: "No immutable references", - artifact: map[string]interface{}{ - "deployedBytecode": map[string]interface{}{}, + name: "Match with immutables", + artifact: newTestArtifact( + withDeployedBytecode("0xAAAABBBBCCCCDDDD"), // Expected + withAstNodes(createTestAstNodes()), + withImmutableRefs(solc.ImmutableReferences{ + "5": {{Start: 2, Length: 2}}, // BBBBB + }), + ), + mockSetup: func() { + mockBytecodeStore[address.Hex()] = "0xAAAA1122CCCCDDDD" // Actual (diff only in immutable) }, - want: map[string][]ImmutableReference{}, - wantLen: 0, + wantErr: false, + wantDiffs: true, // compareBytecode returns diffs, but categorizeDifferences handles it + wantImmutables: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := getImmutableReferences(tt.artifact) - assert.NoError(t, err) - assert.Equal(t, tt.wantLen, len(got)) - - // Check specific values for non-empty cases - if tt.wantLen > 0 { - for k, v := range tt.want { - assert.Contains(t, got, k) - assert.Equal(t, v[0].Offset, got[k][0].Offset) - assert.Equal(t, v[0].Length, got[k][0].Length) + setupMocks(t) + tt.mockSetup() + + // Pass nil client because getOnchainBytecodeImpl is mocked + result := verifyDeployedContractLogic(nil, tt.artifact, contractName, artifactPath, address) + + if tt.wantErr { + require.Error(t, result.ProcessError) + if tt.wantErrContent != "" { + assert.Contains(t, result.ProcessError.Error(), tt.wantErrContent) + } + } else { + require.NoError(t, result.ProcessError) + assert.Equal(t, DeployedContract, result.Type) + assert.Equal(t, address.Hex(), result.Address) + assert.Equal(t, artifactPath, result.ArtifactPath) + assert.Equal(t, contractName, result.ContractName) + if tt.wantDiffs { + assert.NotEmpty(t, result.Differences) + } else { + assert.Empty(t, result.Differences) + } + if tt.wantImmutables { + assert.NotEmpty(t, result.ImmutableInfos) + } else { + assert.Empty(t, result.ImmutableInfos) + } + // Check categorization for the immutable case + if tt.name == "Match with immutables" { + assert.False(t, hasCodeDifferences(result), "Should have no *code* differences") } } }) } } -func TestIsInImmutableReference(t *testing.T) { - immutableRefs := map[string][]ImmutableReference{ - "var1": { - {Offset: 10, Length: 5, Value: ""}, - }, - "var2": { - {Offset: 20, Length: 10, Value: ""}, - {Offset: 40, Length: 5, Value: ""}, - }, - } +func TestVerifyBlueprintLogic(t *testing.T) { + targetArtifactPath := "/mock/TargetContract.json" + targetContractName := "TargetContract" + blueprintAddress := common.HexToAddress("0xABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCD") + blueprintFieldName := "TheBlueprint" + creationCode := "608060405234801561001057600080fd5b5061015ff3" + expectedBlueprintCode := blueprintPreamble + creationCode + actualCodeMatch := expectedBlueprintCode + actualCodeMismatch := blueprintPreamble + "ffffff" + creationCode[6:] // Mismatch after preamble tests := []struct { - name string - position int - wantIn bool - wantVarName string - wantRef bool + name string + artifact *solc.ForgeArtifact + mockSetup func() + wantErr bool + wantErrContent string + wantDiffs bool }{ { - name: "Inside first variable", - position: 12, - wantIn: true, - wantVarName: "var1", - wantRef: true, - }, - { - name: "At start of first variable", - position: 10, - wantIn: true, - wantVarName: "var1", - wantRef: true, + name: "Match", + artifact: newTestArtifact(withCreationBytecode("0x" + creationCode)), + mockSetup: func() { + mockBytecodeStore[blueprintAddress.Hex()] = actualCodeMatch + }, + wantErr: false, + wantDiffs: false, }, { - name: "At end of first variable (exclusive)", - position: 15, - wantIn: false, - wantVarName: "", - wantRef: false, + name: "Mismatch", + artifact: newTestArtifact(withCreationBytecode("0x" + creationCode)), + mockSetup: func() { + mockBytecodeStore[blueprintAddress.Hex()] = actualCodeMismatch + }, + wantErr: false, + wantDiffs: true, }, { - name: "Inside second variable, first reference", - position: 25, - wantIn: true, - wantVarName: "var2", - wantRef: true, + name: "Get blueprint bytecode error", + artifact: newTestArtifact(withCreationBytecode("0x" + creationCode)), + mockSetup: func() { + mockGetBytecodeError = fmt.Errorf("rpc is down") + }, + wantErr: true, + wantErrContent: "getting onchain bytecode for blueprint", }, { - name: "Inside second variable, second reference", - position: 42, - wantIn: true, - wantVarName: "var2", - wantRef: true, + name: "No code at address", + artifact: newTestArtifact(withCreationBytecode("0x" + creationCode)), + mockSetup: func() { + // No entry in mockBytecodeStore + }, + wantErr: true, + wantErrContent: "no code found at address (mock)", }, { - name: "Outside any variable", - position: 30, - wantIn: false, - wantVarName: "", - wantRef: false, + name: "No creation code in artifact", + artifact: newTestArtifact(withCreationBytecode("0x")), // Empty creation code + mockSetup: func() {}, // Bytecode doesn't matter here + wantErr: true, + wantErrContent: "no creation bytecode found", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - inImmutable, varName, ref := isInImmutableReference(tt.position, immutableRefs) - assert.Equal(t, tt.wantIn, inImmutable) - assert.Equal(t, tt.wantVarName, varName) - if tt.wantRef { - assert.NotNil(t, ref) + setupMocks(t) + tt.mockSetup() + + // Pass nil client because getOnchainBytecodeImpl is mocked + result := verifyBlueprintLogic(nil, tt.artifact, targetContractName, targetArtifactPath, blueprintAddress, blueprintFieldName) + + if tt.wantErr { + require.Error(t, result.ProcessError) + if tt.wantErrContent != "" { + assert.Contains(t, result.ProcessError.Error(), tt.wantErrContent) + } } else { - assert.Nil(t, ref) + require.NoError(t, result.ProcessError) + assert.Equal(t, Blueprint, result.Type) + assert.Equal(t, blueprintAddress.Hex(), result.Address) + assert.Equal(t, targetArtifactPath, result.ArtifactPath) + assert.Equal(t, blueprintFieldName, result.FieldName) + assert.Equal(t, targetContractName, result.TargetContract) + assert.Equal(t, fmt.Sprintf("Blueprint for %s", targetContractName), result.ContractName) + if tt.wantDiffs { + assert.NotEmpty(t, result.Differences) + assert.True(t, hasCodeDifferences(result)) // Any blueprint diff is a code diff + } else { + assert.Empty(t, result.Differences) + } } }) } } -func TestFindDifferences(t *testing.T) { +func TestVerifySplitBlueprintLogic(t *testing.T) { + targetArtifactPath := "/mock/SplitTarget.json" + targetContractName := "SplitTarget" + address1 := common.HexToAddress("0xAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBB") + address2 := common.HexToAddress("0xCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDDDDD") + fieldName1 := "SplitBP1" + fieldName2 := "SplitBP2" + + // Create creation code longer than maxInitCodeSize + part1Hex := strings.Repeat("11", maxInitCodeSize) + part2Hex := strings.Repeat("22", 10) + fullCreationCode := part1Hex + part2Hex + artifactLong := newTestArtifact(withCreationBytecode("0x" + fullCreationCode)) + expectedBP1 := blueprintPreamble + part1Hex + expectedBP2 := blueprintPreamble + part2Hex + + // Create creation code shorter than maxInitCodeSize + shortCodeHex := strings.Repeat("33", 100) + artifactShort := newTestArtifact(withCreationBytecode("0x" + shortCodeHex)) + expectedShortBP1 := blueprintPreamble + shortCodeHex + expectedShortBP2 := blueprintPreamble // Empty part 2 + tests := []struct { - name string - expectedBytecode string - actualBytecode string - immutableRefs map[string][]ImmutableReference - wantDiffs int - wantImmutable int - wantErr bool + name string + artifact *solc.ForgeArtifact + mockSetup func() + wantErr1 bool + wantErr2 bool + wantErr1Cont string + wantErr2Cont string + wantDiffs1 bool + wantDiffs2 bool }{ { - name: "No differences", - expectedBytecode: "0x1234567890abcdef", - actualBytecode: "0x1234567890abcdef", - immutableRefs: map[string][]ImmutableReference{}, - wantDiffs: 0, - wantImmutable: 0, - wantErr: false, - }, - { - name: "Difference in immutable reference", - expectedBytecode: "0x1234000000abcdef", - actualBytecode: "0x1234fffffeabcdef", - immutableRefs: map[string][]ImmutableReference{ - "testVar": { - {Offset: 2, Length: 3, Value: ""}, - }, + name: "Match Long Code", + artifact: artifactLong, + mockSetup: func() { + mockBytecodeStore[address1.Hex()] = expectedBP1 + mockBytecodeStore[address2.Hex()] = expectedBP2 }, - wantDiffs: 1, - wantImmutable: 1, - wantErr: false, + wantErr1: false, + wantErr2: false, + wantDiffs1: false, + wantDiffs2: false, }, { - name: "Difference outside immutable reference", - expectedBytecode: "0x1234567890abcdef", - actualBytecode: "0x1234567890abcdee", // Last byte different - immutableRefs: map[string][]ImmutableReference{}, - wantDiffs: 1, - wantImmutable: 0, - wantErr: false, + name: "Match Short Code (part 2 is empty)", + artifact: artifactShort, + mockSetup: func() { + mockBytecodeStore[address1.Hex()] = expectedShortBP1 + mockBytecodeStore[address2.Hex()] = expectedShortBP2 // Expect preamble only for empty code + }, + wantErr1: false, + wantErr2: false, + wantDiffs1: false, + wantDiffs2: false, }, { - name: "Multiple differences", - expectedBytecode: "0x1234000000abcdef", - actualBytecode: "0x1234fffffeabcdee", // Immutable and non-immutable differences - immutableRefs: map[string][]ImmutableReference{ - "testVar": { - {Offset: 2, Length: 3, Value: ""}, - }, + name: "Mismatch Part 1", + artifact: artifactLong, + mockSetup: func() { + mockBytecodeStore[address1.Hex()] = blueprintPreamble + "ff" + part1Hex[2:] + mockBytecodeStore[address2.Hex()] = expectedBP2 + }, + wantErr1: false, + wantErr2: false, + wantDiffs1: true, + wantDiffs2: false, + }, + { + name: "Mismatch Part 2", + artifact: artifactLong, + mockSetup: func() { + mockBytecodeStore[address1.Hex()] = expectedBP1 + mockBytecodeStore[address2.Hex()] = blueprintPreamble + strings.Repeat("ff", 10) // Match length of part2Hex }, - wantDiffs: 2, - wantImmutable: 1, - wantErr: false, + wantErr1: false, + wantErr2: false, + wantDiffs1: false, + wantDiffs2: true, }, { - name: "Invalid expected bytecode", - expectedBytecode: "0xZZZZ", - actualBytecode: "0x1234", - immutableRefs: map[string][]ImmutableReference{}, - wantDiffs: 0, - wantImmutable: 0, - wantErr: true, + name: "Error Getting Part 1 Code", + artifact: artifactLong, + mockSetup: func() { + mockGetBytecodeError = fmt.Errorf("rpc1 down") + // Need more specific mock to only fail for address1, assume global for now + mockBytecodeStore[address2.Hex()] = expectedBP2 // Set this so part 2 fetch succeeds + }, + wantErr1: true, + wantErr1Cont: "getting onchain code for part 1", + wantErr2: true, // Error on addr1 implies error on addr2 too with global mock + wantErr2Cont: "getting onchain code for part 2", }, { - name: "Invalid actual bytecode", - expectedBytecode: "0x1234", - actualBytecode: "0xZZZZ", - immutableRefs: map[string][]ImmutableReference{}, - wantDiffs: 0, - wantImmutable: 0, - wantErr: true, + name: "Error Getting Part 2 Code", + artifact: artifactLong, + mockSetup: func() { + var getBytecodeErr error = fmt.Errorf("rpc2 down") + originalGetBytecode := getOnchainBytecodeImpl + getOnchainBytecodeImpl = func(client *ethclient.Client, addr common.Address) (string, error) { + if addr == address1 { + return expectedBP1, nil + } else if addr == address2 { + return "", getBytecodeErr // Specific error for address 2 + } + return "", fmt.Errorf("unexpected address in mock") + } + t.Cleanup(func() { getOnchainBytecodeImpl = originalGetBytecode }) + }, + wantErr1: false, // Fetch for part 1 succeeds + wantErr2: true, + wantErr2Cont: "rpc2 down", // Check for the core error message }, { - name: "Different lengths", - expectedBytecode: "0x1234", - actualBytecode: "0x123456", - immutableRefs: map[string][]ImmutableReference{}, - wantDiffs: 1, - wantImmutable: 0, - wantErr: false, + name: "No creation code in artifact", + artifact: newTestArtifact(withCreationBytecode("0x")), // Empty + mockSetup: func() { + // Bytecode fetch doesn't matter + }, + wantErr1: true, + wantErr1Cont: "no creation bytecode found", + wantErr2: true, + wantErr2Cont: "no creation bytecode found", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - diffs, err := findDifferences(tt.expectedBytecode, tt.actualBytecode, tt.immutableRefs) + setupMocks(t) // Base setup, might be overridden by tt.mockSetup + tt.mockSetup() - if tt.wantErr { - assert.Error(t, err) - return - } + // Pass nil client because getOnchainBytecodeImpl is mocked + res1, res2 := verifySplitBlueprintLogic(nil, tt.artifact, targetContractName, targetArtifactPath, address1, address2, fieldName1, fieldName2) - assert.NoError(t, err) - assert.Equal(t, tt.wantDiffs, len(diffs)) + // Check Result 1 + if tt.wantErr1 { + require.Error(t, res1.ProcessError) + if tt.wantErr1Cont != "" { + assert.Contains(t, res1.ProcessError.Error(), tt.wantErr1Cont) + } + } else { + require.NoError(t, res1.ProcessError) + assert.Equal(t, SplitBlueprintPart1, res1.Type) + assert.Equal(t, address1.Hex(), res1.Address) + assert.Equal(t, targetArtifactPath, res1.ArtifactPath) + assert.Equal(t, fieldName1, res1.FieldName) + assert.Equal(t, targetContractName, res1.TargetContract) + if tt.wantDiffs1 { + assert.NotEmpty(t, res1.Differences) + } else { + assert.Empty(t, res1.Differences) + } + } - // Count immutable differences - immutableCount := 0 - for _, diff := range diffs { - if diff.InImmutable { - immutableCount++ + // Check Result 2 + if tt.wantErr2 { + require.Error(t, res2.ProcessError) + if tt.wantErr2Cont != "" { + assert.Contains(t, res2.ProcessError.Error(), tt.wantErr2Cont) + } + } else { + require.NoError(t, res2.ProcessError) + assert.Equal(t, SplitBlueprintPart2, res2.Type) + assert.Equal(t, address2.Hex(), res2.Address) + assert.Equal(t, targetArtifactPath, res2.ArtifactPath) + assert.Equal(t, fieldName2, res2.FieldName) + assert.Equal(t, targetContractName, res2.TargetContract) + if tt.wantDiffs2 { + assert.NotEmpty(t, res2.Differences) + } else { + assert.Empty(t, res2.Differences) } } - assert.Equal(t, tt.wantImmutable, immutableCount) }) } } - -func TestFindDifferencesDetailed(t *testing.T) { - // Test with specific bytecode patterns to verify exact difference detection - expected := "0x1234567890abcdef" - actual := "0x1234FF7890abFFef" - - immutableRefs := map[string][]ImmutableReference{ - "testVar": { - {Offset: 2, Length: 1, Value: ""}, // Covers the "FF" difference - }, - } - - diffs, err := findDifferences(expected, actual, immutableRefs) - require.NoError(t, err) - - // Should find 2 differences: one in immutable ref, one outside - assert.Equal(t, 2, len(diffs)) - - // First difference should be in immutable reference - assert.True(t, diffs[0].InImmutable) - assert.Equal(t, "testVar", diffs[0].ImmutableName) - assert.Equal(t, 2, diffs[0].Start) // 0-based index after 0x prefix - assert.Equal(t, 1, diffs[0].Length) - assert.Equal(t, "56", diffs[0].Expected) - assert.Equal(t, "ff", diffs[0].Actual) - - // Second difference should be outside immutable reference - assert.False(t, diffs[1].InImmutable) - assert.Equal(t, "", diffs[1].ImmutableName) - assert.Equal(t, 6, diffs[1].Start) // 0-based index after 0x prefix - assert.Equal(t, 1, diffs[1].Length) - assert.Equal(t, "cd", diffs[1].Expected) - assert.Equal(t, "ff", diffs[1].Actual) - - // Check that immutable reference value was captured - assert.Equal(t, "ff", immutableRefs["testVar"][0].Value) -} - -// TestPrintDifferences doesn't test the actual output (which goes to stdout) -// but ensures the function doesn't panic with various inputs -func TestPrintDifferences(t *testing.T) { - differences := []BytecodeDifference{ - { - Start: 10, - Length: 2, - Expected: "1234", - Actual: "5678", - InImmutable: true, - ImmutableName: "testVar", - }, - { - Start: 20, - Length: 1, - Expected: "ab", - Actual: "cd", - InImmutable: false, - ImmutableName: "", - }, - } - - immutableRefs := map[string][]ImmutableReference{ - "testVar": { - {Offset: 10, Length: 2, Value: "5678"}, - }, - } - - // This should not panic - printDifferences(differences, immutableRefs, false) - - // Test with empty differences - printDifferences([]BytecodeDifference{}, immutableRefs, false) - - // Test with empty immutable references - printDifferences(differences, map[string][]ImmutableReference{}, false) -} - -// Test handling of bytecode with and without 0x prefix -func TestBytecodePrefix(t *testing.T) { - expected := "0x1234" - actual := "1234" // No prefix - - diffs, err := findDifferences(expected, actual, map[string][]ImmutableReference{}) - require.NoError(t, err) - assert.Equal(t, 0, len(diffs), "Should handle different prefixes correctly") - - // Test the reverse - diffs, err = findDifferences(actual, expected, map[string][]ImmutableReference{}) - require.NoError(t, err) - assert.Equal(t, 0, len(diffs), "Should handle different prefixes correctly") -} - -// Test consecutive differences are properly grouped -func TestConsecutiveDifferences(t *testing.T) { - expected := "0x123456789a" - actual := "0x12FFFF789a" // Two consecutive bytes different - - diffs, err := findDifferences(expected, actual, map[string][]ImmutableReference{}) - require.NoError(t, err) - - // Should group consecutive differences - assert.Equal(t, 1, len(diffs), "Consecutive differences should be grouped") - assert.Equal(t, 2, diffs[0].Length, "Difference should span 2 bytes") - assert.Equal(t, "3456", diffs[0].Expected) - assert.Equal(t, "ffff", diffs[0].Actual) -} - -// Test with empty bytecode -func TestEmptyBytecode(t *testing.T) { - _, err := findDifferences("0x", "0x", map[string][]ImmutableReference{}) - assert.NoError(t, err, "Should handle empty bytecode") - - _, err = findDifferences("", "", map[string][]ImmutableReference{}) - assert.NoError(t, err, "Should handle empty bytecode without prefix") -} - -// Test with invalid hex characters -func TestInvalidHex(t *testing.T) { - _, err := findDifferences("0x123Z", "0x1234", map[string][]ImmutableReference{}) - assert.Error(t, err, "Should detect invalid hex in expected bytecode") - - _, err = findDifferences("0x1234", "0x123Z", map[string][]ImmutableReference{}) - assert.Error(t, err, "Should detect invalid hex in actual bytecode") -} From 84a6c730a2c278b73671d9916df23b2e404d3ca9 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 15 Apr 2025 15:00:25 -0400 Subject: [PATCH 06/14] feat: use a contract instead --- .../verify-bytecode/bindings/opcm200.go | 1149 --------------- .../scripts/checks/verify-bytecode/main.go | 1227 ----------------- .../checks/verify-bytecode/main_test.go | 1066 -------------- .../scripts/deploy/VerifyOPCM.s.sol | 468 +++++++ .../test/L1/OPContractsManager.t.sol | 13 + .../test/scripts/VerifyOPCM.t.sol | 214 +++ 6 files changed, 695 insertions(+), 3442 deletions(-) delete mode 100644 packages/contracts-bedrock/scripts/checks/verify-bytecode/bindings/opcm200.go delete mode 100644 packages/contracts-bedrock/scripts/checks/verify-bytecode/main.go delete mode 100644 packages/contracts-bedrock/scripts/checks/verify-bytecode/main_test.go create mode 100644 packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol create mode 100644 packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol diff --git a/packages/contracts-bedrock/scripts/checks/verify-bytecode/bindings/opcm200.go b/packages/contracts-bedrock/scripts/checks/verify-bytecode/bindings/opcm200.go deleted file mode 100644 index b31af018561..00000000000 --- a/packages/contracts-bedrock/scripts/checks/verify-bytecode/bindings/opcm200.go +++ /dev/null @@ -1,1149 +0,0 @@ -// Code generated - DO NOT EDIT. -// This file is a generated binding and any manual changes will be lost. - -package bindings - -import ( - "errors" - "math/big" - "strings" - - ethereum "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/event" -) - -// Reference imports to suppress errors if they are not otherwise used. -var ( - _ = errors.New - _ = big.NewInt - _ = strings.NewReader - _ = ethereum.NotFound - _ = bind.Bind - _ = common.Big1 - _ = types.BloomLookup - _ = event.NewSubscription -) - -// OPContractsManagerAddGameInput is an auto generated low-level Go binding around an user-defined struct. -type OPContractsManagerAddGameInput struct { - SaltMixer string - SystemConfig common.Address - ProxyAdmin common.Address - DelayedWETH common.Address - DisputeGameType uint32 - DisputeAbsolutePrestate [32]byte - DisputeMaxGameDepth *big.Int - DisputeSplitDepth *big.Int - DisputeClockExtension uint64 - DisputeMaxClockDuration uint64 - InitialBond *big.Int - Vm common.Address - Permissioned bool -} - -// OPContractsManagerAddGameOutput is an auto generated low-level Go binding around an user-defined struct. -type OPContractsManagerAddGameOutput struct { - DelayedWETH common.Address - FaultDisputeGame common.Address -} - -// OPContractsManagerBlueprints is an auto generated low-level Go binding around an user-defined struct. -type OPContractsManagerBlueprints struct { - AddressManager common.Address - Proxy common.Address - ProxyAdmin common.Address - L1ChugSplashProxy common.Address - ResolvedDelegateProxy common.Address - PermissionedDisputeGame1 common.Address - PermissionedDisputeGame2 common.Address - PermissionlessDisputeGame1 common.Address - PermissionlessDisputeGame2 common.Address -} - -// OPContractsManagerDeployInput is an auto generated low-level Go binding around an user-defined struct. -type OPContractsManagerDeployInput struct { - Roles OPContractsManagerRoles - BasefeeScalar uint32 - BlobBasefeeScalar uint32 - L2ChainId *big.Int - StartingAnchorRoot []byte - SaltMixer string - GasLimit uint64 - DisputeGameType uint32 - DisputeAbsolutePrestate [32]byte - DisputeMaxGameDepth *big.Int - DisputeSplitDepth *big.Int - DisputeClockExtension uint64 - DisputeMaxClockDuration uint64 -} - -// OPContractsManagerDeployOutput is an auto generated low-level Go binding around an user-defined struct. -type OPContractsManagerDeployOutput struct { - OpChainProxyAdmin common.Address - AddressManager common.Address - L1ERC721BridgeProxy common.Address - SystemConfigProxy common.Address - OptimismMintableERC20FactoryProxy common.Address - L1StandardBridgeProxy common.Address - L1CrossDomainMessengerProxy common.Address - OptimismPortalProxy common.Address - DisputeGameFactoryProxy common.Address - AnchorStateRegistryProxy common.Address - FaultDisputeGame common.Address - PermissionedDisputeGame common.Address - DelayedWETHPermissionedGameProxy common.Address - DelayedWETHPermissionlessGameProxy common.Address -} - -// OPContractsManagerImplementations is an auto generated low-level Go binding around an user-defined struct. -type OPContractsManagerImplementations struct { - SuperchainConfigImpl common.Address - ProtocolVersionsImpl common.Address - L1ERC721BridgeImpl common.Address - OptimismPortalImpl common.Address - SystemConfigImpl common.Address - OptimismMintableERC20FactoryImpl common.Address - L1CrossDomainMessengerImpl common.Address - L1StandardBridgeImpl common.Address - DisputeGameFactoryImpl common.Address - AnchorStateRegistryImpl common.Address - DelayedWETHImpl common.Address - MipsImpl common.Address -} - -// OPContractsManagerOpChainConfig is an auto generated low-level Go binding around an user-defined struct. -type OPContractsManagerOpChainConfig struct { - SystemConfigProxy common.Address - ProxyAdmin common.Address - AbsolutePrestate [32]byte -} - -// OPContractsManagerRoles is an auto generated low-level Go binding around an user-defined struct. -type OPContractsManagerRoles struct { - OpChainProxyAdminOwner common.Address - SystemConfigOwner common.Address - Batcher common.Address - UnsafeBlockSigner common.Address - Proposer common.Address - Challenger common.Address -} - -// Opcm200MetaData contains all meta data concerning the Opcm200 contract. -var Opcm200MetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[{\"internalType\":\"contractISuperchainConfig\",\"name\":\"_superchainConfig\",\"type\":\"address\"},{\"internalType\":\"contractIProtocolVersions\",\"name\":\"_protocolVersions\",\"type\":\"address\"},{\"internalType\":\"contractIProxyAdmin\",\"name\":\"_superchainProxyAdmin\",\"type\":\"address\"},{\"internalType\":\"string\",\"name\":\"_l1ContractsRelease\",\"type\":\"string\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"addressManager\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"proxy\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"proxyAdmin\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1ChugSplashProxy\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"resolvedDelegateProxy\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionedDisputeGame1\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionedDisputeGame2\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionlessDisputeGame1\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionlessDisputeGame2\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.Blueprints\",\"name\":\"_blueprints\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"superchainConfigImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"protocolVersionsImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1ERC721BridgeImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optimismPortalImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"systemConfigImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optimismMintableERC20FactoryImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1CrossDomainMessengerImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1StandardBridgeImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"disputeGameFactoryImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"anchorStateRegistryImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"delayedWETHImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"mipsImpl\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.Implementations\",\"name\":\"_implementations\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"_upgradeController\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"components\":[{\"internalType\":\"string\",\"name\":\"saltMixer\",\"type\":\"string\"},{\"internalType\":\"contractISystemConfig\",\"name\":\"systemConfig\",\"type\":\"address\"},{\"internalType\":\"contractIProxyAdmin\",\"name\":\"proxyAdmin\",\"type\":\"address\"},{\"internalType\":\"contractIDelayedWETH\",\"name\":\"delayedWETH\",\"type\":\"address\"},{\"internalType\":\"GameType\",\"name\":\"disputeGameType\",\"type\":\"uint32\"},{\"internalType\":\"Claim\",\"name\":\"disputeAbsolutePrestate\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"disputeMaxGameDepth\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"disputeSplitDepth\",\"type\":\"uint256\"},{\"internalType\":\"Duration\",\"name\":\"disputeClockExtension\",\"type\":\"uint64\"},{\"internalType\":\"Duration\",\"name\":\"disputeMaxClockDuration\",\"type\":\"uint64\"},{\"internalType\":\"uint256\",\"name\":\"initialBond\",\"type\":\"uint256\"},{\"internalType\":\"contractIBigStepper\",\"name\":\"vm\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"permissioned\",\"type\":\"bool\"}],\"internalType\":\"structOPContractsManager.AddGameInput[]\",\"name\":\"_gameConfigs\",\"type\":\"tuple[]\"}],\"name\":\"addGameType\",\"outputs\":[{\"components\":[{\"internalType\":\"contractIDelayedWETH\",\"name\":\"delayedWETH\",\"type\":\"address\"},{\"internalType\":\"contractIFaultDisputeGame\",\"name\":\"faultDisputeGame\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.AddGameOutput[]\",\"name\":\"\",\"type\":\"tuple[]\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"blueprints\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"addressManager\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"proxy\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"proxyAdmin\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1ChugSplashProxy\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"resolvedDelegateProxy\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionedDisputeGame1\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionedDisputeGame2\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionlessDisputeGame1\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"permissionlessDisputeGame2\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.Blueprints\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_l2ChainId\",\"type\":\"uint256\"}],\"name\":\"chainIdToBatchInboxAddress\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"opChainProxyAdminOwner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"systemConfigOwner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"batcher\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"unsafeBlockSigner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"proposer\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"challenger\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.Roles\",\"name\":\"roles\",\"type\":\"tuple\"},{\"internalType\":\"uint32\",\"name\":\"basefeeScalar\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"blobBasefeeScalar\",\"type\":\"uint32\"},{\"internalType\":\"uint256\",\"name\":\"l2ChainId\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"startingAnchorRoot\",\"type\":\"bytes\"},{\"internalType\":\"string\",\"name\":\"saltMixer\",\"type\":\"string\"},{\"internalType\":\"uint64\",\"name\":\"gasLimit\",\"type\":\"uint64\"},{\"internalType\":\"GameType\",\"name\":\"disputeGameType\",\"type\":\"uint32\"},{\"internalType\":\"Claim\",\"name\":\"disputeAbsolutePrestate\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"disputeMaxGameDepth\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"disputeSplitDepth\",\"type\":\"uint256\"},{\"internalType\":\"Duration\",\"name\":\"disputeClockExtension\",\"type\":\"uint64\"},{\"internalType\":\"Duration\",\"name\":\"disputeMaxClockDuration\",\"type\":\"uint64\"}],\"internalType\":\"structOPContractsManager.DeployInput\",\"name\":\"_input\",\"type\":\"tuple\"}],\"name\":\"deploy\",\"outputs\":[{\"components\":[{\"internalType\":\"contractIProxyAdmin\",\"name\":\"opChainProxyAdmin\",\"type\":\"address\"},{\"internalType\":\"contractIAddressManager\",\"name\":\"addressManager\",\"type\":\"address\"},{\"internalType\":\"contractIL1ERC721Bridge\",\"name\":\"l1ERC721BridgeProxy\",\"type\":\"address\"},{\"internalType\":\"contractISystemConfig\",\"name\":\"systemConfigProxy\",\"type\":\"address\"},{\"internalType\":\"contractIOptimismMintableERC20Factory\",\"name\":\"optimismMintableERC20FactoryProxy\",\"type\":\"address\"},{\"internalType\":\"contractIL1StandardBridge\",\"name\":\"l1StandardBridgeProxy\",\"type\":\"address\"},{\"internalType\":\"contractIL1CrossDomainMessenger\",\"name\":\"l1CrossDomainMessengerProxy\",\"type\":\"address\"},{\"internalType\":\"contractIOptimismPortal2\",\"name\":\"optimismPortalProxy\",\"type\":\"address\"},{\"internalType\":\"contractIDisputeGameFactory\",\"name\":\"disputeGameFactoryProxy\",\"type\":\"address\"},{\"internalType\":\"contractIAnchorStateRegistry\",\"name\":\"anchorStateRegistryProxy\",\"type\":\"address\"},{\"internalType\":\"contractIFaultDisputeGame\",\"name\":\"faultDisputeGame\",\"type\":\"address\"},{\"internalType\":\"contractIPermissionedDisputeGame\",\"name\":\"permissionedDisputeGame\",\"type\":\"address\"},{\"internalType\":\"contractIDelayedWETH\",\"name\":\"delayedWETHPermissionedGameProxy\",\"type\":\"address\"},{\"internalType\":\"contractIDelayedWETH\",\"name\":\"delayedWETHPermissionlessGameProxy\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.DeployOutput\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"implementations\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"superchainConfigImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"protocolVersionsImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1ERC721BridgeImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optimismPortalImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"systemConfigImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"optimismMintableERC20FactoryImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1CrossDomainMessengerImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"l1StandardBridgeImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"disputeGameFactoryImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"anchorStateRegistryImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"delayedWETHImpl\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"mipsImpl\",\"type\":\"address\"}],\"internalType\":\"structOPContractsManager.Implementations\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"isRC\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"l1ContractsRelease\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"protocolVersions\",\"outputs\":[{\"internalType\":\"contractIProtocolVersions\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bool\",\"name\":\"_isRC\",\"type\":\"bool\"}],\"name\":\"setRC\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"superchainConfig\",\"outputs\":[{\"internalType\":\"contractISuperchainConfig\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"superchainProxyAdmin\",\"outputs\":[{\"internalType\":\"contractIProxyAdmin\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"contractISystemConfig\",\"name\":\"systemConfigProxy\",\"type\":\"address\"},{\"internalType\":\"contractIProxyAdmin\",\"name\":\"proxyAdmin\",\"type\":\"address\"},{\"internalType\":\"Claim\",\"name\":\"absolutePrestate\",\"type\":\"bytes32\"}],\"internalType\":\"structOPContractsManager.OpChainConfig[]\",\"name\":\"_opChainConfigs\",\"type\":\"tuple[]\"}],\"name\":\"upgrade\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"upgradeController\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"version\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"l2ChainId\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"deployer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"deployOutput\",\"type\":\"bytes\"}],\"name\":\"Deployed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"l2ChainId\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"GameType\",\"name\":\"gameType\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"contractIDisputeGame\",\"name\":\"newDisputeGame\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"contractIDisputeGame\",\"name\":\"oldDisputeGame\",\"type\":\"address\"}],\"name\":\"GameTypeAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"l2ChainId\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"contractISystemConfig\",\"name\":\"systemConfig\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"upgrader\",\"type\":\"address\"}],\"name\":\"Upgraded\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"who\",\"type\":\"address\"}],\"name\":\"AddressHasNoCode\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"who\",\"type\":\"address\"}],\"name\":\"AddressNotFound\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"AlreadyReleased\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"BytesArrayTooLong\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"DeploymentFailed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EmptyInitcode\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"IdentityPrecompileCallFailed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidChainId\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidGameConfigs\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"role\",\"type\":\"string\"}],\"name\":\"InvalidRoleAddress\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidStartingAnchorRoot\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"LatestReleaseNotSet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NotABlueprint\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyDelegatecall\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"OnlyUpgradeController\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"PrestateNotSet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ReservedBitsSet\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"contractISystemConfig\",\"name\":\"systemConfig\",\"type\":\"address\"}],\"name\":\"SuperchainConfigMismatch\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"SuperchainProxyAdminMismatch\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"UnexpectedPreambleData\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"UnsupportedERCVersion\",\"type\":\"error\"}]", -} - -// Opcm200ABI is the input ABI used to generate the binding from. -// Deprecated: Use Opcm200MetaData.ABI instead. -var Opcm200ABI = Opcm200MetaData.ABI - -// Opcm200 is an auto generated Go binding around an Ethereum contract. -type Opcm200 struct { - Opcm200Caller // Read-only binding to the contract - Opcm200Transactor // Write-only binding to the contract - Opcm200Filterer // Log filterer for contract events -} - -// Opcm200Caller is an auto generated read-only Go binding around an Ethereum contract. -type Opcm200Caller struct { - contract *bind.BoundContract // Generic contract wrapper for the low level calls -} - -// Opcm200Transactor is an auto generated write-only Go binding around an Ethereum contract. -type Opcm200Transactor struct { - contract *bind.BoundContract // Generic contract wrapper for the low level calls -} - -// Opcm200Filterer is an auto generated log filtering Go binding around an Ethereum contract events. -type Opcm200Filterer struct { - contract *bind.BoundContract // Generic contract wrapper for the low level calls -} - -// Opcm200Session is an auto generated Go binding around an Ethereum contract, -// with pre-set call and transact options. -type Opcm200Session struct { - Contract *Opcm200 // Generic contract binding to set the session for - CallOpts bind.CallOpts // Call options to use throughout this session - TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session -} - -// Opcm200CallerSession is an auto generated read-only Go binding around an Ethereum contract, -// with pre-set call options. -type Opcm200CallerSession struct { - Contract *Opcm200Caller // Generic contract caller binding to set the session for - CallOpts bind.CallOpts // Call options to use throughout this session -} - -// Opcm200TransactorSession is an auto generated write-only Go binding around an Ethereum contract, -// with pre-set transact options. -type Opcm200TransactorSession struct { - Contract *Opcm200Transactor // Generic contract transactor binding to set the session for - TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session -} - -// Opcm200Raw is an auto generated low-level Go binding around an Ethereum contract. -type Opcm200Raw struct { - Contract *Opcm200 // Generic contract binding to access the raw methods on -} - -// Opcm200CallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. -type Opcm200CallerRaw struct { - Contract *Opcm200Caller // Generic read-only contract binding to access the raw methods on -} - -// Opcm200TransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. -type Opcm200TransactorRaw struct { - Contract *Opcm200Transactor // Generic write-only contract binding to access the raw methods on -} - -// NewOpcm200 creates a new instance of Opcm200, bound to a specific deployed contract. -func NewOpcm200(address common.Address, backend bind.ContractBackend) (*Opcm200, error) { - contract, err := bindOpcm200(address, backend, backend, backend) - if err != nil { - return nil, err - } - return &Opcm200{Opcm200Caller: Opcm200Caller{contract: contract}, Opcm200Transactor: Opcm200Transactor{contract: contract}, Opcm200Filterer: Opcm200Filterer{contract: contract}}, nil -} - -// NewOpcm200Caller creates a new read-only instance of Opcm200, bound to a specific deployed contract. -func NewOpcm200Caller(address common.Address, caller bind.ContractCaller) (*Opcm200Caller, error) { - contract, err := bindOpcm200(address, caller, nil, nil) - if err != nil { - return nil, err - } - return &Opcm200Caller{contract: contract}, nil -} - -// NewOpcm200Transactor creates a new write-only instance of Opcm200, bound to a specific deployed contract. -func NewOpcm200Transactor(address common.Address, transactor bind.ContractTransactor) (*Opcm200Transactor, error) { - contract, err := bindOpcm200(address, nil, transactor, nil) - if err != nil { - return nil, err - } - return &Opcm200Transactor{contract: contract}, nil -} - -// NewOpcm200Filterer creates a new log filterer instance of Opcm200, bound to a specific deployed contract. -func NewOpcm200Filterer(address common.Address, filterer bind.ContractFilterer) (*Opcm200Filterer, error) { - contract, err := bindOpcm200(address, nil, nil, filterer) - if err != nil { - return nil, err - } - return &Opcm200Filterer{contract: contract}, nil -} - -// bindOpcm200 binds a generic wrapper to an already deployed contract. -func bindOpcm200(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { - parsed, err := abi.JSON(strings.NewReader(Opcm200ABI)) - if err != nil { - return nil, err - } - return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil -} - -// Call invokes the (constant) contract method with params as input values and -// sets the output to result. The result type might be a single field for simple -// returns, a slice of interfaces for anonymous returns and a struct for named -// returns. -func (_Opcm200 *Opcm200Raw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { - return _Opcm200.Contract.Opcm200Caller.contract.Call(opts, result, method, params...) -} - -// Transfer initiates a plain transaction to move funds to the contract, calling -// its default method if one is available. -func (_Opcm200 *Opcm200Raw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { - return _Opcm200.Contract.Opcm200Transactor.contract.Transfer(opts) -} - -// Transact invokes the (paid) contract method with params as input values. -func (_Opcm200 *Opcm200Raw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { - return _Opcm200.Contract.Opcm200Transactor.contract.Transact(opts, method, params...) -} - -// Call invokes the (constant) contract method with params as input values and -// sets the output to result. The result type might be a single field for simple -// returns, a slice of interfaces for anonymous returns and a struct for named -// returns. -func (_Opcm200 *Opcm200CallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { - return _Opcm200.Contract.contract.Call(opts, result, method, params...) -} - -// Transfer initiates a plain transaction to move funds to the contract, calling -// its default method if one is available. -func (_Opcm200 *Opcm200TransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { - return _Opcm200.Contract.contract.Transfer(opts) -} - -// Transact invokes the (paid) contract method with params as input values. -func (_Opcm200 *Opcm200TransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { - return _Opcm200.Contract.contract.Transact(opts, method, params...) -} - -// Blueprints is a free data retrieval call binding the contract method 0xb51f9c2b. -// -// Solidity: function blueprints() view returns((address,address,address,address,address,address,address,address,address)) -func (_Opcm200 *Opcm200Caller) Blueprints(opts *bind.CallOpts) (OPContractsManagerBlueprints, error) { - var out []interface{} - err := _Opcm200.contract.Call(opts, &out, "blueprints") - - if err != nil { - return *new(OPContractsManagerBlueprints), err - } - - out0 := *abi.ConvertType(out[0], new(OPContractsManagerBlueprints)).(*OPContractsManagerBlueprints) - - return out0, err - -} - -// Blueprints is a free data retrieval call binding the contract method 0xb51f9c2b. -// -// Solidity: function blueprints() view returns((address,address,address,address,address,address,address,address,address)) -func (_Opcm200 *Opcm200Session) Blueprints() (OPContractsManagerBlueprints, error) { - return _Opcm200.Contract.Blueprints(&_Opcm200.CallOpts) -} - -// Blueprints is a free data retrieval call binding the contract method 0xb51f9c2b. -// -// Solidity: function blueprints() view returns((address,address,address,address,address,address,address,address,address)) -func (_Opcm200 *Opcm200CallerSession) Blueprints() (OPContractsManagerBlueprints, error) { - return _Opcm200.Contract.Blueprints(&_Opcm200.CallOpts) -} - -// ChainIdToBatchInboxAddress is a free data retrieval call binding the contract method 0x318b1b80. -// -// Solidity: function chainIdToBatchInboxAddress(uint256 _l2ChainId) pure returns(address) -func (_Opcm200 *Opcm200Caller) ChainIdToBatchInboxAddress(opts *bind.CallOpts, _l2ChainId *big.Int) (common.Address, error) { - var out []interface{} - err := _Opcm200.contract.Call(opts, &out, "chainIdToBatchInboxAddress", _l2ChainId) - - if err != nil { - return *new(common.Address), err - } - - out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) - - return out0, err - -} - -// ChainIdToBatchInboxAddress is a free data retrieval call binding the contract method 0x318b1b80. -// -// Solidity: function chainIdToBatchInboxAddress(uint256 _l2ChainId) pure returns(address) -func (_Opcm200 *Opcm200Session) ChainIdToBatchInboxAddress(_l2ChainId *big.Int) (common.Address, error) { - return _Opcm200.Contract.ChainIdToBatchInboxAddress(&_Opcm200.CallOpts, _l2ChainId) -} - -// ChainIdToBatchInboxAddress is a free data retrieval call binding the contract method 0x318b1b80. -// -// Solidity: function chainIdToBatchInboxAddress(uint256 _l2ChainId) pure returns(address) -func (_Opcm200 *Opcm200CallerSession) ChainIdToBatchInboxAddress(_l2ChainId *big.Int) (common.Address, error) { - return _Opcm200.Contract.ChainIdToBatchInboxAddress(&_Opcm200.CallOpts, _l2ChainId) -} - -// Implementations is a free data retrieval call binding the contract method 0x30e9012c. -// -// Solidity: function implementations() view returns((address,address,address,address,address,address,address,address,address,address,address,address)) -func (_Opcm200 *Opcm200Caller) Implementations(opts *bind.CallOpts) (OPContractsManagerImplementations, error) { - var out []interface{} - err := _Opcm200.contract.Call(opts, &out, "implementations") - - if err != nil { - return *new(OPContractsManagerImplementations), err - } - - out0 := *abi.ConvertType(out[0], new(OPContractsManagerImplementations)).(*OPContractsManagerImplementations) - - return out0, err - -} - -// Implementations is a free data retrieval call binding the contract method 0x30e9012c. -// -// Solidity: function implementations() view returns((address,address,address,address,address,address,address,address,address,address,address,address)) -func (_Opcm200 *Opcm200Session) Implementations() (OPContractsManagerImplementations, error) { - return _Opcm200.Contract.Implementations(&_Opcm200.CallOpts) -} - -// Implementations is a free data retrieval call binding the contract method 0x30e9012c. -// -// Solidity: function implementations() view returns((address,address,address,address,address,address,address,address,address,address,address,address)) -func (_Opcm200 *Opcm200CallerSession) Implementations() (OPContractsManagerImplementations, error) { - return _Opcm200.Contract.Implementations(&_Opcm200.CallOpts) -} - -// IsRC is a free data retrieval call binding the contract method 0xf179c48d. -// -// Solidity: function isRC() view returns(bool) -func (_Opcm200 *Opcm200Caller) IsRC(opts *bind.CallOpts) (bool, error) { - var out []interface{} - err := _Opcm200.contract.Call(opts, &out, "isRC") - - if err != nil { - return *new(bool), err - } - - out0 := *abi.ConvertType(out[0], new(bool)).(*bool) - - return out0, err - -} - -// IsRC is a free data retrieval call binding the contract method 0xf179c48d. -// -// Solidity: function isRC() view returns(bool) -func (_Opcm200 *Opcm200Session) IsRC() (bool, error) { - return _Opcm200.Contract.IsRC(&_Opcm200.CallOpts) -} - -// IsRC is a free data retrieval call binding the contract method 0xf179c48d. -// -// Solidity: function isRC() view returns(bool) -func (_Opcm200 *Opcm200CallerSession) IsRC() (bool, error) { - return _Opcm200.Contract.IsRC(&_Opcm200.CallOpts) -} - -// L1ContractsRelease is a free data retrieval call binding the contract method 0x35cb2e9b. -// -// Solidity: function l1ContractsRelease() view returns(string) -func (_Opcm200 *Opcm200Caller) L1ContractsRelease(opts *bind.CallOpts) (string, error) { - var out []interface{} - err := _Opcm200.contract.Call(opts, &out, "l1ContractsRelease") - - if err != nil { - return *new(string), err - } - - out0 := *abi.ConvertType(out[0], new(string)).(*string) - - return out0, err - -} - -// L1ContractsRelease is a free data retrieval call binding the contract method 0x35cb2e9b. -// -// Solidity: function l1ContractsRelease() view returns(string) -func (_Opcm200 *Opcm200Session) L1ContractsRelease() (string, error) { - return _Opcm200.Contract.L1ContractsRelease(&_Opcm200.CallOpts) -} - -// L1ContractsRelease is a free data retrieval call binding the contract method 0x35cb2e9b. -// -// Solidity: function l1ContractsRelease() view returns(string) -func (_Opcm200 *Opcm200CallerSession) L1ContractsRelease() (string, error) { - return _Opcm200.Contract.L1ContractsRelease(&_Opcm200.CallOpts) -} - -// ProtocolVersions is a free data retrieval call binding the contract method 0x6624856a. -// -// Solidity: function protocolVersions() view returns(address) -func (_Opcm200 *Opcm200Caller) ProtocolVersions(opts *bind.CallOpts) (common.Address, error) { - var out []interface{} - err := _Opcm200.contract.Call(opts, &out, "protocolVersions") - - if err != nil { - return *new(common.Address), err - } - - out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) - - return out0, err - -} - -// ProtocolVersions is a free data retrieval call binding the contract method 0x6624856a. -// -// Solidity: function protocolVersions() view returns(address) -func (_Opcm200 *Opcm200Session) ProtocolVersions() (common.Address, error) { - return _Opcm200.Contract.ProtocolVersions(&_Opcm200.CallOpts) -} - -// ProtocolVersions is a free data retrieval call binding the contract method 0x6624856a. -// -// Solidity: function protocolVersions() view returns(address) -func (_Opcm200 *Opcm200CallerSession) ProtocolVersions() (common.Address, error) { - return _Opcm200.Contract.ProtocolVersions(&_Opcm200.CallOpts) -} - -// SuperchainConfig is a free data retrieval call binding the contract method 0x35e80ab3. -// -// Solidity: function superchainConfig() view returns(address) -func (_Opcm200 *Opcm200Caller) SuperchainConfig(opts *bind.CallOpts) (common.Address, error) { - var out []interface{} - err := _Opcm200.contract.Call(opts, &out, "superchainConfig") - - if err != nil { - return *new(common.Address), err - } - - out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) - - return out0, err - -} - -// SuperchainConfig is a free data retrieval call binding the contract method 0x35e80ab3. -// -// Solidity: function superchainConfig() view returns(address) -func (_Opcm200 *Opcm200Session) SuperchainConfig() (common.Address, error) { - return _Opcm200.Contract.SuperchainConfig(&_Opcm200.CallOpts) -} - -// SuperchainConfig is a free data retrieval call binding the contract method 0x35e80ab3. -// -// Solidity: function superchainConfig() view returns(address) -func (_Opcm200 *Opcm200CallerSession) SuperchainConfig() (common.Address, error) { - return _Opcm200.Contract.SuperchainConfig(&_Opcm200.CallOpts) -} - -// SuperchainProxyAdmin is a free data retrieval call binding the contract method 0x2b96b839. -// -// Solidity: function superchainProxyAdmin() view returns(address) -func (_Opcm200 *Opcm200Caller) SuperchainProxyAdmin(opts *bind.CallOpts) (common.Address, error) { - var out []interface{} - err := _Opcm200.contract.Call(opts, &out, "superchainProxyAdmin") - - if err != nil { - return *new(common.Address), err - } - - out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) - - return out0, err - -} - -// SuperchainProxyAdmin is a free data retrieval call binding the contract method 0x2b96b839. -// -// Solidity: function superchainProxyAdmin() view returns(address) -func (_Opcm200 *Opcm200Session) SuperchainProxyAdmin() (common.Address, error) { - return _Opcm200.Contract.SuperchainProxyAdmin(&_Opcm200.CallOpts) -} - -// SuperchainProxyAdmin is a free data retrieval call binding the contract method 0x2b96b839. -// -// Solidity: function superchainProxyAdmin() view returns(address) -func (_Opcm200 *Opcm200CallerSession) SuperchainProxyAdmin() (common.Address, error) { - return _Opcm200.Contract.SuperchainProxyAdmin(&_Opcm200.CallOpts) -} - -// UpgradeController is a free data retrieval call binding the contract method 0x87543ef6. -// -// Solidity: function upgradeController() view returns(address) -func (_Opcm200 *Opcm200Caller) UpgradeController(opts *bind.CallOpts) (common.Address, error) { - var out []interface{} - err := _Opcm200.contract.Call(opts, &out, "upgradeController") - - if err != nil { - return *new(common.Address), err - } - - out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) - - return out0, err - -} - -// UpgradeController is a free data retrieval call binding the contract method 0x87543ef6. -// -// Solidity: function upgradeController() view returns(address) -func (_Opcm200 *Opcm200Session) UpgradeController() (common.Address, error) { - return _Opcm200.Contract.UpgradeController(&_Opcm200.CallOpts) -} - -// UpgradeController is a free data retrieval call binding the contract method 0x87543ef6. -// -// Solidity: function upgradeController() view returns(address) -func (_Opcm200 *Opcm200CallerSession) UpgradeController() (common.Address, error) { - return _Opcm200.Contract.UpgradeController(&_Opcm200.CallOpts) -} - -// Version is a free data retrieval call binding the contract method 0x54fd4d50. -// -// Solidity: function version() pure returns(string) -func (_Opcm200 *Opcm200Caller) Version(opts *bind.CallOpts) (string, error) { - var out []interface{} - err := _Opcm200.contract.Call(opts, &out, "version") - - if err != nil { - return *new(string), err - } - - out0 := *abi.ConvertType(out[0], new(string)).(*string) - - return out0, err - -} - -// Version is a free data retrieval call binding the contract method 0x54fd4d50. -// -// Solidity: function version() pure returns(string) -func (_Opcm200 *Opcm200Session) Version() (string, error) { - return _Opcm200.Contract.Version(&_Opcm200.CallOpts) -} - -// Version is a free data retrieval call binding the contract method 0x54fd4d50. -// -// Solidity: function version() pure returns(string) -func (_Opcm200 *Opcm200CallerSession) Version() (string, error) { - return _Opcm200.Contract.Version(&_Opcm200.CallOpts) -} - -// AddGameType is a paid mutator transaction binding the contract method 0x1661a2e9. -// -// Solidity: function addGameType((string,address,address,address,uint32,bytes32,uint256,uint256,uint64,uint64,uint256,address,bool)[] _gameConfigs) returns((address,address)[]) -func (_Opcm200 *Opcm200Transactor) AddGameType(opts *bind.TransactOpts, _gameConfigs []OPContractsManagerAddGameInput) (*types.Transaction, error) { - return _Opcm200.contract.Transact(opts, "addGameType", _gameConfigs) -} - -// AddGameType is a paid mutator transaction binding the contract method 0x1661a2e9. -// -// Solidity: function addGameType((string,address,address,address,uint32,bytes32,uint256,uint256,uint64,uint64,uint256,address,bool)[] _gameConfigs) returns((address,address)[]) -func (_Opcm200 *Opcm200Session) AddGameType(_gameConfigs []OPContractsManagerAddGameInput) (*types.Transaction, error) { - return _Opcm200.Contract.AddGameType(&_Opcm200.TransactOpts, _gameConfigs) -} - -// AddGameType is a paid mutator transaction binding the contract method 0x1661a2e9. -// -// Solidity: function addGameType((string,address,address,address,uint32,bytes32,uint256,uint256,uint64,uint64,uint256,address,bool)[] _gameConfigs) returns((address,address)[]) -func (_Opcm200 *Opcm200TransactorSession) AddGameType(_gameConfigs []OPContractsManagerAddGameInput) (*types.Transaction, error) { - return _Opcm200.Contract.AddGameType(&_Opcm200.TransactOpts, _gameConfigs) -} - -// Deploy is a paid mutator transaction binding the contract method 0x613e827b. -// -// Solidity: function deploy(((address,address,address,address,address,address),uint32,uint32,uint256,bytes,string,uint64,uint32,bytes32,uint256,uint256,uint64,uint64) _input) returns((address,address,address,address,address,address,address,address,address,address,address,address,address,address)) -func (_Opcm200 *Opcm200Transactor) Deploy(opts *bind.TransactOpts, _input OPContractsManagerDeployInput) (*types.Transaction, error) { - return _Opcm200.contract.Transact(opts, "deploy", _input) -} - -// Deploy is a paid mutator transaction binding the contract method 0x613e827b. -// -// Solidity: function deploy(((address,address,address,address,address,address),uint32,uint32,uint256,bytes,string,uint64,uint32,bytes32,uint256,uint256,uint64,uint64) _input) returns((address,address,address,address,address,address,address,address,address,address,address,address,address,address)) -func (_Opcm200 *Opcm200Session) Deploy(_input OPContractsManagerDeployInput) (*types.Transaction, error) { - return _Opcm200.Contract.Deploy(&_Opcm200.TransactOpts, _input) -} - -// Deploy is a paid mutator transaction binding the contract method 0x613e827b. -// -// Solidity: function deploy(((address,address,address,address,address,address),uint32,uint32,uint256,bytes,string,uint64,uint32,bytes32,uint256,uint256,uint64,uint64) _input) returns((address,address,address,address,address,address,address,address,address,address,address,address,address,address)) -func (_Opcm200 *Opcm200TransactorSession) Deploy(_input OPContractsManagerDeployInput) (*types.Transaction, error) { - return _Opcm200.Contract.Deploy(&_Opcm200.TransactOpts, _input) -} - -// SetRC is a paid mutator transaction binding the contract method 0x6ccdfe11. -// -// Solidity: function setRC(bool _isRC) returns() -func (_Opcm200 *Opcm200Transactor) SetRC(opts *bind.TransactOpts, _isRC bool) (*types.Transaction, error) { - return _Opcm200.contract.Transact(opts, "setRC", _isRC) -} - -// SetRC is a paid mutator transaction binding the contract method 0x6ccdfe11. -// -// Solidity: function setRC(bool _isRC) returns() -func (_Opcm200 *Opcm200Session) SetRC(_isRC bool) (*types.Transaction, error) { - return _Opcm200.Contract.SetRC(&_Opcm200.TransactOpts, _isRC) -} - -// SetRC is a paid mutator transaction binding the contract method 0x6ccdfe11. -// -// Solidity: function setRC(bool _isRC) returns() -func (_Opcm200 *Opcm200TransactorSession) SetRC(_isRC bool) (*types.Transaction, error) { - return _Opcm200.Contract.SetRC(&_Opcm200.TransactOpts, _isRC) -} - -// Upgrade is a paid mutator transaction binding the contract method 0xff2dd5a1. -// -// Solidity: function upgrade((address,address,bytes32)[] _opChainConfigs) returns() -func (_Opcm200 *Opcm200Transactor) Upgrade(opts *bind.TransactOpts, _opChainConfigs []OPContractsManagerOpChainConfig) (*types.Transaction, error) { - return _Opcm200.contract.Transact(opts, "upgrade", _opChainConfigs) -} - -// Upgrade is a paid mutator transaction binding the contract method 0xff2dd5a1. -// -// Solidity: function upgrade((address,address,bytes32)[] _opChainConfigs) returns() -func (_Opcm200 *Opcm200Session) Upgrade(_opChainConfigs []OPContractsManagerOpChainConfig) (*types.Transaction, error) { - return _Opcm200.Contract.Upgrade(&_Opcm200.TransactOpts, _opChainConfigs) -} - -// Upgrade is a paid mutator transaction binding the contract method 0xff2dd5a1. -// -// Solidity: function upgrade((address,address,bytes32)[] _opChainConfigs) returns() -func (_Opcm200 *Opcm200TransactorSession) Upgrade(_opChainConfigs []OPContractsManagerOpChainConfig) (*types.Transaction, error) { - return _Opcm200.Contract.Upgrade(&_Opcm200.TransactOpts, _opChainConfigs) -} - -// Opcm200DeployedIterator is returned from FilterDeployed and is used to iterate over the raw logs and unpacked data for Deployed events raised by the Opcm200 contract. -type Opcm200DeployedIterator struct { - Event *Opcm200Deployed // Event containing the contract specifics and raw log - - contract *bind.BoundContract // Generic contract to use for unpacking event data - event string // Event name to use for unpacking event data - - logs chan types.Log // Log channel receiving the found contract events - sub ethereum.Subscription // Subscription for errors, completion and termination - done bool // Whether the subscription completed delivering logs - fail error // Occurred error to stop iteration -} - -// Next advances the iterator to the subsequent event, returning whether there -// are any more events found. In case of a retrieval or parsing error, false is -// returned and Error() can be queried for the exact failure. -func (it *Opcm200DeployedIterator) Next() bool { - // If the iterator failed, stop iterating - if it.fail != nil { - return false - } - // If the iterator completed, deliver directly whatever's available - if it.done { - select { - case log := <-it.logs: - it.Event = new(Opcm200Deployed) - if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { - it.fail = err - return false - } - it.Event.Raw = log - return true - - default: - return false - } - } - // Iterator still in progress, wait for either a data or an error event - select { - case log := <-it.logs: - it.Event = new(Opcm200Deployed) - if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { - it.fail = err - return false - } - it.Event.Raw = log - return true - - case err := <-it.sub.Err(): - it.done = true - it.fail = err - return it.Next() - } -} - -// Error returns any retrieval or parsing error occurred during filtering. -func (it *Opcm200DeployedIterator) Error() error { - return it.fail -} - -// Close terminates the iteration process, releasing any pending underlying -// resources. -func (it *Opcm200DeployedIterator) Close() error { - it.sub.Unsubscribe() - return nil -} - -// Opcm200Deployed represents a Deployed event raised by the Opcm200 contract. -type Opcm200Deployed struct { - L2ChainId *big.Int - Deployer common.Address - DeployOutput []byte - Raw types.Log // Blockchain specific contextual infos -} - -// FilterDeployed is a free log retrieval operation binding the contract event 0xb40fb1137b92aa97efb20f29c17d36c5947aac681c3315ba854b0232f8349542. -// -// Solidity: event Deployed(uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput) -func (_Opcm200 *Opcm200Filterer) FilterDeployed(opts *bind.FilterOpts, l2ChainId []*big.Int, deployer []common.Address) (*Opcm200DeployedIterator, error) { - - var l2ChainIdRule []interface{} - for _, l2ChainIdItem := range l2ChainId { - l2ChainIdRule = append(l2ChainIdRule, l2ChainIdItem) - } - var deployerRule []interface{} - for _, deployerItem := range deployer { - deployerRule = append(deployerRule, deployerItem) - } - - logs, sub, err := _Opcm200.contract.FilterLogs(opts, "Deployed", l2ChainIdRule, deployerRule) - if err != nil { - return nil, err - } - return &Opcm200DeployedIterator{contract: _Opcm200.contract, event: "Deployed", logs: logs, sub: sub}, nil -} - -// WatchDeployed is a free log subscription operation binding the contract event 0xb40fb1137b92aa97efb20f29c17d36c5947aac681c3315ba854b0232f8349542. -// -// Solidity: event Deployed(uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput) -func (_Opcm200 *Opcm200Filterer) WatchDeployed(opts *bind.WatchOpts, sink chan<- *Opcm200Deployed, l2ChainId []*big.Int, deployer []common.Address) (event.Subscription, error) { - - var l2ChainIdRule []interface{} - for _, l2ChainIdItem := range l2ChainId { - l2ChainIdRule = append(l2ChainIdRule, l2ChainIdItem) - } - var deployerRule []interface{} - for _, deployerItem := range deployer { - deployerRule = append(deployerRule, deployerItem) - } - - logs, sub, err := _Opcm200.contract.WatchLogs(opts, "Deployed", l2ChainIdRule, deployerRule) - if err != nil { - return nil, err - } - return event.NewSubscription(func(quit <-chan struct{}) error { - defer sub.Unsubscribe() - for { - select { - case log := <-logs: - // New log arrived, parse the event and forward to the user - event := new(Opcm200Deployed) - if err := _Opcm200.contract.UnpackLog(event, "Deployed", log); err != nil { - return err - } - event.Raw = log - - select { - case sink <- event: - case err := <-sub.Err(): - return err - case <-quit: - return nil - } - case err := <-sub.Err(): - return err - case <-quit: - return nil - } - } - }), nil -} - -// ParseDeployed is a log parse operation binding the contract event 0xb40fb1137b92aa97efb20f29c17d36c5947aac681c3315ba854b0232f8349542. -// -// Solidity: event Deployed(uint256 indexed l2ChainId, address indexed deployer, bytes deployOutput) -func (_Opcm200 *Opcm200Filterer) ParseDeployed(log types.Log) (*Opcm200Deployed, error) { - event := new(Opcm200Deployed) - if err := _Opcm200.contract.UnpackLog(event, "Deployed", log); err != nil { - return nil, err - } - event.Raw = log - return event, nil -} - -// Opcm200GameTypeAddedIterator is returned from FilterGameTypeAdded and is used to iterate over the raw logs and unpacked data for GameTypeAdded events raised by the Opcm200 contract. -type Opcm200GameTypeAddedIterator struct { - Event *Opcm200GameTypeAdded // Event containing the contract specifics and raw log - - contract *bind.BoundContract // Generic contract to use for unpacking event data - event string // Event name to use for unpacking event data - - logs chan types.Log // Log channel receiving the found contract events - sub ethereum.Subscription // Subscription for errors, completion and termination - done bool // Whether the subscription completed delivering logs - fail error // Occurred error to stop iteration -} - -// Next advances the iterator to the subsequent event, returning whether there -// are any more events found. In case of a retrieval or parsing error, false is -// returned and Error() can be queried for the exact failure. -func (it *Opcm200GameTypeAddedIterator) Next() bool { - // If the iterator failed, stop iterating - if it.fail != nil { - return false - } - // If the iterator completed, deliver directly whatever's available - if it.done { - select { - case log := <-it.logs: - it.Event = new(Opcm200GameTypeAdded) - if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { - it.fail = err - return false - } - it.Event.Raw = log - return true - - default: - return false - } - } - // Iterator still in progress, wait for either a data or an error event - select { - case log := <-it.logs: - it.Event = new(Opcm200GameTypeAdded) - if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { - it.fail = err - return false - } - it.Event.Raw = log - return true - - case err := <-it.sub.Err(): - it.done = true - it.fail = err - return it.Next() - } -} - -// Error returns any retrieval or parsing error occurred during filtering. -func (it *Opcm200GameTypeAddedIterator) Error() error { - return it.fail -} - -// Close terminates the iteration process, releasing any pending underlying -// resources. -func (it *Opcm200GameTypeAddedIterator) Close() error { - it.sub.Unsubscribe() - return nil -} - -// Opcm200GameTypeAdded represents a GameTypeAdded event raised by the Opcm200 contract. -type Opcm200GameTypeAdded struct { - L2ChainId *big.Int - GameType uint32 - NewDisputeGame common.Address - OldDisputeGame common.Address - Raw types.Log // Blockchain specific contextual infos -} - -// FilterGameTypeAdded is a free log retrieval operation binding the contract event 0x4b8d2d3f00ea4ebab553d99606c8aea67fd4deb9ef0abee0e7c4b246c59a0e0f. -// -// Solidity: event GameTypeAdded(uint256 indexed l2ChainId, uint32 indexed gameType, address newDisputeGame, address oldDisputeGame) -func (_Opcm200 *Opcm200Filterer) FilterGameTypeAdded(opts *bind.FilterOpts, l2ChainId []*big.Int, gameType []uint32) (*Opcm200GameTypeAddedIterator, error) { - - var l2ChainIdRule []interface{} - for _, l2ChainIdItem := range l2ChainId { - l2ChainIdRule = append(l2ChainIdRule, l2ChainIdItem) - } - var gameTypeRule []interface{} - for _, gameTypeItem := range gameType { - gameTypeRule = append(gameTypeRule, gameTypeItem) - } - - logs, sub, err := _Opcm200.contract.FilterLogs(opts, "GameTypeAdded", l2ChainIdRule, gameTypeRule) - if err != nil { - return nil, err - } - return &Opcm200GameTypeAddedIterator{contract: _Opcm200.contract, event: "GameTypeAdded", logs: logs, sub: sub}, nil -} - -// WatchGameTypeAdded is a free log subscription operation binding the contract event 0x4b8d2d3f00ea4ebab553d99606c8aea67fd4deb9ef0abee0e7c4b246c59a0e0f. -// -// Solidity: event GameTypeAdded(uint256 indexed l2ChainId, uint32 indexed gameType, address newDisputeGame, address oldDisputeGame) -func (_Opcm200 *Opcm200Filterer) WatchGameTypeAdded(opts *bind.WatchOpts, sink chan<- *Opcm200GameTypeAdded, l2ChainId []*big.Int, gameType []uint32) (event.Subscription, error) { - - var l2ChainIdRule []interface{} - for _, l2ChainIdItem := range l2ChainId { - l2ChainIdRule = append(l2ChainIdRule, l2ChainIdItem) - } - var gameTypeRule []interface{} - for _, gameTypeItem := range gameType { - gameTypeRule = append(gameTypeRule, gameTypeItem) - } - - logs, sub, err := _Opcm200.contract.WatchLogs(opts, "GameTypeAdded", l2ChainIdRule, gameTypeRule) - if err != nil { - return nil, err - } - return event.NewSubscription(func(quit <-chan struct{}) error { - defer sub.Unsubscribe() - for { - select { - case log := <-logs: - // New log arrived, parse the event and forward to the user - event := new(Opcm200GameTypeAdded) - if err := _Opcm200.contract.UnpackLog(event, "GameTypeAdded", log); err != nil { - return err - } - event.Raw = log - - select { - case sink <- event: - case err := <-sub.Err(): - return err - case <-quit: - return nil - } - case err := <-sub.Err(): - return err - case <-quit: - return nil - } - } - }), nil -} - -// ParseGameTypeAdded is a log parse operation binding the contract event 0x4b8d2d3f00ea4ebab553d99606c8aea67fd4deb9ef0abee0e7c4b246c59a0e0f. -// -// Solidity: event GameTypeAdded(uint256 indexed l2ChainId, uint32 indexed gameType, address newDisputeGame, address oldDisputeGame) -func (_Opcm200 *Opcm200Filterer) ParseGameTypeAdded(log types.Log) (*Opcm200GameTypeAdded, error) { - event := new(Opcm200GameTypeAdded) - if err := _Opcm200.contract.UnpackLog(event, "GameTypeAdded", log); err != nil { - return nil, err - } - event.Raw = log - return event, nil -} - -// Opcm200UpgradedIterator is returned from FilterUpgraded and is used to iterate over the raw logs and unpacked data for Upgraded events raised by the Opcm200 contract. -type Opcm200UpgradedIterator struct { - Event *Opcm200Upgraded // Event containing the contract specifics and raw log - - contract *bind.BoundContract // Generic contract to use for unpacking event data - event string // Event name to use for unpacking event data - - logs chan types.Log // Log channel receiving the found contract events - sub ethereum.Subscription // Subscription for errors, completion and termination - done bool // Whether the subscription completed delivering logs - fail error // Occurred error to stop iteration -} - -// Next advances the iterator to the subsequent event, returning whether there -// are any more events found. In case of a retrieval or parsing error, false is -// returned and Error() can be queried for the exact failure. -func (it *Opcm200UpgradedIterator) Next() bool { - // If the iterator failed, stop iterating - if it.fail != nil { - return false - } - // If the iterator completed, deliver directly whatever's available - if it.done { - select { - case log := <-it.logs: - it.Event = new(Opcm200Upgraded) - if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { - it.fail = err - return false - } - it.Event.Raw = log - return true - - default: - return false - } - } - // Iterator still in progress, wait for either a data or an error event - select { - case log := <-it.logs: - it.Event = new(Opcm200Upgraded) - if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { - it.fail = err - return false - } - it.Event.Raw = log - return true - - case err := <-it.sub.Err(): - it.done = true - it.fail = err - return it.Next() - } -} - -// Error returns any retrieval or parsing error occurred during filtering. -func (it *Opcm200UpgradedIterator) Error() error { - return it.fail -} - -// Close terminates the iteration process, releasing any pending underlying -// resources. -func (it *Opcm200UpgradedIterator) Close() error { - it.sub.Unsubscribe() - return nil -} - -// Opcm200Upgraded represents a Upgraded event raised by the Opcm200 contract. -type Opcm200Upgraded struct { - L2ChainId *big.Int - SystemConfig common.Address - Upgrader common.Address - Raw types.Log // Blockchain specific contextual infos -} - -// FilterUpgraded is a free log retrieval operation binding the contract event 0x78bc67b9bf548ef6410becd31a3e10b9ea6c255974ef6b4530728b431df30030. -// -// Solidity: event Upgraded(uint256 indexed l2ChainId, address indexed systemConfig, address indexed upgrader) -func (_Opcm200 *Opcm200Filterer) FilterUpgraded(opts *bind.FilterOpts, l2ChainId []*big.Int, systemConfig []common.Address, upgrader []common.Address) (*Opcm200UpgradedIterator, error) { - - var l2ChainIdRule []interface{} - for _, l2ChainIdItem := range l2ChainId { - l2ChainIdRule = append(l2ChainIdRule, l2ChainIdItem) - } - var systemConfigRule []interface{} - for _, systemConfigItem := range systemConfig { - systemConfigRule = append(systemConfigRule, systemConfigItem) - } - var upgraderRule []interface{} - for _, upgraderItem := range upgrader { - upgraderRule = append(upgraderRule, upgraderItem) - } - - logs, sub, err := _Opcm200.contract.FilterLogs(opts, "Upgraded", l2ChainIdRule, systemConfigRule, upgraderRule) - if err != nil { - return nil, err - } - return &Opcm200UpgradedIterator{contract: _Opcm200.contract, event: "Upgraded", logs: logs, sub: sub}, nil -} - -// WatchUpgraded is a free log subscription operation binding the contract event 0x78bc67b9bf548ef6410becd31a3e10b9ea6c255974ef6b4530728b431df30030. -// -// Solidity: event Upgraded(uint256 indexed l2ChainId, address indexed systemConfig, address indexed upgrader) -func (_Opcm200 *Opcm200Filterer) WatchUpgraded(opts *bind.WatchOpts, sink chan<- *Opcm200Upgraded, l2ChainId []*big.Int, systemConfig []common.Address, upgrader []common.Address) (event.Subscription, error) { - - var l2ChainIdRule []interface{} - for _, l2ChainIdItem := range l2ChainId { - l2ChainIdRule = append(l2ChainIdRule, l2ChainIdItem) - } - var systemConfigRule []interface{} - for _, systemConfigItem := range systemConfig { - systemConfigRule = append(systemConfigRule, systemConfigItem) - } - var upgraderRule []interface{} - for _, upgraderItem := range upgrader { - upgraderRule = append(upgraderRule, upgraderItem) - } - - logs, sub, err := _Opcm200.contract.WatchLogs(opts, "Upgraded", l2ChainIdRule, systemConfigRule, upgraderRule) - if err != nil { - return nil, err - } - return event.NewSubscription(func(quit <-chan struct{}) error { - defer sub.Unsubscribe() - for { - select { - case log := <-logs: - // New log arrived, parse the event and forward to the user - event := new(Opcm200Upgraded) - if err := _Opcm200.contract.UnpackLog(event, "Upgraded", log); err != nil { - return err - } - event.Raw = log - - select { - case sink <- event: - case err := <-sub.Err(): - return err - case <-quit: - return nil - } - case err := <-sub.Err(): - return err - case <-quit: - return nil - } - } - }), nil -} - -// ParseUpgraded is a log parse operation binding the contract event 0x78bc67b9bf548ef6410becd31a3e10b9ea6c255974ef6b4530728b431df30030. -// -// Solidity: event Upgraded(uint256 indexed l2ChainId, address indexed systemConfig, address indexed upgrader) -func (_Opcm200 *Opcm200Filterer) ParseUpgraded(log types.Log) (*Opcm200Upgraded, error) { - event := new(Opcm200Upgraded) - if err := _Opcm200.contract.UnpackLog(event, "Upgraded", log); err != nil { - return nil, err - } - event.Raw = log - return event, nil -} diff --git a/packages/contracts-bedrock/scripts/checks/verify-bytecode/main.go b/packages/contracts-bedrock/scripts/checks/verify-bytecode/main.go deleted file mode 100644 index f8d199c3b18..00000000000 --- a/packages/contracts-bedrock/scripts/checks/verify-bytecode/main.go +++ /dev/null @@ -1,1227 +0,0 @@ -// Package main implements a CLI tool to verify deployed Ethereum contract bytecode -// against local build artifacts. It supports verifying single contracts, blueprints -// (ERC-5202), and the contracts managed by an OPContractsManager instance. -package main - -import ( - "context" - "encoding/hex" - "errors" - "fmt" - "os" - "path/filepath" - "reflect" - "regexp" - "strconv" - "strings" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/fatih/color" - "github.com/urfave/cli/v2" - - "github.com/ethereum-optimism/optimism/op-chain-ops/solc" - ccom "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/common" - "github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/verify-bytecode/bindings" -) - -// VerificationType indicates the kind of verification performed. -type VerificationType string - -// VerificationType constants. -const ( - DeployedContract VerificationType = "deployed contract" - Blueprint VerificationType = "blueprint" - SplitBlueprintPart1 VerificationType = "split blueprint part 1" - SplitBlueprintPart2 VerificationType = "split blueprint part 2" - OPContractsManager VerificationType = "OPContractsManager" - Implementation VerificationType = "implementation" - UnknownImplementation VerificationType = "unknown implementation" - UnknownBlueprint VerificationType = "unknown blueprint" -) - -// BytecodeDifference represents a contiguous block of differing bytes found during comparison. -type BytecodeDifference struct { - Start int // Byte offset where the difference begins. - Length int // Length of the differing block in bytes. - Expected string // Expected bytes (hex encoded). - Actual string // Actual bytes found onchain (hex encoded). - InImmutable bool // True if this difference falls within a known immutable reference range. - ImmutableName string // Name of the immutable variable if InImmutable is true. -} - -// ImmutableValueInfo holds details about a specific immutable variable's location and the value found there. -type ImmutableValueInfo struct { - Name string // Human-readable name (best effort via AST) - Offset int // Byte offset where the immutable value starts. - Length int // Length of the immutable value in bytes. - Value string // Actual value found at this location in the deployed bytecode (hex encoded). -} - -// VerificationResult encapsulates the outcome of a single verification check. -type VerificationResult struct { - Type VerificationType - ContractName string - FieldName string - Address string - AddressPart2 string - ArtifactPath string - ProcessError error - Differences []BytecodeDifference - ImmutableInfos []ImmutableValueInfo - TargetContract string -} - -// ArtifactConfig holds configuration related to finding contract artifacts. -type ArtifactConfig struct { - ArtifactsDir string - ImplementationOverrides map[string]string - BlueprintOverrides map[string]string - DefaultOPCMArtifactName string -} - -// ContractArtifact holds the relevant data extracted from a single artifact JSON file. -type ContractArtifact struct { - ContractName string - DeployedBytecode string - CreationBytecode string - ImmutableRefs map[string][]immutableLocation - RawAST map[string]any -} - -// immutableLocation is an internal helper struct used during artifact parsing and comparison. -type immutableLocation struct { - Offset int - Length int - Value string -} - -// currentDiff is a temporary helper struct used internally by findDifferences logic -type currentDiff struct { - Start int - Expected []string - Actual []string - InImmutable bool - ImmutableName string -} - -// Defaults and constants. -const defaultArtifactsDir = "forge-artifacts" -const defaultOPCMContractName = "OPContractsManager" -const blueprintPreamble = "0xFE7100" -const maxInitCodeSize = 24573 // 24 KiB - 3 byte preamble - -// trailingDigitsRegex matches the last sequence of digits in a string. -var trailingDigitsRegex = regexp.MustCompile(`\d+$`) - -// Function variable for dependency injection / mocking in tests -var getOnchainBytecodeImpl = getOnchainBytecode - -// main is the entrypoint for the verify-bytecode CLI tool. -func main() { - // Default override maps - // These map OPCM struct field names to artifact file paths relative to artifacts-dir - implementationArtifactOverrides := map[string]string{ - "OptimismPortalImpl": "OptimismPortal2.sol/OptimismPortal2.json", - // Add other overrides if needed - } - blueprintArtifactOverrides := map[string]string{ - "PermissionlessDisputeGame1": "FaultDisputeGame.sol/FaultDisputeGame.json", - "PermissionlessDisputeGame2": "FaultDisputeGame.sol/FaultDisputeGame.json", - "PermissionedDisputeGame1": "PermissionedDisputeGame.sol/PermissionedDisputeGame.json", - "PermissionedDisputeGame2": "PermissionedDisputeGame.sol/PermissionedDisputeGame.json", - // Add other overrides if needed - } - - app := &cli.App{ - Name: "verify-bytecode", - Usage: "Verify onchain contract bytecode against local build artifacts", - Commands: []*cli.Command{ - { - Name: "single", - Usage: "Verify a single deployed contract", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "address", - Usage: "Contract address to check", - Required: true, - }, - &cli.StringFlag{ - Name: "artifact", - Usage: "Path to the contract artifact JSON file (can be absolute or relative to artifacts-dir)", - Required: true, - }, - &cli.StringFlag{ - Name: "rpc-url", - Usage: "RPC URL for the network", - Required: true, - EnvVars: []string{"ETH_RPC_URL"}, - }, - &cli.StringFlag{ - Name: "artifacts-dir", - Usage: "Base directory containing the forge compilation artifacts", - Value: defaultArtifactsDir, - EnvVars: []string{"ARTIFACTS_DIR"}, - }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Print detailed immutable diff information even on success", - Value: false, - }, - }, - Action: func(c *cli.Context) error { - return runVerifySingle(c, implementationArtifactOverrides, blueprintArtifactOverrides) - }, - }, - { - Name: "opcm", - Usage: "Verify OPContractsManager and its managed implementations and blueprints", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "opcm-address", - Usage: "OPContractsManager contract address", - Required: true, - }, - &cli.StringFlag{ - Name: "rpc-url", - Usage: "RPC URL for the network", - Required: true, - EnvVars: []string{"ETH_RPC_URL"}, - }, - &cli.StringFlag{ - Name: "artifacts-dir", - Usage: "Base directory containing the forge compilation artifacts", - Value: defaultArtifactsDir, - EnvVars: []string{"ARTIFACTS_DIR"}, - }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Print detailed immutable diff information even on success", - Value: false, - }, - }, - Action: func(c *cli.Context) error { - return runVerifyOPCM(c, implementationArtifactOverrides, blueprintArtifactOverrides) - }, - }, - }, - } - - err := app.Run(os.Args) - if err != nil { - color.Set(color.FgRed) - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - color.Unset() - os.Exit(1) - } -} - -// runVerifySingle performs verification for a single deployed contract. -func runVerifySingle(c *cli.Context, implOverrides, bpOverrides map[string]string) error { - rpcURL := c.String("rpc-url") - artifactsDir := c.String("artifacts-dir") - addressHex := c.String("address") - artifactPathArg := c.String("artifact") - verbose := c.Bool("verbose") - - // Resolve artifact path - artifactPath, err := resolvePath(artifactPathArg, artifactsDir) - if err != nil { - // Print error directly as this is a setup failure before core logic runs - color.Red("Error resolving artifact path: %v", err) - return cli.Exit("", 1) - } - - // Create Ethereum client - client, err := ethclient.Dial(rpcURL) - if err != nil { - color.Red("Error connecting to RPC %s: %v", rpcURL, err) - return cli.Exit("", 1) - } - defer client.Close() - - // Load artifact - artifact, err := ccom.ReadForgeArtifact(artifactPath) - if err != nil { - // Handle artifact loading error before verification - contractName := strings.TrimSuffix(filepath.Base(artifactPath), ".json") // Best effort name from path - color.Red("Error loading artifact %s: %v", artifactPath, err) - printResults([]*VerificationResult{{ - Type: DeployedContract, - Address: addressHex, - ArtifactPath: artifactPath, - ContractName: contractName, - ProcessError: fmt.Errorf("loading artifact: %w", err), - }}, verbose) - return cli.Exit("", 1) // Exit with error code 1 if artifact fails to load - } - // Derive name from path after successful load - contractName := strings.TrimSuffix(filepath.Base(artifactPath), ".json") - - // Perform verification - addr := common.HexToAddress(addressHex) - // Pass loaded artifact, derived name, and original path - result := verifyDeployedContractLogic(client, artifact, contractName, artifactPath, addr) - - // Print results - printResults([]*VerificationResult{result}, verbose) - - // Determine exit code - exitCode := 0 - if result.ProcessError != nil || hasCodeDifferences(result) { - exitCode = 1 - } - return cli.Exit("", exitCode) -} - -func runVerifyOPCM(c *cli.Context, implOverrides, bpOverrides map[string]string) error { - rpcURL := c.String("rpc-url") - artifactsDir := c.String("artifacts-dir") - opcmAddressHex := c.String("opcm-address") - verbose := c.Bool("verbose") - - // Resolve base artifact directory path - baseArtifactsDir, err := resolvePath("", artifactsDir) // Resolve artifactsDir itself - if err != nil { - color.Red("Error resolving artifacts directory path: %v", err) - return cli.Exit("", 1) - } - - // Create ArtifactConfig - config := ArtifactConfig{ - ArtifactsDir: baseArtifactsDir, - ImplementationOverrides: implOverrides, - BlueprintOverrides: bpOverrides, - DefaultOPCMArtifactName: defaultOPCMContractName, - } - - // Create Ethereum client - client, err := ethclient.Dial(rpcURL) - if err != nil { - color.Red("Error connecting to RPC %s: %v", rpcURL, err) - return cli.Exit("", 1) - } - defer client.Close() - - // Perform verification for OPCM and all its managed contracts - opcmAddr := common.HexToAddress(opcmAddressHex) - results := runOPCMVerificationLogic(client, opcmAddr, config) - - // Print results - printResults(results, verbose) - - // Determine exit code - exitCode := 0 - for _, result := range results { - if result.ProcessError != nil || hasCodeDifferences(result) { - exitCode = 1 - break - } - } - return cli.Exit("", exitCode) -} - -// verifyDeployedContractLogic performs verification for a standard deployed contract. -// It now accepts a pre-loaded artifact and contract name. -func verifyDeployedContractLogic(client *ethclient.Client, artifact *solc.ForgeArtifact, contractName string, artifactPath string, address common.Address) *VerificationResult { - result := &VerificationResult{ - Type: DeployedContract, - Address: address.Hex(), - ArtifactPath: artifactPath, // Still store the path for reporting - ContractName: contractName, // Use passed-in name - } - - // Get onchain bytecode - actualBytecode, err := getOnchainBytecodeImpl(client, address) - if err != nil { - result.ProcessError = fmt.Errorf("getting onchain bytecode: %w", err) - return result - } - - // Compare bytecode - differences, immutables, err := compareBytecode(artifact, true, artifact.DeployedBytecode.Object, actualBytecode) - if err != nil { - result.ProcessError = fmt.Errorf("comparing bytecode: %w", err) - return result - } - result.Differences = differences - result.ImmutableInfos = immutables - - return result -} - -// verifyBlueprintLogic performs verification for a single ERC-5202 blueprint. -// It now accepts a pre-loaded artifact and target contract name. -func verifyBlueprintLogic(client *ethclient.Client, artifact *solc.ForgeArtifact, targetContractName string, targetArtifactPath string, blueprintAddress common.Address, blueprintFieldName string) *VerificationResult { - result := &VerificationResult{ - Type: Blueprint, - FieldName: blueprintFieldName, - Address: blueprintAddress.Hex(), - ArtifactPath: targetArtifactPath, // Path to the artifact of the contract *created* by the blueprint - TargetContract: targetContractName, // Use passed-in name - ContractName: fmt.Sprintf("Blueprint for %s", targetContractName), // Use passed-in name - } - - // Error if no creation bytecode found - if artifact.Bytecode.Object == "" || artifact.Bytecode.Object == "0x" { - result.ProcessError = fmt.Errorf("no creation bytecode found in target artifact %s", targetArtifactPath) - return result - } - - // Construct expected blueprint bytecode - expectedBlueprintBytecode := blueprintPreamble + strings.TrimPrefix(artifact.Bytecode.Object, "0x") - - // Get actual blueprint bytecode from chain - actualBytecode, err := getOnchainBytecodeImpl(client, blueprintAddress) - if err != nil { - result.ProcessError = fmt.Errorf("getting onchain bytecode for blueprint %s: %w", blueprintAddress.Hex(), err) - return result - } - - // Compare bytecode (no immutables for blueprints) - differences, _, err := compareBytecode(artifact, false, expectedBlueprintBytecode, actualBytecode) - if err != nil { - result.ProcessError = errors.Join(result.ProcessError, fmt.Errorf("comparing blueprint bytecode: %w", err)) - return result - } - result.Differences = differences - - return result -} - -// verifySplitBlueprintLogic verifies a blueprint split into two parts. -// It now accepts a pre-loaded artifact and target contract name. -func verifySplitBlueprintLogic(client *ethclient.Client, artifact *solc.ForgeArtifact, targetContractName string, targetArtifactPath string, address1, address2 common.Address, fieldName1, fieldName2 string) (*VerificationResult, *VerificationResult) { - result1 := &VerificationResult{ - Type: SplitBlueprintPart1, - FieldName: fieldName1, - Address: address1.Hex(), - AddressPart2: address2.Hex(), - ArtifactPath: targetArtifactPath, - TargetContract: targetContractName, // Use passed-in name - ContractName: fmt.Sprintf("Split BP 1/2 for %s", targetContractName), // Use passed-in name - } - result2 := &VerificationResult{ - Type: SplitBlueprintPart2, - FieldName: fieldName2, - Address: address2.Hex(), - ArtifactPath: targetArtifactPath, - TargetContract: targetContractName, // Use passed-in name - ContractName: fmt.Sprintf("Split BP 2/2 for %s", targetContractName), // Use passed-in name - } - - // Error if no creation bytecode found - if artifact.Bytecode.Object == "" || artifact.Bytecode.Object == "0x" { - err := fmt.Errorf("no creation bytecode found in target artifact %s", targetArtifactPath) - result1.ProcessError = err - result2.ProcessError = err - return result1, result2 - } - - // Split creation code - fullCreationCodeHex := strings.TrimPrefix(artifact.Bytecode.Object, "0x") - fullCreationCodeBytes, err := hex.DecodeString(fullCreationCodeHex) - if err != nil { - err = fmt.Errorf("failed to decode creation code hex from %s: %w", targetArtifactPath, err) - result1.ProcessError = err - result2.ProcessError = err - return result1, result2 - } - - // Split up creation code - part1Bytes := fullCreationCodeBytes - var part2Bytes []byte - if len(fullCreationCodeBytes) > maxInitCodeSize { - part1Bytes = fullCreationCodeBytes[:maxInitCodeSize] - part2Bytes = fullCreationCodeBytes[maxInitCodeSize:] - } else { - // This case should ideally be handled by the caller (runOPCMVerificationLogic) - // If it gets here, treat part 2 as empty. - part2Bytes = []byte{} - } - - // Construct expected bytecodes - expectedBytecode1 := blueprintPreamble + hex.EncodeToString(part1Bytes) - expectedBytecode2 := blueprintPreamble + hex.EncodeToString(part2Bytes) - - // Fetch actual bytecode for address 1 - actualBytecode1, err1 := getOnchainBytecodeImpl(client, address1) - if err1 != nil { - result1.ProcessError = fmt.Errorf("getting onchain code for part 1 (%s): %w", address1.Hex(), err1) - } - - // Fetch actual bytecode for address 2 - actualBytecode2, err2 := getOnchainBytecodeImpl(client, address2) - if err2 != nil { - result2.ProcessError = fmt.Errorf("getting onchain code for part 2 (%s): %w", address2.Hex(), err2) - } - - // Compare Part 1 - if result1.ProcessError == nil { - diffs1, _, cmpErr1 := compareBytecode(artifact, false, expectedBytecode1, actualBytecode1) - if cmpErr1 != nil { - result1.ProcessError = errors.Join(result1.ProcessError, fmt.Errorf("comparing part 1 bytecode: %w", cmpErr1)) - } - result1.Differences = diffs1 - } - - // Compare Part 2 - if result2.ProcessError == nil { - diffs2, _, cmpErr2 := compareBytecode(artifact, false, expectedBytecode2, actualBytecode2) - if cmpErr2 != nil { - result2.ProcessError = errors.Join(result2.ProcessError, fmt.Errorf("comparing part 2 bytecode: %w", cmpErr2)) - } - result2.Differences = diffs2 - } - - return result1, result2 -} - -// runOPCMVerificationLogic orchestrates verification for OPCM and its managed contracts. -func runOPCMVerificationLogic(client *ethclient.Client, opcmAddress common.Address, config ArtifactConfig) []*VerificationResult { - results := []*VerificationResult{} - - // Verify OPCM itself - opcmArtifactBase := config.DefaultOPCMArtifactName - opcmArtifactRelative := filepath.Join(fmt.Sprintf("%s.sol", opcmArtifactBase), fmt.Sprintf("%s.json", opcmArtifactBase)) - opcmArtifactPath := filepath.Join(config.ArtifactsDir, opcmArtifactRelative) - - // Load OPCM artifact first - opcmArtifact, err := ccom.ReadForgeArtifact(opcmArtifactPath) - // Derive name from path before potentially erroring out - opcmContractName := config.DefaultOPCMArtifactName // Use default name as fallback - // Attempt to refine name from path - if nameFromPath := strings.TrimSuffix(filepath.Base(opcmArtifactPath), ".json"); nameFromPath != "" { - opcmContractName = nameFromPath - } - if err != nil { - // If OPCM artifact fails to load, create an error result and cannot proceed - results = append(results, &VerificationResult{ - Type: OPContractsManager, - Address: opcmAddress.Hex(), - ArtifactPath: opcmArtifactPath, - ContractName: opcmContractName, // Use derived name - ProcessError: fmt.Errorf("loading OPCM artifact %s: %w", opcmArtifactPath, err), - }) - return results - } - // Name is already derived above - - // Verify OPCM itself using the loaded artifact - opcmResult := verifyDeployedContractLogic(client, opcmArtifact, opcmContractName, opcmArtifactPath, opcmAddress) - opcmResult.Type = OPContractsManager // Override type - // ContractName is already set correctly by verifyDeployedContractLogic - results = append(results, opcmResult) - - // Cannot proceed if OPCM verification itself had a processing error (e.g., RPC down) - // or if the OPCM address has no code (can't call it). - if opcmResult.ProcessError != nil { - opcmResult.ProcessError = errors.Join(opcmResult.ProcessError, errors.New("cannot query implementations/blueprints due to OPCM verification error")) - return results - } - - // Assuming OPCM verification passed or had only bytecode diffs, we can try to bind - opcmCaller, err := bindings.NewOpcm200Caller(opcmAddress, client) - if err != nil { - // Add a synthetic result to indicate this failure - results = append(results, &VerificationResult{ - Type: OPContractsManager, - ContractName: config.DefaultOPCMArtifactName, - Address: opcmAddress.Hex(), - ProcessError: fmt.Errorf("failed to bind OPCM caller: %w", err), - }) - return results // Cannot proceed without caller - } - - // Verify implementations - implementationsResult, err := opcmCaller.Implementations(nil) - if err != nil { - results = append(results, &VerificationResult{ - Type: OPContractsManager, // Attributing error to OPCM interaction - ContractName: config.DefaultOPCMArtifactName, - Address: opcmAddress.Hex(), - ProcessError: fmt.Errorf("failed to call implementations() on OPCM: %w", err), - }) - } else { - implValue := reflect.ValueOf(implementationsResult) - implType := implValue.Type() - - for i := 0; i < implValue.NumField(); i++ { - fieldName := implType.Field(i).Name - fieldValue := implValue.Field(i).Interface().(common.Address) - - if fieldValue == (common.Address{}) { - continue // Skip zero addresses silently - } - - implAddressStr := fieldValue.Hex() - var relativePath string - var ok bool - - // Determine artifact path using overrides or convention - if relativePath, ok = config.ImplementationOverrides[fieldName]; !ok { - if strings.HasSuffix(fieldName, "Impl") { - baseName := strings.TrimSuffix(fieldName, "Impl") - relativePath = filepath.Join(fmt.Sprintf("%s.sol", baseName), fmt.Sprintf("%s.json", baseName)) - } else { - // Cannot determine path - results = append(results, &VerificationResult{ - Type: UnknownImplementation, - FieldName: fieldName, - ContractName: fmt.Sprintf("Unknown (%s)", fieldName), - Address: implAddressStr, - ProcessError: fmt.Errorf("cannot infer artifact path for implementation field '%s' (no override and doesn't end in 'Impl')", fieldName), - }) - continue - } - } - - artifactPath := filepath.Join(config.ArtifactsDir, relativePath) - - // Load implementation artifact - implArtifact, err := ccom.ReadForgeArtifact(artifactPath) - // Derive name from path before potentially erroring out - implContractName := strings.TrimSuffix(filepath.Base(artifactPath), ".json") - if err != nil { - // Handle artifact loading error for this implementation - results = append(results, &VerificationResult{ - Type: Implementation, - FieldName: fieldName, - ContractName: implContractName, // Use derived name - Address: implAddressStr, - ArtifactPath: artifactPath, - ProcessError: fmt.Errorf("loading implementation artifact %s: %w", artifactPath, err), - }) - continue // Skip to next implementation - } - // Name is already derived - - // Verify implementation using loaded artifact - implResult := verifyDeployedContractLogic(client, implArtifact, implContractName, artifactPath, fieldValue) - implResult.Type = Implementation // Override type - implResult.FieldName = fieldName // Store the field name - // ContractName is already set correctly - results = append(results, implResult) - } - } - - // Verify blueprints - blueprintsResult, err := opcmCaller.Blueprints(nil) - if err != nil { - results = append(results, &VerificationResult{ - Type: OPContractsManager, // Attributing error to OPCM interaction - ContractName: config.DefaultOPCMArtifactName, - Address: opcmAddress.Hex(), - ProcessError: fmt.Errorf("failed to call blueprints() on OPCM: %w", err), - }) - } else { - blueprintValue := reflect.ValueOf(blueprintsResult) - blueprintType := blueprintValue.Type() - blueprintFields := make(map[string]common.Address) - processedPart2 := make(map[string]bool) // Track part 2 blueprints already handled - - // First pass: collect all blueprint addresses - for i := 0; i < blueprintValue.NumField(); i++ { - fieldName := blueprintType.Field(i).Name - fieldValue := blueprintValue.Field(i).Interface().(common.Address) - blueprintFields[fieldName] = fieldValue - } - - // Second pass: verify each blueprint, handling splits - for i := 0; i < blueprintValue.NumField(); i++ { - fieldName := blueprintType.Field(i).Name - fieldValue := blueprintValue.Field(i).Interface().(common.Address) - - if processedPart2[fieldName] || fieldValue == (common.Address{}) { - continue // Skip zero addresses and already processed part 2s - } - - blueprintAddressStr := fieldValue.Hex() - var relativePath string - var baseName string - var ok bool - - // Determine artifact path for the *target* contract - if relativePath, ok = config.BlueprintOverrides[fieldName]; !ok { - baseName = trailingDigitsRegex.ReplaceAllString(fieldName, "") - if baseName == "" { - results = append(results, &VerificationResult{ - Type: UnknownBlueprint, - FieldName: fieldName, - ContractName: fmt.Sprintf("Unknown (%s)", fieldName), - Address: blueprintAddressStr, - ProcessError: fmt.Errorf("cannot infer artifact path for blueprint field '%s' (no override and empty base name)", fieldName), - }) - continue - } - relativePath = filepath.Join(fmt.Sprintf("%s.sol", baseName), fmt.Sprintf("%s.json", baseName)) - } else { - // Infer baseName from override path if possible, fallback to field name - parts := strings.Split(filepath.ToSlash(relativePath), "/") - if len(parts) == 2 && strings.HasSuffix(parts[0], ".sol") && strings.HasSuffix(parts[1], ".json") { - baseName = strings.TrimSuffix(parts[1], ".json") - } else { - baseName = trailingDigitsRegex.ReplaceAllString(fieldName, "") // Fallback - } - } - - targetArtifactPath := filepath.Join(config.ArtifactsDir, relativePath) - - // Load target artifact (used for both single and split blueprints) - targetArtifact, err := ccom.ReadForgeArtifact(targetArtifactPath) - // Derive target contract name from baseName determined earlier - // baseName was derived from field name or override path - targetContractName := baseName - if err != nil { - // Handle artifact loading error - errResult := VerificationResult{ - Type: UnknownBlueprint, // Or SplitBlueprintPart1 if applicable - FieldName: fieldName, - // Use baseName for the failed contract's name if possible - ContractName: fmt.Sprintf("Unknown (%s - loading failed)", targetContractName), // Indicate loading failure - Address: blueprintAddressStr, - ArtifactPath: targetArtifactPath, - ProcessError: fmt.Errorf("loading target artifact %s: %w", targetArtifactPath, err), - } - results = append(results, &errResult) - // If it was potentially a split, maybe add a placeholder for part 2? Less clear. - // For now, just report the loading error once. - continue // Skip to next blueprint field - } - // Name is already derived - - // Check for split blueprint - if strings.HasSuffix(fieldName, "1") { - part2FieldName := strings.TrimSuffix(fieldName, "1") + "2" - if part2Addr, exists := blueprintFields[part2FieldName]; exists && part2Addr != (common.Address{}) { - // Verify as split blueprint using loaded artifact - res1, res2 := verifySplitBlueprintLogic(client, targetArtifact, targetContractName, targetArtifactPath, fieldValue, part2Addr, fieldName, part2FieldName) - results = append(results, res1, res2) - processedPart2[part2FieldName] = true // Mark part 2 as handled - continue // Move to next field - } else { - // Error out - results = append(results, &VerificationResult{ - Type: UnknownBlueprint, - FieldName: fieldName, - ContractName: fmt.Sprintf("Unknown (%s)", fieldName), - Address: blueprintAddressStr, - ProcessError: fmt.Errorf("split blueprint part 2 not found for %s", fieldName), - }) - } - } - - // Verify as a standard (single) blueprint using loaded artifact - bpResult := verifyBlueprintLogic(client, targetArtifact, targetContractName, targetArtifactPath, fieldValue, fieldName) - results = append(results, bpResult) - } - } - - return results -} - -// getOnchainBytecode fetches bytecode from the chain. Returns hex string or error. -func getOnchainBytecode(client *ethclient.Client, address common.Address) (string, error) { - if client == nil { - return "", errors.New("ethereum client is nil") - } - code, err := client.CodeAt(context.Background(), address, nil) - if err != nil { - return "", err - } - if len(code) == 0 { - return "0x", errors.New("no code found at address") - } - return "0x" + hex.EncodeToString(code), nil -} - -// compareBytecode compares expected and actual bytecode, handling immutables. -// It uses the artifact to find immutable names via the AST. -func compareBytecode( - artifact *solc.ForgeArtifact, - checkImmutables bool, - expectedBytecodeHex string, - actualBytecodeHex string, -) ([]BytecodeDifference, []ImmutableValueInfo, error) { - // Input validation and decoding - expectedClean := strings.TrimPrefix(expectedBytecodeHex, "0x") - actualClean := strings.TrimPrefix(actualBytecodeHex, "0x") - - if len(expectedClean)%2 != 0 { - return nil, nil, fmt.Errorf("invalid expected bytecode hex length: %d", len(expectedClean)) - } - // Allow empty or odd length for actual if it came from chain (e.g., "0x")? - // For now, strict check on actual too. If empty actual is valid, adjust here. - if actualClean != "" && len(actualClean)%2 != 0 { - // return nil, nil, fmt.Errorf("invalid actual bytecode hex length: %d", len(actualClean)) - // Or treat as empty if needed: actualClean = "" - } - - expectedBytes, err := hex.DecodeString(expectedClean) - if err != nil { - return nil, nil, fmt.Errorf("failed to decode expected bytecode: %w", err) - } - - actualBytes, err := hex.DecodeString(actualClean) - if err != nil { - // Allow comparison if actual is empty/invalid hex from chain (treat as empty) - if actualClean == "" { - actualBytes = []byte{} - } else { - return nil, nil, fmt.Errorf("failed to decode actual bytecode '%s': %w", actualBytecodeHex, err) - } - } - - // Error if bytecode lengths don't match - if len(expectedBytes) != len(actualBytes) { - return nil, nil, fmt.Errorf("bytecode length mismatch, expected: %d, actual: %d", len(expectedBytes), len(actualBytes)) - } - - // Precompute immutable locations - type immutableByteInfo struct { - Name string - } - immutableBytes := make(map[int]immutableByteInfo) - - if artifact != nil && artifact.DeployedBytecode.ImmutableReferences != nil { - for refKey, locations := range artifact.DeployedBytecode.ImmutableReferences { - name := getImmutableName(artifact, refKey) - if name == "" { - name = fmt.Sprintf("immutable(id:%s)", refKey) // Fallback name - } - - info := immutableByteInfo{Name: name} - for _, loc := range locations { - start := int(loc.Start) - length := int(loc.Length) - if length <= 0 { - continue // Skip invalid length - } - - // Mark each byte within this location using 'j' - for j := 0; j < length; j++ { - offset := start + j - if existing, exists := immutableBytes[offset]; exists { - if existing.Name != info.Name { - fmt.Fprintf(os.Stderr, "Warning: Overlapping immutable reference at offset %d. Prev: '%s', New: '%s'\n", - offset, existing.Name, info.Name) - } - } - immutableBytes[offset] = info - } - } - } - } - - // Compare byte by byte - differences := []BytecodeDifference{} - var currDiff *currentDiff = nil // Tracks the current block of differences - maxLength := max(len(expectedBytes), len(actualBytes)) - - for i := 0; i < maxLength; i++ { - // Determine immutable status for the current byte offset - inImmutableRange := false - immName := "" - if info, ok := immutableBytes[i]; ok { - inImmutableRange = true - immName = info.Name - } - - // Get bytes and hex representations, handling out-of-bounds access - var expectedByte, actualByte byte - var expectedHex, actualHex string = "..", ".." // Use ".." for out-of-bounds - - if i < len(expectedBytes) { - expectedByte = expectedBytes[i] - expectedHex = fmt.Sprintf("%02x", expectedByte) - } - if i < len(actualBytes) { - actualByte = actualBytes[i] - actualHex = fmt.Sprintf("%02x", actualByte) - } - - bytesDiffer := expectedByte != actualByte - - // State machine logic for tracking differences - if bytesDiffer { - if currDiff == nil { - // Start a new difference block - currDiff = ¤tDiff{ - Start: i, - Expected: []string{expectedHex}, - Actual: []string{actualHex}, - InImmutable: inImmutableRange, - ImmutableName: immName, - } - } else if currDiff.InImmutable != inImmutableRange || (inImmutableRange && currDiff.ImmutableName != immName) { - // End the previous block because immutable status or name changed, then start a new one - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) - // Start new diff block - currDiff = ¤tDiff{ - Start: i, - Expected: []string{expectedHex}, - Actual: []string{actualHex}, - InImmutable: inImmutableRange, - ImmutableName: immName, - } - } else { - // Extend the current difference block (same immutable status/name) - currDiff.Expected = append(currDiff.Expected, expectedHex) - currDiff.Actual = append(currDiff.Actual, actualHex) - } - } else { // Bytes match - if currDiff != nil { - // End the current difference block as the mismatch ended - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) - currDiff = nil // Reset tracker - } - // No action needed if bytes match and not in a diff block - } - } - - // Record the final difference block if the loop ended while in a diff - if currDiff != nil { - differences = append(differences, BytecodeDifference{ - Start: currDiff.Start, - Length: len(currDiff.Expected), - Expected: strings.Join(currDiff.Expected, ""), - Actual: strings.Join(currDiff.Actual, ""), - InImmutable: currDiff.InImmutable, - ImmutableName: currDiff.ImmutableName, - }) - } - - // Collect and return immutable differences - immutableValues := []ImmutableValueInfo{} - if checkImmutables && artifact != nil && artifact.DeployedBytecode.ImmutableReferences != nil { - // Iterate through the defined locations in the artifact - for refKey, locations := range artifact.DeployedBytecode.ImmutableReferences { - name := getImmutableName(artifact, refKey) - if name == "" { - name = fmt.Sprintf("immutable(id:%s)", refKey) // Fallback - } - - for _, loc := range locations { - start := int(loc.Start) - length := int(loc.Length) - if length <= 0 { - // Invalid length - return an error - return nil, nil, fmt.Errorf( - "immutable '%s' location (offset %d, length %d) has invalid length %d", - name, start, length, length, - ) - } - - // Extract the actual value directly from actualBytes based on this location - var actualValueHex string - upperBound := start + length - if start >= 0 && upperBound <= len(actualBytes) { - // Safely extract the slice - actualValueBytes := actualBytes[start:upperBound] - actualValueHex = "0x" + hex.EncodeToString(actualValueBytes) - } else { - // Fundamental mismatch - return an error - return nil, nil, fmt.Errorf( - "immutable '%s' location (offset %d, length %d) is out of bounds for actual bytecode length %d", - name, start, length, len(actualBytes), - ) - } - - immutableValues = append(immutableValues, ImmutableValueInfo{ - Name: name, - Offset: start, - Length: length, - Value: actualValueHex, - }) - } - } - } - - return differences, immutableValues, nil -} - -// printResults formats and prints the outcomes of verification checks. -func printResults(results []*VerificationResult, verbose bool) { - // Track if any *code* mismatches occurred - overallSuccess := true - - for i, result := range results { - if i > 0 { - // Add spacing between results - fmt.Println() - } - - // Print header - printResultHeader(result) - - // Handle and print process errors - if result.ProcessError != nil { - color.Red(" ERROR during verification: %v", result.ProcessError) - overallSuccess = false - continue - } - - // Analyze differences - codeDiffs, immDiffs := categorizeDifferences(result) - - // Print status and details - if len(codeDiffs) > 0 { - overallSuccess = false - color.Red(" ✗ Verification FAILED: Found unexpected differences in code.") - printCodeDifferences(codeDiffs) - if verbose && (result.Type == DeployedContract || result.Type == Implementation || result.Type == OPContractsManager) && len(result.ImmutableInfos) > 0 { - printImmutableDetails(result.ImmutableInfos, immDiffs, true) // Pass true for includeConsistencyWarning - } - } else if len(immDiffs) > 0 && (result.Type == DeployedContract || result.Type == Implementation || result.Type == OPContractsManager) { - color.Green(" ✓ Verification successful (differences only in known immutable locations).") - if verbose { - printImmutableDetails(result.ImmutableInfos, immDiffs, true) - } - } else { - // Exact match (or blueprint match where immutables aren't checked) - color.Green(" ✓ Verification successful (exact match).") - if verbose && (result.Type == DeployedContract || result.Type == Implementation || result.Type == OPContractsManager) && len(result.ImmutableInfos) > 0 { - printImmutableDetails(result.ImmutableInfos, nil, verbose) - } - } - } - - // Print overall summary - fmt.Println() - if overallSuccess { - color.Green("OK") - } else { - color.Red("FAILED") - } -} - -// printResultHeader prints the title section for a single result. -func printResultHeader(result *VerificationResult) { - title := "" - switch result.Type { - case OPContractsManager: - title = fmt.Sprintf("Verifying %s: %s", result.Type, result.Address) - case Implementation: - title = fmt.Sprintf("Verifying %s (%s): %s", result.FieldName, result.ContractName, result.Address) - case DeployedContract: - title = fmt.Sprintf("Verifying %s: %s (%s)", result.Type, result.ContractName, result.Address) - case Blueprint: - title = fmt.Sprintf("Verifying %s (%s for %s): %s", result.Type, result.FieldName, result.TargetContract, result.Address) - case SplitBlueprintPart1: - title = fmt.Sprintf("Verifying %s (%s for %s): %s", result.Type, result.FieldName, result.TargetContract, result.Address) - case SplitBlueprintPart2: - title = fmt.Sprintf("Verifying %s (%s for %s): %s", result.Type, result.FieldName, result.TargetContract, result.Address) - default: - title = fmt.Sprintf("Verifying %s (%s): %s", result.Type, result.ContractName, result.Address) - } - - color.Cyan(title) - fmt.Printf(" Artifact: %s\n", result.ArtifactPath) -} - -// categorizeDifferences separates differences into code/unknown and immutable. -func categorizeDifferences(result *VerificationResult) (codeDiffs, immutableDiffs []BytecodeDifference) { - isDeployed := result.Type == DeployedContract || result.Type == Implementation || result.Type == OPContractsManager - codeDiffs = make([]BytecodeDifference, 0) - immutableDiffs = make([]BytecodeDifference, 0) - for _, diff := range result.Differences { - if isDeployed && diff.InImmutable { - immutableDiffs = append(immutableDiffs, diff) - } else { - codeDiffs = append(codeDiffs, diff) - } - } - return codeDiffs, immutableDiffs -} - -// hasCodeDifferences checks if a result has any non-immutable differences. -func hasCodeDifferences(result *VerificationResult) bool { - codeDiffs, _ := categorizeDifferences(result) - return len(codeDiffs) > 0 -} - -// printCodeDifferences formats and prints code/unknown differences. -func printCodeDifferences(diffs []BytecodeDifference) { - color.Set(color.FgRed) - fmt.Println(" --- Code Differences Found ---") - color.Unset() - for _, diff := range diffs { - endPos := diff.Start + diff.Length - 1 - color.Red(" Byte %d-%d (%d bytes):", diff.Start, endPos, diff.Length) - fmt.Printf(" Expected: 0x%s\n", maybeTruncate(diff.Expected, 64)) - fmt.Printf(" Actual: 0x%s\n", maybeTruncate(diff.Actual, 64)) - } -} - -// printImmutableDetails formats and prints immutable variable info. -func printImmutableDetails(infos []ImmutableValueInfo, diffs []BytecodeDifference, verbose bool) { - if len(infos) == 0 { - return - } - - color.Cyan(" --- Immutable Reference Details ---") - - // Group infos by name for consistency check - infosByName := make(map[string][]ImmutableValueInfo) - var names []string - for _, info := range infos { - if _, exists := infosByName[info.Name]; !exists { - names = append(names, info.Name) // Keep order of first appearance - } - infosByName[info.Name] = append(infosByName[info.Name], info) - } - - for _, name := range names { - locations := infosByName[name] - color.Yellow(" Variable: %s", name) - inconsistent := false - firstValue := "" - populatedCount := 0 - - for i, loc := range locations { - fmt.Printf(" [%d] Location: Offset %d, Length %d bytes\n", i, loc.Offset, loc.Length) - if loc.Value != "" && loc.Value != "0x" { - fmt.Printf(" Actual Value: %s\n", loc.Value) - if populatedCount == 0 { - firstValue = loc.Value - } else if loc.Value != firstValue { - inconsistent = true - } - populatedCount++ - } else { - fmt.Printf(" Actual Value: (Not populated - check comparison logic or bytecode length)\n") - } - } - - if inconsistent { - color.Red(" ! Consistency WARNING: Found differing values for '%s' across its locations.", name) - } else if verbose && populatedCount > 1 { - color.Green(" ✓ Consistency: All populated values for '%s' are identical.", name) - } else if verbose && populatedCount == 0 && len(locations) > 0 { - color.Yellow(" - Consistency: No values populated for '%s'.", name) - } - } -} - -// maybeTruncate shortens a string if it exceeds maxLen. -func maybeTruncate(s string, maxLen int) string { - if len(s) > maxLen && maxLen > 3 { - return s[:maxLen-3] + "..." - } - return s -} - -// resolvePath resolves a potentially relative path against a base directory. -func resolvePath(path, baseDir string) (string, error) { - if filepath.IsAbs(path) { - return path, nil - } - cwd, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("failed to get current working directory: %w", err) - } - absBaseDir := baseDir - if !filepath.IsAbs(absBaseDir) { - absBaseDir = filepath.Join(cwd, absBaseDir) - } - return filepath.Join(absBaseDir, path), nil -} - -// max returns the greater of two integers. -func max(a, b int) int { - if a > b { - return a - } - return b -} - -// getImmutableName finds the human-readable name of an immutable variable within a ForgeArtifact's AST, -// given the reference key from the artifact's ImmutableReferences map. -// The refKey is usually a string representation of the variable's AST node ID (e.g., "36"). -func getImmutableName(artifact *solc.ForgeArtifact, refKey string) string { - if artifact == nil { - fmt.Fprintln(os.Stderr, "Warning: Cannot get immutable name, artifact is nil") - return "" - } - - // Extract the numeric ID from the key. - // Handles formats like "36" or "t_int256:36". - parts := strings.Split(refKey, ":") - idStr := parts[len(parts)-1] // Take the last part after splitting by ':' - - numericID, err := strconv.Atoi(idStr) - if err != nil { - // If the key itself wasn't purely numeric and splitting didn't help, try parsing the whole key. - numericID, err = strconv.Atoi(refKey) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: Could not parse numeric ID from immutable reference key '%s': %v\n", refKey, err) - return "" // Cannot parse numeric ID, cannot search AST - } - } - - // Search the AST nodes recursively - return findAstNodeNameByID(artifact.Ast.Nodes, numericID) -} - -// findAstNodeNameByID recursively searches a slice of AST nodes for a node with the target ID -// and returns its name. -func findAstNodeNameByID(nodes []solc.AstNode, targetID int) string { - for _, node := range nodes { - // Check if the current node matches the target ID - if node.Id == targetID { - // Ensure the node is a variable declaration, as IDs can be reused for other node types - if node.NodeType == "VariableDeclaration" && node.Name != "" { - return node.Name // Found the name - } - // If the ID matches but it's not a VariableDeclaration or has no name, - // we might still find the right node deeper, so we don't return early. - // However, typically the ID in immutable references points directly to the VariableDeclaration. - } - - // Recursively search within nested nodes - // Common places for nested declarations or structures: - // 1. Direct children (`node.Nodes`) - Covers ContractDefinition, StructDefinition, etc. - if len(node.Nodes) > 0 { - if name := findAstNodeNameByID(node.Nodes, targetID); name != "" { - return name - } - } - - // 2. Function bodies (`node.Body.Statements`) - if node.Body != nil && len(node.Body.Statements) > 0 { - // Note: Immutables are state variables, usually not declared inside function bodies, - // but searching here for completeness doesn't hurt. - if name := findAstNodeNameByID(node.Body.Statements, targetID); name != "" { - return name - } - } - - // 3. Blocks within control structures (If, For, While - less likely for immutables) - if node.TrueBody != nil && len(node.TrueBody.Statements) > 0 { - if name := findAstNodeNameByID(node.TrueBody.Statements, targetID); name != "" { - return name - } - } - if node.FalseBody != nil && len(node.FalseBody.Statements) > 0 { - if name := findAstNodeNameByID(node.FalseBody.Statements, targetID); name != "" { - return name - } - } - } - - return "" // Not found in this slice or its children -} diff --git a/packages/contracts-bedrock/scripts/checks/verify-bytecode/main_test.go b/packages/contracts-bedrock/scripts/checks/verify-bytecode/main_test.go deleted file mode 100644 index 348b9472817..00000000000 --- a/packages/contracts-bedrock/scripts/checks/verify-bytecode/main_test.go +++ /dev/null @@ -1,1066 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/ethereum-optimism/optimism/op-chain-ops/solc" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" -) - -// Helper to create a basic ForgeArtifact for testing -func newTestArtifact(opts ...func(*solc.ForgeArtifact)) *solc.ForgeArtifact { - artifact := &solc.ForgeArtifact{ - Abi: solc.AbiType{}, // Usually not needed for bytecode verification tests - Bytecode: solc.CompilerOutputBytecode{Object: "0x"}, - DeployedBytecode: solc.CompilerOutputBytecode{Object: "0x"}, - Ast: solc.Ast{Nodes: []solc.AstNode{}}, - } - for _, opt := range opts { - opt(artifact) - } - return artifact -} - -// Option to set deployed bytecode -func withDeployedBytecode(code string) func(*solc.ForgeArtifact) { - return func(a *solc.ForgeArtifact) { - a.DeployedBytecode.Object = code - } -} - -// Option to set creation bytecode -func withCreationBytecode(code string) func(*solc.ForgeArtifact) { - return func(a *solc.ForgeArtifact) { - a.Bytecode.Object = code - } -} - -// Option to add immutable references -func withImmutableRefs(refs map[string][]solc.ImmutableReference) func(*solc.ForgeArtifact) { - return func(a *solc.ForgeArtifact) { - if a.DeployedBytecode.ImmutableReferences == nil { - a.DeployedBytecode.ImmutableReferences = make(map[string][]solc.ImmutableReference) - } - for k, v := range refs { - a.DeployedBytecode.ImmutableReferences[k] = v - } - } -} - -// Option to add AST nodes -func withAstNodes(nodes []solc.AstNode) func(*solc.ForgeArtifact) { - return func(a *solc.ForgeArtifact) { - a.Ast.Nodes = nodes - } -} - -// Helper to create AST nodes for immutable tests -func createTestAstNodes() []solc.AstNode { - return []solc.AstNode{ - { // Contract Definition - Id: 10, - NodeType: "ContractDefinition", - Name: "MyContract", - Nodes: []solc.AstNode{ - { // State Variable 1 (immutable) - Id: 5, - NodeType: "VariableDeclaration", - Name: "IMMUTABLE_VAR_1", - StateVariable: true, - Mutability: "immutable", - Constant: false, - TypeDescriptions: &solc.AstTypeDescriptions{TypeString: "uint256"}, - }, - { // State Variable 2 (regular) - Id: 6, - NodeType: "VariableDeclaration", - Name: "regularVar", - StateVariable: true, - TypeDescriptions: &solc.AstTypeDescriptions{TypeString: "bool"}, - }, - { // Function Definition - Id: 8, - NodeType: "FunctionDefinition", - Name: "doSomething", - Body: &solc.AstBlock{ - NodeType: "Block", - Id: 9, - Statements: []solc.AstNode{ - { // Local Variable (shouldn't be found by ID 5) - Id: 7, - NodeType: "ExpressionStatement", - Src: "placeholder;", - }, - }, - }, - }, - { // State Variable 3 (immutable, nested struct type not important for name lookup) - Id: 15, - NodeType: "VariableDeclaration", - Name: "IMMUTABLE_VAR_2", - StateVariable: true, - Mutability: "immutable", - TypeDescriptions: &solc.AstTypeDescriptions{TypeString: "struct MyStruct"}, - }, - }, - }, - { // Another top-level node (e.g., ImportDirective, ErrorDefinition) - Id: 11, - NodeType: "ImportDirective", - }, - { // Struct Definition (containing a node with ID 5, but wrong type) - Id: 12, - NodeType: "StructDefinition", - Name: "MyStruct", - Nodes: []solc.AstNode{ - { - Id: 5, // Duplicate ID, but wrong node type - NodeType: "MemberAccess", - Name: "", // Not a declaration name - TypeDescriptions: &solc.AstTypeDescriptions{TypeString: "uint"}, - }, - }, - }, - } -} - -func TestGetImmutableName(t *testing.T) { - astNodes := createTestAstNodes() - artifact := newTestArtifact(withAstNodes(astNodes)) - - tests := []struct { - name string - refKey string - artifact *solc.ForgeArtifact - wantName string - }{ - { - name: "Valid ID simple", - refKey: "5", - artifact: artifact, - wantName: "IMMUTABLE_VAR_1", - }, - { - name: "Valid ID with type prefix", - refKey: "t_struct:MyStruct:15", - artifact: artifact, - wantName: "IMMUTABLE_VAR_2", - }, - { - name: "ID exists but not VariableDeclaration", - refKey: "7", - artifact: artifact, - wantName: "", - }, - { - name: "ID not found", - refKey: "999", - artifact: artifact, - wantName: "", - }, - { - name: "Invalid refKey format", - refKey: "invalid-key", - artifact: artifact, - wantName: "", - }, - { - name: "Nil artifact", - refKey: "5", - artifact: nil, - wantName: "", - }, - { - name: "Artifact with no AST", - refKey: "5", - artifact: newTestArtifact(), - wantName: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Note: We don't directly test findAstNodeNameByID as it's an internal helper. - // Its behavior is tested via getImmutableName. - gotName := getImmutableName(tt.artifact, tt.refKey) - assert.Equal(t, tt.wantName, gotName) - }) - } -} - -func TestCompareBytecode_ExactMatch(t *testing.T) { - artifact := newTestArtifact() // No immutables needed - expected := "0x12345678" - actual := "0x12345678" - - diffs, infos, err := compareBytecode(artifact, true, expected, actual) - require.NoError(t, err) - assert.Empty(t, diffs, "Should be no differences") - assert.Empty(t, infos, "Should be no immutable info") -} - -func TestCompareBytecode_SimpleMismatch(t *testing.T) { - artifact := newTestArtifact() - expected := "0x12345678" - actual := "0x1234ff78" // Mismatch at byte 2 (0-indexed) - - diffs, infos, err := compareBytecode(artifact, true, expected, actual) - require.NoError(t, err) - assert.Empty(t, infos) - require.Len(t, diffs, 1) - - diff := diffs[0] - assert.Equal(t, 2, diff.Start) - assert.Equal(t, 1, diff.Length) - assert.Equal(t, "56", diff.Expected) - assert.Equal(t, "ff", diff.Actual) - assert.False(t, diff.InImmutable) - assert.Equal(t, "", diff.ImmutableName) -} - -func TestCompareBytecode_DifferentLengths(t *testing.T) { - artifact := newTestArtifact() - expected := "0x12345678" - actualShort := "0x123456" - actualLong := "0x1234567890" - - _, _, err := compareBytecode(artifact, true, expected, actualShort) - require.Error(t, err) - assert.Contains(t, err.Error(), "bytecode length mismatch") - - _, _, err = compareBytecode(artifact, true, expected, actualLong) - require.Error(t, err) - assert.Contains(t, err.Error(), "bytecode length mismatch") - - _, _, err = compareBytecode(artifact, true, actualShort, expected) - require.Error(t, err) - assert.Contains(t, err.Error(), "bytecode length mismatch") -} - -func TestCompareBytecode_PrefixHandling(t *testing.T) { - artifact := newTestArtifact() - expected := "0x1234" - actualNoPrefix := "1234" - actualWithPrefix := "0x1234" - - // Expected has prefix, actual does not - diffs, infos, err := compareBytecode(artifact, true, expected, actualNoPrefix) - require.NoError(t, err) - assert.Empty(t, diffs) - assert.Empty(t, infos) - - // Expected does not have prefix, actual does - diffs, infos, err = compareBytecode(artifact, true, actualNoPrefix, actualWithPrefix) - require.NoError(t, err) - assert.Empty(t, diffs) - assert.Empty(t, infos) -} - -func TestCompareBytecode_EmptyBytecode(t *testing.T) { - artifact := newTestArtifact() - - // Both empty with prefix - diffs, infos, err := compareBytecode(artifact, true, "0x", "0x") - require.NoError(t, err) - assert.Empty(t, diffs) - assert.Empty(t, infos) - - // Both empty without prefix - diffs, infos, err = compareBytecode(artifact, true, "", "") - require.NoError(t, err) - assert.Empty(t, diffs) - assert.Empty(t, infos) - - // One empty, one not (should fail length check) - _, _, err = compareBytecode(artifact, true, "0x12", "0x") - require.Error(t, err) - assert.Contains(t, err.Error(), "bytecode length mismatch") - - _, _, err = compareBytecode(artifact, true, "", "12") - require.Error(t, err) - assert.Contains(t, err.Error(), "bytecode length mismatch") -} - -func TestCompareBytecode_InvalidHex(t *testing.T) { - artifact := newTestArtifact() - valid := "0x1234" - invalid := "0x123G" // Invalid character 'G' - oddLen := "0x123" // Odd length - - _, _, err := compareBytecode(artifact, true, invalid, valid) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to decode expected bytecode") - - _, _, err = compareBytecode(artifact, true, valid, invalid) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to decode actual bytecode") - - _, _, err = compareBytecode(artifact, true, oddLen, valid) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid expected bytecode hex length") - - _, _, err = compareBytecode(artifact, true, valid, oddLen) - require.Error(t, err) - assert.Contains(t, err.Error(), "failed to decode actual bytecode") - assert.Contains(t, err.Error(), "odd length hex string") // Specific error from hex pkg -} - -func TestCompareBytecode_Immutables(t *testing.T) { - tests := []struct { - name string - expected string - actual string - checkImmut bool - immutables solc.ImmutableReferences - wantDiffs []BytecodeDifference - wantInfos []ImmutableValueInfo - wantErr bool - wantErrMsg string - }{ - { - name: "Match", - expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", - actual: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", - checkImmut: true, - immutables: solc.ImmutableReferences{ - "5": { - {Start: 2, Length: 3}, - }, - "15": { - {Start: 8, Length: 2}, - {Start: 12, Length: 1}, - }, - }, - wantDiffs: []BytecodeDifference{}, - wantInfos: []ImmutableValueInfo{ - {Name: "IMMUTABLE_VAR_1", Offset: 2, Length: 3, Value: "0xaaaa00"}, - {Name: "IMMUTABLE_VAR_2", Offset: 8, Length: 2, Value: "0xbbbb"}, - {Name: "IMMUTABLE_VAR_2", Offset: 12, Length: 1, Value: "0xcc"}, - }, - wantErr: false, - }, - { - name: "Diff inside immutable only (checkImmut=false)", - expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", - actual: "0xAAAAAAAA112233BBBBBBBBCCCCDDDDDD", - checkImmut: false, - immutables: solc.ImmutableReferences{}, - wantDiffs: []BytecodeDifference{ - {Start: 4, Length: 3, Expected: "000000", Actual: "112233", InImmutable: false, ImmutableName: ""}, - }, - wantInfos: []ImmutableValueInfo{}, - wantErr: false, - }, - { - name: "Diff outside immutable only", - expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", - actual: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDFF", - checkImmut: true, - immutables: solc.ImmutableReferences{ - "5": { - {Start: 2, Length: 3}, - }, - "15": { - {Start: 8, Length: 2}, - {Start: 12, Length: 1}, - }, - }, - wantDiffs: []BytecodeDifference{ - {Start: 15, Length: 1, Expected: "dd", Actual: "ff", InImmutable: false, ImmutableName: ""}, - }, - wantInfos: []ImmutableValueInfo{ - {Name: "IMMUTABLE_VAR_1", Offset: 2, Length: 3, Value: "0xaaaa00"}, - {Name: "IMMUTABLE_VAR_2", Offset: 8, Length: 2, Value: "0xbbbb"}, - {Name: "IMMUTABLE_VAR_2", Offset: 12, Length: 1, Value: "0xcc"}, - }, - wantErr: false, - }, - { - name: "Diffs inside and outside immutable", - expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", - actual: "0xAAAA1122330000BBBBBBBBCCCCDDDDFF", - checkImmut: true, - immutables: solc.ImmutableReferences{ - "5": { - {Start: 2, Length: 3}, - }, - "15": { - {Start: 8, Length: 2}, - {Start: 12, Length: 1}, - }, - }, - wantDiffs: []BytecodeDifference{ - {Start: 2, Length: 3, Expected: "aaaa00", Actual: "112233", InImmutable: true, ImmutableName: "IMMUTABLE_VAR_1"}, - {Start: 15, Length: 1, Expected: "dd", Actual: "ff", InImmutable: false, ImmutableName: ""}, - }, - wantInfos: []ImmutableValueInfo{ - {Name: "IMMUTABLE_VAR_1", Offset: 2, Length: 3, Value: "0x112233"}, - {Name: "IMMUTABLE_VAR_2", Offset: 8, Length: 2, Value: "0xbbbb"}, - {Name: "IMMUTABLE_VAR_2", Offset: 12, Length: 1, Value: "0xcc"}, - }, - wantErr: false, - }, - { - name: "Diff spanning immutable boundary", - expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", - actual: "0xAAAAAAAA112200BBBBBBBBCCCCDDDDDD", - checkImmut: true, - immutables: solc.ImmutableReferences{ - "5": { - {Start: 2, Length: 3}, - }, - "15": { - {Start: 8, Length: 2}, - {Start: 12, Length: 1}, - }, - }, - wantDiffs: []BytecodeDifference{ - {Start: 4, Length: 1, Expected: "00", Actual: "11", InImmutable: true, ImmutableName: "IMMUTABLE_VAR_1"}, - {Start: 5, Length: 1, Expected: "00", Actual: "22", InImmutable: false, ImmutableName: ""}, - }, - wantInfos: []ImmutableValueInfo{ - {Name: "IMMUTABLE_VAR_1", Offset: 2, Length: 3, Value: "0xaaaa11"}, - {Name: "IMMUTABLE_VAR_2", Offset: 8, Length: 2, Value: "0xbbbb"}, - {Name: "IMMUTABLE_VAR_2", Offset: 12, Length: 1, Value: "0xcc"}, - }, - wantErr: false, - }, - { - name: "Immutable ref out of bounds for actual bytecode", - expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", - actual: "0xAAAAAAAA000000BBBBBBBBCCCC", - checkImmut: true, - immutables: solc.ImmutableReferences{ - "5": { - {Start: 2, Length: 3}, - }, - "15": { - {Start: 8, Length: 2}, - {Start: 12, Length: 1}, - }, - }, - wantDiffs: nil, - wantInfos: nil, - wantErr: true, - wantErrMsg: "bytecode length mismatch", - }, - { - name: "Immutable ref has invalid length (zero)", - expected: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", - actual: "0xAAAAAAAA000000BBBBBBBBCCCCDDDDDD", - checkImmut: true, - immutables: solc.ImmutableReferences{ - "5": { - {Start: 2, Length: 0}, - }, - }, - wantDiffs: nil, - wantInfos: nil, - wantErr: true, - wantErrMsg: "immutable 'IMMUTABLE_VAR_1' location (offset 2, length 0) has invalid length 0", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - astNodes := createTestAstNodes() - art := newTestArtifact(withAstNodes(astNodes), withImmutableRefs(tt.immutables)) - diffs, infos, err := compareBytecode(art, tt.checkImmut, tt.expected, tt.actual) - - if tt.wantErr { - require.Error(t, err) - if tt.wantErrMsg != "" { - assert.Contains(t, err.Error(), tt.wantErrMsg) - } - } else { - require.NoError(t, err) - assert.Equal(t, tt.wantDiffs, diffs, "Differences mismatch") - assert.ElementsMatch(t, tt.wantInfos, infos, "Immutable infos mismatch") - } - }) - } -} - -func TestCompareBytecode_DifferenceGrouping(t *testing.T) { - artifact := newTestArtifact() - expected := "0x112233445566" - actual := "0x11aabbcc5566" // Differs @ 1,2,3 (0x223344 -> 0xaabbcc) - - diffs, infos, err := compareBytecode(artifact, true, expected, actual) - require.NoError(t, err) - assert.Empty(t, infos) - require.Len(t, diffs, 1, "Differences should be grouped") - - diff := diffs[0] - assert.Equal(t, 1, diff.Start) - assert.Equal(t, 3, diff.Length) - assert.Equal(t, "223344", diff.Expected) - assert.Equal(t, "aabbcc", diff.Actual) - assert.False(t, diff.InImmutable) -} - -func TestCategorizeDifferences(t *testing.T) { - tests := []struct { - name string - resultType VerificationType - allDiffs []BytecodeDifference - wantCodeDiffs []BytecodeDifference - wantImmDiffs []BytecodeDifference - wantHasCode bool - }{ - { - name: "No diffs", - resultType: DeployedContract, - allDiffs: []BytecodeDifference{}, - wantCodeDiffs: []BytecodeDifference{}, - wantImmDiffs: []BytecodeDifference{}, - wantHasCode: false, - }, - { - name: "Only code diffs", - resultType: DeployedContract, - allDiffs: []BytecodeDifference{ - {Start: 1, Length: 1, Expected: "aa", Actual: "bb", InImmutable: false}, - {Start: 5, Length: 2, Expected: "cccc", Actual: "dddd", InImmutable: false}, - }, - wantCodeDiffs: []BytecodeDifference{ - {Start: 1, Length: 1, Expected: "aa", Actual: "bb", InImmutable: false}, - {Start: 5, Length: 2, Expected: "cccc", Actual: "dddd", InImmutable: false}, - }, - wantImmDiffs: []BytecodeDifference{}, - wantHasCode: true, - }, - { - name: "Only immutable diffs (DeployedContract)", - resultType: DeployedContract, - allDiffs: []BytecodeDifference{ - {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, - {Start: 20, Length: 1, Expected: "ee", Actual: "ff", InImmutable: true, ImmutableName: "VarB"}, - }, - wantCodeDiffs: []BytecodeDifference{}, - wantImmDiffs: []BytecodeDifference{ - {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, - {Start: 20, Length: 1, Expected: "ee", Actual: "ff", InImmutable: true, ImmutableName: "VarB"}, - }, - wantHasCode: false, - }, - { - name: "Mixed diffs (DeployedContract)", - resultType: DeployedContract, - allDiffs: []BytecodeDifference{ - {Start: 1, Length: 1, Expected: "aa", Actual: "bb", InImmutable: false}, - {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, - {Start: 5, Length: 2, Expected: "cccc", Actual: "dddd", InImmutable: false}, - {Start: 20, Length: 1, Expected: "ee", Actual: "ff", InImmutable: true, ImmutableName: "VarB"}, - }, - wantCodeDiffs: []BytecodeDifference{ - {Start: 1, Length: 1, Expected: "aa", Actual: "bb", InImmutable: false}, - {Start: 5, Length: 2, Expected: "cccc", Actual: "dddd", InImmutable: false}, - }, - wantImmDiffs: []BytecodeDifference{ - {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, - {Start: 20, Length: 1, Expected: "ee", Actual: "ff", InImmutable: true, ImmutableName: "VarB"}, - }, - wantHasCode: true, - }, - { - name: "Only immutable diffs (Implementation)", - resultType: Implementation, // Also checks immutables - allDiffs: []BytecodeDifference{ - {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, - }, - wantCodeDiffs: []BytecodeDifference{}, - wantImmDiffs: []BytecodeDifference{ - {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, - }, - wantHasCode: false, - }, - { - name: "Only immutable diffs (OPContractsManager)", - resultType: OPContractsManager, // Also checks immutables - allDiffs: []BytecodeDifference{ - {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, - }, - wantCodeDiffs: []BytecodeDifference{}, - wantImmDiffs: []BytecodeDifference{ - {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, - }, - wantHasCode: false, - }, - { - name: "Only immutable diffs (Blueprint)", - resultType: Blueprint, // Does NOT check immutables - allDiffs: []BytecodeDifference{ - {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, - }, - wantCodeDiffs: []BytecodeDifference{ - {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, - }, // Immutable diffs treated as code diffs - wantImmDiffs: []BytecodeDifference{}, - wantHasCode: true, - }, - { - name: "Mixed diffs (SplitBlueprintPart1)", - resultType: SplitBlueprintPart1, // Does NOT check immutables - allDiffs: []BytecodeDifference{ - {Start: 1, Length: 1, Expected: "aa", Actual: "bb", InImmutable: false}, - {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, - }, - wantCodeDiffs: []BytecodeDifference{ - {Start: 1, Length: 1, Expected: "aa", Actual: "bb", InImmutable: false}, - {Start: 10, Length: 4, Expected: "00000000", Actual: "11111111", InImmutable: true, ImmutableName: "VarA"}, - }, // Immutable diffs treated as code diffs - wantImmDiffs: []BytecodeDifference{}, - wantHasCode: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := &VerificationResult{ - Type: tt.resultType, - Differences: tt.allDiffs, - } - gotCodeDiffs, gotImmDiffs := categorizeDifferences(result) - assert.Equal(t, tt.wantCodeDiffs, gotCodeDiffs, "Code differences mismatch") - assert.Equal(t, tt.wantImmDiffs, gotImmDiffs, "Immutable differences mismatch") - - // Test hasCodeDifferences as well - assert.Equal(t, tt.wantHasCode, hasCodeDifferences(result), "hasCodeDifferences mismatch") - }) - } -} - -// --- Tests for verify*Logic functions --- -// Note: These tests focus on the orchestration logic, assuming dependencies like -// getOnchainBytecode, ccom.ReadForgeArtifact, and compareBytecode work correctly (or are mocked implicitly). -// They primarily check the structure and fields of the returned VerificationResult. - -// Mock implementations (replace with actual mocking library if needed) -var mockBytecodeStore = make(map[string]string) -var mockArtifactStore = make(map[string]*solc.ForgeArtifact) -var mockReadArtifactError error -var mockGetBytecodeError error - -// setupMocks resets mock state and installs mock implementations for the current test. -func setupMocks(t *testing.T) { - t.Helper() - - mockBytecodeStore = make(map[string]string) - mockArtifactStore = make(map[string]*solc.ForgeArtifact) // Although ReadForgeArtifact isn't directly mocked here now - mockReadArtifactError = nil - mockGetBytecodeError = nil - - // Keep track of the original implementation - originalGetBytecode := getOnchainBytecodeImpl - - // Define the mock implementation - getOnchainBytecodeImpl = func(client *ethclient.Client, addr common.Address) (string, error) { - if mockGetBytecodeError != nil { - // Check if the error is specific to this address (optional enhancement) - // For now, any error applies globally. - return "", mockGetBytecodeError - } - code, ok := mockBytecodeStore[addr.Hex()] - if !ok { - // Return 0x for unknown addresses to simulate no code found, common case - return "0x", fmt.Errorf("no code found at address (mock)") - } - return code, nil - } - - // Use t.Cleanup to restore the original implementation after the test - t.Cleanup(func() { - getOnchainBytecodeImpl = originalGetBytecode - }) -} - -func TestVerifyDeployedContractLogic(t *testing.T) { - artifactPath := "/mock/MyContract.json" // Path is still used for metadata - contractName := "MyContract" - address := common.HexToAddress("0x1234567890123456789012345678901234567890") - expectedCode := "0x6080604052348015600f57600080fd5b50604051602080606f8339810160405280600a5f5260005f60005f5151f3fe" - actualCodeMatch := expectedCode - actualCodeMismatch := "0x6080604052348015600f57600080fd5b50604051602080606f8339810160405280ffff5f5260005f60005f5151f3fe" // Mismatch '600a' -> 'ffff' (same length) - - tests := []struct { - name string - artifact *solc.ForgeArtifact - mockSetup func() - wantErr bool - wantErrContent string - wantDiffs bool - wantImmutables bool - }{ - { - name: "Match", - artifact: newTestArtifact(withDeployedBytecode(expectedCode)), - mockSetup: func() { - mockBytecodeStore[address.Hex()] = actualCodeMatch - }, - wantErr: false, - wantDiffs: false, - wantImmutables: false, - }, - { - name: "Mismatch", - artifact: newTestArtifact(withDeployedBytecode(expectedCode)), - mockSetup: func() { - mockBytecodeStore[address.Hex()] = actualCodeMismatch - }, - wantErr: false, - wantDiffs: true, - wantImmutables: false, - }, - { - name: "Get bytecode error", - artifact: newTestArtifact(withDeployedBytecode(expectedCode)), - mockSetup: func() { - mockGetBytecodeError = fmt.Errorf("rpc is down") - }, - wantErr: true, - wantErrContent: "getting onchain bytecode: rpc is down", - }, - { - name: "No code at address", - artifact: newTestArtifact(withDeployedBytecode(expectedCode)), - mockSetup: func() { - // No entry in mockBytecodeStore triggers the mock's error - }, - wantErr: true, - wantErrContent: "no code found at address (mock)", - }, - { - name: "Match with immutables", - artifact: newTestArtifact( - withDeployedBytecode("0xAAAABBBBCCCCDDDD"), // Expected - withAstNodes(createTestAstNodes()), - withImmutableRefs(solc.ImmutableReferences{ - "5": {{Start: 2, Length: 2}}, // BBBBB - }), - ), - mockSetup: func() { - mockBytecodeStore[address.Hex()] = "0xAAAA1122CCCCDDDD" // Actual (diff only in immutable) - }, - wantErr: false, - wantDiffs: true, // compareBytecode returns diffs, but categorizeDifferences handles it - wantImmutables: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - setupMocks(t) - tt.mockSetup() - - // Pass nil client because getOnchainBytecodeImpl is mocked - result := verifyDeployedContractLogic(nil, tt.artifact, contractName, artifactPath, address) - - if tt.wantErr { - require.Error(t, result.ProcessError) - if tt.wantErrContent != "" { - assert.Contains(t, result.ProcessError.Error(), tt.wantErrContent) - } - } else { - require.NoError(t, result.ProcessError) - assert.Equal(t, DeployedContract, result.Type) - assert.Equal(t, address.Hex(), result.Address) - assert.Equal(t, artifactPath, result.ArtifactPath) - assert.Equal(t, contractName, result.ContractName) - if tt.wantDiffs { - assert.NotEmpty(t, result.Differences) - } else { - assert.Empty(t, result.Differences) - } - if tt.wantImmutables { - assert.NotEmpty(t, result.ImmutableInfos) - } else { - assert.Empty(t, result.ImmutableInfos) - } - // Check categorization for the immutable case - if tt.name == "Match with immutables" { - assert.False(t, hasCodeDifferences(result), "Should have no *code* differences") - } - } - }) - } -} - -func TestVerifyBlueprintLogic(t *testing.T) { - targetArtifactPath := "/mock/TargetContract.json" - targetContractName := "TargetContract" - blueprintAddress := common.HexToAddress("0xABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCD") - blueprintFieldName := "TheBlueprint" - creationCode := "608060405234801561001057600080fd5b5061015ff3" - expectedBlueprintCode := blueprintPreamble + creationCode - actualCodeMatch := expectedBlueprintCode - actualCodeMismatch := blueprintPreamble + "ffffff" + creationCode[6:] // Mismatch after preamble - - tests := []struct { - name string - artifact *solc.ForgeArtifact - mockSetup func() - wantErr bool - wantErrContent string - wantDiffs bool - }{ - { - name: "Match", - artifact: newTestArtifact(withCreationBytecode("0x" + creationCode)), - mockSetup: func() { - mockBytecodeStore[blueprintAddress.Hex()] = actualCodeMatch - }, - wantErr: false, - wantDiffs: false, - }, - { - name: "Mismatch", - artifact: newTestArtifact(withCreationBytecode("0x" + creationCode)), - mockSetup: func() { - mockBytecodeStore[blueprintAddress.Hex()] = actualCodeMismatch - }, - wantErr: false, - wantDiffs: true, - }, - { - name: "Get blueprint bytecode error", - artifact: newTestArtifact(withCreationBytecode("0x" + creationCode)), - mockSetup: func() { - mockGetBytecodeError = fmt.Errorf("rpc is down") - }, - wantErr: true, - wantErrContent: "getting onchain bytecode for blueprint", - }, - { - name: "No code at address", - artifact: newTestArtifact(withCreationBytecode("0x" + creationCode)), - mockSetup: func() { - // No entry in mockBytecodeStore - }, - wantErr: true, - wantErrContent: "no code found at address (mock)", - }, - { - name: "No creation code in artifact", - artifact: newTestArtifact(withCreationBytecode("0x")), // Empty creation code - mockSetup: func() {}, // Bytecode doesn't matter here - wantErr: true, - wantErrContent: "no creation bytecode found", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - setupMocks(t) - tt.mockSetup() - - // Pass nil client because getOnchainBytecodeImpl is mocked - result := verifyBlueprintLogic(nil, tt.artifact, targetContractName, targetArtifactPath, blueprintAddress, blueprintFieldName) - - if tt.wantErr { - require.Error(t, result.ProcessError) - if tt.wantErrContent != "" { - assert.Contains(t, result.ProcessError.Error(), tt.wantErrContent) - } - } else { - require.NoError(t, result.ProcessError) - assert.Equal(t, Blueprint, result.Type) - assert.Equal(t, blueprintAddress.Hex(), result.Address) - assert.Equal(t, targetArtifactPath, result.ArtifactPath) - assert.Equal(t, blueprintFieldName, result.FieldName) - assert.Equal(t, targetContractName, result.TargetContract) - assert.Equal(t, fmt.Sprintf("Blueprint for %s", targetContractName), result.ContractName) - if tt.wantDiffs { - assert.NotEmpty(t, result.Differences) - assert.True(t, hasCodeDifferences(result)) // Any blueprint diff is a code diff - } else { - assert.Empty(t, result.Differences) - } - } - }) - } -} - -func TestVerifySplitBlueprintLogic(t *testing.T) { - targetArtifactPath := "/mock/SplitTarget.json" - targetContractName := "SplitTarget" - address1 := common.HexToAddress("0xAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBB") - address2 := common.HexToAddress("0xCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDDDDD") - fieldName1 := "SplitBP1" - fieldName2 := "SplitBP2" - - // Create creation code longer than maxInitCodeSize - part1Hex := strings.Repeat("11", maxInitCodeSize) - part2Hex := strings.Repeat("22", 10) - fullCreationCode := part1Hex + part2Hex - artifactLong := newTestArtifact(withCreationBytecode("0x" + fullCreationCode)) - expectedBP1 := blueprintPreamble + part1Hex - expectedBP2 := blueprintPreamble + part2Hex - - // Create creation code shorter than maxInitCodeSize - shortCodeHex := strings.Repeat("33", 100) - artifactShort := newTestArtifact(withCreationBytecode("0x" + shortCodeHex)) - expectedShortBP1 := blueprintPreamble + shortCodeHex - expectedShortBP2 := blueprintPreamble // Empty part 2 - - tests := []struct { - name string - artifact *solc.ForgeArtifact - mockSetup func() - wantErr1 bool - wantErr2 bool - wantErr1Cont string - wantErr2Cont string - wantDiffs1 bool - wantDiffs2 bool - }{ - { - name: "Match Long Code", - artifact: artifactLong, - mockSetup: func() { - mockBytecodeStore[address1.Hex()] = expectedBP1 - mockBytecodeStore[address2.Hex()] = expectedBP2 - }, - wantErr1: false, - wantErr2: false, - wantDiffs1: false, - wantDiffs2: false, - }, - { - name: "Match Short Code (part 2 is empty)", - artifact: artifactShort, - mockSetup: func() { - mockBytecodeStore[address1.Hex()] = expectedShortBP1 - mockBytecodeStore[address2.Hex()] = expectedShortBP2 // Expect preamble only for empty code - }, - wantErr1: false, - wantErr2: false, - wantDiffs1: false, - wantDiffs2: false, - }, - { - name: "Mismatch Part 1", - artifact: artifactLong, - mockSetup: func() { - mockBytecodeStore[address1.Hex()] = blueprintPreamble + "ff" + part1Hex[2:] - mockBytecodeStore[address2.Hex()] = expectedBP2 - }, - wantErr1: false, - wantErr2: false, - wantDiffs1: true, - wantDiffs2: false, - }, - { - name: "Mismatch Part 2", - artifact: artifactLong, - mockSetup: func() { - mockBytecodeStore[address1.Hex()] = expectedBP1 - mockBytecodeStore[address2.Hex()] = blueprintPreamble + strings.Repeat("ff", 10) // Match length of part2Hex - }, - wantErr1: false, - wantErr2: false, - wantDiffs1: false, - wantDiffs2: true, - }, - { - name: "Error Getting Part 1 Code", - artifact: artifactLong, - mockSetup: func() { - mockGetBytecodeError = fmt.Errorf("rpc1 down") - // Need more specific mock to only fail for address1, assume global for now - mockBytecodeStore[address2.Hex()] = expectedBP2 // Set this so part 2 fetch succeeds - }, - wantErr1: true, - wantErr1Cont: "getting onchain code for part 1", - wantErr2: true, // Error on addr1 implies error on addr2 too with global mock - wantErr2Cont: "getting onchain code for part 2", - }, - { - name: "Error Getting Part 2 Code", - artifact: artifactLong, - mockSetup: func() { - var getBytecodeErr error = fmt.Errorf("rpc2 down") - originalGetBytecode := getOnchainBytecodeImpl - getOnchainBytecodeImpl = func(client *ethclient.Client, addr common.Address) (string, error) { - if addr == address1 { - return expectedBP1, nil - } else if addr == address2 { - return "", getBytecodeErr // Specific error for address 2 - } - return "", fmt.Errorf("unexpected address in mock") - } - t.Cleanup(func() { getOnchainBytecodeImpl = originalGetBytecode }) - }, - wantErr1: false, // Fetch for part 1 succeeds - wantErr2: true, - wantErr2Cont: "rpc2 down", // Check for the core error message - }, - { - name: "No creation code in artifact", - artifact: newTestArtifact(withCreationBytecode("0x")), // Empty - mockSetup: func() { - // Bytecode fetch doesn't matter - }, - wantErr1: true, - wantErr1Cont: "no creation bytecode found", - wantErr2: true, - wantErr2Cont: "no creation bytecode found", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - setupMocks(t) // Base setup, might be overridden by tt.mockSetup - tt.mockSetup() - - // Pass nil client because getOnchainBytecodeImpl is mocked - res1, res2 := verifySplitBlueprintLogic(nil, tt.artifact, targetContractName, targetArtifactPath, address1, address2, fieldName1, fieldName2) - - // Check Result 1 - if tt.wantErr1 { - require.Error(t, res1.ProcessError) - if tt.wantErr1Cont != "" { - assert.Contains(t, res1.ProcessError.Error(), tt.wantErr1Cont) - } - } else { - require.NoError(t, res1.ProcessError) - assert.Equal(t, SplitBlueprintPart1, res1.Type) - assert.Equal(t, address1.Hex(), res1.Address) - assert.Equal(t, targetArtifactPath, res1.ArtifactPath) - assert.Equal(t, fieldName1, res1.FieldName) - assert.Equal(t, targetContractName, res1.TargetContract) - if tt.wantDiffs1 { - assert.NotEmpty(t, res1.Differences) - } else { - assert.Empty(t, res1.Differences) - } - } - - // Check Result 2 - if tt.wantErr2 { - require.Error(t, res2.ProcessError) - if tt.wantErr2Cont != "" { - assert.Contains(t, res2.ProcessError.Error(), tt.wantErr2Cont) - } - } else { - require.NoError(t, res2.ProcessError) - assert.Equal(t, SplitBlueprintPart2, res2.Type) - assert.Equal(t, address2.Hex(), res2.Address) - assert.Equal(t, targetArtifactPath, res2.ArtifactPath) - assert.Equal(t, fieldName2, res2.FieldName) - assert.Equal(t, targetContractName, res2.TargetContract) - if tt.wantDiffs2 { - assert.NotEmpty(t, res2.Differences) - } else { - assert.Empty(t, res2.Differences) - } - } - }) - } -} diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol new file mode 100644 index 00000000000..81bf2959008 --- /dev/null +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -0,0 +1,468 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +// Foundry +import { Vm } from "forge-std/Vm.sol"; +import { Script } from "forge-std/Script.sol"; +import { console2 as console } from "forge-std/console2.sol"; +import { stdJson } from "forge-std/StdJson.sol"; + +// Libraries +import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; +import { LibString } from "@solady/utils/LibString.sol"; +import { Process } from "scripts/libraries/Process.sol"; +import { Bytes } from "src/libraries/Bytes.sol"; + +// Interfaces +import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; + +/// @title VerifyOPCM +/// @notice Verifies the bytecode of an OPContractsManager instance and all associated blueprints +/// and implementations against locally built artifacts. +contract VerifyOPCM is Script { + using stdJson for string; + + /// @notice Thrown when the top-level verification fails. + error VerifyOPCM_Failed(); + + /// @notice Thrown when no implementations are found in the OPCM. + error VerifyOPCM_NoImplementations(); + + /// @notice Thrown when no blueprints are found in the OPCM.` + error VerifyOPCM_NoBlueprints(); + + /// @notice Thrown when an unexpected part number is found in the blueprint. + error VerifyOPCM_UnexpectedPart(); + + /// @notice Thrown when an artifact file is empty. + error VerifyOPCM_EmptyArtifactFile(string _artifactPath); + + /// @notice Thrown when the creation bytecode is not found in an artifact file. + error VerifyOPCM_CreationBytecodeNotFound(string _artifactPath); + + /// @notice Thrown when the runtime bytecode is not found in an artifact file. + error VerifyOPCM_RuntimeBytecodeNotFound(string _artifactPath); + + /// @notice Preamble used for blueprint contracts. + bytes constant BLUEPRINT_PREAMBLE = hex"FE7100"; + + /// @notice Maximum init code size for blueprints. + uint256 constant MAX_INIT_CODE_SIZE = 23500; + + /// @notice Represents a contract name and its corresponding address. + /// @param field Name of the field the address was extracted from. + /// @param name Name of the contract. + /// @param addr Address of the contract. + struct OpcmContractRef { + string field; + string name; + address addr; + bool blueprint; + } + + /// @notice Represents an immutable reference within bytecode. + /// @param length Length of the immutable reference in bytes. + /// @param offset Offset of the immutable reference within the bytecode. + struct ImmutableRef { + uint256 length; + uint256 offset; + } + + /// @notice Represents info loaded from a contract artifact JSON file. + /// @param bytecode The creation bytecode. + /// @param deployedBytecode The runtime bytecode. + /// @param immutableRefs Array of immutable references found in the deployed bytecode. + struct ArtifactInfo { + bytes bytecode; + bytes deployedBytecode; + ImmutableRef[] immutableRefs; + } + + /// @notice Maps OPCM field names (as strings) to an overriding contract name. + mapping(string => string) internal fieldNameOverrides; + + /// @notice Setup flag. + bool internal ready; + + /// @notice Populate the field name overrides mapping. + function setUp() public { + fieldNameOverrides["optimismPortalImpl"] = "OptimismPortal2"; + fieldNameOverrides["mipsImpl"] = "MIPS64"; + fieldNameOverrides["ethLockboxImpl"] = "ETHLockbox"; + fieldNameOverrides["permissionlessDisputeGame1"] = "FaultDisputeGame"; + fieldNameOverrides["permissionlessDisputeGame2"] = "FaultDisputeGame"; + fieldNameOverrides["permissionedDisputeGame1"] = "PermissionedDisputeGame"; + fieldNameOverrides["permissionedDisputeGame2"] = "PermissionedDisputeGame"; + fieldNameOverrides["superPermissionlessDisputeGame1"] = "SuperFaultDisputeGame"; + fieldNameOverrides["superPermissionlessDisputeGame2"] = "SuperFaultDisputeGame"; + fieldNameOverrides["superPermissionedDisputeGame1"] = "SuperPermissionedDisputeGame"; + fieldNameOverrides["superPermissionedDisputeGame2"] = "SuperPermissionedDisputeGame"; + ready = true; + } + + /// @notice Entry point for the script when run via `forge script`, reads the OPCM address from + /// the environment variable OPCM_ADDRESS. Use run(address) if you want to specify the + /// address as an argument instead. + function run() external { + address opcmAddress = vm.envAddress("OPCM_ADDRESS"); + require(opcmAddress != address(0), "OPCM_ADDRESS env var not set or invalid"); + run(opcmAddress); + } + + /// @notice Main verification logic. + /// @param _opcmAddress Address of the OPContractsManager contract to verify. + function run(address _opcmAddress) public { + // Make sure the setup function has been called. + if (!ready) { + setUp(); + } + + // Fetch Implementations & Blueprints from OPCM + IOPContractsManager opcm = IOPContractsManager(_opcmAddress); + + // Collect all the references. + OpcmContractRef[] memory refs = _collectOpcmContractRefs(opcm); + + // Verify each reference. + bool success = true; + for (uint256 i = 0; i < refs.length; i++) { + success = _verifyOpcmContractRef(refs[i]) && success; + } + + // Final Result + console.log(); + if (success) { + console.log("Overall Verification Status: SUCCESS"); + } else { + console.log("Overall Verification Status: FAILED"); + revert VerifyOPCM_Failed(); + } + } + + /// @notice Collects all the references from the OPCM contract. + /// @param _opcm The live OPCM contract. + /// @return Array of OpcmContractRef structs containing contract names/addresses. + function _collectOpcmContractRefs(IOPContractsManager _opcm) internal returns (OpcmContractRef[] memory) { + // Collect implementation references. + OpcmContractRef[] memory implRefs = _getOpcmContractRefs(_opcm, "implementations", false); + if (implRefs.length == 0) { + revert VerifyOPCM_NoImplementations(); + } + + // Collect blueprint references. + OpcmContractRef[] memory bpRefs = _getOpcmContractRefs(_opcm, "blueprints", true); + if (bpRefs.length == 0) { + revert VerifyOPCM_NoBlueprints(); + } + + // Create a single array to join everything together. + OpcmContractRef[] memory refs = new OpcmContractRef[](implRefs.length + bpRefs.length + 1); + + // Reference for OPCM itself. + refs[0] = OpcmContractRef({ field: "OPCM", name: "OPContractsManager", addr: address(_opcm), blueprint: false }); + + // Add the implementation references. + for (uint256 i = 0; i < implRefs.length; i++) { + refs[i + 1] = implRefs[i]; + } + + // Add the blueprint references. + for (uint256 i = 0; i < bpRefs.length; i++) { + refs[i + implRefs.length + 1] = bpRefs[i]; + } + + // Return the combined references. + return refs; + } + + /// @notice Verifies a single OPCM contract reference (implementation or bytecode). + /// @param _target The target contract reference to verify. + /// @return True if the contract reference is verified, false otherwise. + function _verifyOpcmContractRef(OpcmContractRef memory _target) internal view returns (bool) { + console.log(); + console.log(string.concat("Checking Contract: ", _target.field)); + console.log(string.concat(" Type: ", _target.blueprint ? "Blueprint" : "Implementation")); + console.log(string.concat(" Contract: ", _target.name)); + console.log(string.concat(" Address: ", vm.toString(_target.addr))); + + // Build the expected path to the artifact file. + string memory artifactPath = _buildArtifactPath(_target.name); + console.log(string.concat(" Expected Runtime Artifact: ", artifactPath)); + + // Load artifact information (bytecode, immutable refs) for detailed comparison + ArtifactInfo memory artifact = _loadArtifactInfo(artifactPath); + + // Grab the actual code. + bytes memory actualCode = _target.addr.code; + + // Figure out expected code. + bytes memory expectedCode; + if (_target.blueprint) { + // Determine which part of the blueprint this is using final digit as signifier. + uint8 partNumber = 1; + bytes memory fieldBytes = bytes(_target.field); + if (fieldBytes.length > 0) { + uint8 lastChar = uint8(fieldBytes[fieldBytes.length - 1]); + if (lastChar >= uint8(bytes1("1")) && lastChar <= uint8(bytes1("9"))) { + partNumber = lastChar - uint8(bytes1("0")); + } + } + + // Split the creation code. + bytes memory creationCodePart; + if (partNumber == 1) { + // First part: take initial MAX_INIT_CODE_SIZE bytes. + creationCodePart = + Bytes.slice(artifact.bytecode, 0, Math.min(MAX_INIT_CODE_SIZE, artifact.bytecode.length)); + } else if (partNumber == 2) { + // Second part: take remaining bytes. + creationCodePart = + Bytes.slice(artifact.bytecode, MAX_INIT_CODE_SIZE, artifact.bytecode.length - MAX_INIT_CODE_SIZE); + } else { + // We don't support >2 parts for now, this is an explicit error. + revert VerifyOPCM_UnexpectedPart(); + } + + // Create expected blueprint code for this part. + expectedCode = abi.encodePacked(BLUEPRINT_PREAMBLE, creationCodePart); + } else { + expectedCode = artifact.deployedBytecode; + } + + // Perform detailed bytecode comparison. + bool success = _compareBytecode(actualCode, expectedCode, _target.name, artifact, !_target.blueprint); + + // Log final status for this field. + if (success) { + console.log(string.concat("Status: [OK] Verified ", _target.name)); + } else { + console.log(string.concat("Status: [FAIL] Verification failed for ", _target.name)); + } + + return success; + } + + /// @notice Loads artifact info from a JSON file using Foundry's parsing capabilities. + /// @param _artifactPath Path to the artifact JSON file. + /// @return info The parsed artifact information containing bytecode and immutable references. + function _loadArtifactInfo(string memory _artifactPath) internal view returns (ArtifactInfo memory) { + // Read and parse the artifact file. + string memory artifactJson = vm.readFile(_artifactPath); + if (bytes(artifactJson).length == 0) { + revert VerifyOPCM_EmptyArtifactFile(_artifactPath); + } + + // Parse the creation bytecode. + bytes memory bytecode = vm.parseBytes(artifactJson.readString(".bytecode.object")); + if (bytecode.length == 0) { + revert VerifyOPCM_CreationBytecodeNotFound(_artifactPath); + } + + // Parse the runtime bytecode. + bytes memory deployedBytecode = vm.parseBytes(artifactJson.readString(".deployedBytecode.object")); + if (deployedBytecode.length == 0) { + revert VerifyOPCM_RuntimeBytecodeNotFound(_artifactPath); + } + + // Put together the artifact info struct. + return ArtifactInfo({ + bytecode: bytecode, + deployedBytecode: deployedBytecode, + immutableRefs: _parseImmutableRefs(artifactJson) + }); + } + + /// @notice Parses immutable references from the artifact JSON. + /// @param _artifactJson Complete artifact JSON string. + /// @return Array of parsed immutable reference structs {offset, length}. + function _parseImmutableRefs(string memory _artifactJson) internal view returns (ImmutableRef[] memory) { + // Check if immutableReferences exists, skip if not. + if (!vm.keyExistsJson(_artifactJson, ".deployedBytecode.immutableReferences")) { + return new ImmutableRef[](0); + } + + // Grab all keys (AST node IDs) from the immutableReferences object. + string[] memory keys = vm.parseJsonKeys(_artifactJson, ".deployedBytecode.immutableReferences"); + if (keys.length == 0) { + return new ImmutableRef[](0); + } + + // Count the total number of individual references across all keys. + uint256 totalRefs = 0; + for (uint256 i = 0; i < keys.length; i++) { + string memory key = keys[i]; + string memory refsPath = string.concat(".deployedBytecode.immutableReferences.", key); + ImmutableRef[] memory positions = abi.decode(vm.parseJson(_artifactJson, refsPath), (ImmutableRef[])); + totalRefs += positions.length; + } + + // Allocate the final array to hold all references. + ImmutableRef[] memory refs = new ImmutableRef[](totalRefs); + uint256 refIdx = 0; + + // Populate the final array with references from each key. + for (uint256 i = 0; i < keys.length; i++) { + string memory key = keys[i]; + string memory refsPath = string.concat(".deployedBytecode.immutableReferences.", key); + ImmutableRef[] memory positions = abi.decode(vm.parseJson(_artifactJson, refsPath), (ImmutableRef[])); + for (uint256 j = 0; j < positions.length; j++) { + refs[refIdx++] = positions[j]; + } + } + + return refs; + } + + /// @notice Compares two bytecode arrays for differences. + /// @param _actual The actual bytecode obtained from the chain. + /// @param _expected The expected bytecode from the local artifact. + /// @param _contractName The name of the contract being compared (for logging). + /// @param _artifact Additional artifact info (used for immutable reference checking). + /// @param _allowImmutables True if immutables are allowed to be different, false otherwise. + /// @return True if bytecodes match exactly or if differences only occur within known immutables. + function _compareBytecode( + bytes memory _actual, + bytes memory _expected, + string memory _contractName, + ArtifactInfo memory _artifact, + bool _allowImmutables + ) + internal + pure + returns (bool) + { + // Basic length check + if (_actual.length != _expected.length) { + console.log(string.concat("[FAIL] ERROR: Bytecode length mismatch for ", _contractName)); + console.log(string.concat(" Expected length: ", vm.toString(_expected.length))); + console.log(string.concat(" Actual length: ", vm.toString(_actual.length))); + return false; + } + + // Simplified logic, compare each byte individually, check if that difference falls within + // an immutable range (if immutables are allowed) or if it's a code difference. + for (uint256 i = 0; i < _actual.length; i++) { + if (_actual[i] != _expected[i] && (!_allowImmutables || !_posInsideImmutable(i, _artifact))) { + console.log(string.concat("[FAIL] ERROR: Bytecode difference found for ", _contractName)); + console.log(string.concat(" Offset: ", vm.toString(i))); + console.log(string.concat(" Expected: ", vm.toString(_expected[i]))); + console.log(string.concat(" Actual: ", vm.toString(_actual[i]))); + return false; + } + } + + // If we're here, the bytecode is identical. + console.log("Status: [OK] Exact Match"); + return true; + } + + /// @notice Uses the OPContractsManager ABI JSON and the live OPCM contract to extract a list + /// of contract names and their corresponding addresses for a given property/struct on + /// the OPCM contract. + /// @param _opcm The live OPCM contract. + /// @param _property The property/struct to extract contract names and addresses from. + /// @param _blueprint Whether this is a blueprint or an implementation. + /// @return Array of OpcmContractRef structs containing contract names/addresses. + function _getOpcmContractRefs( + IOPContractsManager _opcm, + string memory _property, + bool _blueprint + ) + internal + returns (OpcmContractRef[] memory) + { + // Use jq to grab the field names from the ABI. + string[] memory fieldNames = abi.decode( + vm.parseJson( + Process.bash( + string.concat( + "jq -r '[.abi[] | select(.name == \"", + _property, + "\") | .outputs[0].components[].name]' ", + _buildArtifactPath("OPContractsManager") + ) + ) + ), + (string[]) + ); + + // Call the corresponding function on the OPCM contract. + (bool callSuccess, bytes memory returnedData) = + address(_opcm).staticcall(abi.encodeWithSignature(string.concat(_property, "()"))); + if (!callSuccess) { + console.log(string.concat("[FAIL] ERROR: Failed to call ", _property, "() function on OPCM.")); + return new OpcmContractRef[](0); + } + + // Expected length check: numFields * 32 bytes/address. + uint256 expectedDataLength = fieldNames.length * 32; + if (returnedData.length != expectedDataLength) { + console.log(string.concat("[FAIL] ERROR: Returned data length mismatch from ", _property, "() call.")); + console.log(string.concat(" Expected length: ", vm.toString(expectedDataLength))); + console.log(string.concat(" Actual length: ", vm.toString(returnedData.length))); + return new OpcmContractRef[](0); + } + + // Extract the addresses from the returned data. + OpcmContractRef[] memory opcmContractRefs = new OpcmContractRef[](fieldNames.length); + for (uint256 i = 0; i < fieldNames.length; i++) { + string memory fieldName = fieldNames[i]; + uint256 offset = i * 32; + address implAddress = abi.decode(Bytes.slice(returnedData, offset, 32), (address)); + string memory contractName = _getContractNameFromFieldName(fieldName); + opcmContractRefs[i] = + OpcmContractRef({ field: fieldName, name: contractName, addr: implAddress, blueprint: _blueprint }); + } + + // Return the extracted addresses. + return opcmContractRefs; + } + + /// @notice Converts an OPCM field name to a contract name. Not 100% reliable, so use overrides + /// if necessary. Works most of the time though. + /// @param _fieldName The field name to convert. + /// @return The contract name. + function _getContractNameFromFieldName(string memory _fieldName) internal view returns (string memory) { + // Check for an explicit override + string memory overrideName = fieldNameOverrides[_fieldName]; + if (bytes(overrideName).length > 0) { + return overrideName; + } + + // Make a copy of the field name. + string memory fieldName = LibString.slice(_fieldName, 0, bytes(_fieldName).length); + + // Uppercase the first character + bytes memory fieldBytes = bytes(fieldName); + fieldBytes[0] = bytes1(uint8(bytes1("A")) + uint8(fieldBytes[0]) - uint8(bytes1("a"))); + + // If it ends in impl, strip that. + if (LibString.endsWith(_fieldName, "Impl")) { + fieldBytes = Bytes.slice(fieldBytes, 0, fieldBytes.length - 4); + } + + // Return the field name with the first character uppercase + return string(fieldBytes); + } + + /// @notice Checks if a position is inside an immutable reference. + /// @param _pos The position to check. + /// @param _artifact The artifact info. + /// @return True if the position is inside an immutable reference, false otherwise. + function _posInsideImmutable(uint256 _pos, ArtifactInfo memory _artifact) internal pure returns (bool) { + for (uint256 i = 0; i < _artifact.immutableRefs.length; i++) { + ImmutableRef memory ref = _artifact.immutableRefs[i]; + if (_pos >= ref.offset && _pos < ref.offset + ref.length) { + return true; + } + } + return false; + } + + /// @notice Constructs the expected path to Foundry artifact JSON file based on contract name. + /// @param _contractName The simple contract name (e.g., "SystemConfig", "FaultDisputeGame"). + /// @return Path to the artifact file. + function _buildArtifactPath(string memory _contractName) internal pure returns (string memory) { + return string.concat("forge-artifacts/", _contractName, ".sol/", _contractName, ".json"); + } +} diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index df73a7108e5..9de12d1cab4 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -12,12 +12,14 @@ import { DelegateCaller } from "test/mocks/Callers.sol"; import { DeployOPChainInput } from "scripts/deploy/DeployOPChain.s.sol"; import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { Deploy } from "scripts/deploy/Deploy.s.sol"; +import { VerifyOPCM } from "scripts/deploy/VerifyOPCM.s.sol"; // Libraries import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Blueprint } from "src/libraries/Blueprint.sol"; import { ForgeArtifacts } from "scripts/libraries/ForgeArtifacts.sol"; import { Bytes } from "src/libraries/Bytes.sol"; +import { Process } from "scripts/libraries/Process.sol"; // Interfaces import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; @@ -634,6 +636,17 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { runUpgradeTestAndChecks(upgrader); } + function test_verifyOpcmCorrectness_succeeds() public { + skipIfNotOpFork("test_verifyOpcmCorrectness_succeeds"); + + // Run the upgrade test and checks + runUpgradeTestAndChecks(upgrader); + + // Run the verification script + VerifyOPCM verify = new VerifyOPCM(); + verify.run(address(opcm)); + } + function test_isRcFalseAfterCalledByUpgrader_works() public { skipIfNotOpFork("test_isRcFalseAfterCalledByUpgrader_works"); assertTrue(opcm.isRC()); diff --git a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol new file mode 100644 index 00000000000..bb654e87332 --- /dev/null +++ b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Foundry +import { Test } from "forge-std/Test.sol"; +import { Script } from "forge-std/Script.sol"; +import { console2 as console } from "forge-std/console2.sol"; + +// Tests +import { OPContractsManager_TestInit } from "test/L1/OPContractsManager.t.sol"; + +// Scripts +import { VerifyOPCM } from "scripts/deploy/VerifyOPCM.s.sol"; + +// Interfaces +import { IOPContractsManager } from "interfaces/L1/IOPContractsManager.sol"; + +contract VerifyOPCM_Harness is VerifyOPCM { + function loadArtifactInfo(string memory _artifactPath) public view returns (ArtifactInfo memory) { + return _loadArtifactInfo(_artifactPath); + } + + function getOpcmContractRefs( + IOPContractsManager _opcm, + string memory _property, + bool _blueprint + ) + public + returns (OpcmContractRef[] memory) + { + return _getOpcmContractRefs(_opcm, _property, _blueprint); + } + + function buildArtifactPath(string memory _contractName) public pure returns (string memory) { + return _buildArtifactPath(_contractName); + } +} + +contract VerifyOPCM_TestInit is OPContractsManager_TestInit { + VerifyOPCM_Harness internal harness; + + function setUp() public override { + super.setUp(); + harness = new VerifyOPCM_Harness(); + harness.setUp(); + } +} + +contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { + /// @notice Tests that the script succeeds when no changes are introduced. + function test_run_succeeds() public { + harness.run(address(opcm)); + } + + /// @notice Tests that the script succeeds when differences are introduced into the immutable + /// variables of implementation contracts. + /// @param _diffCount Number of differences to introduce. + function testFuzz_run_implementationDifferentInsideImmutable_succeeds(uint8 _diffCount) public { + // At least one diff. + _diffCount = uint8(bound(_diffCount, 1, type(uint8).max)); + + // Grab the list of implementations. + VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "implementations", false); + + // Change bytes at random. + for (uint8 i = 0; i < _diffCount; i++) { + // Pick a random implementation to change. + uint256 randomImplIndex = vm.randomUint(0, refs.length - 1); + VerifyOPCM.OpcmContractRef memory ref = refs[randomImplIndex]; + + // Get the code for the implementation. + bytes memory implCode = ref.addr.code; + + // Grab the artifact info for the implementation. + VerifyOPCM.ArtifactInfo memory artifact = harness.loadArtifactInfo( + harness.buildArtifactPath(ref.name) + ); + + // Skip, no immutable references. Will make some fuzz runs useless but it's not worth + // the extra complexity to handle this properly. + if (artifact.immutableRefs.length == 0) { + continue; + } + + // Find a random byte that's inside an immutable reference. + bool inImmutable = false; + uint256 randomDiffPosition; + while (!inImmutable) { + randomDiffPosition = vm.randomUint(0, implCode.length - 1); + inImmutable = false; + for (uint256 j = 0; j < artifact.immutableRefs.length; j++) { + VerifyOPCM.ImmutableRef memory immRef = artifact.immutableRefs[j]; + if (randomDiffPosition >= immRef.offset && randomDiffPosition < immRef.offset + immRef.length) { + inImmutable = true; + break; + } + } + } + + // Change the byte to something new. + bytes1 existingByte = implCode[randomDiffPosition]; + bytes1 newByte = bytes1(uint8(vm.randomUint(0, 255))); + while (newByte == existingByte) { + newByte = bytes1(uint8(vm.randomUint(0, 255))); + } + + // Write the new byte to the code. + implCode[randomDiffPosition] = newByte; + vm.etch(ref.addr, implCode); + } + + // Run the script. + // No revert expected. + harness.run(address(opcm)); + } + + /// @notice Tests that the script reverts when differences are introduced into the code of + /// implementation contracts that are not inside immutable references. + /// @param _diffCount Number of differences to introduce. + function testFuzz_run_implementationDifferentOutsideImmutable_reverts(uint8 _diffCount) public { + // At least one diff. + _diffCount = uint8(bound(_diffCount, 1, type(uint8).max)); + + // Grab the list of implementations. + VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "implementations", false); + + // Change bytes at random. + for (uint8 i = 0; i < _diffCount; i++) { + // Pick a random implementation to change. + uint256 randomImplIndex = vm.randomUint(0, refs.length - 1); + VerifyOPCM.OpcmContractRef memory ref = refs[randomImplIndex]; + + // Get the code for the implementation. + bytes memory implCode = ref.addr.code; + + // Grab the artifact info for the implementation. + VerifyOPCM.ArtifactInfo memory artifact = harness.loadArtifactInfo( + harness.buildArtifactPath(ref.name) + ); + + // Find a random byte that isn't in an immutable reference. + bool inImmutable = true; + uint256 randomDiffPosition; + while (inImmutable) { + randomDiffPosition = vm.randomUint(0, implCode.length - 1); + inImmutable = false; + for (uint256 j = 0; j < artifact.immutableRefs.length; j++) { + VerifyOPCM.ImmutableRef memory immRef = artifact.immutableRefs[j]; + if (randomDiffPosition >= immRef.offset && randomDiffPosition < immRef.offset + immRef.length) { + inImmutable = true; + break; + } + } + } + + // Change the byte to something new. + bytes1 existingByte = implCode[randomDiffPosition]; + bytes1 newByte = bytes1(uint8(vm.randomUint(0, 255))); + while (newByte == existingByte) { + newByte = bytes1(uint8(vm.randomUint(0, 255))); + } + + // Write the new byte to the code. + implCode[randomDiffPosition] = newByte; + vm.etch(ref.addr, implCode); + } + + // Run the script. + vm.expectRevert(VerifyOPCM.VerifyOPCM_Failed.selector); + harness.run(address(opcm)); + } + + /// @notice Tests that the script reverts when differences are introduced into the code of + /// blueprints. Unlike immutables, any difference anywhere in the blueprint should + /// cause the script to revert. + /// @param _diffCount Number of differences to introduce. + function testFuzz_run_blueprintAnyDifference_reverts(uint8 _diffCount) public { + // At least one diff. + _diffCount = uint8(bound(_diffCount, 1, type(uint8).max)); + + // Grab the list of blueprints. + VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "blueprints", true); + + // Change bytes at random. + for (uint8 i = 0; i < _diffCount; i++) { + // Pick a random blueprint to change. + uint256 randomBlueprintIndex = vm.randomUint(0, refs.length - 1); + VerifyOPCM.OpcmContractRef memory ref = refs[randomBlueprintIndex]; + + // Get the code for the blueprint. + address blueprint = ref.addr; + bytes memory blueprintCode = blueprint.code; + + // We don't care about immutable references for blueprints. + // Pick a random position. + uint256 randomDiffPosition = vm.randomUint(0, blueprintCode.length - 1); + + // Change the byte to something new. + bytes1 existingByte = blueprintCode[randomDiffPosition]; + bytes1 newByte = bytes1(uint8(vm.randomUint(0, 255))); + while (newByte == existingByte) { + newByte = bytes1(uint8(vm.randomUint(0, 255))); + } + + // Write the new byte to the code. + blueprintCode[randomDiffPosition] = newByte; + vm.etch(blueprint, blueprintCode); + } + + // Run the script. + vm.expectRevert(VerifyOPCM.VerifyOPCM_Failed.selector); + harness.run(address(opcm)); + } +} From 798da8a68fe5503f957bbe3b91e308dab2dfb5ef Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 15 Apr 2025 15:06:26 -0400 Subject: [PATCH 07/14] fix: ci checks --- .circleci/config.yml | 1 - .../contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol | 8 +++----- .../contracts-bedrock/test/L1/OPContractsManager.t.sol | 1 - packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol | 5 ----- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 947ffbe3378..7230a6429d4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1800,7 +1800,6 @@ workflows: op-e2e/actions op-e2e/faultproofs packages/contracts-bedrock/scripts/checks - packages/contracts-bedrock/scripts/verify op-dripper devnet-sdk op-acceptance-tests diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol index 81bf2959008..5f314962ab8 100644 --- a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.15; // Foundry -import { Vm } from "forge-std/Vm.sol"; import { Script } from "forge-std/Script.sol"; import { console2 as console } from "forge-std/console2.sol"; import { stdJson } from "forge-std/StdJson.sol"; @@ -104,9 +103,7 @@ contract VerifyOPCM is Script { /// the environment variable OPCM_ADDRESS. Use run(address) if you want to specify the /// address as an argument instead. function run() external { - address opcmAddress = vm.envAddress("OPCM_ADDRESS"); - require(opcmAddress != address(0), "OPCM_ADDRESS env var not set or invalid"); - run(opcmAddress); + run(vm.envAddress("OPCM_ADDRESS")); } /// @notice Main verification logic. @@ -386,7 +383,8 @@ contract VerifyOPCM is Script { (string[]) ); - // Call the corresponding function on the OPCM contract. + // Call the corresponding function on the OPCM contract.] + // nosemgrep: sol-style-use-abi-encodecall (bool callSuccess, bytes memory returnedData) = address(_opcm).staticcall(abi.encodeWithSignature(string.concat(_property, "()"))); if (!callSuccess) { diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 9de12d1cab4..8f1796b7a60 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -19,7 +19,6 @@ import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import { Blueprint } from "src/libraries/Blueprint.sol"; import { ForgeArtifacts } from "scripts/libraries/ForgeArtifacts.sol"; import { Bytes } from "src/libraries/Bytes.sol"; -import { Process } from "scripts/libraries/Process.sol"; // Interfaces import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; diff --git a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol index bb654e87332..607d9c3cb35 100644 --- a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol +++ b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol @@ -1,11 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -// Foundry -import { Test } from "forge-std/Test.sol"; -import { Script } from "forge-std/Script.sol"; -import { console2 as console } from "forge-std/console2.sol"; - // Tests import { OPContractsManager_TestInit } from "test/L1/OPContractsManager.t.sol"; From f275174d826127b2ea6368b2e19f32d48d06ba1a Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 15 Apr 2025 15:31:41 -0400 Subject: [PATCH 08/14] fix: use randomness not fuzzing --- .../scripts/deploy/VerifyOPCM.s.sol | 62 ++++++++++++++++--- .../test/scripts/VerifyOPCM.t.sol | 50 ++++++--------- 2 files changed, 72 insertions(+), 40 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol index 5f314962ab8..62abbc4f753 100644 --- a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -80,11 +80,15 @@ contract VerifyOPCM is Script { /// @notice Maps OPCM field names (as strings) to an overriding contract name. mapping(string => string) internal fieldNameOverrides; + /// @notice Maps contract names to an overriding source file name. + mapping(string => string) internal sourceNameOverrides; + /// @notice Setup flag. bool internal ready; - /// @notice Populate the field name overrides mapping. + /// @notice Populates override mappings. function setUp() public { + // Overrides for situations where field names do not cleanly map to contract names. fieldNameOverrides["optimismPortalImpl"] = "OptimismPortal2"; fieldNameOverrides["mipsImpl"] = "MIPS64"; fieldNameOverrides["ethLockboxImpl"] = "ETHLockbox"; @@ -96,6 +100,14 @@ contract VerifyOPCM is Script { fieldNameOverrides["superPermissionlessDisputeGame2"] = "SuperFaultDisputeGame"; fieldNameOverrides["superPermissionedDisputeGame1"] = "SuperPermissionedDisputeGame"; fieldNameOverrides["superPermissionedDisputeGame2"] = "SuperPermissionedDisputeGame"; + + // Overrides for situations where contracts have differently named source files. + sourceNameOverrides["OPContractsManagerGameTypeAdder"] = "OPContractsManager"; + sourceNameOverrides["OPContractsManagerDeployer"] = "OPContractsManager"; + sourceNameOverrides["OPContractsManagerUpgrader"] = "OPContractsManager"; + sourceNameOverrides["OPContractsManagerInteropMigrator"] = "OPContractsManager"; + + // Mark as ready. ready = true; } @@ -153,19 +165,44 @@ contract VerifyOPCM is Script { } // Create a single array to join everything together. - OpcmContractRef[] memory refs = new OpcmContractRef[](implRefs.length + bpRefs.length + 1); - - // Reference for OPCM itself. - refs[0] = OpcmContractRef({ field: "OPCM", name: "OPContractsManager", addr: address(_opcm), blueprint: false }); + uint256 extraRefs = 5; + OpcmContractRef[] memory refs = new OpcmContractRef[](implRefs.length + bpRefs.length + extraRefs); + + // References for OPCM and linked contracts. + refs[0] = OpcmContractRef({ field: "opcm", name: "OPContractsManager", addr: address(_opcm), blueprint: false }); + refs[1] = OpcmContractRef({ + field: "opcmGameTypeAdder", + name: "OPContractsManagerGameTypeAdder", + addr: address(_opcm.opcmGameTypeAdder()), + blueprint: false + }); + refs[2] = OpcmContractRef({ + field: "opcmDeployer", + name: "OPContractsManagerDeployer", + addr: address(_opcm.opcmDeployer()), + blueprint: false + }); + refs[3] = OpcmContractRef({ + field: "opcmUpgrader", + name: "OPContractsManagerUpgrader", + addr: address(_opcm.opcmUpgrader()), + blueprint: false + }); + refs[4] = OpcmContractRef({ + field: "opcmInteropMigrator", + name: "OPContractsManagerInteropMigrator", + addr: address(_opcm.opcmInteropMigrator()), + blueprint: false + }); // Add the implementation references. for (uint256 i = 0; i < implRefs.length; i++) { - refs[i + 1] = implRefs[i]; + refs[i + extraRefs] = implRefs[i]; } // Add the blueprint references. for (uint256 i = 0; i < bpRefs.length; i++) { - refs[i + implRefs.length + 1] = bpRefs[i]; + refs[i + implRefs.length + extraRefs] = bpRefs[i]; } // Return the combined references. @@ -460,7 +497,14 @@ contract VerifyOPCM is Script { /// @notice Constructs the expected path to Foundry artifact JSON file based on contract name. /// @param _contractName The simple contract name (e.g., "SystemConfig", "FaultDisputeGame"). /// @return Path to the artifact file. - function _buildArtifactPath(string memory _contractName) internal pure returns (string memory) { - return string.concat("forge-artifacts/", _contractName, ".sol/", _contractName, ".json"); + function _buildArtifactPath(string memory _contractName) internal view returns (string memory) { + // Potentially need to override the source name if multiple contracts are defined in the same file. + string memory sourceName = _contractName; + if (bytes(sourceNameOverrides[_contractName]).length > 0) { + sourceName = sourceNameOverrides[_contractName]; + } + + // Return computed path, relative to the contracts-bedrock directory. + return string.concat("forge-artifacts/", sourceName, ".sol/", _contractName, ".json"); } } diff --git a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol index 607d9c3cb35..4a2a0db35f1 100644 --- a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol +++ b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; +import { console2 as console } from "forge-std/console2.sol"; + // Tests import { OPContractsManager_TestInit } from "test/L1/OPContractsManager.t.sol"; @@ -26,7 +28,7 @@ contract VerifyOPCM_Harness is VerifyOPCM { return _getOpcmContractRefs(_opcm, _property, _blueprint); } - function buildArtifactPath(string memory _contractName) public pure returns (string memory) { + function buildArtifactPath(string memory _contractName) public view returns (string memory) { return _buildArtifactPath(_contractName); } } @@ -48,17 +50,14 @@ contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { } /// @notice Tests that the script succeeds when differences are introduced into the immutable - /// variables of implementation contracts. - /// @param _diffCount Number of differences to introduce. - function testFuzz_run_implementationDifferentInsideImmutable_succeeds(uint8 _diffCount) public { - // At least one diff. - _diffCount = uint8(bound(_diffCount, 1, type(uint8).max)); - + /// variables of implementation contracts. Fuzzing is too slow here, randomness is good + /// enough. + function test_run_implementationDifferentInsideImmutable_succeeds() public { // Grab the list of implementations. VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "implementations", false); - // Change bytes at random. - for (uint8 i = 0; i < _diffCount; i++) { + // Change 256 bytes at random. + for (uint8 i = 0; i < 255; i++) { // Pick a random implementation to change. uint256 randomImplIndex = vm.randomUint(0, refs.length - 1); VerifyOPCM.OpcmContractRef memory ref = refs[randomImplIndex]; @@ -67,9 +66,7 @@ contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { bytes memory implCode = ref.addr.code; // Grab the artifact info for the implementation. - VerifyOPCM.ArtifactInfo memory artifact = harness.loadArtifactInfo( - harness.buildArtifactPath(ref.name) - ); + VerifyOPCM.ArtifactInfo memory artifact = harness.loadArtifactInfo(harness.buildArtifactPath(ref.name)); // Skip, no immutable references. Will make some fuzz runs useless but it's not worth // the extra complexity to handle this properly. @@ -110,17 +107,14 @@ contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { } /// @notice Tests that the script reverts when differences are introduced into the code of - /// implementation contracts that are not inside immutable references. - /// @param _diffCount Number of differences to introduce. - function testFuzz_run_implementationDifferentOutsideImmutable_reverts(uint8 _diffCount) public { - // At least one diff. - _diffCount = uint8(bound(_diffCount, 1, type(uint8).max)); - + /// implementation contracts that are not inside immutable references. Fuzzing is too + /// slow here, randomness is good enough. + function test_run_implementationDifferentOutsideImmutable_reverts() public { // Grab the list of implementations. VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "implementations", false); - // Change bytes at random. - for (uint8 i = 0; i < _diffCount; i++) { + // Change 256 bytes at random. + for (uint8 i = 0; i < 255; i++) { // Pick a random implementation to change. uint256 randomImplIndex = vm.randomUint(0, refs.length - 1); VerifyOPCM.OpcmContractRef memory ref = refs[randomImplIndex]; @@ -129,9 +123,7 @@ contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { bytes memory implCode = ref.addr.code; // Grab the artifact info for the implementation. - VerifyOPCM.ArtifactInfo memory artifact = harness.loadArtifactInfo( - harness.buildArtifactPath(ref.name) - ); + VerifyOPCM.ArtifactInfo memory artifact = harness.loadArtifactInfo(harness.buildArtifactPath(ref.name)); // Find a random byte that isn't in an immutable reference. bool inImmutable = true; @@ -167,17 +159,13 @@ contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { /// @notice Tests that the script reverts when differences are introduced into the code of /// blueprints. Unlike immutables, any difference anywhere in the blueprint should - /// cause the script to revert. - /// @param _diffCount Number of differences to introduce. - function testFuzz_run_blueprintAnyDifference_reverts(uint8 _diffCount) public { - // At least one diff. - _diffCount = uint8(bound(_diffCount, 1, type(uint8).max)); - + /// cause the script to revert. Fuzzing is too slow here, randomness is good enough. + function test_run_blueprintAnyDifference_reverts() public { // Grab the list of blueprints. VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "blueprints", true); - // Change bytes at random. - for (uint8 i = 0; i < _diffCount; i++) { + // Change 256 bytes at random. + for (uint8 i = 0; i < 255; i++) { // Pick a random blueprint to change. uint256 randomBlueprintIndex = vm.randomUint(0, refs.length - 1); VerifyOPCM.OpcmContractRef memory ref = refs[randomBlueprintIndex]; From 6eecbc1526667ccb9a4b9ac17f57f96eee0544f0 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Tue, 15 Apr 2025 15:46:47 -0400 Subject: [PATCH 09/14] fix: skip tests for coverage --- .../test/scripts/VerifyOPCM.t.sol | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol index 4a2a0db35f1..7292111825b 100644 --- a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol +++ b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { console2 as console } from "forge-std/console2.sol"; +// Foundry +import { VmSafe } from "forge-std/Vm.sol"; // Tests import { OPContractsManager_TestInit } from "test/L1/OPContractsManager.t.sol"; @@ -41,11 +42,22 @@ contract VerifyOPCM_TestInit is OPContractsManager_TestInit { harness = new VerifyOPCM_Harness(); harness.setUp(); } + + /// @notice Skips if running in coverage mode. + function skipIfCoverage() public { + if (vm.isContext(VmSafe.ForgeContext.Coverage)) { + vm.skip(true); + } + } } contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { /// @notice Tests that the script succeeds when no changes are introduced. function test_run_succeeds() public { + // Coverage changes bytecode and causes failures, skip. + skipIfCoverage(); + + // Run the script. harness.run(address(opcm)); } @@ -53,6 +65,9 @@ contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { /// variables of implementation contracts. Fuzzing is too slow here, randomness is good /// enough. function test_run_implementationDifferentInsideImmutable_succeeds() public { + // Coverage changes bytecode and causes failures, skip. + skipIfCoverage(); + // Grab the list of implementations. VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "implementations", false); @@ -110,6 +125,9 @@ contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { /// implementation contracts that are not inside immutable references. Fuzzing is too /// slow here, randomness is good enough. function test_run_implementationDifferentOutsideImmutable_reverts() public { + // Coverage changes bytecode and causes failures, skip. + skipIfCoverage(); + // Grab the list of implementations. VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "implementations", false); @@ -161,6 +179,9 @@ contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { /// blueprints. Unlike immutables, any difference anywhere in the blueprint should /// cause the script to revert. Fuzzing is too slow here, randomness is good enough. function test_run_blueprintAnyDifference_reverts() public { + // Coverage changes bytecode and causes failures, skip. + skipIfCoverage(); + // Grab the list of blueprints. VerifyOPCM.OpcmContractRef[] memory refs = harness.getOpcmContractRefs(opcm, "blueprints", true); From e86c519428bd95ee8d282552014f4ac91171bf8e Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Wed, 16 Apr 2025 14:12:41 -0400 Subject: [PATCH 10/14] feat: automatically scan opcm prefixed properties --- .../scripts/deploy/VerifyOPCM.s.sol | 96 +++++++++++++------ .../test/scripts/VerifyOPCM.t.sol | 4 + 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol index 62abbc4f753..ff21474691b 100644 --- a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -24,6 +24,9 @@ contract VerifyOPCM is Script { /// @notice Thrown when the top-level verification fails. error VerifyOPCM_Failed(); + /// @notice Thrown when no properties are found in the OPCM. + error VerifyOPCM_NoProperties(); + /// @notice Thrown when no implementations are found in the OPCM. error VerifyOPCM_NoImplementations(); @@ -100,6 +103,10 @@ contract VerifyOPCM is Script { fieldNameOverrides["superPermissionlessDisputeGame2"] = "SuperFaultDisputeGame"; fieldNameOverrides["superPermissionedDisputeGame1"] = "SuperPermissionedDisputeGame"; fieldNameOverrides["superPermissionedDisputeGame2"] = "SuperPermissionedDisputeGame"; + fieldNameOverrides["opcmGameTypeAdder"] = "OPContractsManagerGameTypeAdder"; + fieldNameOverrides["opcmDeployer"] = "OPContractsManagerDeployer"; + fieldNameOverrides["opcmUpgrader"] = "OPContractsManagerUpgrader"; + fieldNameOverrides["opcmInteropMigrator"] = "OPContractsManagerInteropMigrator"; // Overrides for situations where contracts have differently named source files. sourceNameOverrides["OPContractsManagerGameTypeAdder"] = "OPContractsManager"; @@ -152,6 +159,12 @@ contract VerifyOPCM is Script { /// @param _opcm The live OPCM contract. /// @return Array of OpcmContractRef structs containing contract names/addresses. function _collectOpcmContractRefs(IOPContractsManager _opcm) internal returns (OpcmContractRef[] memory) { + // Collect property references. + OpcmContractRef[] memory propRefs = _getOpcmPropertyRefs(_opcm); + if (propRefs.length == 0) { + revert VerifyOPCM_NoProperties(); + } + // Collect implementation references. OpcmContractRef[] memory implRefs = _getOpcmContractRefs(_opcm, "implementations", false); if (implRefs.length == 0) { @@ -165,44 +178,26 @@ contract VerifyOPCM is Script { } // Create a single array to join everything together. - uint256 extraRefs = 5; - OpcmContractRef[] memory refs = new OpcmContractRef[](implRefs.length + bpRefs.length + extraRefs); + uint256 extraRefs = 1; + OpcmContractRef[] memory refs = + new OpcmContractRef[](propRefs.length + implRefs.length + bpRefs.length + extraRefs); // References for OPCM and linked contracts. refs[0] = OpcmContractRef({ field: "opcm", name: "OPContractsManager", addr: address(_opcm), blueprint: false }); - refs[1] = OpcmContractRef({ - field: "opcmGameTypeAdder", - name: "OPContractsManagerGameTypeAdder", - addr: address(_opcm.opcmGameTypeAdder()), - blueprint: false - }); - refs[2] = OpcmContractRef({ - field: "opcmDeployer", - name: "OPContractsManagerDeployer", - addr: address(_opcm.opcmDeployer()), - blueprint: false - }); - refs[3] = OpcmContractRef({ - field: "opcmUpgrader", - name: "OPContractsManagerUpgrader", - addr: address(_opcm.opcmUpgrader()), - blueprint: false - }); - refs[4] = OpcmContractRef({ - field: "opcmInteropMigrator", - name: "OPContractsManagerInteropMigrator", - addr: address(_opcm.opcmInteropMigrator()), - blueprint: false - }); + + // Add the property references. + for (uint256 i = 0; i < propRefs.length; i++) { + refs[i + extraRefs] = propRefs[i]; + } // Add the implementation references. for (uint256 i = 0; i < implRefs.length; i++) { - refs[i + extraRefs] = implRefs[i]; + refs[i + extraRefs + propRefs.length] = implRefs[i]; } // Add the blueprint references. for (uint256 i = 0; i < bpRefs.length; i++) { - refs[i + implRefs.length + extraRefs] = bpRefs[i]; + refs[i + extraRefs + propRefs.length + implRefs.length] = bpRefs[i]; } // Return the combined references. @@ -390,6 +385,51 @@ contract VerifyOPCM is Script { return true; } + /// @notice Uses the OPContractsManager ABI JSON and the live OPCM contract to extract a list + /// of contract names and their corresonding addresses for the various immutable + /// references to other OPCM contracts. + /// @param _opcm The live OPCM contract. + /// @return Array of OpcmContractRef structs containing contract names/addresses. + function _getOpcmPropertyRefs(IOPContractsManager _opcm) internal returns (OpcmContractRef[] memory) { + // Find all functions that start with "opcm". + string[] memory functionNames = abi.decode( + vm.parseJson( + Process.bash( + string.concat( + "jq -r '[.abi[] | select(.name? and (.name | type == \"string\") and (.name | startswith(\"opcm\"))) | .name]' ", + _buildArtifactPath("OPContractsManager") + ) + ) + ), + (string[]) + ); + + // For each of these, turn into a contract reference. + OpcmContractRef[] memory refs = new OpcmContractRef[](functionNames.length); + for (uint256 i = 0; i < functionNames.length; i++) { + // Get the function name. + string memory functionName = functionNames[i]; + + // Call the function to retrieve the encoded address. + (bool callSuccess, bytes memory returnedData) = + address(_opcm).staticcall(abi.encodeWithSignature(string.concat(functionName, "()"))); + if (!callSuccess) { + console.log(string.concat("[FAIL] ERROR: Failed to call ", functionName, "() function on OPCM.")); + return new OpcmContractRef[](0); + } + + // Decode as an address. + address implAddress = abi.decode(returnedData, (address)); + + // Add to the list. + string memory contractName = _getContractNameFromFieldName(functionName); + refs[i] = OpcmContractRef({ field: functionName, name: contractName, addr: implAddress, blueprint: false }); + } + + // Return the results. + return refs; + } + /// @notice Uses the OPContractsManager ABI JSON and the live OPCM contract to extract a list /// of contract names and their corresponding addresses for a given property/struct on /// the OPCM contract. diff --git a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol index 7292111825b..6e4c2107fa5 100644 --- a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol +++ b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol @@ -18,6 +18,10 @@ contract VerifyOPCM_Harness is VerifyOPCM { return _loadArtifactInfo(_artifactPath); } + function getOpcmPropertyRefs(IOPContractsManager _opcm) public returns (OpcmContractRef[] memory) { + return _getOpcmPropertyRefs(_opcm); + } + function getOpcmContractRefs( IOPContractsManager _opcm, string memory _property, From a3c3e6e94a40f8354f0e98b4fd30643b37579bcd Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Thu, 17 Apr 2025 11:41:19 -0400 Subject: [PATCH 11/14] fix: few more test fixes --- packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol | 3 ++- packages/contracts-bedrock/test/L1/OPContractsManager.t.sol | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol index ff21474691b..b08e3e95b48 100644 --- a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -411,6 +411,7 @@ contract VerifyOPCM is Script { string memory functionName = functionNames[i]; // Call the function to retrieve the encoded address. + // nosemgrep: sol-style-use-abi-encodecall (bool callSuccess, bytes memory returnedData) = address(_opcm).staticcall(abi.encodeWithSignature(string.concat(functionName, "()"))); if (!callSuccess) { @@ -460,7 +461,7 @@ contract VerifyOPCM is Script { (string[]) ); - // Call the corresponding function on the OPCM contract.] + // Call the corresponding function on the OPCM contract. // nosemgrep: sol-style-use-abi-encodecall (bool callSuccess, bytes memory returnedData) = address(_opcm).staticcall(abi.encodeWithSignature(string.concat(_property, "()"))); diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 8f1796b7a60..85c2986e01e 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -637,6 +637,7 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { function test_verifyOpcmCorrectness_succeeds() public { skipIfNotOpFork("test_verifyOpcmCorrectness_succeeds"); + skipIfCoverage(); // Coverage changes bytecode and breaks the verification script. // Run the upgrade test and checks runUpgradeTestAndChecks(upgrader); From ac4e95333e845ad9f7cda1d25444414f0ae403ba Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 2 May 2025 16:22:56 -0400 Subject: [PATCH 12/14] feat: add single verification check --- packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol index b08e3e95b48..d66bbaf7df7 100644 --- a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -125,6 +125,13 @@ contract VerifyOPCM is Script { run(vm.envAddress("OPCM_ADDRESS")); } + /// @notice Entry point for the script when trying to verify a single contract by name. + /// @param _name Name of the contract to verify. + /// @param _addr Address of the contract to verify. + function runSingle(string memory _name, address _addr) public view { + _verifyOpcmContractRef(OpcmContractRef({ field: _name, name: _name, addr: _addr, blueprint: false })); + } + /// @notice Main verification logic. /// @param _opcmAddress Address of the OPContractsManager contract to verify. function run(address _opcmAddress) public { From 989c3968a65d9c3b3297d0a64d7e96031fc41621 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Fri, 2 May 2025 17:15:56 -0400 Subject: [PATCH 13/14] feat: add etherscan constructor verification --- .../scripts/deploy/VerifyOPCM.s.sol | 89 +++++++++++++++++-- .../scripts/libraries/Config.sol | 5 ++ .../test/L1/OPContractsManager.t.sol | 6 +- .../test/scripts/VerifyOPCM.t.sol | 8 +- 4 files changed, 95 insertions(+), 13 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol index d66bbaf7df7..1efa5b7da54 100644 --- a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -10,6 +10,7 @@ import { stdJson } from "forge-std/StdJson.sol"; import { Math } from "openzeppelin-contracts/contracts/utils/math/Math.sol"; import { LibString } from "@solady/utils/LibString.sol"; import { Process } from "scripts/libraries/Process.sol"; +import { Config } from "scripts/libraries/Config.sol"; import { Bytes } from "src/libraries/Bytes.sol"; // Interfaces @@ -120,26 +121,38 @@ contract VerifyOPCM is Script { /// @notice Entry point for the script when run via `forge script`, reads the OPCM address from /// the environment variable OPCM_ADDRESS. Use run(address) if you want to specify the - /// address as an argument instead. + /// address as an argument instead. Running in this mode will not allow you to skip + /// constructor verification. function run() external { - run(vm.envAddress("OPCM_ADDRESS")); + run(vm.envAddress("OPCM_ADDRESS"), false); } /// @notice Entry point for the script when trying to verify a single contract by name. /// @param _name Name of the contract to verify. /// @param _addr Address of the contract to verify. - function runSingle(string memory _name, address _addr) public view { - _verifyOpcmContractRef(OpcmContractRef({ field: _name, name: _name, addr: _addr, blueprint: false })); + /// @param _skipConstructorVerification Whether to skip constructor verification. + function runSingle(string memory _name, address _addr, bool _skipConstructorVerification) public { + _verifyOpcmContractRef( + OpcmContractRef({ field: _name, name: _name, addr: _addr, blueprint: false }), _skipConstructorVerification + ); } /// @notice Main verification logic. /// @param _opcmAddress Address of the OPContractsManager contract to verify. - function run(address _opcmAddress) public { + /// @param _skipConstructorVerification Whether to skip constructor verification. + function run(address _opcmAddress, bool _skipConstructorVerification) public { // Make sure the setup function has been called. if (!ready) { setUp(); } + // Log a warning if constructor verification is being skipped. + if (_skipConstructorVerification) { + console.log("WARNING: Constructor verification is being skipped"); + console.log(" ONLY to be used in test environments"); + console.log(" Do NOT do this in production"); + } + // Fetch Implementations & Blueprints from OPCM IOPContractsManager opcm = IOPContractsManager(_opcmAddress); @@ -149,7 +162,7 @@ contract VerifyOPCM is Script { // Verify each reference. bool success = true; for (uint256 i = 0; i < refs.length; i++) { - success = _verifyOpcmContractRef(refs[i]) && success; + success = _verifyOpcmContractRef(refs[i], _skipConstructorVerification) && success; } // Final Result @@ -213,8 +226,15 @@ contract VerifyOPCM is Script { /// @notice Verifies a single OPCM contract reference (implementation or bytecode). /// @param _target The target contract reference to verify. + /// @param _skipConstructorVerification Whether to skip constructor verification. /// @return True if the contract reference is verified, false otherwise. - function _verifyOpcmContractRef(OpcmContractRef memory _target) internal view returns (bool) { + function _verifyOpcmContractRef( + OpcmContractRef memory _target, + bool _skipConstructorVerification + ) + internal + returns (bool) + { console.log(); console.log(string.concat("Checking Contract: ", _target.field)); console.log(string.concat(" Type: ", _target.blueprint ? "Blueprint" : "Implementation")); @@ -268,6 +288,61 @@ contract VerifyOPCM is Script { // Perform detailed bytecode comparison. bool success = _compareBytecode(actualCode, expectedCode, _target.name, artifact, !_target.blueprint); + // If requested and this is not a blueprint, we also need to check the creation code. + if (!_target.blueprint && !_skipConstructorVerification) { + // Use the Etherscan API to get the creation code. + bytes memory actualCreationCode = bytes( + Process.bash( + string.concat( + "curl -s 'https://api.etherscan.io/v2/api?chainid=", + vm.toString(block.chainid), + "&module=contract&action=getcontractcreation&contractaddresses=", + vm.toString(_target.addr), + "&apikey=", + Config.etherscanApiKey(), + "' | jq -r '.result[0].creationBytecode'" + ) + ) + ); + + // If we got a creation code, try to grab the constructor arguments from etherscan too. + if (actualCreationCode.length > 0) { + // Now try to grab the constructor arguments from etherscan too. + bytes memory constructorArgs = bytes( + Process.bash( + string.concat( + "curl -s 'https://api.etherscan.io/v2/api?chainid=", + vm.toString(block.chainid), + "&module=contract&action=getsourcecode&address=", + vm.toString(_target.addr), + "&apikey=", + Config.etherscanApiKey(), + "' | jq -r '.result[0].ConstructorArguments'" + ) + ) + ); + + // If we got a constructor args, try to compare the bytecode. + if (constructorArgs.length > 0) { + success = _compareBytecode( + actualCreationCode, + bytes.concat(artifact.bytecode, constructorArgs), + _target.name, + artifact, + !_target.blueprint + ); + } else { + console.log( + string.concat("[FAIL] ERROR: Failed to retrieve constructor arguments for ", _target.name) + ); + success = false; + } + } else { + console.log(string.concat("[FAIL] ERROR: Failed to retrieve creation code for ", _target.name)); + success = false; + } + } + // Log final status for this field. if (success) { console.log(string.concat("Status: [OK] Verified ", _target.name)); diff --git a/packages/contracts-bedrock/scripts/libraries/Config.sol b/packages/contracts-bedrock/scripts/libraries/Config.sol index c4c893699db..69d5c1c4545 100644 --- a/packages/contracts-bedrock/scripts/libraries/Config.sol +++ b/packages/contracts-bedrock/scripts/libraries/Config.sol @@ -123,6 +123,11 @@ library Config { env_ = vm.envUint("DRIPPIE_OWNER_PRIVATE_KEY"); } + /// @notice Returns the API key for the Etherscan API. + function etherscanApiKey() internal view returns (string memory env_) { + env_ = vm.envString("ETHERSCAN_API_KEY"); + } + /// @notice Returns the OutputMode for genesis allocs generation. /// It reads the mode from the environment variable OUTPUT_MODE. /// If it is unset, OutputMode.ALL is returned. diff --git a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol index 85c2986e01e..296b618f71a 100644 --- a/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol +++ b/packages/contracts-bedrock/test/L1/OPContractsManager.t.sol @@ -642,9 +642,11 @@ contract OPContractsManager_Upgrade_Test is OPContractsManager_Upgrade_Harness { // Run the upgrade test and checks runUpgradeTestAndChecks(upgrader); - // Run the verification script + // Run the verification script without etherscan verificatin. Hard to run with etherscan + // verification in these tests, can do it but means we add even more dependencies to the + // test environment. VerifyOPCM verify = new VerifyOPCM(); - verify.run(address(opcm)); + verify.run(address(opcm), true); } function test_isRcFalseAfterCalledByUpgrader_works() public { diff --git a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol index 6e4c2107fa5..4be8e6fbae2 100644 --- a/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol +++ b/packages/contracts-bedrock/test/scripts/VerifyOPCM.t.sol @@ -62,7 +62,7 @@ contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { skipIfCoverage(); // Run the script. - harness.run(address(opcm)); + harness.run(address(opcm), true); } /// @notice Tests that the script succeeds when differences are introduced into the immutable @@ -122,7 +122,7 @@ contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { // Run the script. // No revert expected. - harness.run(address(opcm)); + harness.run(address(opcm), true); } /// @notice Tests that the script reverts when differences are introduced into the code of @@ -176,7 +176,7 @@ contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { // Run the script. vm.expectRevert(VerifyOPCM.VerifyOPCM_Failed.selector); - harness.run(address(opcm)); + harness.run(address(opcm), true); } /// @notice Tests that the script reverts when differences are introduced into the code of @@ -217,6 +217,6 @@ contract VerifyOPCM_run_Test is VerifyOPCM_TestInit { // Run the script. vm.expectRevert(VerifyOPCM.VerifyOPCM_Failed.selector); - harness.run(address(opcm)); + harness.run(address(opcm), true); } } From 05573c296c05baf3a4a0f4f656e792c8d00afbe9 Mon Sep 17 00:00:00 2001 From: Kelvin Fichter Date: Mon, 5 May 2025 17:19:44 -0400 Subject: [PATCH 14/14] fix: errors found while testing --- .../scripts/deploy/VerifyOPCM.s.sol | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol index 1efa5b7da54..f403efadf0c 100644 --- a/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/VerifyOPCM.s.sol @@ -31,7 +31,7 @@ contract VerifyOPCM is Script { /// @notice Thrown when no implementations are found in the OPCM. error VerifyOPCM_NoImplementations(); - /// @notice Thrown when no blueprints are found in the OPCM.` + /// @notice Thrown when no blueprints are found in the OPCM. error VerifyOPCM_NoBlueprints(); /// @notice Thrown when an unexpected part number is found in the blueprint. @@ -124,6 +124,7 @@ contract VerifyOPCM is Script { /// address as an argument instead. Running in this mode will not allow you to skip /// constructor verification. function run() external { + // nosemgrep: sol-style-vm-env-only-in-config-sol run(vm.envAddress("OPCM_ADDRESS"), false); } @@ -322,21 +323,14 @@ contract VerifyOPCM is Script { ) ); - // If we got a constructor args, try to compare the bytecode. - if (constructorArgs.length > 0) { - success = _compareBytecode( - actualCreationCode, - bytes.concat(artifact.bytecode, constructorArgs), - _target.name, - artifact, - !_target.blueprint - ); - } else { - console.log( - string.concat("[FAIL] ERROR: Failed to retrieve constructor arguments for ", _target.name) - ); - success = false; - } + // Constructor args might be empty, so we check regardless of the result. + success = _compareBytecode( + actualCreationCode, + bytes.concat(artifact.bytecode, constructorArgs), + _target.name, + artifact, + !_target.blueprint + ); } else { console.log(string.concat("[FAIL] ERROR: Failed to retrieve creation code for ", _target.name)); success = false;