Commit 8167f363 authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

contracts-bedrock: fix deploy config for mainnet MCP upgrade (#9865)

* contracts-bedrock: fix deploy config for mainnet MCP upgrade

The config param for the scalar was not updated in the
deploy config when it was changed on chain. If we can enforce
that the deploy config is always used as the source of truth
for when doing on chain config changes, it can help to scale
the team as other teams can use the same config file and know
that the values in there represent the truth. This is a fundamental
problem with the `initialize` pattern, we need to move away from
it eventually.

The deploy config is updated with the value that is used on
mainnet and the parsing is updated to handle the new ecotone
style config, which tightly packs the values into a single
bytes32.

* op-chain-ops: more cleanup

* op-e2e: fix build

* cleanup: modularize scalar encoding and decoding

Ensures that the same consensus code is used
to encode and decode the scalar in various places.

* op-chain-ops: fix L2 genesis generation

* config: fix serialization

* op-chain-ops: refactor config

Make backwards compatible

* op-chain-ops: fix build

* deploy-config: update mainnet fee scalar config

Should match mainnet values

* op-chain-ops: fix test

* genesis: test L1Block predeploy state setting

* op-upgrade: delete dead code

* build: fix

* op-chain-ops: add deprecation warning
Co-authored-by: default avatarSebastian Stammler <seb@oplabs.co>

* deploy-config: use mainnet values
Co-authored-by: default avatarSebastian Stammler <seb@oplabs.co>

* deploy-config: use mainnet values
Co-authored-by: default avatarSebastian Stammler <seb@oplabs.co>

* op-service: end to end encode/decode scalar tests

* tests: cleanup

* op-chain-ops: fix nits, adapt to breaking simulated backend changes

* op-chain-ops: fix comment and address-type conversion nits

---------
Co-authored-by: default avatarSebastian Stammler <seb@oplabs.co>
Co-authored-by: default avatarprotolambda <proto@protolambda.com>
parent 35c0ad85
package main package main
import ( import (
"encoding/binary"
"flag" "flag"
"fmt" "fmt"
"math" "math"
"math/big" "math/big"
"os" "os"
"github.com/ethereum-optimism/optimism/op-service/eth"
) )
func main() { func main() {
...@@ -26,15 +27,15 @@ func main() { ...@@ -26,15 +27,15 @@ func main() {
os.Exit(2) os.Exit(2)
} }
var n [32]byte encoded := eth.EncodeScalar(eth.EcostoneScalars{
n[0] = 1 // version BlobBaseFeeScalar: uint32(blobScalar),
binary.BigEndian.PutUint32(n[32-4:], uint32(scalar)) BaseFeeScalar: uint32(scalar),
binary.BigEndian.PutUint32(n[32-8:], uint32(blobScalar)) })
i := new(big.Int).SetBytes(n[:]) i := new(big.Int).SetBytes(encoded[:])
fmt.Println("# base fee scalar :", scalar) fmt.Println("# base fee scalar :", scalar)
fmt.Println("# blob base fee scalar:", blobScalar) fmt.Println("# blob base fee scalar:", blobScalar)
fmt.Printf("# v1 hex encoding : 0x%x\n", n[:]) fmt.Printf("# v1 hex encoding : 0x%x\n", encoded[:])
fmt.Println("# uint value for the 'scalar' parameter in SystemConfigProxy.setGasConfig():") fmt.Println("# uint value for the 'scalar' parameter in SystemConfigProxy.setGasConfig():")
fmt.Println(i) fmt.Println(i)
} }
...@@ -92,7 +92,7 @@ func main() { ...@@ -92,7 +92,7 @@ func main() {
oplog.SetGlobalLogHandler(log.NewTerminalHandler(os.Stderr, color)) oplog.SetGlobalLogHandler(log.NewTerminalHandler(os.Stderr, color))
app := &cli.App{ app := &cli.App{
Name: "op-upgrade", Name: "op-upgrade-mcp",
Usage: "Build transactions useful for upgrading the Superchain", Usage: "Build transactions useful for upgrading the Superchain",
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
...@@ -179,6 +179,11 @@ func entrypoint(ctx *cli.Context) error { ...@@ -179,6 +179,11 @@ func entrypoint(ctx *cli.Context) error {
return fmt.Errorf("no chain config for chain ID %d", l2ChainID) return fmt.Errorf("no chain config for chain ID %d", l2ChainID)
} }
superchainConfig, ok := superchain.Superchains[chainConfig.Superchain]
if !ok {
return fmt.Errorf("no superchain config for superchain %s", chainConfig.Superchain)
}
log.Info("Upgrading to the following versions") log.Info("Upgrading to the following versions")
log.Info("L1CrossDomainMessenger", "version", list.L1CrossDomainMessenger.Version, "address", list.L1CrossDomainMessenger.Address) log.Info("L1CrossDomainMessenger", "version", list.L1CrossDomainMessenger.Version, "address", list.L1CrossDomainMessenger.Address)
log.Info("L1ERC721Bridge", "version", list.L1ERC721Bridge.Version, "address", list.L1ERC721Bridge.Address) log.Info("L1ERC721Bridge", "version", list.L1ERC721Bridge.Version, "address", list.L1ERC721Bridge.Address)
...@@ -193,7 +198,7 @@ func entrypoint(ctx *cli.Context) error { ...@@ -193,7 +198,7 @@ func entrypoint(ctx *cli.Context) error {
} }
// Build the batch // Build the batch
if err := upgrades.L1(&batch, list, *proxyAddresses, config, chainConfig, clients.L1Client); err != nil { if err := upgrades.L1(&batch, list, *proxyAddresses, config, chainConfig, superchainConfig, clients.L1Client); err != nil {
return fmt.Errorf("cannot build L1 upgrade batch: %w", err) return fmt.Errorf("cannot build L1 upgrade batch: %w", err)
} }
......
...@@ -203,7 +203,9 @@ func entrypoint(ctx *cli.Context) error { ...@@ -203,7 +203,9 @@ func entrypoint(ctx *cli.Context) error {
} }
// Build the batch // Build the batch
if err := upgrades.L1(&batch, list, *addresses, config, chainConfig, clients.L1Client); err != nil { // op-upgrade assumes a superchain config for L1 contract-implementations set.
// The nil superchainConfig here is a placeholder, until op-upgrade and op-upgrade-mcp are consolidated.
if err := upgrades.L1(&batch, list, *addresses, config, chainConfig, nil, clients.L1Client); err != nil {
return err return err
} }
} }
......
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
gstate "github.com/ethereum/go-ethereum/core/state" gstate "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -169,9 +170,15 @@ type DeployConfig struct { ...@@ -169,9 +170,15 @@ type DeployConfig struct {
// as part of the derivation pipeline. // as part of the derivation pipeline.
OptimismPortalProxy common.Address `json:"optimismPortalProxy"` OptimismPortalProxy common.Address `json:"optimismPortalProxy"`
// GasPriceOracleOverhead represents the initial value of the gas overhead in the GasPriceOracle predeploy. // GasPriceOracleOverhead represents the initial value of the gas overhead in the GasPriceOracle predeploy.
// Deprecated: Since Ecotone, this field is superseded by GasPriceOracleBaseFeeScalar and GasPriceOracleBlobBaseFeeScalar.
GasPriceOracleOverhead uint64 `json:"gasPriceOracleOverhead"` GasPriceOracleOverhead uint64 `json:"gasPriceOracleOverhead"`
// GasPriceOracleScalar represents the initial value of the gas scalar in the GasPriceOracle predeploy. // GasPriceOracleScalar represents the initial value of the gas scalar in the GasPriceOracle predeploy.
// Deprecated: Since Ecotone, this field is superseded by GasPriceOracleBaseFeeScalar and GasPriceOracleBlobBaseFeeScalar.
GasPriceOracleScalar uint64 `json:"gasPriceOracleScalar"` GasPriceOracleScalar uint64 `json:"gasPriceOracleScalar"`
// GasPriceOracleBaseFeeScalar represents the value of the base fee scalar used for fee calculations.
GasPriceOracleBaseFeeScalar uint32 `json:"gasPriceOracleBaseFeeScalar"`
// GasPriceOracleBlobBaseFeeScalar represents the value of the blob base fee scalar used for fee calculations.
GasPriceOracleBlobBaseFeeScalar uint32 `json:"gasPriceOracleBlobBaseFeeScalar"`
// EnableGovernance configures whether or not include governance token predeploy. // EnableGovernance configures whether or not include governance token predeploy.
EnableGovernance bool `json:"enableGovernance"` EnableGovernance bool `json:"enableGovernance"`
// GovernanceTokenSymbol represents the ERC20 symbol of the GovernanceToken. // GovernanceTokenSymbol represents the ERC20 symbol of the GovernanceToken.
...@@ -356,7 +363,13 @@ func (d *DeployConfig) Check() error { ...@@ -356,7 +363,13 @@ func (d *DeployConfig) Check() error {
log.Warn("GasPriceOracleOverhead is 0") log.Warn("GasPriceOracleOverhead is 0")
} }
if d.GasPriceOracleScalar == 0 { if d.GasPriceOracleScalar == 0 {
return fmt.Errorf("%w: GasPriceOracleScalar cannot be 0", ErrInvalidDeployConfig) log.Warn("GasPriceOracleScalar is 0")
}
if d.GasPriceOracleBaseFeeScalar == 0 {
log.Warn("GasPriceOracleBaseFeeScalar is 0")
}
if d.GasPriceOracleBlobBaseFeeScalar == 0 {
log.Warn("GasPriceOracleBlobBaseFeeScalar is 0")
} }
if d.EIP1559Denominator == 0 { if d.EIP1559Denominator == 0 {
return fmt.Errorf("%w: EIP1559Denominator cannot be 0", ErrInvalidDeployConfig) return fmt.Errorf("%w: EIP1559Denominator cannot be 0", ErrInvalidDeployConfig)
...@@ -447,6 +460,18 @@ func (d *DeployConfig) Check() error { ...@@ -447,6 +460,18 @@ func (d *DeployConfig) Check() error {
return nil return nil
} }
// FeeScalar returns the raw serialized fee scalar. Uses pre-Ecotone if legacy config is present,
// otherwise uses the post-Ecotone scalar serialization.
func (d *DeployConfig) FeeScalar() [32]byte {
if d.GasPriceOracleScalar != 0 {
return common.BigToHash(big.NewInt(int64(d.GasPriceOracleScalar)))
}
return eth.EncodeScalar(eth.EcostoneScalars{
BlobBaseFeeScalar: d.GasPriceOracleBlobBaseFeeScalar,
BaseFeeScalar: d.GasPriceOracleBaseFeeScalar,
})
}
// CheckAddresses will return an error if the addresses are not set. // CheckAddresses will return an error if the addresses are not set.
// These values are required to create the L2 genesis state and are present in the deploy config // These values are required to create the L2 genesis state and are present in the deploy config
// even though the deploy config is required to deploy the contracts on L1. This creates a // even though the deploy config is required to deploy the contracts on L1. This creates a
...@@ -573,7 +598,7 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *types.Block, l2GenesisBlockHas ...@@ -573,7 +598,7 @@ func (d *DeployConfig) RollupConfig(l1StartBlock *types.Block, l2GenesisBlockHas
SystemConfig: eth.SystemConfig{ SystemConfig: eth.SystemConfig{
BatcherAddr: d.BatchSenderAddress, BatcherAddr: d.BatchSenderAddress,
Overhead: eth.Bytes32(common.BigToHash(new(big.Int).SetUint64(d.GasPriceOracleOverhead))), Overhead: eth.Bytes32(common.BigToHash(new(big.Int).SetUint64(d.GasPriceOracleOverhead))),
Scalar: eth.Bytes32(common.BigToHash(new(big.Int).SetUint64(d.GasPriceOracleScalar))), Scalar: eth.Bytes32(d.FeeScalar()),
GasLimit: uint64(d.L2GenesisBlockGasLimit), GasLimit: uint64(d.L2GenesisBlockGasLimit),
}, },
}, },
...@@ -874,7 +899,7 @@ func NewL2ImmutableConfig(config *DeployConfig, block *types.Block) (*immutables ...@@ -874,7 +899,7 @@ func NewL2ImmutableConfig(config *DeployConfig, block *types.Block) (*immutables
return &cfg, nil return &cfg, nil
} }
// NewL2StorageConfig will create a StorageConfig given an instance of a DeployConfig and genesis block. // NewL2StorageConfig will create a StorageConfig given an instance of a DeployConfig and genesis L1 anchor block.
func NewL2StorageConfig(config *DeployConfig, block *types.Block) (state.StorageConfig, error) { func NewL2StorageConfig(config *DeployConfig, block *types.Block) (state.StorageConfig, error) {
storage := make(state.StorageConfig) storage := make(state.StorageConfig)
...@@ -912,15 +937,24 @@ func NewL2StorageConfig(config *DeployConfig, block *types.Block) (state.Storage ...@@ -912,15 +937,24 @@ func NewL2StorageConfig(config *DeployConfig, block *types.Block) (state.Storage
"_initializing": false, "_initializing": false,
"bridge": predeploys.L2StandardBridgeAddr, "bridge": predeploys.L2StandardBridgeAddr,
} }
excessBlobGas := block.ExcessBlobGas()
if excessBlobGas == nil {
excessBlobGas = u64ptr(0)
}
storage["L1Block"] = state.StorageValues{ storage["L1Block"] = state.StorageValues{
"number": block.Number(), "number": block.Number(),
"timestamp": block.Time(), "timestamp": block.Time(),
"basefee": block.BaseFee(), "basefee": block.BaseFee(),
"hash": block.Hash(), "hash": block.Hash(),
"sequenceNumber": 0, "sequenceNumber": 0,
"batcherHash": eth.AddressAsLeftPaddedHash(config.BatchSenderAddress), "blobBaseFeeScalar": config.GasPriceOracleBlobBaseFeeScalar,
"l1FeeOverhead": config.GasPriceOracleOverhead, "baseFeeScalar": config.GasPriceOracleBaseFeeScalar,
"l1FeeScalar": config.GasPriceOracleScalar, "batcherHash": eth.AddressAsLeftPaddedHash(config.BatchSenderAddress),
"l1FeeOverhead": config.GasPriceOracleOverhead,
"l1FeeScalar": config.GasPriceOracleScalar,
"blobBaseFee": eip4844.CalcBlobFee(*excessBlobGas),
} }
storage["LegacyERC20ETH"] = state.StorageValues{ storage["LegacyERC20ETH"] = state.StorageValues{
"_name": "Ether", "_name": "Ether",
......
...@@ -10,15 +10,19 @@ import ( ...@@ -10,15 +10,19 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig"
) )
var writeFile bool var writeFile bool
...@@ -47,9 +51,52 @@ func testBuildL2Genesis(t *testing.T, config *genesis.DeployConfig) *core.Genesi ...@@ -47,9 +51,52 @@ func testBuildL2Genesis(t *testing.T, config *genesis.DeployConfig) *core.Genesi
proxyBytecode, err := bindings.GetDeployedBytecode("Proxy") proxyBytecode, err := bindings.GetDeployedBytecode("Proxy")
require.NoError(t, err) require.NoError(t, err)
// for simulation we need a regular EVM, not with system-deposit information.
chainConfig := params.ChainConfig{
ChainID: big.NewInt(1337),
HomesteadBlock: big.NewInt(0),
DAOForkBlock: nil,
DAOForkSupport: false,
EIP150Block: big.NewInt(0),
EIP155Block: big.NewInt(0),
EIP158Block: big.NewInt(0),
ByzantiumBlock: big.NewInt(0),
ConstantinopleBlock: big.NewInt(0),
PetersburgBlock: big.NewInt(0),
IstanbulBlock: big.NewInt(0),
MuirGlacierBlock: big.NewInt(0),
BerlinBlock: big.NewInt(0),
LondonBlock: big.NewInt(0),
ArrowGlacierBlock: big.NewInt(0),
GrayGlacierBlock: big.NewInt(0),
// Activated proof of stake. We manually build/commit blocks in the simulator anyway,
// and the timestamp verification of PoS is not against the wallclock,
// preventing blocks from getting stuck temporarily in the future-blocks queue, decreasing setup time a lot.
MergeNetsplitBlock: big.NewInt(0),
TerminalTotalDifficulty: big.NewInt(0),
TerminalTotalDifficultyPassed: true,
ShanghaiTime: new(uint64),
}
// Apply the genesis to the backend
cfg := ethconfig.Defaults
cfg.Preimages = true
cfg.Genesis = &core.Genesis{
Config: &chainConfig,
Timestamp: 1234567,
Difficulty: big.NewInt(0),
Alloc: gen.Alloc,
GasLimit: 30_000_000,
}
backend = backends.NewSimulatedBackendFromConfig(cfg)
for name, predeploy := range predeploys.Predeploys { for name, predeploy := range predeploys.Predeploys {
addr := predeploy.Address addr := predeploy.Address
if addr == predeploys.L1BlockAddr {
testL1Block(t, backend, config, block)
}
account, ok := gen.Alloc[addr] account, ok := gen.Alloc[addr]
require.Equal(t, true, ok, name) require.Equal(t, true, ok, name)
require.Greater(t, len(account.Code), 0) require.Greater(t, len(account.Code), 0)
...@@ -84,6 +131,59 @@ func testBuildL2Genesis(t *testing.T, config *genesis.DeployConfig) *core.Genesi ...@@ -84,6 +131,59 @@ func testBuildL2Genesis(t *testing.T, config *genesis.DeployConfig) *core.Genesi
return gen return gen
} }
// testL1Block tests that the state is set correctly in the L1Block predeploy
func testL1Block(t *testing.T, caller bind.ContractCaller, config *genesis.DeployConfig, block *types.Block) {
contract, err := bindings.NewL1BlockCaller(predeploys.L1BlockAddr, caller)
require.NoError(t, err)
number, err := contract.Number(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, block.Number().Uint64(), number)
timestamp, err := contract.Timestamp(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, block.Time(), timestamp)
basefee, err := contract.Basefee(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, block.BaseFee(), basefee)
hash, err := contract.Hash(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, block.Hash(), common.Hash(hash))
sequenceNumber, err := contract.SequenceNumber(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, uint64(0), sequenceNumber)
blobBaseFeeScalar, err := contract.BlobBaseFeeScalar(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, config.GasPriceOracleBlobBaseFeeScalar, blobBaseFeeScalar)
baseFeeScalar, err := contract.BaseFeeScalar(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, config.GasPriceOracleBaseFeeScalar, baseFeeScalar)
batcherHeader, err := contract.BatcherHash(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, eth.AddressAsLeftPaddedHash(config.BatchSenderAddress), common.Hash(batcherHeader))
l1FeeOverhead, err := contract.L1FeeOverhead(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, config.GasPriceOracleOverhead, l1FeeOverhead.Uint64())
l1FeeScalar, err := contract.L1FeeScalar(&bind.CallOpts{})
require.NoError(t, err)
require.Equal(t, config.GasPriceOracleScalar, l1FeeScalar.Uint64())
blobBaseFee, err := contract.BlobBaseFee(&bind.CallOpts{})
require.NoError(t, err)
if excessBlobGas := block.ExcessBlobGas(); excessBlobGas != nil {
require.Equal(t, uint64(0), *excessBlobGas)
}
require.Equal(t, big.NewInt(1), blobBaseFee)
}
func TestBuildL2MainnetGenesis(t *testing.T) { func TestBuildL2MainnetGenesis(t *testing.T) {
config, err := genesis.NewDeployConfig("./testdata/test-deploy-config-devnet-l1.json") config, err := genesis.NewDeployConfig("./testdata/test-deploy-config-devnet-l1.json")
require.Nil(t, err) require.Nil(t, err)
......
...@@ -54,6 +54,8 @@ ...@@ -54,6 +54,8 @@
"systemConfigProxy": "0x4200000000000000000000000000000000000061", "systemConfigProxy": "0x4200000000000000000000000000000000000061",
"optimismPortalProxy": "0x4200000000000000000000000000000000000062", "optimismPortalProxy": "0x4200000000000000000000000000000000000062",
"proxyAdminOwner": "0x0000000000000000000000000000000000000222", "proxyAdminOwner": "0x0000000000000000000000000000000000000222",
"gasPriceOracleBaseFeeScalar": 0,
"gasPriceOracleBlobBaseFeeScalar": 0,
"gasPriceOracleOverhead": 2100, "gasPriceOracleOverhead": 2100,
"gasPriceOracleScalar": 1000000, "gasPriceOracleScalar": 1000000,
"enableGovernance": true, "enableGovernance": true,
......
...@@ -26,41 +26,36 @@ const ( ...@@ -26,41 +26,36 @@ const (
var ( var (
// storageSetterAddr represents the address of the StorageSetter contract. // storageSetterAddr represents the address of the StorageSetter contract.
storageSetterAddr = common.HexToAddress("0xd81f43eDBCAcb4c29a9bA38a13Ee5d79278270cC") storageSetterAddr = common.HexToAddress("0xd81f43eDBCAcb4c29a9bA38a13Ee5d79278270cC")
// superchainConfigProxy refers to the address of the Sepolia superchain config proxy.
// NOTE: this is currently hardcoded and we will need to move this to the superchain-registry
// and have 1 deployed for each superchain target.
superchainConfigProxy = common.HexToAddress("0xC2Be75506d5724086DEB7245bd260Cc9753911Be")
) )
// L1 will add calls for upgrading each of the L1 contracts. // L1 will add calls for upgrading each of the L1 contracts.
func L1(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, backend bind.ContractBackend) error { func L1(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, superchainConfig *superchain.Superchain, backend bind.ContractBackend) error {
if err := L1CrossDomainMessenger(batch, implementations, list, config, chainConfig, backend); err != nil { if err := L1CrossDomainMessenger(batch, implementations, list, config, chainConfig, superchainConfig, backend); err != nil {
return fmt.Errorf("upgrading L1CrossDomainMessenger: %w", err) return fmt.Errorf("upgrading L1CrossDomainMessenger: %w", err)
} }
if err := L1ERC721Bridge(batch, implementations, list, config, chainConfig, backend); err != nil { if err := L1ERC721Bridge(batch, implementations, list, config, chainConfig, superchainConfig, backend); err != nil {
return fmt.Errorf("upgrading L1ERC721Bridge: %w", err) return fmt.Errorf("upgrading L1ERC721Bridge: %w", err)
} }
if err := L1StandardBridge(batch, implementations, list, config, chainConfig, backend); err != nil { if err := L1StandardBridge(batch, implementations, list, config, chainConfig, superchainConfig, backend); err != nil {
return fmt.Errorf("upgrading L1StandardBridge: %w", err) return fmt.Errorf("upgrading L1StandardBridge: %w", err)
} }
if err := L2OutputOracle(batch, implementations, list, config, chainConfig, backend); err != nil { if err := L2OutputOracle(batch, implementations, list, config, chainConfig, superchainConfig, backend); err != nil {
return fmt.Errorf("upgrading L2OutputOracle: %w", err) return fmt.Errorf("upgrading L2OutputOracle: %w", err)
} }
if err := OptimismMintableERC20Factory(batch, implementations, list, config, chainConfig, backend); err != nil { if err := OptimismMintableERC20Factory(batch, implementations, list, config, chainConfig, superchainConfig, backend); err != nil {
return fmt.Errorf("upgrading OptimismMintableERC20Factory: %w", err) return fmt.Errorf("upgrading OptimismMintableERC20Factory: %w", err)
} }
if err := OptimismPortal(batch, implementations, list, config, chainConfig, backend); err != nil { if err := OptimismPortal(batch, implementations, list, config, chainConfig, superchainConfig, backend); err != nil {
return fmt.Errorf("upgrading OptimismPortal: %w", err) return fmt.Errorf("upgrading OptimismPortal: %w", err)
} }
if err := SystemConfig(batch, implementations, list, config, chainConfig, backend); err != nil { if err := SystemConfig(batch, implementations, list, config, chainConfig, superchainConfig, backend); err != nil {
return fmt.Errorf("upgrading SystemConfig: %w", err) return fmt.Errorf("upgrading SystemConfig: %w", err)
} }
return nil return nil
} }
// L1CrossDomainMessenger will add a call to the batch that upgrades the L1CrossDomainMessenger. // L1CrossDomainMessenger will add a call to the batch that upgrades the L1CrossDomainMessenger.
func L1CrossDomainMessenger(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, backend bind.ContractBackend) error { func L1CrossDomainMessenger(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, superchainConfig *superchain.Superchain, backend bind.ContractBackend) error {
proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi() proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi()
if err != nil { if err != nil {
return err return err
...@@ -122,7 +117,7 @@ func L1CrossDomainMessenger(batch *safe.Batch, implementations superchain.Implem ...@@ -122,7 +117,7 @@ func L1CrossDomainMessenger(batch *safe.Batch, implementations superchain.Implem
return fmt.Errorf("OtherMessenger address doesn't match config") return fmt.Errorf("OtherMessenger address doesn't match config")
} }
calldata, err := l1CrossDomainMessengerABI.Pack("initialize", superchainConfigProxy, optimismPortal) calldata, err := l1CrossDomainMessengerABI.Pack("initialize", common.Address(*superchainConfig.Config.SuperchainConfigAddr), optimismPortal)
if err != nil { if err != nil {
return err return err
} }
...@@ -142,7 +137,7 @@ func L1CrossDomainMessenger(batch *safe.Batch, implementations superchain.Implem ...@@ -142,7 +137,7 @@ func L1CrossDomainMessenger(batch *safe.Batch, implementations superchain.Implem
} }
// L1ERC721Bridge will add a call to the batch that upgrades the L1ERC721Bridge. // L1ERC721Bridge will add a call to the batch that upgrades the L1ERC721Bridge.
func L1ERC721Bridge(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, backend bind.ContractBackend) error { func L1ERC721Bridge(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, superchainConfig *superchain.Superchain, backend bind.ContractBackend) error {
proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi() proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi()
if err != nil { if err != nil {
return err return err
...@@ -204,7 +199,7 @@ func L1ERC721Bridge(batch *safe.Batch, implementations superchain.Implementation ...@@ -204,7 +199,7 @@ func L1ERC721Bridge(batch *safe.Batch, implementations superchain.Implementation
return fmt.Errorf("OtherBridge address doesn't match config") return fmt.Errorf("OtherBridge address doesn't match config")
} }
calldata, err := l1ERC721BridgeABI.Pack("initialize", messenger, superchainConfigProxy) calldata, err := l1ERC721BridgeABI.Pack("initialize", messenger, common.Address(*(superchainConfig.Config.SuperchainConfigAddr)))
if err != nil { if err != nil {
return err return err
} }
...@@ -224,7 +219,7 @@ func L1ERC721Bridge(batch *safe.Batch, implementations superchain.Implementation ...@@ -224,7 +219,7 @@ func L1ERC721Bridge(batch *safe.Batch, implementations superchain.Implementation
} }
// L1StandardBridge will add a call to the batch that upgrades the L1StandardBridge. // L1StandardBridge will add a call to the batch that upgrades the L1StandardBridge.
func L1StandardBridge(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, backend bind.ContractBackend) error { func L1StandardBridge(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, superchainConfig *superchain.Superchain, backend bind.ContractBackend) error {
proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi() proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi()
if err != nil { if err != nil {
return err return err
...@@ -288,7 +283,7 @@ func L1StandardBridge(batch *safe.Batch, implementations superchain.Implementati ...@@ -288,7 +283,7 @@ func L1StandardBridge(batch *safe.Batch, implementations superchain.Implementati
return fmt.Errorf("OtherBridge address doesn't match config") return fmt.Errorf("OtherBridge address doesn't match config")
} }
calldata, err := l1StandardBridgeABI.Pack("initialize", messenger, superchainConfigProxy) calldata, err := l1StandardBridgeABI.Pack("initialize", messenger, common.Address(*(superchainConfig.Config.SuperchainConfigAddr)))
if err != nil { if err != nil {
return err return err
} }
...@@ -308,7 +303,7 @@ func L1StandardBridge(batch *safe.Batch, implementations superchain.Implementati ...@@ -308,7 +303,7 @@ func L1StandardBridge(batch *safe.Batch, implementations superchain.Implementati
} }
// L2OutputOracle will add a call to the batch that upgrades the L2OutputOracle. // L2OutputOracle will add a call to the batch that upgrades the L2OutputOracle.
func L2OutputOracle(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, backend bind.ContractBackend) error { func L2OutputOracle(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, superchainConfig *superchain.Superchain, backend bind.ContractBackend) error {
proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi() proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi()
if err != nil { if err != nil {
return err return err
...@@ -452,7 +447,7 @@ func L2OutputOracle(batch *safe.Batch, implementations superchain.Implementation ...@@ -452,7 +447,7 @@ func L2OutputOracle(batch *safe.Batch, implementations superchain.Implementation
} }
// OptimismMintableERC20Factory will add a call to the batch that upgrades the OptimismMintableERC20Factory. // OptimismMintableERC20Factory will add a call to the batch that upgrades the OptimismMintableERC20Factory.
func OptimismMintableERC20Factory(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, backend bind.ContractBackend) error { func OptimismMintableERC20Factory(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, superchainConfig *superchain.Superchain, backend bind.ContractBackend) error {
proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi() proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi()
if err != nil { if err != nil {
return err return err
...@@ -527,7 +522,7 @@ func OptimismMintableERC20Factory(batch *safe.Batch, implementations superchain. ...@@ -527,7 +522,7 @@ func OptimismMintableERC20Factory(batch *safe.Batch, implementations superchain.
} }
// OptimismPortal will add a call to the batch that upgrades the OptimismPortal. // OptimismPortal will add a call to the batch that upgrades the OptimismPortal.
func OptimismPortal(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, backend bind.ContractBackend) error { func OptimismPortal(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, superchainConfig *superchain.Superchain, backend bind.ContractBackend) error {
proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi() proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi()
if err != nil { if err != nil {
return err return err
...@@ -589,7 +584,7 @@ func OptimismPortal(batch *safe.Batch, implementations superchain.Implementation ...@@ -589,7 +584,7 @@ func OptimismPortal(batch *safe.Batch, implementations superchain.Implementation
return fmt.Errorf("SystemConfig address doesn't match config") return fmt.Errorf("SystemConfig address doesn't match config")
} }
calldata, err := optimismPortalABI.Pack("initialize", l2OutputOracle, systemConfig, superchainConfigProxy) calldata, err := optimismPortalABI.Pack("initialize", l2OutputOracle, systemConfig, common.Address(*superchainConfig.Config.SuperchainConfigAddr))
if err != nil { if err != nil {
return err return err
} }
...@@ -609,7 +604,7 @@ func OptimismPortal(batch *safe.Batch, implementations superchain.Implementation ...@@ -609,7 +604,7 @@ func OptimismPortal(batch *safe.Batch, implementations superchain.Implementation
} }
// SystemConfig will add a call to the batch that upgrades the SystemConfig. // SystemConfig will add a call to the batch that upgrades the SystemConfig.
func SystemConfig(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, backend bind.ContractBackend) error { func SystemConfig(batch *safe.Batch, implementations superchain.ImplementationList, list superchain.AddressList, config *genesis.DeployConfig, chainConfig *superchain.ChainConfig, superchainConfig *superchain.Superchain, backend bind.ContractBackend) error {
proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi() proxyAdminABI, err := bindings.ProxyAdminMetaData.GetAbi()
if err != nil { if err != nil {
return err return err
......
...@@ -188,7 +188,7 @@ func SystemConfigFromDeployConfig(deployConfig *genesis.DeployConfig) eth.System ...@@ -188,7 +188,7 @@ func SystemConfigFromDeployConfig(deployConfig *genesis.DeployConfig) eth.System
return eth.SystemConfig{ return eth.SystemConfig{
BatcherAddr: deployConfig.BatchSenderAddress, BatcherAddr: deployConfig.BatchSenderAddress,
Overhead: eth.Bytes32(common.BigToHash(new(big.Int).SetUint64(deployConfig.GasPriceOracleOverhead))), Overhead: eth.Bytes32(common.BigToHash(new(big.Int).SetUint64(deployConfig.GasPriceOracleOverhead))),
Scalar: eth.Bytes32(common.BigToHash(new(big.Int).SetUint64(deployConfig.GasPriceOracleScalar))), Scalar: eth.Bytes32(deployConfig.FeeScalar()),
GasLimit: uint64(deployConfig.L2GenesisBlockGasLimit), GasLimit: uint64(deployConfig.L2GenesisBlockGasLimit),
} }
} }
......
...@@ -1004,10 +1004,10 @@ func TestL1InfoContract(t *testing.T) { ...@@ -1004,10 +1004,10 @@ func TestL1InfoContract(t *testing.T) {
BatcherAddr: sys.RollupConfig.Genesis.SystemConfig.BatcherAddr, BatcherAddr: sys.RollupConfig.Genesis.SystemConfig.BatcherAddr,
} }
if sys.RollupConfig.IsEcotone(b.Time()) && !sys.RollupConfig.IsEcotoneActivationBlock(b.Time()) { if sys.RollupConfig.IsEcotone(b.Time()) && !sys.RollupConfig.IsEcotoneActivationBlock(b.Time()) {
blobBaseFeeScalar, baseFeeScalar, err := sys.RollupConfig.Genesis.SystemConfig.EcotoneScalars() scalars, err := sys.RollupConfig.Genesis.SystemConfig.EcotoneScalars()
require.NoError(t, err) require.NoError(t, err)
l1blocks[h].BlobBaseFeeScalar = blobBaseFeeScalar l1blocks[h].BlobBaseFeeScalar = scalars.BlobBaseFeeScalar
l1blocks[h].BaseFeeScalar = baseFeeScalar l1blocks[h].BaseFeeScalar = scalars.BaseFeeScalar
if excess := b.ExcessBlobGas(); excess != nil { if excess := b.ExcessBlobGas(); excess != nil {
l1blocks[h].BlobBaseFee = eip4844.CalcBlobFee(*excess) l1blocks[h].BlobBaseFee = eip4844.CalcBlobFee(*excess)
} else { } else {
......
...@@ -276,12 +276,12 @@ func L1InfoDeposit(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber ...@@ -276,12 +276,12 @@ func L1InfoDeposit(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber
// The L2 spec states to use the MIN_BLOB_GASPRICE from EIP-4844 if not yet active on L1. // The L2 spec states to use the MIN_BLOB_GASPRICE from EIP-4844 if not yet active on L1.
l1BlockInfo.BlobBaseFee = big.NewInt(1) l1BlockInfo.BlobBaseFee = big.NewInt(1)
} }
blobBaseFeeScalar, baseFeeScalar, err := sysCfg.EcotoneScalars() scalars, err := sysCfg.EcotoneScalars()
if err != nil { if err != nil {
return nil, err return nil, err
} }
l1BlockInfo.BlobBaseFeeScalar = blobBaseFeeScalar l1BlockInfo.BlobBaseFeeScalar = scalars.BlobBaseFeeScalar
l1BlockInfo.BaseFeeScalar = baseFeeScalar l1BlockInfo.BaseFeeScalar = scalars.BaseFeeScalar
out, err := l1BlockInfo.marshalBinaryEcotone() out, err := l1BlockInfo.marshalBinaryEcotone()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to marshal Ecotone l1 block info: %w", err) return nil, fmt.Errorf("failed to marshal Ecotone l1 block info: %w", err)
......
...@@ -12,6 +12,7 @@ fuzz: ...@@ -12,6 +12,7 @@ fuzz:
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzOBP01 ./eth go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzOBP01 ./eth
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzEncodeDecodeBlob ./eth go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzEncodeDecodeBlob ./eth
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzDetectNonBijectivity ./eth go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzDetectNonBijectivity ./eth
go test -run NOTAREALTEST -v -fuzztime 10s -fuzz FuzzEncodeScalar ./eth
.PHONY: \ .PHONY: \
test \ test \
......
...@@ -7,8 +7,9 @@ import ( ...@@ -7,8 +7,9 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-service/eth"
) )
type dataJson struct { type dataJson struct {
......
...@@ -393,25 +393,48 @@ const ( ...@@ -393,25 +393,48 @@ const (
L1ScalarEcotone = byte(1) L1ScalarEcotone = byte(1)
) )
func (sysCfg *SystemConfig) EcotoneScalars() (blobBaseFeeScalar, baseFeeScalar uint32, err error) { type EcostoneScalars struct {
BlobBaseFeeScalar uint32
BaseFeeScalar uint32
}
func (sysCfg *SystemConfig) EcotoneScalars() (EcostoneScalars, error) {
if err := CheckEcotoneL1SystemConfigScalar(sysCfg.Scalar); err != nil { if err := CheckEcotoneL1SystemConfigScalar(sysCfg.Scalar); err != nil {
if errors.Is(err, ErrBedrockScalarPaddingNotEmpty) { if errors.Is(err, ErrBedrockScalarPaddingNotEmpty) {
// L2 spec mandates we set baseFeeScalar to MaxUint32 if there are non-zero bytes in // L2 spec mandates we set baseFeeScalar to MaxUint32 if there are non-zero bytes in
// the padding area. // the padding area.
return 0, math.MaxUint32, nil return EcostoneScalars{BlobBaseFeeScalar: 0, BaseFeeScalar: math.MaxUint32}, nil
} }
return 0, 0, err return EcostoneScalars{}, err
} }
switch sysCfg.Scalar[0] { return DecodeScalar(sysCfg.Scalar)
}
// DecodeScalar decodes the blobBaseFeeScalar and baseFeeScalar from a 32-byte scalar value.
// It uses the first byte to determine the scalar format.
func DecodeScalar(scalar [32]byte) (EcostoneScalars, error) {
switch scalar[0] {
case L1ScalarBedrock: case L1ScalarBedrock:
blobBaseFeeScalar = 0 return EcostoneScalars{
baseFeeScalar = binary.BigEndian.Uint32(sysCfg.Scalar[28:32]) BlobBaseFeeScalar: 0,
BaseFeeScalar: binary.BigEndian.Uint32(scalar[28:32]),
}, nil
case L1ScalarEcotone: case L1ScalarEcotone:
blobBaseFeeScalar = binary.BigEndian.Uint32(sysCfg.Scalar[24:28]) return EcostoneScalars{
baseFeeScalar = binary.BigEndian.Uint32(sysCfg.Scalar[28:32]) BlobBaseFeeScalar: binary.BigEndian.Uint32(scalar[24:28]),
BaseFeeScalar: binary.BigEndian.Uint32(scalar[28:32]),
}, nil
default: default:
err = fmt.Errorf("unexpected system config scalar: %s", sysCfg.Scalar) return EcostoneScalars{}, fmt.Errorf("unexpected system config scalar: %s", scalar)
} }
}
// EncodeScalar encodes the EcostoneScalars into a 32-byte scalar value
// for the Ecotone serialization format.
func EncodeScalar(scalars EcostoneScalars) (scalar [32]byte) {
scalar[0] = L1ScalarEcotone
binary.BigEndian.PutUint32(scalar[24:28], scalars.BlobBaseFeeScalar)
binary.BigEndian.PutUint32(scalar[28:32], scalars.BaseFeeScalar)
return return
} }
......
...@@ -45,13 +45,24 @@ func TestEcotoneScalars(t *testing.T) { ...@@ -45,13 +45,24 @@ func TestEcotoneScalars(t *testing.T) {
tc := tc tc := tc
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
sysConfig := SystemConfig{Scalar: tc.val} sysConfig := SystemConfig{Scalar: tc.val}
blobScalar, regScalar, err := sysConfig.EcotoneScalars() scalars, err := sysConfig.EcotoneScalars()
if tc.fail { if tc.fail {
require.NotNil(t, err) require.NotNil(t, err)
} else { } else {
require.Equal(t, tc.blobBaseFeeScalar, blobScalar) require.Equal(t, tc.blobBaseFeeScalar, scalars.BlobBaseFeeScalar)
require.Equal(t, tc.baseFeeScalar, regScalar) require.Equal(t, tc.baseFeeScalar, scalars.BaseFeeScalar)
require.NoError(t, err)
} }
}) })
} }
} }
func FuzzEncodeScalar(f *testing.F) {
f.Fuzz(func(t *testing.T, blobBaseFeeScalar uint32, baseFeeScalar uint32) {
encoded := EncodeScalar(EcostoneScalars{BlobBaseFeeScalar: blobBaseFeeScalar, BaseFeeScalar: baseFeeScalar})
scalars, err := DecodeScalar(encoded)
require.NoError(t, err)
require.Equal(t, blobBaseFeeScalar, scalars.BlobBaseFeeScalar)
require.Equal(t, baseFeeScalar, scalars.BaseFeeScalar)
})
}
...@@ -18,6 +18,8 @@ ...@@ -18,6 +18,8 @@
"l2OutputOracleStartingBlockNumber": 1, "l2OutputOracleStartingBlockNumber": 1,
"gasPriceOracleOverhead": 2100, "gasPriceOracleOverhead": 2100,
"gasPriceOracleScalar": 1000000, "gasPriceOracleScalar": 1000000,
"gasPriceOracleBaseFeeScalar": 1368,
"gasPriceOracleBlobBaseFeeScalar": 810949,
"l2OutputOracleProposer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "l2OutputOracleProposer": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"l2OutputOracleChallenger": "0x6925B8704Ff96DEe942623d6FB5e946EF5884b63", "l2OutputOracleChallenger": "0x6925B8704Ff96DEe942623d6FB5e946EF5884b63",
"l2GenesisBlockBaseFeePerGas": "0x3B9ACA00", "l2GenesisBlockBaseFeePerGas": "0x3B9ACA00",
......
...@@ -34,8 +34,10 @@ ...@@ -34,8 +34,10 @@
"governanceTokenOwner": "0x5C4e7Ba1E219E47948e6e3F55019A647bA501005", "governanceTokenOwner": "0x5C4e7Ba1E219E47948e6e3F55019A647bA501005",
"l2GenesisBlockGasLimit": "0x1c9c380", "l2GenesisBlockGasLimit": "0x1c9c380",
"l2GenesisBlockBaseFeePerGas": "0x3b9aca00", "l2GenesisBlockBaseFeePerGas": "0x3b9aca00",
"gasPriceOracleOverhead": 188, "gasPriceOracleOverhead": 0,
"gasPriceOracleScalar": 684000, "gasPriceOracleScalar": 0,
"gasPriceOracleBaseFeeScalar": 1368,
"gasPriceOracleBlobBaseFeeScalar": 810949,
"eip1559Denominator": 50, "eip1559Denominator": 50,
"eip1559Elasticity": 6, "eip1559Elasticity": 6,
"l2GenesisRegolithTimeOffset": "0x0", "l2GenesisRegolithTimeOffset": "0x0",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment