diff --git a/internal/ethereum/event_stream_test.go b/internal/ethereum/event_stream_test.go index bb0ea7d..77fbf0c 100644 --- a/internal/ethereum/event_stream_test.go +++ b/internal/ethereum/event_stream_test.go @@ -41,6 +41,7 @@ func testEventStream(t *testing.T, listeners ...*ffcapi.EventListenerAddRequest) func testEventStreamExistingConnector(t *testing.T, ctx context.Context, done func(), c *ethConnector, mRPC *rpcbackendmocks.Backend, listeners ...*ffcapi.EventListenerAddRequest) (*eventStream, chan *ffcapi.ListenerEvent, *rpcbackendmocks.Backend, func()) { events := make(chan *ffcapi.ListenerEvent) esID := fftypes.NewUUID() + c.chainID = "12345" // set chainID before streamLoop starts, so enrich does not call net_version _, _, err := c.EventStreamStart(ctx, &ffcapi.EventStreamStartRequest{ ID: esID, StreamContext: ctx, @@ -52,7 +53,6 @@ func testEventStreamExistingConnector(t *testing.T, ctx context.Context, done fu es := c.eventStreams[*esID] es.c.eventFilterPollingInterval = 1 * time.Millisecond es.c.retry.MaximumDelay = 1 * time.Microsecond - c.chainID = "12345" assert.NotNil(t, es) es.preStartProcessing() diff --git a/mocks/ethblocklistenermocks/block_listener.go b/mocks/ethblocklistenermocks/block_listener.go index 43fbd17..904c659 100644 --- a/mocks/ethblocklistenermocks/block_listener.go +++ b/mocks/ethblocklistenermocks/block_listener.go @@ -5,16 +5,12 @@ package ethblocklistenermocks import ( context "context" + fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" ethblocklistener "github.com/hyperledger/firefly-evmconnect/pkg/ethblocklistener" ethrpc "github.com/hyperledger/firefly-evmconnect/pkg/ethrpc" - ethtypes "github.com/hyperledger/firefly-signer/pkg/ethtypes" - - fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" - - mock "github.com/stretchr/testify/mock" - rpcbackend "github.com/hyperledger/firefly-signer/pkg/rpcbackend" + mock "github.com/stretchr/testify/mock" ) // BlockListener is an autogenerated mock type for the BlockListener type @@ -280,6 +276,36 @@ func (_m *BlockListener) GetHighestBlock(ctx context.Context) (uint64, bool) { return r0, r1 } +// GetHighestBlockInfo provides a mock function with given fields: ctx +func (_m *BlockListener) GetHighestBlockInfo(ctx context.Context) (*ethrpc.BlockInfoJSONRPC, bool) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetHighestBlockInfo") + } + + var r0 *ethrpc.BlockInfoJSONRPC + var r1 bool + if rf, ok := ret.Get(0).(func(context.Context) (*ethrpc.BlockInfoJSONRPC, bool)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *ethrpc.BlockInfoJSONRPC); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethrpc.BlockInfoJSONRPC) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) bool); ok { + r1 = rf(ctx) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + // GetMonitoredHeadLength provides a mock function with no fields func (_m *BlockListener) GetMonitoredHeadLength() int { ret := _m.Called() diff --git a/mocks/fftmmocks/manager.go b/mocks/fftmmocks/manager.go index 444bc8c..8f0bc44 100644 --- a/mocks/fftmmocks/manager.go +++ b/mocks/fftmmocks/manager.go @@ -6,18 +6,13 @@ import ( context "context" apitypes "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" - eventapi "github.com/hyperledger/firefly-transaction-manager/pkg/eventapi" - ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" - metric "github.com/hyperledger/firefly-common/pkg/metric" - - mock "github.com/stretchr/testify/mock" - mux "github.com/gorilla/mux" - + metric "github.com/hyperledger/firefly-common/pkg/metric" txhandler "github.com/hyperledger/firefly-transaction-manager/pkg/txhandler" + mock "github.com/stretchr/testify/mock" ) // Manager is an autogenerated mock type for the Manager type diff --git a/mocks/rpcbackendmocks/subscription.go b/mocks/rpcbackendmocks/subscription.go index af6e98f..ac02c73 100644 --- a/mocks/rpcbackendmocks/subscription.go +++ b/mocks/rpcbackendmocks/subscription.go @@ -6,9 +6,8 @@ import ( context "context" fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" - mock "github.com/stretchr/testify/mock" - rpcbackend "github.com/hyperledger/firefly-signer/pkg/rpcbackend" + mock "github.com/stretchr/testify/mock" ) // Subscription is an autogenerated mock type for the Subscription type diff --git a/pkg/ethblocklistener/blocklistener.go b/pkg/ethblocklistener/blocklistener.go index 41c99e9..ce7bb6a 100644 --- a/pkg/ethblocklistener/blocklistener.go +++ b/pkg/ethblocklistener/blocklistener.go @@ -74,6 +74,7 @@ type BlockListener interface { AddConsumer(ctx context.Context, c *BlockUpdateConsumer) RemoveConsumer(ctx context.Context, id *fftypes.UUID) GetHighestBlock(ctx context.Context) (uint64, bool) + GetHighestBlockInfo(ctx context.Context) (*ethrpc.BlockInfoJSONRPC, bool) GetBlockGasLimit() *ethtypes.HexInteger // nil if unknown GetBlockInfoByNumber(ctx context.Context, blockNumber uint64, allowCache bool, expectedParentHashStr string, expectedBlockHashStr string) (*ethrpc.BlockInfoJSONRPC, error) GetBlockInfoByHash(ctx context.Context, hash0xString string) (*ethrpc.BlockInfoJSONRPC, error) @@ -125,12 +126,12 @@ type blockListener struct { BlockListenerConfig // canonical chain - monitoredHeadLength uint64 - canonicalChainLock sync.RWMutex // covers highestBlock and canonicalChain - canonicalChain *list.List - highestBlockSet bool - highestBlock uint64 - highestBlockGasLimit *ethtypes.HexInteger + monitoredHeadLength uint64 + canonicalChainLock sync.RWMutex // covers highestBlock and canonicalChain + canonicalChain *list.List + highestBlockSet bool + highestBlock uint64 + headBlockInfo *ethrpc.BlockInfoJSONRPC // full info for the current head block, when seen // headBlockNumber mode: last head value sent on the block listener channel (only written from listenLoop) currentChainHead uint64 @@ -475,7 +476,7 @@ func (bl *blockListener) reconcileCanonicalChain(bi *ethrpc.BlockInfoJSONRPC) *l bl.canonicalChainLock.Lock() defer bl.canonicalChainLock.Unlock() - bl.checkAndSetHighestBlock(bi.Number.Uint64(), bi.GasLimit) + bl.checkAndSetHighestBlock(bi) // Find the position of this block in the block sequence pos := bl.canonicalChain.Back() @@ -604,7 +605,7 @@ func (bl *blockListener) rebuildCanonicalChain() *list.Element { notifyPos = newElem } - bl.checkAndSetHighestBlock(bi.Number.Uint64(), bi.GasLimit) + bl.checkAndSetHighestBlock(bi) } return notifyPos @@ -693,21 +694,27 @@ func (bl *blockListener) RemoveConsumer(_ context.Context, id *fftypes.UUID) { delete(bl.consumers, *id) } +func (bl *blockListener) waitForBlockHeightInit(ctx context.Context) bool { + bl.canonicalChainLock.RLock() + highestBlockSet := bl.highestBlockSet + bl.canonicalChainLock.RUnlock() + if highestBlockSet { + return true + } + select { + case <-bl.initialBlockHeightObtained: + return true + case <-ctx.Done(): + return false + } +} + func (bl *blockListener) GetHighestBlock(ctx context.Context) (uint64, bool) { bl.checkAndStartListenerLoop() // block height will be established as the first step of listener startup process // so we don't need to wait for the entire startup process to finish to return the result - bl.canonicalChainLock.RLock() - highestBlockSet := bl.highestBlockSet - bl.canonicalChainLock.RUnlock() - // if not yet initialized, wait to be initialized - if !highestBlockSet { - select { - case <-bl.initialBlockHeightObtained: - case <-ctx.Done(): - // Inform caller we timed out, or were closed - return 0, false - } + if !bl.waitForBlockHeightInit(ctx) { + return 0, false } bl.canonicalChainLock.RLock() highestBlock := bl.highestBlock @@ -716,11 +723,30 @@ func (bl *blockListener) GetHighestBlock(ctx context.Context) (uint64, bool) { return highestBlock, true } +func (bl *blockListener) GetHighestBlockInfo(ctx context.Context) (*ethrpc.BlockInfoJSONRPC, bool) { + bl.checkAndStartListenerLoop() + if !bl.waitForBlockHeightInit(ctx) { + return nil, false + } + bl.canonicalChainLock.RLock() + defer bl.canonicalChainLock.RUnlock() + if bl.headBlockInfo == nil { + return nil, false + } + return bl.headBlockInfo, true +} + // Gives a non-nil value only if the block listener is tracking the head and has access to the full block func (bl *blockListener) GetBlockGasLimit() *ethtypes.HexInteger { bl.canonicalChainLock.RLock() defer bl.canonicalChainLock.RUnlock() - return bl.highestBlockGasLimit + if bl.headBlockInfo == nil || bl.headBlockInfo.GasLimit == nil { + return nil + } + if bl.headBlockInfo.GasLimit.BigInt().Sign() <= 0 { + return nil + } + return bl.headBlockInfo.GasLimit } func (bl *blockListener) GetHeadBlockNumber(_ context.Context) uint64 { @@ -734,15 +760,20 @@ func (bl *blockListener) setHighestBlock(block uint64) { bl.highestBlockSet = true } +// checkAndSetHighestBlock records the chain head height and caches full block info for the head. +// highestBlock is often set first by eth_blockNumber during startup, before any full block arrives. // Caller MUST hold the canonicalChain WRITE LOCK -func (bl *blockListener) checkAndSetHighestBlock(block uint64, blockGasLimit *ethtypes.HexInteger) { +func (bl *blockListener) checkAndSetHighestBlock(bi *ethrpc.BlockInfoJSONRPC) { + block := bi.Number.Uint64() if block > bl.highestBlock { bl.highestBlock = block bl.highestBlockSet = true - if blockGasLimit != nil && blockGasLimit.BigInt().Sign() > 0 { - bl.highestBlockGasLimit = blockGasLimit - } + bl.headBlockInfo = bi + } else if block == bl.highestBlock { + // Height already known from eth_blockNumber. Store the first full block at that height. + bl.headBlockInfo = bi } + // Lower blocks are ignored. reconcileCanonicalChain also processes historical blocks during fork rebuilds. } // snapshot the whole view using the read-lock. diff --git a/pkg/ethblocklistener/blocklistener_test.go b/pkg/ethblocklistener/blocklistener_test.go index 05abb94..a100360 100644 --- a/pkg/ethblocklistener/blocklistener_test.go +++ b/pkg/ethblocklistener/blocklistener_test.go @@ -351,10 +351,11 @@ func TestBlockListenerOKSequential(t *testing.T) { // block1003 has GasLimit set — inline to capture the extra field mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByHash", block1003Hash.String(), false).Return(nil).Run(func(args mock.Arguments) { *args[1].(**ethrpc.EVMBlockWithTxHashesJSONRPC) = ðrpc.EVMBlockWithTxHashesJSONRPC{BlockHeaderJSONRPC: ethrpc.BlockHeaderJSONRPC{ - Number: 1003, - Hash: block1003Hash, - ParentHash: block1002Hash, - GasLimit: ethtypes.NewHexInteger64(10000), + Number: 1003, + Hash: block1003Hash, + ParentHash: block1002Hash, + GasLimit: ethtypes.NewHexInteger64(10000), + BaseFeePerGas: ethtypes.NewHexInteger64(7), }} }) }) @@ -382,6 +383,10 @@ func TestBlockListenerOKSequential(t *testing.T) { mRPC.AssertExpectations(t) assert.Len(t, bl.SnapshotMonitoredHeadChain(), bl.MonitoredHeadLength) require.Equal(t, int64(10000), bl.GetBlockGasLimit().Int64()) + headBlockInfo, ok := bl.GetHighestBlockInfo(context.Background()) + require.True(t, ok) + require.True(t, headBlockInfo.SupportsEIP1559()) + require.Equal(t, int64(10000), headBlockInfo.GasLimit.Int64()) } func TestBlockListenerWSShoulderTap(t *testing.T) { @@ -1336,3 +1341,105 @@ func TestWaitUntilStartedCancelledCtx(t *testing.T) { done() bl.waitUntilStarted(context.Background()) } + +func TestCheckAndSetHighestBlock(t *testing.T) { + _, bl, _, _ := newTestBlockListener(t) + + bi500 := ðrpc.BlockInfoJSONRPC{ + Number: 500, + GasLimit: ethtypes.NewHexInteger64(10000), + } + bl.canonicalChainLock.Lock() + bl.checkAndSetHighestBlock(bi500) + require.Equal(t, uint64(500), bl.highestBlock) + require.True(t, bl.highestBlockSet) + require.Same(t, bi500, bl.headBlockInfo) + + bi500EIP1559 := ðrpc.BlockInfoJSONRPC{ + Number: 500, + GasLimit: ethtypes.NewHexInteger64(20000), + BaseFeePerGas: ethtypes.NewHexInteger64(7), + } + bl.checkAndSetHighestBlock(bi500EIP1559) + require.Same(t, bi500EIP1559, bl.headBlockInfo) + + bl.checkAndSetHighestBlock(ðrpc.BlockInfoJSONRPC{Number: 499}) + require.Same(t, bi500EIP1559, bl.headBlockInfo) + bl.canonicalChainLock.Unlock() +} + +func TestGetBlockGasLimitFromHeadBlockInfo(t *testing.T) { + _, bl, _, _ := newTestBlockListener(t) + require.Nil(t, bl.GetBlockGasLimit()) + + bl.canonicalChainLock.Lock() + bl.headBlockInfo = ðrpc.BlockInfoJSONRPC{GasLimit: ethtypes.NewHexInteger64(0)} + bl.canonicalChainLock.Unlock() + require.Nil(t, bl.GetBlockGasLimit()) + + bl.canonicalChainLock.Lock() + bl.headBlockInfo = ðrpc.BlockInfoJSONRPC{} + bl.canonicalChainLock.Unlock() + require.Nil(t, bl.GetBlockGasLimit()) +} + +func TestGetHighestBlockInfoBeforeHeadBlockSeen(t *testing.T) { + _, bl, mRPC, done := newTestBlockListener(t) + + mockInitialBlockHeight(mRPC, 500) + mockSeedBlockNotFound(mRPC, 500-(50-1)).Maybe() + mockNewBlockFilter(mRPC, testBlockFilterID1).Maybe() + mockFilterChangesEmpty(mRPC).Maybe() + + _, ok := bl.GetHighestBlock(bl.ctx) + require.True(t, ok) + + headInfo, ok := bl.GetHighestBlockInfo(bl.ctx) + require.False(t, ok) + require.Nil(t, headInfo) + + done() +} + +func TestGetHighestBlockInfoReturnsHeadBlock(t *testing.T) { + _, bl, mRPC, done := newTestBlockListener(t) + + mockInitialBlockHeight(mRPC, 123) + mockSeedBlockNotFound(mRPC, 123-(50-1)).Maybe() + mockNewBlockFilter(mRPC, testBlockFilterID1).Maybe() + mockFilterChangesEmpty(mRPC).Maybe() + + bi := ðrpc.BlockInfoJSONRPC{ + Number: 123, + BaseFeePerGas: ethtypes.NewHexInteger64(1), + GasLimit: ethtypes.NewHexInteger64(5000), + } + bl.canonicalChainLock.Lock() + bl.headBlockInfo = bi + bl.canonicalChainLock.Unlock() + + headInfo, ok := bl.GetHighestBlockInfo(bl.ctx) + require.True(t, ok) + require.Same(t, bi, headInfo) + require.True(t, headInfo.SupportsEIP1559()) + require.Equal(t, int64(5000), bl.GetBlockGasLimit().Int64()) + + done() +} + +func TestGetHighestBlockInfoCancelledBeforeInit(t *testing.T) { + _, bl, mRPC, done := newTestBlockListener(t) + mockNewBlockFilter(mRPC, testBlockFilterID1).Maybe() + mockFilterChangesEmpty(mRPC).Maybe() + done() + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_blockNumber"). + Return(&rpcbackend.RPCError{Message: "pop"}) + + cancelledCtx, cancel := context.WithCancel(context.Background()) + cancel() + _, ok := bl.GetHighestBlockInfo(cancelledCtx) + require.False(t, ok) + + <-bl.listenLoopDone +} diff --git a/pkg/ethrpc/ethrpc.go b/pkg/ethrpc/ethrpc.go index 8056a8e..315f089 100644 --- a/pkg/ethrpc/ethrpc.go +++ b/pkg/ethrpc/ethrpc.go @@ -164,13 +164,14 @@ func (l *LogJSONRPC) MarshalFormat(jss *JSONSerializerSet, opts ...MarshalOption // BlockInfoJSONRPC are the info fields we parse from the JSON/RPC response, and cache type BlockInfoJSONRPC struct { - Number ethtypes.HexUint64 `json:"number" ffstruct:"BlockInfoJSONRPC"` - Hash ethtypes.HexBytes0xPrefix `json:"hash" ffstruct:"BlockInfoJSONRPC"` - ParentHash ethtypes.HexBytes0xPrefix `json:"parentHash" ffstruct:"BlockInfoJSONRPC"` - Timestamp ethtypes.HexUint64 `json:"timestamp" ffstruct:"BlockInfoJSONRPC"` - GasLimit *ethtypes.HexInteger `json:"gasLimit" ffstruct:"BlockInfoJSONRPC"` - LogsBloom ethtypes.HexBytes0xPrefix `json:"logsBloom" ffstruct:"BlockInfoJSONRPC"` - Transactions []ethtypes.HexBytes0xPrefix `json:"transactions" ffstruct:"BlockInfoJSONRPC"` + Number ethtypes.HexUint64 `json:"number" ffstruct:"BlockInfoJSONRPC"` + Hash ethtypes.HexBytes0xPrefix `json:"hash" ffstruct:"BlockInfoJSONRPC"` + ParentHash ethtypes.HexBytes0xPrefix `json:"parentHash" ffstruct:"BlockInfoJSONRPC"` + Timestamp ethtypes.HexUint64 `json:"timestamp" ffstruct:"BlockInfoJSONRPC"` + GasLimit *ethtypes.HexInteger `json:"gasLimit" ffstruct:"BlockInfoJSONRPC"` + BaseFeePerGas *ethtypes.HexInteger `json:"baseFeePerGas,omitempty" ffstruct:"BlockInfoJSONRPC"` + LogsBloom ethtypes.HexBytes0xPrefix `json:"logsBloom" ffstruct:"BlockInfoJSONRPC"` + Transactions []ethtypes.HexBytes0xPrefix `json:"transactions" ffstruct:"BlockInfoJSONRPC"` } func (bi *BlockInfoJSONRPC) MarshalFormat(jss *JSONSerializerSet, opts ...MarshalOption) (_ json.RawMessage, err error) { @@ -198,6 +199,11 @@ func (bi *BlockInfoJSONRPC) IsParentOf(other *BlockInfoJSONRPC) bool { return bi.Hash.Equals(other.ParentHash) && (bi.Number.Uint64()+1) == other.Number.Uint64() } +// SupportsEIP1559 returns true if the block includes baseFeePerGas (EIP-1559 / post-London). +func (bi *BlockInfoJSONRPC) SupportsEIP1559() bool { + return bi != nil && bi.BaseFeePerGas != nil +} + type MinimalBlockInfo struct { // duplicate of apitypes.Confirmation due to circular dependency BlockNumber fftypes.FFuint64 `json:"blockNumber"` BlockHash ethtypes.HexBytes0xPrefix `json:"blockHash"` @@ -274,11 +280,12 @@ func (b *BlockHeaderJSONRPC) getFormatMap() map[string]any { func (b *BlockHeaderJSONRPC) ToBlockInfo(includeLogsBloom bool) *BlockInfoJSONRPC { bi := &BlockInfoJSONRPC{ - Number: b.Number, - Hash: b.Hash, - ParentHash: b.ParentHash, - Timestamp: b.Timestamp, - GasLimit: b.GasLimit, + Number: b.Number, + Hash: b.Hash, + ParentHash: b.ParentHash, + Timestamp: b.Timestamp, + GasLimit: b.GasLimit, + BaseFeePerGas: b.BaseFeePerGas, } if includeLogsBloom { bi.LogsBloom = b.LogsBloom diff --git a/pkg/ethrpc/ethrpc_test.go b/pkg/ethrpc/ethrpc_test.go index 004eae1..91411c0 100644 --- a/pkg/ethrpc/ethrpc_test.go +++ b/pkg/ethrpc/ethrpc_test.go @@ -229,6 +229,8 @@ func TestFormatBlockFullWithHashes(t *testing.T) { require.NotNil(t, block.ToBlockInfo(true).ToMinimalBlockInfo()) require.True(t, block.ToBlockInfo(true).Equal(block.ToBlockInfo(true))) require.True(t, block.ToBlockInfo(true).ToMinimalBlockInfo().Equal(block.ToBlockInfo(true).ToMinimalBlockInfo())) + require.True(t, block.ToBlockInfo(true).SupportsEIP1559()) + require.Equal(t, block.BaseFeePerGas, block.ToBlockInfo(true).BaseFeePerGas) require.Nil(t, (*EVMBlockWithTxHashesJSONRPC)(nil).ToBlockInfo(true)) require.False(t, block.ToBlockInfo(true).ToMinimalBlockInfo().IsParentOf(block.ToBlockInfo(true).ToMinimalBlockInfo())) } @@ -259,6 +261,51 @@ func TestFormatBlockFullWithTxns(t *testing.T) { } +func TestBlockInfoSupportsEIP1559(t *testing.T) { + require.False(t, (*BlockInfoJSONRPC)(nil).SupportsEIP1559()) + require.False(t, (&BlockInfoJSONRPC{}).SupportsEIP1559()) + require.True(t, (&BlockInfoJSONRPC{BaseFeePerGas: ethtypes.NewHexInteger64(0)}).SupportsEIP1559()) +} + +func TestBlockHeaderToBlockInfo(t *testing.T) { + header := BlockHeaderJSONRPC{ + Number: 100, + Hash: ethtypes.MustNewHexBytes0xPrefix("0x0101010101010101010101010101010101010101010101010101010101010101"), + ParentHash: ethtypes.MustNewHexBytes0xPrefix("0x0202020202020202020202020202020202020202020202020202020202020202"), + Timestamp: 12345, + GasLimit: ethtypes.NewHexInteger64(30000000), + BaseFeePerGas: ethtypes.NewHexInteger64(7), + LogsBloom: ethtypes.MustNewHexBytes0xPrefix("0x000011112222"), + } + + withBloom := header.ToBlockInfo(true) + require.Equal(t, header.GasLimit, withBloom.GasLimit) + require.Equal(t, header.BaseFeePerGas, withBloom.BaseFeePerGas) + require.Equal(t, header.LogsBloom, withBloom.LogsBloom) + require.True(t, withBloom.SupportsEIP1559()) + + withoutBloom := header.ToBlockInfo(false) + require.Equal(t, header.BaseFeePerGas, withoutBloom.BaseFeePerGas) + require.Empty(t, withoutBloom.LogsBloom) + + legacyHeader := BlockHeaderJSONRPC{ + Number: 99, + GasLimit: ethtypes.NewHexInteger64(8000000), + } + legacyBlock := legacyHeader.ToBlockInfo(false) + require.Equal(t, legacyHeader.GasLimit, legacyBlock.GasLimit) + require.Nil(t, legacyBlock.BaseFeePerGas) + require.False(t, legacyBlock.SupportsEIP1559()) + + var fullBlock EVMBlockWithTransactionsJSONRPC + err := json.Unmarshal([]byte(sampleBlockHeadersOnly), &fullBlock) + require.NoError(t, err) + fullBlock.Transactions = []*TxInfoJSONRPC{} + blockInfo := fullBlock.ToBlockInfo(false) + require.Equal(t, fullBlock.GasLimit, blockInfo.GasLimit) + require.Equal(t, fullBlock.BaseFeePerGas, blockInfo.BaseFeePerGas) +} + func TestBlockInfoIsParent(t *testing.T) { bi1 := &BlockInfoJSONRPC{ Number: 1000,