diff --git a/core/rawdb/accessors_celo.go b/core/rawdb/accessors_celo.go new file mode 100644 index 0000000000..fd8343efb2 --- /dev/null +++ b/core/rawdb/accessors_celo.go @@ -0,0 +1,51 @@ +package rawdb + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/rlp" +) + +var ( + preGingerbreadFieldsPrefix = []byte("preGingerbreadFields-") +) + +type PreGingerbreadAdditionalFields struct { + BaseFee *big.Int `rlp:"nil"` + GasLimit *big.Int `rlp:"nil"` +} + +func preGingerbreadAdditionalFieldsKey(hash common.Hash) []byte { + return append(preGingerbreadFieldsPrefix, hash.Bytes()...) +} + +func ReadPreGingerbreadAdditionalFields(db ethdb.KeyValueReader, blockHash common.Hash) (*PreGingerbreadAdditionalFields, error) { + data, _ := db.Get(preGingerbreadAdditionalFieldsKey(blockHash)) + if len(data) == 0 { + return nil, nil + } + + fields := &PreGingerbreadAdditionalFields{} + + err := rlp.DecodeBytes(data, fields) + if err != nil { + return nil, err + } + + return fields, nil +} + +func WritePreGingerbreadAdditionalFields(db ethdb.KeyValueWriter, blockHash common.Hash, data *PreGingerbreadAdditionalFields) error { + rawData, err := rlp.EncodeToBytes(data) + if err != nil { + return err + } + + if err := db.Put(preGingerbreadAdditionalFieldsKey(blockHash), rawData); err != nil { + return err + } + + return nil +} diff --git a/core/rawdb/accessors_celo_test.go b/core/rawdb/accessors_celo_test.go new file mode 100644 index 0000000000..0ded2926cc --- /dev/null +++ b/core/rawdb/accessors_celo_test.go @@ -0,0 +1,93 @@ +package rawdb + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/assert" +) + +func TestPreGingerbreadAdditionalFields(t *testing.T) { + db := NewMemoryDatabase() + + mockData, err := rlp.EncodeToBytes(&PreGingerbreadAdditionalFields{ + BaseFee: big.NewInt(1000), + GasLimit: big.NewInt(2000), + }) + assert.NoError(t, err) + + type SeedData struct { + hash common.Hash + data []byte + } + + tests := []struct { + name string + seedData []SeedData + hash common.Hash + expectedRes *PreGingerbreadAdditionalFields + returnsError bool + }{ + { + name: "should return nil if data is not found", + seedData: []SeedData{}, + hash: common.HexToHash("0x1"), + expectedRes: nil, + returnsError: false, + }, + { + name: "should return data", + seedData: []SeedData{ + { + hash: common.HexToHash("0x2"), + data: mockData, + }, + }, + hash: common.HexToHash("0x2"), + expectedRes: &PreGingerbreadAdditionalFields{ + BaseFee: big.NewInt(1000), + GasLimit: big.NewInt(2000), + }, + returnsError: false, + }, + { + name: "should return error if data is broken", + seedData: []SeedData{ + { + hash: common.HexToHash("0x3"), + data: []byte{0x1, 0x2, 0x3}, + }, + }, + hash: common.HexToHash("0x3"), + expectedRes: nil, + returnsError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + for _, seed := range test.seedData { + err := db.Put(preGingerbreadAdditionalFieldsKey(seed.hash), seed.data) + assert.NoError(t, err) + } + + t.Cleanup(func() { + for _, seed := range test.seedData { + err := db.Delete(preGingerbreadAdditionalFieldsKey(seed.hash)) + assert.NoError(t, err) + } + }) + + res, err := ReadPreGingerbreadAdditionalFields(db, test.hash) + assert.Equal(t, test.expectedRes, res) + + if test.returnsError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 69f4fe16c9..3e299d3f86 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -843,6 +843,7 @@ func decodeHash(s string) (h common.Hash, inputLength int, err error) { func (api *BlockChainAPI) GetHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (map[string]interface{}, error) { header, err := api.b.HeaderByNumber(ctx, number) if header != nil && err == nil { + header := PopulatePreGingerbreadHeaderFields(ctx, api.b, header) response := RPCMarshalHeader(header) if number == rpc.PendingBlockNumber && api.b.ChainConfig().Optimism == nil { // Pending header need to nil out a few fields @@ -859,6 +860,7 @@ func (api *BlockChainAPI) GetHeaderByNumber(ctx context.Context, number rpc.Bloc func (api *BlockChainAPI) GetHeaderByHash(ctx context.Context, hash common.Hash) map[string]interface{} { header, _ := api.b.HeaderByHash(ctx, hash) if header != nil { + header := PopulatePreGingerbreadHeaderFields(ctx, api.b, header) return RPCMarshalHeader(header) } return nil @@ -874,6 +876,7 @@ func (api *BlockChainAPI) GetHeaderByHash(ctx context.Context, hash common.Hash) func (api *BlockChainAPI) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { block, err := api.b.BlockByNumber(ctx, number) if block != nil && err == nil { + block := PopulatePreGingerbreadBlockFields(ctx, api.b, block) response, err := RPCMarshalBlock(ctx, block, true, fullTx, api.b.ChainConfig(), api.b) if err == nil && number == rpc.PendingBlockNumber && api.b.ChainConfig().Optimism == nil { // Pending blocks need to nil out a few fields @@ -881,6 +884,7 @@ func (api *BlockChainAPI) GetBlockByNumber(ctx context.Context, number rpc.Block response[field] = nil } } + return response, err } return nil, err @@ -891,6 +895,7 @@ func (api *BlockChainAPI) GetBlockByNumber(ctx context.Context, number rpc.Block func (api *BlockChainAPI) GetBlockByHash(ctx context.Context, hash common.Hash, fullTx bool) (map[string]interface{}, error) { block, err := api.b.BlockByHash(ctx, hash) if block != nil { + block := PopulatePreGingerbreadBlockFields(ctx, api.b, block) return RPCMarshalBlock(ctx, block, true, fullTx, api.b.ChainConfig(), api.b) } return nil, err @@ -906,6 +911,7 @@ func (api *BlockChainAPI) GetUncleByBlockNumberAndIndex(ctx context.Context, blo return nil, nil } block = types.NewBlockWithHeader(uncles[index]) + block = PopulatePreGingerbreadBlockFields(ctx, api.b, block) return RPCMarshalBlock(ctx, block, false, false, api.b.ChainConfig(), api.b) } return nil, err @@ -921,6 +927,7 @@ func (api *BlockChainAPI) GetUncleByBlockHashAndIndex(ctx context.Context, block return nil, nil } block = types.NewBlockWithHeader(uncles[index]) + block = PopulatePreGingerbreadBlockFields(ctx, api.b, block) return RPCMarshalBlock(ctx, block, false, false, api.b.ChainConfig(), api.b) } return nil, err diff --git a/internal/ethapi/celo_block.go b/internal/ethapi/celo_block.go new file mode 100644 index 0000000000..565e428514 --- /dev/null +++ b/internal/ethapi/celo_block.go @@ -0,0 +1,161 @@ +package ethapi + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" +) + +var ( + gasPriceMinimumABIJson = `[{"inputs":[{"internalType":"uint256","name":"gasPriceMinimum","type":"uint256"}],"name":"GasPriceMinimumUpdated","outputs":[],"type":"event"}]` + gasPriceMinimumABI abi.ABI +) + +func init() { + parsedAbi, _ := abi.JSON(strings.NewReader(gasPriceMinimumABIJson)) + gasPriceMinimumABI = parsedAbi +} + +// PopulatePreGingerbreadBlockFields populates the baseFee and gasLimit fields of the block for pre-gingerbread blocks +func PopulatePreGingerbreadBlockFields(ctx context.Context, backend CeloBackend, block *types.Block) *types.Block { + newHeader := PopulatePreGingerbreadHeaderFields(ctx, backend, block.Header()) + block = block.WithSeal(newHeader) + return block +} + +// PopulatePreGingerbreadHeaderFields populates the baseFee and gasLimit fields of the header for pre-gingerbread blocks +func PopulatePreGingerbreadHeaderFields(ctx context.Context, backend CeloBackend, header *types.Header) *types.Header { + isGingerbread := backend.ChainConfig().IsGingerbread(header.Number) + if isGingerbread { + return header + } + + var ( + baseFee *big.Int + gasLimit *big.Int + ) + + fields, err := rawdb.ReadPreGingerbreadAdditionalFields(backend.ChainDb(), header.Hash()) + if fields != nil { + // If the record is found, use the values from the record + baseFee = fields.BaseFee + gasLimit = fields.GasLimit + } else { + if err != nil { + log.Debug("failed to read pre-gingerbread fields", "err", err) + } + + // If the record is not found, get the values and store them + baseFee, err = retrievePreGingerbreadBlockBaseFee(ctx, backend, header.Number) + if err != nil { + log.Debug("Not adding baseFeePerGas to RPC response, failed to retrieve gas price minimum", "block", header.Number.Uint64(), "err", err) + } + + gasLimit = retrievePreGingerbreadGasLimit(backend, header.Number) + + if baseFee != nil || gasLimit != nil { + err = rawdb.WritePreGingerbreadAdditionalFields(backend.ChainDb(), header.Hash(), &rawdb.PreGingerbreadAdditionalFields{ + BaseFee: baseFee, + GasLimit: gasLimit, + }) + if err != nil { + log.Debug("failed to write pre-gingerbread fields", "err", err) + } + } + } + + if baseFee != nil { + header.BaseFee = baseFee + } + if gasLimit != nil { + header.GasLimit = gasLimit.Uint64() + } + + return header +} + +// retrievePreGingerbreadGasLimit retrieves a gas limit at given height from hardcoded values +func retrievePreGingerbreadGasLimit(backend CeloBackend, height *big.Int) *big.Int { + if backend.ChainConfig().ChainID == nil { + log.Debug("Not adding gasLimit to RPC response, unknown network") + return nil + } + + limits, ok := params.PreGingerbreadNetworkGasLimits[backend.ChainConfig().ChainID.Uint64()] + if !ok { + log.Debug("Not adding gasLimit to RPC response, unknown network", "chainID", backend.ChainConfig().ChainID) + return nil + } + + return new(big.Int).SetUint64(limits.Limit(height)) +} + +// retrievePreGingerbreadBlockBaseFee retrieves a base fee at given height from the previous block +func retrievePreGingerbreadBlockBaseFee(ctx context.Context, backend CeloBackend, height *big.Int) (*big.Int, error) { + if height.Cmp(common.Big0) <= 0 { + return nil, nil + } + + prevBlock, err := backend.BlockByNumber(ctx, rpc.BlockNumber(height.Uint64()-1)) + if err != nil { + return nil, err + } + if prevBlock == nil { + return nil, fmt.Errorf("block #%d not found", height.Int64()) + } + + prevReceipts, err := backend.GetReceipts(ctx, prevBlock.Hash()) + if err != nil { + return nil, err + } + + numTxs, numReceipts := len(prevBlock.Transactions()), len(prevReceipts) + if numReceipts <= numTxs { + return nil, fmt.Errorf("receipts of block #%d don't contain system logs", height.Int64()) + } + + systemReceipt := prevReceipts[numTxs] + for _, logRecord := range systemReceipt.Logs { + if logRecord.Topics[0] != gasPriceMinimumABI.Events["GasPriceMinimumUpdated"].ID { + continue + } + + baseFee, err := parseGasPriceMinimumUpdated(logRecord.Data) + if err != nil { + return nil, err + } + + return baseFee, nil + } + + return nil, fmt.Errorf("gas price minimum updated event is not included in a receipt of block #%d", height.Int64()) +} + +// parseGasPriceMinimumUpdated parses the data of GasPriceMinimumUpdated event +func parseGasPriceMinimumUpdated(data []byte) (*big.Int, error) { + values, err := gasPriceMinimumABI.Unpack("GasPriceMinimumUpdated", data) + if err != nil { + return nil, err + } + + // safe check, actually Unpack will parse first 32 bytes as a single value + if len(values) != 1 { + return nil, fmt.Errorf("unexpected format of values in GasPriceMinimumUpdated event") + } + + baseFee, ok := values[0].(*big.Int) + if !ok { + return nil, fmt.Errorf("unexpected base fee type in GasPriceMinimumUpdated event: expected *big.Int, got %T", values[0]) + } + + return baseFee, nil +} diff --git a/internal/ethapi/celo_block_test.go b/internal/ethapi/celo_block_test.go new file mode 100644 index 0000000000..c1e063236e --- /dev/null +++ b/internal/ethapi/celo_block_test.go @@ -0,0 +1,45 @@ +package ethapi + +import ( + "errors" + "math/big" + "testing" + + "github.com/status-im/keycard-go/hexutils" + "github.com/stretchr/testify/assert" +) + +func Test_parseGasPriceMinimumUpdated(t *testing.T) { + tests := []struct { + name string + data []byte + result *big.Int + err error + }{ + { + name: "should parse gas price successfully", + data: hexutils.HexToBytes("00000000000000000000000000000000000000000000000000000000000f4240"), + result: big.NewInt(1_000_000), + err: nil, + }, + { + name: "should return error if data is not in the expected format", + data: hexutils.HexToBytes("123456"), + result: nil, + err: errors.New("abi: cannot marshal in to go type: length insufficient 3 require 32"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := parseGasPriceMinimumUpdated(test.data) + assert.Equal(t, test.result, result) + + if test.err == nil { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, test.err.Error()) + } + }) + } +} diff --git a/params/celo_config.go b/params/celo_config.go index c761cb4605..dd0b7fbf44 100644 --- a/params/celo_config.go +++ b/params/celo_config.go @@ -1,7 +1,74 @@ package params +import "math/big" + const ( CeloMainnetChainID = 42220 - CeloBaklavaChainID = 62320 CeloAlfajoresChainID = 44787 + CeloBaklavaChainID = 62320 +) + +type GasLimits struct { + // changes holds all gas limit changes, it is assumed that the first change ocurrs at block 0. + changes []LimitChange +} + +type LimitChange struct { + block *big.Int + gasLimit uint64 +} + +func (g *GasLimits) Limit(block *big.Int) uint64 { + // Grab the gas limit at block 0 + curr := g.changes[0].gasLimit + for _, c := range g.changes[1:] { + if block.Cmp(c.block) < 0 { + return curr + } + curr = c.gasLimit + } + return curr +} + +var ( + // Hardcoded set of gas limit changes derived from historical state of Celo L1 chain + // Ported from celo-blockchain (https://github.com/celo-org/celo-blockchain/blob/master/params/config.go#L189-L220) + mainnetGasLimits = &GasLimits{ + changes: []LimitChange{ + {big.NewInt(0), 20e6}, + {big.NewInt(3317), 10e6}, + {big.NewInt(3251772), 13e6}, + {big.NewInt(6137285), 20e6}, + {big.NewInt(13562578), 50e6}, + {big.NewInt(14137511), 20e6}, + {big.NewInt(21355415), 32e6}, + }, + } + + alfajoresGasLimits = &GasLimits{ + changes: []LimitChange{ + {big.NewInt(0), 20e6}, + {big.NewInt(912), 10e6}, + {big.NewInt(1392355), 130e6}, + {big.NewInt(1507905), 13e6}, + {big.NewInt(4581182), 20e6}, + {big.NewInt(11143973), 35e6}, + }, + } + + baklavaGasLimits = &GasLimits{ + changes: []LimitChange{ + {big.NewInt(0), 20e6}, + {big.NewInt(1230), 10e6}, + {big.NewInt(1713181), 130e6}, + {big.NewInt(1945003), 13e6}, + {big.NewInt(15158971), 20e6}, + }, + } + + PreGingerbreadNetworkGasLimits = map[uint64]*GasLimits{ + CeloMainnetChainID: mainnetGasLimits, + CeloAlfajoresChainID: alfajoresGasLimits, + CeloBaklavaChainID: baklavaGasLimits, + } ) diff --git a/params/celo_config_test.go b/params/celo_config_test.go new file mode 100644 index 0000000000..f10a13a0fd --- /dev/null +++ b/params/celo_config_test.go @@ -0,0 +1,30 @@ +package params + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGasLimits_Limit(t *testing.T) { + subTest := func(t *testing.T, name string, chainId uint64, limits []LimitChange) { + t.Run(name, func(t *testing.T) { + for i, l := range limits { + beginningHeight := l.block + beginningLimit := PreGingerbreadNetworkGasLimits[chainId].Limit(beginningHeight) + assert.Equal(t, l.gasLimit, beginningLimit, "gas limit at block %d (%s)", beginningHeight, name) + + if i < len(limits)-1 { + endHeight := new(big.Int).Sub(limits[i+1].block, big.NewInt(1)) + endLimit := PreGingerbreadNetworkGasLimits[chainId].Limit(endHeight) + assert.Equal(t, l.gasLimit, endLimit, "gas limit at block %d (%s)", endHeight, name) + } + } + }) + } + + subTest(t, "mainnet", CeloMainnetChainID, mainnetGasLimits.changes) + subTest(t, "alfajores", CeloAlfajoresChainID, alfajoresGasLimits.changes) + subTest(t, "baklava", CeloBaklavaChainID, baklavaGasLimits.changes) +}