Skip to content

fix(ethmonitor): handle Avalanche settled headers (ACP-194) and stop failing on a full publish queue - #221

Merged
py-zoid merged 7 commits into
masterfrom
fix/ethmonitor-helicon-settled-blooms
Oct 22, 2026
Merged

py-zoid merged 7 commits into
masterfrom
fix/ethmonitor-helicon-settled-blooms

Conversation

@py-zoid

@py-zoid py-zoid commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Two related changes to ethmonitor. They solve different problems and are split into separate commits.

1. Handle Avalanche settled block headers (ACP-194 / Helicon)

Since Avalanche's Helicon upgrade (Continuous Execution; Fuji block 57403047, C-Chain mainnet block 95921105), a block is accepted before it is executed. Each header carries a new settledHeight field, and its logsBloom / receiptsRoot cover the blocks it settles ((parent.settledHeight, settledHeight], about 5s behind), not the block's own logs. eth_getLogs still returns the block's own logs, and does not wait for execution.

ethmonitor treated "bloom set but eth_getLogs returned []" as an untrustworthy response and retried the block forever. The block blocked the publish queue until it overflowed into ErrFatal(ErrQueueFull), so monitors on Avalanche C-Chain and Fuji crash repeatedly. The opposite case was a silent data-loss path: a header that settles nothing has a zero bloom, so a premature [] for a block that does have logs was accepted.

  • Settled headers are detected from the raw block JSON (settledHeight present and non-null). No chain-ID list is needed.
  • On those chains, the header bloom is no longer used to validate logs. An empty eth_getLogs result for a block with transactions is checked against the block's receipts, which wait for execution, and the receipt logs are used when they disagree. An unconfirmed [] is never accepted or cached.
  • Receipts come from eth_getBlockReceipts. If the node answers that the method doesn't exist, at any HTTP status, the monitor falls back to eth_getTransactionReceipt in bounded batches of 20, plus eth_getBlockTransactionCountByHash, so transactions ethrpc can't decode can't silently lose logs. Other receipts errors, including rate limits, are retried.

Chains without settledHeight are untouched by this part.

2. Stop failing on a full publish queue (all chains)

Independent of Avalanche: on any chain, a block whose logs keep failing (for example a provider that keeps erroring on eth_getLogs) held the queue until ErrFatal(ErrQueueFull).

  • After MaxLogsAttempts failed fetches (default 10), the block's logs are taken from its receipts. On chains without settled headers, empty receipts for a block whose bloom is set are rejected, which keeps the original bloom guard.
  • A block still unresolved when it leaves the chain cache (BlockRetentionLimit blocks) is published with empty logs and LogsUnconfirmed set, with an Error log and an Alerter alert, instead of stopping the monitor.
Situation Before After
Logs fetched normally Published No change on most chains. On Avalanche, an empty eth_getLogs result for a block with transactions adds one receipts lookup, about 3–4% more requests
eth_getLogs fails, recovers within MaxLogsAttempts Published No change
eth_getLogs keeps failing, receipts work ErrFatal once the queue overflows Published with receipt logs
eth_getLogs and receipts both fail for the whole cache window ErrFatal Published with empty logs + LogsUnconfirmed + Error log + alert (StrictLogs: still ErrFatal)

⚠️ Behaviour change

Queue overflow caused by unresolved logs is no longer fatal by default. A consumer can now receive a block published without confirmed logs (only after eth_getLogs and receipts have both failed for the block's whole stay in the chain cache; it's flagged with LogsUnconfirmed, logged at Error and alerted).

New options and fields:

  • Options.MaxLogsAttempts (default 10): failed eth_getLogs attempts before falling back to receipts.
  • Options.StrictLogs (default false): set it to restore the previous behaviour. Unconfirmed logs are never published, and overflow returns ErrFatal(ErrQueueFull). Consumers that persist progress and would rather stop than record a gap should consider it.
  • Block.LogsUnconfirmed: set on a block published without confirmed logs (default mode only). Its Logs is empty, but the block may have had logs.

No API breaks: filterLogs is unexported, Block gains an unexported field and the new LogsUnconfirmed field, and the Options changes are additive.

Known limitations

  • Topic-filtered monitors on settled-header chains (LogTopics set) don't get the receipts check, so a premature empty filtered result is accepted. This is pre-existing: filtered empty results were never validated.
  • Very small retention: with BlockRetentionLimit + TrailNumBlocksBehindHead <= 5, the queue can still overflow in default mode, because the chain retains at least 10 blocks while the queue cap is 2 × BlockRetentionLimit.
  • Reorgs as deep as the whole chain cache can still overflow the queue (pre-existing).
  • Slow outages: backfillChainLogs retries stuck blocks under the chain lock, and each can now cost an eth_getLogs plus a receipts call (the pattern is pre-existing).

Testing

go test ./ethmonitor/ -count=1 -race -skip TestMonitorBasic passes, and each commit builds and passes on its own. New tests:

  • TestHasSettledHeight, TestSettledHeadersDetection: detection, including nested and in-string keys, null and malformed JSON.
  • TestSettledEmptyLogResponses: receipts confirmation, receipt logs, caching and replay, block-receipts fallback and memoisation, transient errors, validation failures, and batches staying under a 40-request node limit.
  • TestIsMethodNotFoundError: only real method-not-found errors stop eth_getBlockReceipts from being used, at any HTTP status.
  • Cached [] recheck (j_premature_empty_cached_by_peer_*) and the HTTP 429 and 400 receipts cases in TestSettledEmptyLogResponses.
  • TestBlockLogsUnconfirmedSurvivesCopyAndSnapshot: the new flag survives Blocks.Copy and block snapshots.
  • TestLogsAttemptsFallBackToReceipts: the MaxLogsAttempts fallback, topic filtering, the bloom guard on receipts, and one receipts call per attempt.
  • TestMonitorUnresolvedBlocksAreNotFatal: end to end, default mode publishes with one Error log and one alert per block, and strict mode returns ErrFatal + ErrQueueFull.
  • TestMonitorSettledChainDoesNotStall: end to end, a settled chain with set blooms and empty eth_getLogs publishes every block. The control case (no settledHeight, strict) reproduces the old overflow.

go test ./... also fails in go-ethereum/core/types and go-ethereum/rpc; those tests fail the same way on master and are untouched here.

Avalanche Helicon headers carry settledHeight, and their logsBloom covers the blocks they settle rather than the block's own logs. Detect them from the raw block JSON, stop using the bloom to validate their logs, and check an empty eth_getLogs result against the block's receipts, which wait for execution, using the receipt logs when they disagree. eth_getBlockReceipts is tried first; per-transaction receipts are fetched in bounded batches when the node doesn't support it, or for a single call when it answers with a non-2xx error.
…ng on a full queue

A block whose logs keep failing used to hold the publish queue until it overflowed into ErrFatal(ErrQueueFull), on any chain. After MaxLogsAttempts (default 10) failed fetches its logs are now taken from its receipts; outside settled-header chains, empty receipts for a block with a set bloom are rejected. A block still unresolved when it leaves the chain cache is published with empty logs, with an Error log and an alert, instead of stopping the monitor.

This changes behaviour for every consumer: queue overflow is no longer fatal by default. Options.StrictLogs restores the previous behaviour for consumers that would rather stop than publish unconfirmed logs.
@py-zoid
py-zoid force-pushed the fix/ethmonitor-helicon-settled-blooms branch from b545794 to 2a7e529 Compare October 21, 2026 07:17
@py-zoid py-zoid changed the title ethmonitor: handle Avalanche settled headers (ACP-194) and stop failing on a full publish queue fix(ethmonitor): handle Avalanche settled headers (ACP-194) and stop failing on a full publish queue Oct 21, 2026
@py-zoid
py-zoid marked this pull request as ready for review October 21, 2026 09:33
@py-zoid
py-zoid requested a review from a team October 21, 2026 09:33
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T16:11:25.072077Z 013e2e9 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a7e529761

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ethmonitor/ethmonitor.go
// On settled-header chains eth_getLogs does not wait for execution, so
// an empty result may be premature. Receipt calls do wait, so confirm
// it against them before it can be cached.
if settled && len(topics) == 0 && len(logs) == 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Revalidate cached empty logs against receipts

When a cache backend returns an existing [] entry for a settled-header block, the getter—and therefore this receipt check—is skipped; the cache-hit path merely unmarshals with expectLogs == false and marks the block ready. Such an entry can come from an older monitor that cached a premature empty response for a zero-bloom settled header, causing the upgraded monitor to publish the block without its actual logs until cache expiry. Apply the receipt confirmation to cached empty responses as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in c7206b3. Details on @patrislav's thread below.

@patrislav patrislav left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this, the Avalanche analysis is right and the tests are thorough. I ran the ethmonitor package with -race on the PR head and everything passes. CI is green.

Verified

  • Lock ordering is safe: the queue lock is only ever taken inside the chain lock.
  • logsAttempts lives on the chain's copy, which is the same pointer the queue holds, so eviction keeps the count.
  • checkBlockReceipts decodable threshold matches what ethrpc actually drops, types above 2.
  • matchTopics follows geth filter semantics.
  • Strict mode does not re-enable the bloom check on settled chains. I ran the settled end-to-end test with StrictLogs = true and it passes.

Comment thread ethmonitor/ethmonitor.go
Comment thread ethmonitor/chain.go
Comment thread ethmonitor/resolve.go
Comment thread ethmonitor/settled.go Outdated
Comment thread ethmonitor/ethmonitor.go Outdated
Comment thread ethmonitor/settled.go Outdated
Comment thread ethmonitor/settled.go
Comment thread ethmonitor/settled_test.go
…retry rate-limited receipts

- A cached empty logs result for a settled-header block is now checked
  against the block's receipts, and replaced with the receipt logs when
  they disagree. Entries written by monitors that did not confirm empty
  results, such as earlier versions sharing the cache, were otherwise
  accepted until they expired.
- Block.LogsUnconfirmed is set on blocks published without confirmed
  logs, after eth_getLogs and receipts both failed for the block's whole
  stay in the chain cache, so consumers can tell them apart from blocks
  without logs. It is kept in block snapshots and by Blocks.Copy.
- eth_getBlockReceipts errors in non-2xx responses no longer trigger the
  per-transaction receipts fallback, which multiplied requests against
  rate-limited providers. A method-not-found error is recognised at any
  HTTP status; any other error is retried.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7206b3007

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ethmonitor/settled.go Outdated
Comment on lines +142 to +144
if len(receipts) < numTxs || decodable > numTxs {
return fmt.Errorf("%d receipts for %d transactions", len(receipts), numTxs)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify every decoded transaction receipt

When a block mixes transactions decoded by ethrpc with skipped types above DynamicFeeTxType, a partial eth_getBlockReceipts response can satisfy this count check. For example, a block with two decoded transactions and one skipped transaction is accepted if the response contains one decoded receipt and the skipped receipt, because len(receipts) == numTxs and decodable < numTxs; the block is then marked ready using logs that omit the missing decoded transaction. Validate the returned TransactionHash values against every transaction in block.Transactions() rather than relying only on these counts.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in b38f14f. checkBlockReceipts now requires a receipt for every decoded transaction, matched by transaction hash, on top of the count checks. A new test, i_block_receipts_missing_decoded_tx_with_typed_receipt, covers your example: two decoded transactions, with the second one's receipt replaced by a type-3 receipt. That block is no longer accepted. The per-tx fallback already fetches each receipt by its transaction hash, so it needed no change.

…ky receipts fallback

- An empty transaction list now counts as an empty block only when the
  header carries the empty transactions root. Headers always include
  transactionsRoot, so the zero-root case only served test blocks, which
  now use the real empty root.
- Note that once eth_getBlockReceipts is found unsupported, the monitor
  uses per-transaction receipts for the rest of its lifetime.
@py-zoid

py-zoid commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review, @patrislav.

  • c7206b3 fixes the three must-fix items:
    • cached [] is rechecked against receipts;
    • Block.LogsUnconfirmed marks blocks sent without confirmed logs;
    • rate-limit and other non-2xx errors are retried instead of switching to per-tx receipts.
  • a87d007 handles the two nits.
  • Head-of-chain cost: measured for an hour on C-Chain and Fuji. No wrong [] results, no receipts timeouts, about 3–4% more requests. Details are in the thread.

The description is updated to match.

@py-zoid
py-zoid requested a review from patrislav October 22, 2026 15:12
eth_getBlockReceipts results were only checked by count, so a partial
response could pass when a receipt for a transaction type ethrpc does
not decode stood in for a missing one, and the block was accepted
without that transaction's logs. Every decoded transaction must now
have a receipt, matched by transaction hash.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b38f14f3a5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ethmonitor/settled.go
Comment on lines +147 to +148
if len(receipts) < len(txs) || decodable > len(txs) {
return fmt.Errorf("%d receipts for %d transactions", len(receipts), len(txs))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate receipt count against the actual transaction count

When a block contains transaction types that ethrpc skips, this lower bound only reflects the decoded transactions. If eth_getBlockReceipts returns every decoded receipt but omits a skipped transaction's receipt—for example, one legacy and one type-3 transaction with only the legacy receipt returned—these checks and the hash loop both pass, and a premature empty eth_getLogs response is accepted without the skipped transaction's logs. Query the block's actual transaction count on this path too and require the receipt count to match it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Valid, but I'm keeping it as a known limitation rather than fixing it. It needs a node returning a partial eth_getBlockReceipts and the missing receipt belonging to a transaction type ethrpc doesn't decode. Closing it would add an eth_getBlockTransactionCountByHash call to every receipts lookup, plus extra error handling, for a case we haven't seen. 013e2e9 documents it in checkBlockReceipts. The more likely variant, a missing receipt for a decoded transaction, is caught since b38f14f.

@py-zoid
py-zoid merged commit 450a56a into master Oct 22, 2026
14 checks passed
@py-zoid
py-zoid deleted the fix/ethmonitor-helicon-settled-blooms branch October 22, 2026 18:06
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.

2 participants