From 5aef595f7395131a9535c98fae78fc56ac101e44 Mon Sep 17 00:00:00 2001 From: Karl Bartel Date: Tue, 6 Jun 2023 12:50:08 +0200 Subject: [PATCH] Add GASLIMIT opcode (#2062) * Add gasLimit to block header ...and include it in JSON-RPC responses. * Set GasLimit header field on prepareBlock * Add GASLIMIT opcode * No gasLimit when EthCompatible is off Preserve old Celo behavior when EthCompatible is turned off. * tests: gasLimit is now available for pruned blocks * Optional gasLimit in RLP-serialized block headers to provide compatibility with blocks before the gasLimit has been added to the header. * Keep gasLimit in RPC response if in block header See also https://github.com/celo-org/celo-blockchain/pull/2062#discussion_r1210288481 * Move GASLIMIT opcode to G-fork * mycelo: enable GFork by default We always want to run and test the latest hard fork by default. * Ensure correct presence of gasLimit in RPC Before GFork, gasLimit is not in the header, so we have to * Add it if RPCEthCompatibility * Remove it if not RPCEthCompatibility, since it is now in the header struct and we would otherwise return a zero value After GFork it should be returned directly from the header, even if not RPCEthCompatibility, since it is now part of the header hash. * Only add gasLimit to header after GFork * Update monorepo_commit Required changes: * Using gasLimit in hash if present * Turn GFork off for CIP-35 test * Fix pending block when no RPCEthCompatibility The RPCEthCompatibility check was not intended here. See https://github.com/celo-org/celo-blockchain/pull/2062#discussion_r1218286282 --- core/types/block.go | 14 ++++--- core/types/gen_header_json.go | 6 +++ core/vm/evm.go | 1 + core/vm/instructions.go | 5 +++ core/vm/jump_table.go | 6 +++ core/vm/opcodes.go | 1 + core/vm/runtime/runtime_test.go | 1 + core/vm/vmcontext/context.go | 1 + e2e_test/e2e_test.go | 51 +++++++++++++++++++++++- e2e_test/ethersjs-api-check/test/test.ts | 14 +------ internal/ethapi/api.go | 43 ++++++++++++-------- miner/block.go | 3 ++ monorepo_commit | 2 +- mycelo/genesis/genesis.go | 1 + 14 files changed, 113 insertions(+), 36 deletions(-) diff --git a/core/types/block.go b/core/types/block.go index c035e0296e..ba39db5434 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -58,15 +58,18 @@ type Header struct { extraLock sync.Mutex extraValue *IstanbulExtra extraError error + + GasLimit uint64 `json:"gasLimit" rlp:"optional"` } // field type overrides for gencodec type headerMarshaling struct { - Number *hexutil.Big - GasUsed hexutil.Uint64 - Time hexutil.Uint64 - Extra hexutil.Bytes - Hash common.Hash `json:"hash"` // adds call to Hash() in MarshalJSON + Number *hexutil.Big + GasLimit hexutil.Uint64 + GasUsed hexutil.Uint64 + Time hexutil.Uint64 + Extra hexutil.Bytes + Hash common.Hash `json:"hash"` // adds call to Hash() in MarshalJSON } // Hash returns the block hash of the header, which is simply the keccak256 hash of its @@ -289,6 +292,7 @@ func CopyHeader(h *Header) *Header { ReceiptHash: h.ReceiptHash, Bloom: h.Bloom, Number: new(big.Int), + GasLimit: h.GasLimit, GasUsed: h.GasUsed, Time: h.Time, } diff --git a/core/types/gen_header_json.go b/core/types/gen_header_json.go index e2fc8096f0..fbdc0cf284 100644 --- a/core/types/gen_header_json.go +++ b/core/types/gen_header_json.go @@ -26,6 +26,7 @@ func (h Header) MarshalJSON() ([]byte, error) { GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` Time hexutil.Uint64 `json:"timestamp" gencodec:"required"` Extra hexutil.Bytes `json:"extraData" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" rlp:"optional"` Hash common.Hash `json:"hash"` } var enc Header @@ -39,6 +40,7 @@ func (h Header) MarshalJSON() ([]byte, error) { enc.GasUsed = hexutil.Uint64(h.GasUsed) enc.Time = hexutil.Uint64(h.Time) enc.Extra = h.Extra + enc.GasLimit = hexutil.Uint64(h.GasLimit) enc.Hash = h.Hash() return json.Marshal(&enc) } @@ -56,6 +58,7 @@ func (h *Header) UnmarshalJSON(input []byte) error { GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"` Extra *hexutil.Bytes `json:"extraData" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" rlp:"optional"` } var dec Header if err := json.Unmarshal(input, &dec); err != nil { @@ -101,5 +104,8 @@ func (h *Header) UnmarshalJSON(input []byte) error { return errors.New("missing required field 'extraData' for Header") } h.Extra = *dec.Extra + if dec.GasLimit != nil { + h.GasLimit = uint64(*dec.GasLimit) + } return nil } diff --git a/core/vm/evm.go b/core/vm/evm.go index d9b7e3a08c..0900dc0df0 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -89,6 +89,7 @@ type BlockContext struct { // Block information Coinbase common.Address // Provides information for COINBASE + GasLimit uint64 // Provides information for GASLIMIT BlockNumber *big.Int // Provides information for NUMBER Time *big.Int // Provides information for TIME diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 28b410ef69..6106fb2c9a 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -469,6 +469,11 @@ func opNumber(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b return nil, nil } +func opGasLimit(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + scope.Stack.push(new(uint256.Int).SetUint64(interpreter.evm.Context.GasLimit)) + return nil, nil +} + func opPop(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { scope.Stack.pop() return nil, nil diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 51a0f16658..75829559f8 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -67,6 +67,12 @@ type JumpTable [256]*operation // constantinople, istanbul, petersburg, espresso and g-fork instructions. func newGforkInstructionSet() JumpTable { instructionSet := newEspressoInstructionSet() + instructionSet[GASLIMIT] = &operation{ + execute: opGasLimit, + constantGas: GasQuickStep, + minStack: minStack(0, 1), + maxStack: maxStack(0, 1), + } return instructionSet } diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index c1334c8df0..e910cb1277 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -99,6 +99,7 @@ const ( COINBASE TIMESTAMP NUMBER + GASLIMIT OpCode = 0x45 CHAINID OpCode = 0x46 SELFBALANCE OpCode = 0x47 BASEFEE OpCode = 0x48 diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index 7a64c17192..f648051e96 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -70,6 +70,7 @@ func TestEVM(t *testing.T) { Execute([]byte{ byte(vm.TIMESTAMP), + byte(vm.GASLIMIT), byte(vm.PUSH1), byte(vm.ORIGIN), byte(vm.BLOCKHASH), diff --git a/core/vm/vmcontext/context.go b/core/vm/vmcontext/context.go index 5a9c098ccf..8140094ebd 100644 --- a/core/vm/vmcontext/context.go +++ b/core/vm/vmcontext/context.go @@ -50,6 +50,7 @@ func NewBlockContext(header *types.Header, chain chainContext, txFeeRecipient *c GetHash: GetHashFn(header, chain), VerifySeal: VerifySealFn(header, chain), Coinbase: beneficiary, + GasLimit: header.GasLimit, BlockNumber: new(big.Int).Set(header.Number), Time: new(big.Int).SetUint64(header.Time), diff --git a/e2e_test/e2e_test.go b/e2e_test/e2e_test.go index 7dbc3e82b9..cfb227f5be 100644 --- a/e2e_test/e2e_test.go +++ b/e2e_test/e2e_test.go @@ -606,7 +606,7 @@ func TestEthersJSCompatibilityDisable(t *testing.T) { _, ok = result["baseFeePerGas"] assert.True(t, ok, "baseFeePerGas field should be present on RPC block") - // Turn of compatibility and check fields are not present + // Turn off compatibility and check fields are not present ec.RPCEthCompatibility = false network, shutdown, err = test.NewNetwork(ac, gc, ec) require.NoError(t, err) @@ -619,8 +619,55 @@ func TestEthersJSCompatibilityDisable(t *testing.T) { err = network[0].WsClient.GetRPCClient().CallContext(ctx, &result, "eth_getBlockByNumber", "latest", true) require.NoError(t, err) + // After GFork, gasLimit should be returned directly from the header, even if + // RPCEthCompatibility is off, since it is now part of the header hash. _, ok = result["gasLimit"] - assert.False(t, ok, "gasLimit field should not be present on RPC block") + assert.True(t, ok, "gasLimit field must be present on RPC block after GFork") + _, ok = result["baseFeePerGas"] + assert.False(t, ok, "baseFeePerGas field must be present on RPC block") +} + +// This test checks the functionality of the configuration to enable/disable +// returning the 'gasLimit' and 'baseFeePerGas' fields on RPC blocks before the GFork happened. +// GFork is relevant because it added the gasLimit to the header. +func TestEthersJSCompatibilityDisableBeforeGFork(t *testing.T) { + ac := test.AccountConfig(1, 1) + gc, ec, err := test.BuildConfig(ac) + gc.Hardforks.GForkBlock = nil + require.NoError(t, err) + + // Check fields present (compatibility set by default) + network, shutdown, err := test.NewNetwork(ac, gc, ec) + require.NoError(t, err) + defer shutdown() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + + result := make(map[string]interface{}) + err = network[0].WsClient.GetRPCClient().CallContext(ctx, &result, "eth_getBlockByNumber", "latest", true) + require.NoError(t, err) + + _, ok := result["gasLimit"] + assert.True(t, ok, "gasLimit field should be present on RPC block") + _, ok = result["baseFeePerGas"] + assert.True(t, ok, "baseFeePerGas field should be present on RPC block") + + // Turn off compatibility and check fields are not present + ec.RPCEthCompatibility = false + network, shutdown, err = test.NewNetwork(ac, gc, ec) + require.NoError(t, err) + defer shutdown() + + ctx, cancel = context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + + result = make(map[string]interface{}) + err = network[0].WsClient.GetRPCClient().CallContext(ctx, &result, "eth_getBlockByNumber", "latest", true) + require.NoError(t, err) + + _, ok = result["gasLimit"] + assert.False(t, ok, "gasLimit field should not be present on RPC block before GFork") _, ok = result["baseFeePerGas"] assert.False(t, ok, "baseFeePerGas field should not be present on RPC block") } diff --git a/e2e_test/ethersjs-api-check/test/test.ts b/e2e_test/ethersjs-api-check/test/test.ts index e3fc9943f9..b2ded59323 100644 --- a/e2e_test/ethersjs-api-check/test/test.ts +++ b/e2e_test/ethersjs-api-check/test/test.ts @@ -56,23 +56,13 @@ describe('ethers.js compatibility tests with state', () => { describe('ethers.js compatibility tests with no state', () => { - it('provider.getBlock throws exception (no gasLimit)', async () => { - let provider = new ethers.JsonRpcProvider(process.env.npm_config_networkaddr); - try { - await provider.getBlock(process.env.npm_config_blocknum as string); - } catch (e) { - return - } - assert.fail("Expecting exception to be thrown when getting block") - }); - - it('block has no gasLimit', async () => { + it('block has gasLimit', async () => { let provider = new ethers.JsonRpcProvider(process.env.npm_config_networkaddr); const fullBlock = await provider.send( 'eth_getBlockByNumber', [ethers.toQuantity(process.env.npm_config_blocknum as string), true] ) - assert.isFalse(fullBlock.hasOwnProperty('gasLimit')) + assert.isTrue(fullBlock.hasOwnProperty('gasLimit')) }); it('block has no baseFeePerGas', async () => { diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 1e2e7dfc3e..d54b319bc7 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -709,8 +709,8 @@ func (s *PublicBlockChainAPI) GetBlockByNumber(ctx context.Context, number rpc.B if block != nil && err == nil { response, err := s.rpcMarshalBlock(ctx, block, true, fullTx) - if err == nil && s.b.RPCEthCompatibility() { - addEthCompatibilityFields(ctx, response, s.b, block.Header()) + if err == nil { + addEthCompatibilityFields(ctx, response, s.b, block) if number == rpc.PendingBlockNumber { // Pending blocks need to nil out a few fields for _, field := range []string{"hash", "nonce", "miner"} { @@ -732,9 +732,7 @@ func (s *PublicBlockChainAPI) GetBlockByHash(ctx context.Context, hash common.Ha if err != nil { return nil, err } - if s.b.RPCEthCompatibility() { - addEthCompatibilityFields(ctx, result, s.b, block.Header()) - } + addEthCompatibilityFields(ctx, result, s.b, block) return result, nil } return nil, err @@ -744,16 +742,28 @@ func (s *PublicBlockChainAPI) GetBlockByHash(ctx context.Context, hash common.Ha // and ethers.js (and potentially other web3 clients) by adding fields to our // rpc response that ethers.js depends upon. // See https://github.com/celo-org/celo-blockchain/issues/1945 -func addEthCompatibilityFields(ctx context.Context, block map[string]interface{}, b Backend, header *types.Header) { - hash := header.Hash() - numhash := rpc.BlockNumberOrHash{ - BlockHash: &hash, +func addEthCompatibilityFields(ctx context.Context, response map[string]interface{}, b Backend, block *types.Block) { + isGFork := b.ChainConfig().IsGFork(block.Number()) + if !b.RPCEthCompatibility() { + if !isGFork { + delete(response, "gasLimit") + } + return } - gasLimit, err := b.GetRealBlockGasLimit(ctx, numhash) - if err != nil { - log.Debug("Not adding gasLimit to RPC response, failed to retrieve it", "block", header.Number.Uint64(), "err", err) - } else { - block["gasLimit"] = hexutil.Uint64(gasLimit) + + header := block.Header() + if !isGFork { + // Before GFork, the header did not include the gasLimit, so we have to manually add it for eth-compatible RPC responses. + hash := header.Hash() + numhash := rpc.BlockNumberOrHash{ + BlockHash: &hash, + } + gasLimit, err := b.GetRealBlockGasLimit(ctx, numhash) + if err != nil { + log.Debug("Not adding gasLimit to RPC response, failed to retrieve it", "block", header.Number.Uint64(), "err", err) + } else { + response["gasLimit"] = hexutil.Uint64(gasLimit) + } } // Providing nil as the currency address gets the gas price minimum for the native celo asset. @@ -761,10 +771,10 @@ func addEthCompatibilityFields(ctx context.Context, block map[string]interface{} if err != nil { log.Debug("Not adding baseFeePerGas to RPC response, failed to retrieve gas price minimum", "block", header.Number.Uint64(), "err", err) } else { - block["baseFeePerGas"] = (*hexutil.Big)(baseFee) + response["baseFeePerGas"] = (*hexutil.Big)(baseFee) } - block["difficulty"] = "0x0" + response["difficulty"] = "0x0" } // GetUncleByBlockNumberAndIndex returns the uncle block for the given block hash and index. When fullTx is true @@ -1133,6 +1143,7 @@ func RPCMarshalHeader(head *types.Header) map[string]interface{} { "miner": head.Coinbase, "extraData": hexutil.Bytes(head.Extra), "size": hexutil.Uint64(head.Size()), + "gasLimit": hexutil.Uint64(head.GasLimit), "gasUsed": hexutil.Uint64(head.GasUsed), "timestamp": hexutil.Uint64(head.Time), "transactionsRoot": head.TxHash, diff --git a/miner/block.go b/miner/block.go index c1d74655a7..cf01eeb44f 100644 --- a/miner/block.go +++ b/miner/block.go @@ -111,6 +111,9 @@ func prepareBlock(w *worker) (*blockState, error) { sysCtx: core.NewSysContractCallCtx(header, state.Copy(), w.chain), } b.gasPool = new(core.GasPool).AddGas(b.gasLimit) + if w.chainConfig.IsGFork(header.Number) { + header.GasLimit = b.gasLimit + } // Play our part in generating the random beacon. if w.isRunning() && random.IsRunning(vmRunner) { diff --git a/monorepo_commit b/monorepo_commit index 980711852a..7d0ae641ec 100644 --- a/monorepo_commit +++ b/monorepo_commit @@ -1 +1 @@ -ad770ace57ec3fa1bc1fd540d0321ab2e7317076 +d216e7599f499adec2e0200e599f601ea5424cde diff --git a/mycelo/genesis/genesis.go b/mycelo/genesis/genesis.go index 0d1112f8c1..d2f68e62d1 100644 --- a/mycelo/genesis/genesis.go +++ b/mycelo/genesis/genesis.go @@ -28,6 +28,7 @@ func CreateCommonGenesisConfig(chainID *big.Int, adminAccountAddress common.Addr ChurritoBlock: common.Big0, DonutBlock: common.Big0, EspressoBlock: common.Big0, + GForkBlock: common.Big0, } // Make admin account manager of Governance & Reserve