Skip to content

feat(eth): EVM block-gas for EIP-1559 fee projection (takeover of #1585)#1586

Merged
pragmaxim merged 24 commits into
masterfrom
review/evm-1559-fields
Jul 1, 2026
Merged

feat(eth): EVM block-gas for EIP-1559 fee projection (takeover of #1585)#1586
pragmaxim merged 24 commits into
masterfrom
review/evm-1559-fields

Conversation

@pragmaxim

@pragmaxim pragmaxim commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

This builds on and takes over #1585 (@53gur0's feat/evm-1559-fields). The four original commits are preserved as-is; everything else here is review fixes and the decisions described below.

Built on the existing PR

A takeover of #1585. The original push-path work (block-level gas data on the subscribeNewBlock notification, plus its tests) is kept unchanged. The added commits address issues found in review, correct the on-chain fee tiers, and document the result.

What it delivers

EVM block-level gas data so wallets can project EIP-1559 base fees, via two paths:

  • PushsubscribeNewBlock now carries evmData { baseFeePerGas, blockGasUsed, blockGasLimit } taken from the just-connected block header. All three values come from one block, with no extra RPC; the wallet can apply the consensus base-fee formula to get the next block deterministically.
  • PullestimateFee keeps returning eip1559.baseFeePerGas, which (via eth_feeHistory) is already the projected base fee of the next block, and now returns corrected on-chain fee tiers (see below).

Companion Trezor Suite change

The user-facing "fees too high" fix lives in the wallet, on the companion branch:
https://github.com/trezor/trezor-suite/tree/fix/evm-fee

We went with Option B — the wallet owns the fee-buffer policy:

  • Suite computes maxFeePerGas = 2 × baseFee + tip itself, from the chain's base fee, instead of passing a provider's pre-padded suggestedMaxFeePerGas straight through. Infura pads the high tier to ~2.5× the base fee and Suite used maxFeePerGas verbatim — that was the "too high".
  • In EthereumFeeLevels the block-based estimate is preferred and falls back to the backend tiers when block data is unavailable, so nothing regresses.
  • Coverage today: evm-rpc-backed networks get the block-based fees via getBlock('latest'); blockbook-backed networks fall back to the backend (Infura) tiers until the subscribeNewBlock evmData from this PR is wired in as their block-data source.

docs/fees.md (added here) documents the full cross-repo flow.

Why setLatestBlockGas was removed

The original approach added a setLatestBlockGas step to EthereumTypeGetEip1559Fees, issuing an extra eth_getBlockByNumber("latest") to populate BlockGasUsed/BlockGasLimit. Two real problems and one minor one:

  • Data race — the alternative fee provider returns the same cached *Eip1559Fees pointer to every caller; setLatestBlockGas then mutated BlockGasUsed/BlockGasLimit on it without a lock, racing concurrent estimateFee goroutines (reproduced with -race).
  • Incoherent databaseFeePerGas (from eth_feeHistory, newest = pending) and gasUsed/gasLimit (from a separate latest block) described different blocks, so the trio could not form a correct single-block EIP-1559 projection.
  • Minor — extra RPC — it added an eth_getBlockByNumber("latest", false) per estimateFee. That call is cheap on its own (header only, usually served from the node's memory), so it is not a significant cost. The fair point is architectural rather than raw overhead: it also fired on the alternative-fee-provider path, which is deliberately served from an in-memory cache with no node RPC, so it quietly undid that optimization.

The deciding factor is that the data was not needed in the pull path at all: baseFeePerGas is already the next block's projected fee, and the push evmData carries per-block gas consistently. Fix: drop setLatestBlockGas — nothing is lost.

Corrected the on-chain fee tiers

On the on-chain estimate (coins with no alternative provider — ethereum non-archive and the ETH testnets), each tier's maxFeePerGas was set to the priority fee alone, omitting the base fee — i.e. below the block's base fee, so not mineable. Since Suite uses maxFeePerGas directly, these coins produced unusable EIP-1559 fees.

Now each tier is built from the eth_feeHistory response: maxPriorityFeePerGas = the tier's reward percentile (the real tip), and maxFeePerGas = eip1559BaseFeeMultiplier(2) × baseFee + tip — the EIP-1559-standard 2× buffer (survives ~6 full blocks of +12.5%/block growth), in a documented, tunable constant. This also drops the now-redundant eth_maxPriorityFeePerGas call, so the on-chain path makes one RPC (eth_feeHistory) instead of two. The Infura path is untouched.

Decision: no separate "next-block base fee" field in the pull path

We considered exposing the next-block base fee from the eth_feeHistory response already fetched (no extra RPC). Investigation (go-ethereum eth/gasprice/feehistory.go + live probes against the deployed backends) showed the extra projected element eth_feeHistory returns has backend-dependent semantics:

  • ethereum (Erigon, no distinct pending block): go-ethereum drops the extra element, so the existing baseFeePerGas is already the next block's projected fee.
  • arbitrum / optimism / base: the indices shift, so the extra element is N+1 while baseFeePerGas becomes the latest mined block.
  • polygon (real pending block): the extra element is an N+2 estimate computed off an incomplete pending block.

No single field can describe all of these consistently, so we deliberately did not add one. The reasoning is captured inline in ethrpc.go. Clients needing an exact next-block base fee should use the push evmData (the real previous-block header). The existing pull-side baseFeePerGas is left as-is; its own backend-dependent meaning is a pre-existing property, out of scope here.

Could it be richer?

The one genuinely-sound enrichment would be a server-computed next-block base fee on the push side — safe there because we have the exact header (deterministic), unlike the pull path:

export interface EthereumGasData {
    baseFeePerGas?: string;
    blockGasUsed?: string;
    blockGasLimit?: string;
    nextBaseFeePerGas?: string;  // computed from THIS block's header
}

But I'd lean against it:

  1. the client can derive it from the trio in ~2 lines;
  2. the formula's parameters are chain-specific — Ethereum uses denominator 8 / elasticity 2, some OP-stack L2s differ, and Arbitrum's base-fee mechanism is different altogether — so computing it server-side risks being subtly wrong across the EVM family. Shipping the raw trio lets each client apply the rule it trusts.

Fee observability (Grafana + metrics)

The dashboard had only two fee panels (UTXO estimated_fee and alternative_fee_provider_requests), both buried in General, and EVM EIP-1559 fees were invisible in metrics entirely. This adds a dedicated Fees section and the metrics behind it, aimed at troubleshooting the "fees too high" reports and at making both the pull and push paths observable.

Six new metrics (declared in configs/metrics.yaml, the single source of truth; the dashboard is generated from template.json + panels.yaml by render_grafana.py):

  • Pull patheth_eip1559_fee{tier,kind} (the per-tier maxFeePerGas / priority fee actually served) and eth_eip1559_base_fee (the base fee they are built on), in raw wei; plus eth_eip1559_fee_source_total{source} = provider / onchain_fallback / onchain, i.e. how often the served estimate bypassed the provider.
  • Provideralternative_fee_provider_last_sync_timestamp_seconds{provider}: cache age via time() - metric (a timestamp, not a computed age, so it keeps rising when a provider wedges). The leading indicator that explains a fallback.
  • Push patheth_block_gas_used_ratio (latest connected block's congestion, the driver of base-fee moves) and eth_block_base_fee (realized base fee), the latter overlaid against the pull-path projection.

All values are emitted in base units (wei / ratio), nil-guarded, and only on the relevant success paths; nil tiers create no empty series. The push gauges are set synchronously in OnNewBlock (invoked in monotonic height order) rather than in the per-block broadcast goroutines, so an older block can't clobber a newer value.

The Fees section orders the panels pull → provider-health → push → legacy and includes a buffer-ratio panel (maxFeePerGas / baseFee, with a dashed 2× reference) that separates a congested chain from estimator/provider over-buffering — the direct read on "fees too high". Every panel carries a self-contained description.

The design and the final diff were each run through an adversarial multi-lens review; the only changes it produced were wording-precision tweaks (already applied). It also surfaced one pre-existing, out-of-scope issue: the 1inch provider never sets staleSyncDuration, so its cache is treated as stale immediately — its series is dormant in current deployments, and the new fee_source / cache-age panels would now make it visible.

Other fixes

  • Wire naming — renamed the new-block field evm_dataevmData (it was the only snake_case key among the websocket types) before it ships, since changing a wire key after release is breaking.
  • OpenAPI / parity — documented the subscribeNewBlock payload (WsNewBlock / EthereumGasData, with evmData modeled as nullable in 3.1) and added them to the parity check. The parity typecheck (a CI gate) had been failing because the new Eip1559Fees fields were not mirrored in openapi.yaml; it passes again now.
  • Tests — added coverage for the block → evmData mapping (non-EVM, pre-London, post-London) and for the corrected on-chain tier computation (asserting maxFeePerGas > baseFee).
  • Docs — added docs/fees.md with diagrams of the full cross-repo fee flow (pull path, push path, Suite policy, end-to-end).

Testing

  • Unit: full suite green (725 tests across common/eth/server, including new coverage for every fee-metric emit helper); go vet clean (the only vet warnings are pre-existing in server/public_test.go, unrelated).
  • Integration: ethereum=main/rpc (incl. EstimateFee — which now exercises the corrected on-chain tiers — and GetBlock) and bsc=main/rpc/EstimateFee pass.
  • OpenAPI: lint:spec, generate, and parity typecheck all pass.
  • Grafana: render_grafana.py --check consistent (102 panels, 85 metrics); go.mod/go.sum untouched. grafana.json is the git-ignored rendered artifact.

Deployment / compatibility

This change is backward-compatible and can be deployed independently of, and before, the Suite release:

  • subscribeNewBlock gains evmData additively ({height, hash}{height, hash, evmData}); existing clients ignore the new field, and the deployed Suite reads only height/hash from the notification.
  • The estimateFee / Eip1559Fees response shape is unchanged vs master.

The only behavioral change for current clients is on the no-provider on-chain coins, where maxFeePerGas goes from unmineable to correct (a bugfix). The user-facing "too high" improvement lands only once the Suite branch ships; deploying blockbook first is safe and makes evmData available for it.

1inch provider tier remapping

1inch returns 4 fee tiers (low/medium/high/instant) with systematically tighter values per tier than Infura (3 tiers). The previous 1:1 mapping (suite low/medium/high ← 1inch low/medium/high) produced fees that were too conservative compared to Infura, causing ~2× fee differences between providers that made reliable cross-provider fee estimation impossible.

Fix: remap the tiers in the 1inch provider so the suite's low/medium/high are fed from comparable aggressiveness regardless of which provider serves the response:

Suite tier Infura source 1inch source (after remap)
low low medium
medium medium high
high high instant

1inch's low tier is discarded (too conservative to be useful). The instant field (not consumed by the suite) is left nil, consistent with Infura's behavior. The on-chain fallback path is unaffected — its percentile-based tiers (p20/p70/p90/p99) already produce correct per-tier values. The fee formula (maxFeePerGas = 2×baseFee + tip) is already handled on the suite side; this change ensures the input tier values are comparable.

Verified live against dev blockbooks via WebSocket:

# 1inch provider (eth-dev:9116)
echo '{"id":"1","method":"estimateFee","params":{"blocks":[1],"specific":{"from":"0x0000000000000000000000000000000000000000"}}}' | websocat -k wss://blockbook-dev.corp.sldev.cz:9116/websocket

# Infura provider for comparison (base-dev:9311)
echo '{"id":"1","method":"estimateFee","params":{"blocks":[1],"specific":{"from":"0x0000000000000000000000000000000000000000"}}}' | websocat -k wss://blockbook-dev3.corp.sldev.cz:9311/websocket

1inch response (commit 66d7aff deployed — tier remapping active):

Suite tier Mapped from 1inch baseFeePerGas maxFeePerGas maxPriorityFeePerGas Ratio to base
low medium 84,922,820 102,094,050 186,666 1.20×
normal high 84,922,820 102,374,049 466,665 1.21×
high instant 84,922,820 204,748,098 933,330 2.41×

Infura response for comparison:

Suite tier Source baseFeePerGas maxFeePerGas maxPriorityFeePerGas Ratio to base
low low 5,000,000 11,100,000 1,100,000 2.22×
normal medium 5,000,000 24,000,000 4,000,000 4.80×
high high 5,000,000 55,000,000 5,000,000 11.0×

Absolute values differ (different chains/networks), but the remapping ensures each suite tier receives a meaningful priority fee — without it, the suite's low would have been populated from 1inch's low (~100K wei priority fee, too conservative to be reliably mineable).

53gur0 and others added 11 commits June 23, 2026 18:43
EthereumTypeGetEip1559Fees called setLatestBlockGas on every request,
which issued an extra synchronous eth_getBlockByNumber("latest") RPC to
populate Eip1559Fees.BlockGasUsed/BlockGasLimit. This had three problems:

- Extra overhead: a third blocking RPC was added to every websocket
  estimateFee call (on top of eth_maxPriorityFeePerGas + eth_feeHistory),
  and it also fired on the alternative-fee-provider fast path that was
  deliberately RPC-free (served from an in-memory cache). On fast L2s with
  many subscribers polling fees this scales linearly with load.

- Data race: the alternative provider returns the same cached *Eip1559Fees
  pointer to every caller; setLatestBlockGas then mutated BlockGasUsed/
  BlockGasLimit on that shared struct with no lock, racing concurrent
  estimateFee goroutines (and the field reads in server/websocket.go).

- Incoherent data: baseFeePerGas came from eth_feeHistory (newest=pending)
  or an off-chain oracle, while gasUsed/gasLimit came from a separate
  "latest" block, so the three values described different blocks and could
  not be combined into a correct EIP-1559 next-base-fee projection.

Remove getLatestBlockGas/setLatestBlockGas and the now-unpopulated
BlockGasUsed/BlockGasLimit fields from the pull-side Eip1559Fees (bchain,
api, blockbook-api.ts) and the websocket mapping. The push path
(subscribeNewBlock evm_data) is unaffected: it still carries the block's
baseFeePerGas/gasUsed/gasLimit from a single header, with no extra RPC.

A follow-up commit exposes the next-block base fee from the eth_feeHistory
response already fetched, so the pull path keeps a (consistent, free)
projection input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The new-block notification payload used a snake_case json tag (evm_data),
the only such tag among the websocket/JSON wire types, which otherwise use
lowerCamelCase (height, hash, baseFeePerGas, feePerUnit, ...). Rename it to
evmData for consistency before the field ships, since changing a wire-level
key after release would be a breaking change.

Updates the Go json tag, the JSON shape test, blockbook-api.ts and a code
comment. The field is documented in openapi.yaml in the next commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The subscribeNewBlock notification grew an evmData field (WsNewBlock /
EthereumGasData) that was undocumented in openapi.yaml. Because these
types are not in the parity allow-list they did not break CI, but the new
websocket payload was unspecified.

Add EthereumGasData and WsNewBlock schemas (evmData modeled as nullable via
the 3.1 oneOf/"null" pattern, matching the Go *EthereumGasData json tag
without omitempty), and add both to the parity check so the generated
blockbook-api.ts and openapi.yaml stay in sync. Parity typecheck passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The push path (subscribeNewBlock -> evmData) is the consistent, RPC-free
delivery mechanism for the EIP-1559 projection inputs, but only its JSON
wire shape was tested, not the block -> payload mapping.

Extract that mapping out of onNewBlockAsync into a pure newBlockNotification
helper (behavior unchanged) and unit-test it: non-EVM blocks and EVM
pre-London blocks leave evmData nil, while EVM post-London blocks carry
baseFeePerGas/gasUsed/gasLimit taken from the connected block header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WsNewBlock's height, hash and evmData have no omitempty in Go, so they are
always present on the wire (evmData serializes as null for non-EVM/pre-London
blocks). Mark them required in the schema so the spec reflects the actual
always-present (evmData nullable) shape. Parity typecheck still passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e fee

Records the conclusion of the fee-history investigation inline: eth_feeHistory's
extra projected element has backend-dependent semantics (dropped by no-pending
nodes like Erigon so baseFeePerGas is already the next-block fee; kept but
index-shifted to N+1 or a noisy N+2 by some L2s), so a single "next block" field
cannot be defined consistently. Clients needing an exact next-block base fee use
the subscribeNewBlock evmData push instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The on-chain estimate (used when no alternative fee provider is configured —
e.g. ethereum non-archive and the ethereum testnets) set each tier's
maxFeePerGas to the priority fee alone, omitting the base fee entirely. That
value is below the block's base fee and therefore not mineable. Trezor Suite
consumes maxFeePerGas directly (connect EthereumFeeLevels: it only clamps to a
floor, it does not add the base fee), so the on-chain path produced unusable
EIP-1559 fees.

Compute each tier correctly from the eth_feeHistory response:
- maxPriorityFeePerGas = the tier's reward percentile (20/70/90/99), i.e. the
  actual tip distribution, instead of the node's single suggestion doubled.
- maxFeePerGas = eip1559BaseFeeMultiplier*baseFee + tip, with the multiplier (2)
  documented as the EIP-1559-standard buffer (survives ~6 full blocks of
  +12.5%/block base-fee growth). Tunable via the named constant.

This also drops the now-redundant eth_maxPriorityFeePerGas call (Suite floors
the tip with coinInfo.minPriorityFee), so the on-chain path makes one RPC
(eth_feeHistory) instead of two. Adds a unit test asserting the per-tier values
and the maxFeePerGas > baseFee invariant.

Note: this does not touch the alternative-fee (Infura) path, where the
"fees too high" reports originate — that is a fee-policy decision pending
coordination with the Suite team.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pragmaxim pragmaxim force-pushed the review/evm-1559-fields branch from 9aadb76 to 461f9f0 Compare June 24, 2026 09:09
Document, with mermaid diagrams, how EIP-1559 fees are produced across Blockbook
and Trezor Suite and which component owns which decision: blockbook provides
ground-truth inputs (chain base fee, priority-fee tiers, congestion/trend, block
gas), while the wallet owns the fee policy (base-fee source, 2x head-room buffer,
maxFeePerGas composition, per-coin clamps). Covers the pull path
(EthereumTypeGetEip1559Fees), the push path (subscribeNewBlock evmData), Suite's
EthereumFeeLevels, the end-to-end flow, and the maxFeePerGas formula.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pragmaxim pragmaxim force-pushed the review/evm-1559-fields branch from 461f9f0 to 9c93cf2 Compare June 24, 2026 09:33
@pragmaxim pragmaxim requested a review from cranycrane June 24, 2026 09:40
pragmaxim and others added 7 commits June 24, 2026 12:53
Move the two existing fee panels (estimated fee rate, alternative fee
provider requests) out of General into a new top-level "Fees" section,
so fee observability has one home as EVM EIP-1559 fee metrics are added.

Pure relocation: panels, queries and the metrics they read are unchanged;
only their x-panel-keys move general.* -> fees.* and a row.fees section is
introduced. grafana.json is the git-ignored artifact rendered from these
two sources by contrib/scripts/render_grafana.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EVM EIP-1559 fees were invisible in metrics (blockbook_estimated_fee is
UTXO-only). Add two gauges populated on the successful estimateFee /
EthereumTypeGetEip1559Fees paths (provider cache hit and on-chain estimate):

- blockbook_eth_eip1559_fee{tier,kind}: per-tier maxFeePerGas / priority fee
- blockbook_eth_eip1559_base_fee: the next-block base fee underlying them

Values are raw wei (base units, like estimated_fee); Grafana divides by 1e9
to show Gwei. Emitted only on the two successful returns so error/disabled
paths never write zeros, and nil tiers are skipped so no empty series appear.

Dashboard (Fees section): maxFeePerGas by tier, priority tip by tier (own
axis - tips are ~1000x smaller), and the maxFee/baseFee buffer ratio with a
dashed 2x reference, which separates a congested chain from an estimator
regression - the "fees too high" diagnostic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add blockbook_eth_eip1559_fee_source_total{source} incremented on each served
estimate at the serve boundary: provider (alternative provider cache hit),
onchain_fallback (provider configured but cache stale/unready, so eth_feeHistory
was used) or onchain (no provider configured).

This is the one signal neither alternative_fee_provider_requests (background
fetch outcome) nor the provider cache-age gauge captures: how often the wallet-
facing estimate actually bypassed the provider. Emitted only on the two
successful returns, alongside the fee gauges.

Dashboard: a non-stacked "EIP-1559 fee source" panel so a provider->fallback
transition shows as a visible crossover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add blockbook_eth..._provider_last_sync_timestamp_seconds{provider}, the unix
time of the last successful refresh of the cached EVM provider (infura/1inch)
fees, set via a shared observeSync() called from both providers' processData
(replacing the bare p.lastSync = time.Now()) so lastSync and the metric always
share one instant.

Cache age is plotted as time() - metric. A timestamp gauge, not a computed
*_age_seconds one (the repo's usual form): it is written only on a successful
refresh, so the plotted age keeps climbing when a provider wedges and survives
restarts. This is the leading indicator that explains the onchain_fallback
counted by eth_eip1559_fee_source_total - once age crosses the stale window
(30x the poll period) the pull path falls back to on-chain.

Dashboard: a cache-age panel with a dashed default-cutoff (30m) reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add blockbook_eth_block_gas_used_ratio, gasUsed/gasLimit of the most recently
connected EVM block (0..1) - the congestion signal that drives the next base
fee (>0.5 it rises up to +12.5%/block, <0.5 it falls). This is the push path's
own view of why fees move, independent of any provider.

Set synchronously in OnNewBlock before the async broadcast: OnNewBlock is
invoked in monotonic height order by the single writeBlockWorker, whereas the
per-block broadcast goroutines can reorder and let an older block clobber a
newer gauge value. Last-value semantics, nil-guarded for non-EVM/pre-London.

Dashboard: a "Latest block gas-used ratio" panel (percentunit) with a dashed
50% balance-point reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…verlay

Add blockbook_eth_block_base_fee, the realized base fee of the most recently
connected block (push path), in raw wei. Set in the same OnNewBlock helper as
the gas-used ratio, nil-guarded.

Paired with eth_eip1559_base_fee (the pull path's next-block projection) on one
overlay panel - solid mined vs dashed projected, both in Gwei. The two should
track within a block's +/-12.5% step; a persistent gap means the feeHistory
projection (or a provider's estimatedBaseFee) is drifting from the chain - the
base-fee half of a "fees too high" investigation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documentation-only follow-up from an adversarial review of the new Fees
section (no functional change, no Go change):

- order the section pull-estimate -> provider-health -> push -> legacy, and
  label the relocated UTXO "Estimated fee rate" so an EVM $coin selection
  reads as expected-empty rather than an outage
- buffer-ratio: note Infura's high tier sits ~2.5x by design, so the dashed
  2x line is the on-chain estimator's target only
- base-fee overlay & cache-age: scope the "tracks within +/-12.5%" and "30x
  poll period" expectations to the path/provider they actually hold for
  (provider coins serve a cached projection up to the stale window old)
- eth_eip1559_base_fee help: drop the unconditional "next-block projection"
  claim (true only on Erigon-like backends) and Infura's estimatedBaseFee
  field name (1inch feeds the same gauge from baseFee)
- priority-tip: "separate panel", not "own axis"

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cranycrane

Copy link
Copy Markdown
Contributor

Review notes

Went through the diff carefully. The PR is in good shape — most of the things I initially suspected turned out to be non-issues (see bottom). Two points worth a look, one actionable.

1. On-chain priority fee now comes solely from eth_feeHistory rewards — tip can be 0 on idle chains

bchain/coins/eth/ethrpc.go:1999-2007

Dropping eth_maxPriorityFeePerGas means the per-tier tip is now the average of the reward percentile over the window. On chains/blocks where the backend returns an empty reward array or all-zero percentiles (idle testnets like Sepolia/Holesky, or a backend that omits rewards), priorityFee stays 0 for every tier, so maxPriorityFeePerGas = 0. maxFeePerGas still covers 2×baseFee so the tx stays mineable, but the served tip drops to zero where the old node-suggested minimum wasn't. This is the documented consequence of removing the RPC — just flagging that the zero-tip case on low-activity networks should be a conscious accept.

2. Unchecked h.Reward[j][i] can panic on a non-compliant backend

bchain/coins/eth/ethrpc.go:2000

p, _ := hexutil.DecodeUint64(h.Reward[j][i])

This indexes columns 0–3 assuming every reward row has the 4 entries matching the requested percentiles. A backend returning a row shorter than 4 elements panics the EthereumTypeGetEip1559Fees request. Pre-existing, but the function is reworked here and the new len(h.Reward) > 0 guard just below shows the intent to harden this block — a len(h.Reward[j]) > i check would close the remaining hole cheaply.


Checked and cleared (not bugs)

  • No data race on the cached feesGetEip1559Fees returns the pointer under p.mux, and a refresh swaps in a new struct (p.eip1559Fees = &fees) rather than mutating the live one, so observeEip1559Fees reads a stable snapshot. This is exactly the property the setLatestBlockGas removal restores.
  • observeNewBlockGas GasLimit.Sign() > 0 is correct div-by-zero protection, not a missed-metric case (real blocks never have limit 0).
  • bigIntToFloat "duplication" in websocket.go is a cross-package inline (server vs. eth) and not trivially shareable.

@cranycrane cranycrane left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Otherwise LGTM

Bounds-check the per-tier column index so a non-compliant backend
returning a reward row shorter than the requested percentile count is
skipped instead of panicking EthereumTypeGetEip1559Fees. Divide the tip
average by the rows that actually contributed, so a skipped short row
does not deflate the tier. Document the accepted zero-tip case on idle
chains, and add a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pragmaxim

Copy link
Copy Markdown
Contributor Author

Fixed in 17c494d — bounds-guarded the reward column index (short rows skipped, divisor counts only contributing rows) and documented the zero-tip-on-idle-chains accept. Added a regression test.

The 1inch alternative fee provider never set staleSyncDuration, so it
defaulted to the zero value (0s), making the cache immediately stale
after every write. GetEip1559Fees always fell back to on-chain estimation
via eth_feeHistory, defeating the purpose of having a provider cache.

Mirror the same pattern used by the Infura provider: introduce
oneInchFeeStalePeriods=30 and oneInchFeeStaleDuration, and set
p.staleSyncDuration in the constructor so cached fees are reused within
the 30×pollPeriod window (e.g. 30min for a 60s poll).
Replace the Infura gas API with 1inch Gas Price API for Ethereum mainnet archive.
The 1inch API returns raw wei integers and uses Bearer token auth (ONE_INCH_API_KEY).
The staleSyncDuration bug that made 1inch cache always immediately stale was fixed in the preceding commit.
Grafana:
- alt_fee_provider_requests tooltip mode: single -> multi (inconsistent with other fee panels)
- eip1559_max_fee description: generalize Infura-only language to mention both providers and
  clarify instant tier behavior differs per provider
- eip1559_buffer_ratio description: generalize Infura-only padding reference
- alt_fee_provider_cache_age description: generalize "Infura (the EVM default)" to mention both
  providers use the same 30x stale window

docs/fees.md:
- Replace Infura-only references with provider-agnostic language throughout
- Keep accurate historical context (Infura as original source of fees-too-high reports)
- Update mermaid flowcharts from "Infura fees" to "provider fees"
- Document both Infura and 1inch behavior in the provider section
1inch returns 4 tiers (low/medium/high/instant) with tighter per-tier values than
Infura (3 tiers). The previous 1:1 mapping (suite low/medium/high ← 1inch
low/medium/high) produced fees that were too conservative compared to Infura,
causing ~2x fee differences between providers.

Remap so the suite's low/medium/high tiers are fed from comparable
aggressiveness:
  1inch medium → suite low
  1inch high   → suite medium
  1inch instant → suite high
1inch's low tier is discarded (too conservative to be useful).

The fee formula (maxFeePerGas = 2×baseFee + tip) is already correct on the
suite side (fix/evm-fee branch) — this change ensures the input tier values are
comparable regardless of which provider serves the response.
@pragmaxim pragmaxim merged commit a2ec3b3 into master Jul 1, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants