From f4f78bf7547bbb6dd9f436b7495600a7f0aa086e Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 30 Apr 2026 21:14:41 -0400 Subject: [PATCH] Limit estimate gas to the current blockLimit Signed-off-by: Peter Broadhurst --- internal/ethereum/estimate_gas.go | 17 +++++++++-- internal/ethereum/estimate_gas_test.go | 30 ++++++++++++++++++- internal/msgs/en_error_messages.go | 1 + mocks/ethblocklistenermocks/block_listener.go | 20 +++++++++++++ pkg/ethblocklistener/blocklistener.go | 26 +++++++++++----- pkg/ethblocklistener/blocklistener_test.go | 3 ++ pkg/ethrpc/ethrpc.go | 2 ++ 7 files changed, 89 insertions(+), 10 deletions(-) diff --git a/internal/ethereum/estimate_gas.go b/internal/ethereum/estimate_gas.go index 8dac887..62c425b 100644 --- a/internal/ethereum/estimate_gas.go +++ b/internal/ethereum/estimate_gas.go @@ -90,6 +90,19 @@ func (c *ethConnector) gasEstimate(ctx context.Context, tx *ethsigner.Transactio // Multiply the gas estimate by the configured factor fGasEstimate := new(big.Float).SetInt(gasEstimate.BigInt()) _ = fGasEstimate.Mul(fGasEstimate, c.gasEstimationFactor) - _, _ = fGasEstimate.Int(gasEstimate.BigInt()) - return &gasEstimate, "", nil + scaledLimit, _ := fGasEstimate.Int(nil) + + // If the gas estimate is larger than the maximum gas limit on the block, then we fail at this + // early stage. Because otherwise we'll accept the transaction in, but fail to actually submit + // it to the mempool of the blockchain. It's odd that the default configuration of chains (Besu inc.) + // allow the gas estimate max to be so much higher than the block max - but they do. + if c.blockListener != nil { + gasLimit := c.blockListener.GetBlockGasLimit() + if gasLimit != nil && gasLimit.BigInt().Cmp(scaledLimit) < 0 { + return nil, "", i18n.NewError(ctx, msgs.MsgTransactionEstimateTooLargeForBlock, + scaledLimit.String(), c.gasEstimationFactor, gasEstimate.BigInt().String(), gasLimit.BigInt().String()) + } + } + + return (*ethtypes.HexInteger)(scaledLimit), "", nil } diff --git a/internal/ethereum/estimate_gas_test.go b/internal/ethereum/estimate_gas_test.go index fe5b413..a8552c6 100644 --- a/internal/ethereum/estimate_gas_test.go +++ b/internal/ethereum/estimate_gas_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-evmconnect/mocks/ethblocklistenermocks" "github.com/hyperledger/firefly-signer/pkg/abi" "github.com/hyperledger/firefly-signer/pkg/ethsigner" "github.com/hyperledger/firefly-signer/pkg/ethtypes" @@ -74,6 +75,34 @@ func TestGasEstimateOK(t *testing.T) { } +func TestGasEstimateAboveBlockLimit(t *testing.T) { + + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + mbl := ethblocklistenermocks.NewBlockListener(t) + mbl.On("GetBlockGasLimit").Return(ethtypes.NewHexInteger64(1000)) + mbl.On("WaitClosed").Return() + c.blockListener = mbl + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_estimateGas", + mock.MatchedBy(func(tx *ethsigner.Transaction) bool { + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + args[1].(*ethtypes.HexInteger).BigInt().SetString("12345", 10) + }) + + var req ffcapi.TransactionInput + err := json.Unmarshal([]byte(sampleGasEstimate), &req) + assert.NoError(t, err) + _, reason, err := c.GasEstimate(ctx, &req) + assert.Regexp(t, "FF23071", err) + assert.Empty(t, reason) + +} + func TestGasEstimateFail(t *testing.T) { ctx, c, mRPC, done := newTestConnector(t) @@ -285,4 +314,3 @@ func TestGasEstimateFailCustomErrorCannotParse(t *testing.T) { assert.Nil(t, res) } - diff --git a/internal/msgs/en_error_messages.go b/internal/msgs/en_error_messages.go index 73485fc..a495ff5 100644 --- a/internal/msgs/en_error_messages.go +++ b/internal/msgs/en_error_messages.go @@ -88,4 +88,5 @@ var ( MsgReturnedBlockHashMismatch = ffe("FF23068", "Returned block %d hash %s does not match requested hash %s") MsgInvalidChainTrackingMode = ffe("FF23069", "Invalid chain tracking mode '%s': must be 'light' or 'full'") MsgTransactionNotIncludedInChainHead = ffe("FF23070", "Transaction '%s' cannot be reconciled because chain head %d is before receipt block %s") + MsgTransactionEstimateTooLargeForBlock = ffe("FF23071", "Gas estimate %s (scaled at %.2f from estimate %s) too large for the current block gas limit %s") ) diff --git a/mocks/ethblocklistenermocks/block_listener.go b/mocks/ethblocklistenermocks/block_listener.go index df3c908..43fbd17 100644 --- a/mocks/ethblocklistenermocks/block_listener.go +++ b/mocks/ethblocklistenermocks/block_listener.go @@ -52,6 +52,26 @@ func (_m *BlockListener) GetBackend() rpcbackend.RPC { return r0 } +// GetBlockGasLimit provides a mock function with no fields +func (_m *BlockListener) GetBlockGasLimit() *ethtypes.HexInteger { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetBlockGasLimit") + } + + var r0 *ethtypes.HexInteger + if rf, ok := ret.Get(0).(func() *ethtypes.HexInteger); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ethtypes.HexInteger) + } + } + + return r0 +} + // GetBlockInfoByHash provides a mock function with given fields: ctx, hash0xString func (_m *BlockListener) GetBlockInfoByHash(ctx context.Context, hash0xString string) (*ethrpc.BlockInfoJSONRPC, error) { ret := _m.Called(ctx, hash0xString) diff --git a/pkg/ethblocklistener/blocklistener.go b/pkg/ethblocklistener/blocklistener.go index 3462aa3..f001e33 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) + 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) GetEVMBlockWithTxHashesByHash(ctx context.Context, hash0xString string) (b *ethrpc.EVMBlockWithTxHashesJSONRPC, err error) @@ -124,10 +125,11 @@ type blockListener struct { BlockListenerConfig // canonical chain - canonicalChainLock sync.RWMutex // covers highestBlock and canonicalChain - canonicalChain *list.List - highestBlockSet bool - highestBlock uint64 + canonicalChainLock sync.RWMutex // covers highestBlock and canonicalChain + canonicalChain *list.List + highestBlockSet bool + highestBlock uint64 + highestBlockGasLimit *ethtypes.HexInteger // headBlockNumber mode: last head value sent on the block listener channel (only written from listenLoop) currentChainHead uint64 @@ -424,7 +426,7 @@ func (bl *blockListener) reconcileCanonicalChain(bi *ethrpc.BlockInfoJSONRPC) *l bl.canonicalChainLock.Lock() defer bl.canonicalChainLock.Unlock() - bl.checkAndSetHighestBlock(bi.Number.Uint64()) + bl.checkAndSetHighestBlock(bi.Number.Uint64(), bi.GasLimit) // Find the position of this block in the block sequence pos := bl.canonicalChain.Back() @@ -553,7 +555,7 @@ func (bl *blockListener) rebuildCanonicalChain() *list.Element { notifyPos = newElem } - bl.checkAndSetHighestBlock(bi.Number.Uint64()) + bl.checkAndSetHighestBlock(bi.Number.Uint64(), bi.GasLimit) } return notifyPos @@ -665,6 +667,13 @@ func (bl *blockListener) GetHighestBlock(ctx context.Context) (uint64, bool) { return highestBlock, 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 +} + func (bl *blockListener) GetHeadBlockNumber(_ context.Context) uint64 { return bl.currentChainHead } @@ -677,10 +686,13 @@ func (bl *blockListener) setHighestBlock(block uint64) { } // Caller MUST hold the canonicalChain WRITE LOCK -func (bl *blockListener) checkAndSetHighestBlock(block uint64) { +func (bl *blockListener) checkAndSetHighestBlock(block uint64, blockGasLimit *ethtypes.HexInteger) { if block > bl.highestBlock { bl.highestBlock = block bl.highestBlockSet = true + if blockGasLimit != nil && blockGasLimit.BigInt().Sign() > 0 { + bl.highestBlockGasLimit = blockGasLimit + } } } diff --git a/pkg/ethblocklistener/blocklistener_test.go b/pkg/ethblocklistener/blocklistener_test.go index e7999a9..536106c 100644 --- a/pkg/ethblocklistener/blocklistener_test.go +++ b/pkg/ethblocklistener/blocklistener_test.go @@ -219,6 +219,7 @@ func TestBlockListenerOKSequential(t *testing.T) { Number: 1003, Hash: block1003Hash, ParentHash: block1002Hash, + GasLimit: ethtypes.NewHexInteger64(10000), }} }) }) @@ -253,6 +254,8 @@ func TestBlockListenerOKSequential(t *testing.T) { assert.Len(t, bl.SnapshotMonitoredHeadChain(), bl.MonitoredHeadLength) + require.Equal(t, int64(10000), bl.GetBlockGasLimit().Int64()) + } func TestBlockListenerWSShoulderTap(t *testing.T) { diff --git a/pkg/ethrpc/ethrpc.go b/pkg/ethrpc/ethrpc.go index d543939..8056a8e 100644 --- a/pkg/ethrpc/ethrpc.go +++ b/pkg/ethrpc/ethrpc.go @@ -168,6 +168,7 @@ type BlockInfoJSONRPC struct { 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"` } @@ -277,6 +278,7 @@ func (b *BlockHeaderJSONRPC) ToBlockInfo(includeLogsBloom bool) *BlockInfoJSONRP Hash: b.Hash, ParentHash: b.ParentHash, Timestamp: b.Timestamp, + GasLimit: b.GasLimit, } if includeLogsBloom { bi.LogsBloom = b.LogsBloom