Skip to content

types: remove heap allocations from GetEffectiveGasTip#20808

Open
Sahil-4555 wants to merge 2 commits intoerigontech:mainfrom
Sahil-4555:optimize-effective-gastip
Open

types: remove heap allocations from GetEffectiveGasTip#20808
Sahil-4555 wants to merge 2 commits intoerigontech:mainfrom
Sahil-4555:optimize-effective-gastip

Conversation

@Sahil-4555
Copy link
Copy Markdown
Contributor

@Sahil-4555 Sahil-4555 commented Apr 25, 2026

This PR optimizes the GetEffectiveGasTip calculation to drastically reduce heap allocations during block assembly and RPC fee processing loops.

Previously, GetEffectiveGasTip returned a pointer (*uint256.Int), forcing Go's escape analysis to allocate dynamically computed effective tips onto the heap for every single transaction. This was a massive garbage collection bottleneck in loops processing hundreds of transactions per block.

Instead of patching callers with repetitive inline math logic, this PR fixes the root cause by updating the core Transaction interface to return uint256.Int by value.

Because uint256.Int is a fixed 32-byte struct, passing it by value is extremely cheap and completely bypasses the heap. For most callers, this flatlines loop allocations to exactly zero. For callers that must persist the result (like gasprice.go), allocations are drastically reduced from O(N) (every transaction) to O(K) (only the kept items).

goos: linux
goarch: amd64
pkg: github.com/erigontech/erigon/rpc/jsonrpc
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
                          │ old_bench_1.txt │           new_bench_1.txt           │
                          │     sec/op      │   sec/op     vs base                │
DelegateBlockFees/txs=1         334.8n ± 3%   137.4n ± 2%  -58.95% (p=0.000 n=10)
DelegateBlockFees/txs=50        9.698µ ± 7%   1.363µ ± 4%  -85.95% (p=0.000 n=10)
DelegateBlockFees/txs=200      37.105µ ± 5%   5.626µ ± 5%  -84.84% (p=0.000 n=10)
geomean                         4.939µ        1.018µ       -79.40%

                          │ old_bench_1.txt │          new_bench_1.txt           │
                          │      B/op       │    B/op     vs base                │
DelegateBlockFees/txs=1         232.00 ± 0%   96.00 ± 0%  -58.62% (p=0.000 n=10)
DelegateBlockFees/txs=50       6552.00 ± 0%   96.00 ± 0%  -98.53% (p=0.000 n=10)
DelegateBlockFees/txs=200     25752.00 ± 0%   96.00 ± 0%  -99.63% (p=0.000 n=10)
geomean                        3.316Ki        96.00       -97.17%

                          │ old_bench_1.txt │          new_bench_1.txt           │
                          │    allocs/op    │ allocs/op   vs base                │
DelegateBlockFees/txs=1          9.000 ± 0%   3.000 ± 0%  -66.67% (p=0.000 n=10)
DelegateBlockFees/txs=50       206.000 ± 0%   3.000 ± 0%  -98.54% (p=0.000 n=10)
DelegateBlockFees/txs=200      806.000 ± 0%   3.000 ± 0%  -99.63% (p=0.000 n=10)
geomean                          114.3        3.000       -97.38%
goos: linux
goarch: amd64
pkg: github.com/erigontech/erigon/execution/execmodule
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
                  │ old_bench.txt │            new_bench.txt             │
                  │    sec/op     │    sec/op     vs base                │
TotalFees/txs=1       68.93n ± 8%   65.36n ± 23%        ~ (p=0.853 n=10)
TotalFees/txs=50      2.075µ ± 7%   1.196µ ±  3%  -42.35% (p=0.000 n=10)
TotalFees/txs=200     8.668µ ± 6%   4.775µ ±  4%  -44.92% (p=0.000 n=10)
geomean               1.074µ        720.0n        -32.98%

                  │ old_bench.txt │           new_bench.txt            │
                  │     B/op      │    B/op     vs base                │
TotalFees/txs=1        64.00 ± 0%   32.00 ± 0%  -50.00% (p=0.000 n=10)
TotalFees/txs=50     1632.00 ± 0%   32.00 ± 0%  -98.04% (p=0.000 n=10)
TotalFees/txs=200    6432.00 ± 0%   32.00 ± 0%  -99.50% (p=0.000 n=10)
geomean                875.8        32.00       -96.35%

                  │ old_bench.txt │           new_bench.txt            │
                  │   allocs/op   │ allocs/op   vs base                │
TotalFees/txs=1        2.000 ± 0%   1.000 ± 0%  -50.00% (p=0.000 n=10)
TotalFees/txs=50      51.000 ± 0%   1.000 ± 0%  -98.04% (p=0.000 n=10)
TotalFees/txs=200    201.000 ± 0%   1.000 ± 0%  -99.50% (p=0.000 n=10)
geomean                27.37        1.000       -96.35%

@Sahil-4555 Sahil-4555 force-pushed the optimize-effective-gastip branch from 453e746 to dd83d25 Compare April 25, 2026 08:42
@Sahil-4555
Copy link
Copy Markdown
Contributor Author

Block Building Benchmark

package execmodule

import (
	"fmt"
	"testing"

	"github.com/holiman/uint256"

	"github.com/erigontech/erigon/execution/types"
)

// buildTotalFeesFixture creates a block with `n` dynamic-fee transactions
// and corresponding receipts.
func buildTotalFeesFixture(n int) (*types.BlockWithReceipts, *uint256.Int) {
	baseFee := uint256.NewInt(1_000_000_000) // 1 gwei

	txs := make([]types.Transaction, n)
	receipts := make(types.Receipts, n)
	for i := 0; i < n; i++ {
		gasFeeCap := uint256.NewInt(3_000_000_000)
		gasTipCap := uint256.NewInt(2_000_000_000)

		tx := &types.DynamicFeeTransaction{
			CommonTx: types.CommonTx{
				Nonce:    uint64(i),
				GasLimit: 21000,
			},
			ChainID: *uint256.NewInt(1),
			FeeCap:  *gasFeeCap,
			TipCap:  *gasTipCap,
		}

		txs[i] = tx
		receipts[i] = &types.Receipt{GasUsed: 21000}
	}

	header := &types.Header{
		Number:   *uint256.NewInt(1),
		GasLimit: 30_000_000,
		BaseFee:  baseFee,
	}

	block := types.NewBlock(header, txs, nil, receipts, nil)
	return &types.BlockWithReceipts{
		Block:    block,
		Receipts: receipts,
	}, baseFee
}

func BenchmarkTotalFees(b *testing.B) {
	for _, n := range []int{1, 50, 200} {
		br, baseFee := buildTotalFeesFixture(n)
		b.Run(fmt.Sprintf("txs=%d", n), func(b *testing.B) {
			b.ReportAllocs()
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_ = blockValue(br, baseFee)
			}
		})
	}
}

@Sahil-4555
Copy link
Copy Markdown
Contributor Author

Otter Scan API Bench

package jsonrpc

import (
	"context"
	"fmt"
	"testing"

	"github.com/holiman/uint256"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/execution/chain"
	"github.com/erigontech/erigon/execution/types"
)

func buildDelegateBlockFeesFixture(n int) (*types.Block, []common.Address, *chain.Config, types.Receipts) {
	baseFee := uint256.NewInt(1_000_000_000)

	txs := make([]types.Transaction, n)
	receipts := make(types.Receipts, n)
	senders := make([]common.Address, n)
	for i := 0; i < n; i++ {
		gasFeeCap := uint256.NewInt(3_000_000_000)
		gasTipCap := uint256.NewInt(2_000_000_000)

		tx := &types.DynamicFeeTransaction{
			CommonTx: types.CommonTx{
				Nonce:    uint64(i),
				GasLimit: 21000,
			},
			ChainID: *uint256.NewInt(1),
			FeeCap:  *gasFeeCap,
			TipCap:  *gasTipCap,
		}

		txs[i] = tx
		receipts[i] = &types.Receipt{GasUsed: 21000, TransactionIndex: uint(i)}
	}

	header := &types.Header{
		Number:   *uint256.NewInt(1),
		GasLimit: 30_000_000,
		BaseFee:  baseFee,
	}

	block := types.NewBlock(header, txs, nil, receipts, nil)
	zero := uint64(0)
	chainConfig := &chain.Config{LondonBlock: &zero}

	return block, senders, chainConfig, receipts
}

func BenchmarkDelegateBlockFees(b *testing.B) {
	ctx := context.Background()
	for _, n := range []int{1, 50, 200} {
		block, senders, chainConfig, receipts := buildDelegateBlockFeesFixture(n)
		b.Run(fmt.Sprintf("txs=%d", n), func(b *testing.B) {
			b.ReportAllocs()
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				_, _ = delegateBlockFees(ctx, nil, block, senders, chainConfig, receipts)
			}
		})
	}
}

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes EIP-1559 effective-tip calculations by changing types.Transaction.GetEffectiveGasTip to return a uint256.Int by value, avoiding per-call heap allocations in hot loops (block assembly, gas price oracle, receipt marshaling).

Changes:

  • Updated Transaction interface and CalcEffectiveGasTip to return uint256.Int by value (no escaping pointers).
  • Refactored key call sites (RPC, execmodule, AA gas, tracing/tests) to use the new value-returning API and reduce intermediate allocations.
  • Adjusted gas price oracle sorting/precomputation to store tips by value and only allocate when returning pointer slices.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
rpc/jsonrpc/otterscan_api.go Switch block fee aggregation to uint256.Int temporaries and value-based effective tip usage.
rpc/gasprice/gasprice.go Store precomputed tips by value; update sort comparator and output append logic.
execution/types/transaction.go Change GetEffectiveGasTip to return by value; refactor CalcEffectiveGasTip to avoid heap allocations.
execution/types/legacy_tx.go Update legacy tx implementation to match new GetEffectiveGasTip signature.
execution/types/ethutils/receipt.go Adjust effective gas price marshaling to use value-returning effective tip.
execution/types/dynamic_fee_tx.go Update dynamic fee tx implementation to match new GetEffectiveGasTip signature.
execution/types/blob_tx_wrapper.go Update wrapper to forward new value-returning GetEffectiveGasTip.
execution/types/aa_transaction.go Update AA tx implementation to match new GetEffectiveGasTip signature.
execution/tracing/tracers/internal/tracetest/calltrace_test.go Update tracer benchmark to use value-returning GetEffectiveGasTip.
execution/tests/blockchain_test.go Update test to handle value return when reading Uint64().
execution/protocol/aa/aa_gas.go Update AA gas accounting to add baseFee + effectiveTip using value return.
execution/execmodule/block_building.go Rework block value calculation to reuse uint256.Int temporaries and value-returning tips.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread execution/execmodule/block_building.go Outdated
Comment thread execution/types/transaction.go
Copy link
Copy Markdown
Member

@yperbasis yperbasis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "zero allocations" claim is overstated for gasprice.go

In rpc/gasprice/gasprice.go:323, the new code adds:
tipCopy := new(uint256.Int).Set(&item.tip)
*out = append(*out, tipCopy)
This is a heap allocation per kept item. It's better than before (only items passing the coinbase filter allocate, vs. all of them), but not zero. Update the PR description to clarify "drastically reduced" rather than "flatlines to zero" for this caller.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants