Skip to content

Conversation

@m-Peter
Copy link
Collaborator

@m-Peter m-Peter commented Nov 4, 2025

Closes: #906

Description

When eth_subscribe is given the transactionReceipts type, it creates a subscription that fires transaction receipts when transactions are included in blocks. Optional tx hashes can be passed in.


For contributor use:

  • Targeted PR against master branch
  • Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
  • Code follows the standards mentioned here.
  • Updated relevant documentation
  • Re-reviewed Files changed in the Github PR explorer
  • Added appropriate labels

Summary by CodeRabbit

  • New Features

    • Transaction receipts streaming via WebSocket with optional filtering by transaction hashes; receipts delivered in batched notifications
    • Subscription enforces a maximum allowed transaction-hash count
  • Tests

    • WebSocket integration tests validating receipt streaming and verifying received receipts against on-chain data

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 4, 2025

Walkthrough

Adds a TransactionReceipts streaming subscription, threads a receipts publisher through bootstrap → ingestion → API, publishes receipts in batches during event processing, validates optional tx-hash filters (limit 200), marshals receipts, and adds integration tests.

Changes

Cohort / File(s) Summary
Stream API
api/stream.go
Adds receiptsPublisher *models.Publisher[[]*models.Receipt], new constant maxTxHashes = 200, updates NewStreamAPI(...) signature, and implements TransactionReceipts(ctx, filter) subscription with tx-hash count validation, optional filtering (using slices.Contains), receipt marshaling (ethTypes.MarshalReceipt), and batch notifications.
Bootstrap wiring
bootstrap/bootstrap.go
Adds Receipts *models.Publisher[[]*models.Receipt] to Publishers, initializes it in New(...), and wires the receipts publisher through StartEventIngestion and StartAPIServer calls.
Ingestion engine
services/ingestion/engine.go, services/ingestion/engine_test.go
Adds receiptsPublisher field to Engine, extends NewEventIngestionEngine(...) signature to accept receipts publisher, initializes the field, and publishes receipts in processEvents. Tests updated to pass the new publisher argument.
Models / Errors
models/errors/errors.go, models/events.go
Adds exported error ErrExceedMaxTxHashes with message for exceeding tx-hash limit. Sets BlockTimestamp for logs from block timestamp when building cadence events.
Integration tests
tests/web3js/eth_streaming_test.js
Adds WebSocket tests subscribing to transactionReceipts (with and without hash filters), collects streamed receipts, signs/sends transactions, and validates streamed receipts against on-chain receipts.

Sequence Diagram(s)

sequenceDiagram
    participant EventSource as Event Source
    participant Ingestion as Ingestion Engine
    participant Pub as Receipts Publisher
    participant API as Stream API
    participant Client as Subscriber (WS)

    EventSource->>Ingestion: emit block, txs, receipts
    Ingestion->>Pub: publish([]*Receipt)           %% publish batch
    Pub->>API: notify(receipt batch)
    
    Client->>API: TransactionReceipts(ctx, filter)
    API->>API: validate txHashes count (<=200)
    API->>Pub: subscribe
    alt on batch notification
        Pub->>API: batch notification
        API->>API: optionally filter by txHashes
        API->>Client: send batch notification
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review receipt filtering logic and tx-hash count validation in TransactionReceipts.
  • Verify correctness of ethTypes.MarshalReceipt usage and marshaled payload shape.
  • Confirm receiptsPublisher wiring through bootstrap → ingestion → API and updated test adjustments.

Possibly related PRs

Suggested labels

Improvement

Suggested reviewers

  • janezpodhostnik
  • peterargue
  • zhangchiqing

Poem

🐰 A little rabbit hops with cheer,
Batches of receipts now appear,
Hashes counted, filters set right,
Streaming proofs by day and night,
Hooray — receipts flow out of sight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: implementing the new transactionReceipts subscription API, which matches the PR's core objective.
Linked Issues check ✅ Passed The PR implements all requirements from issue #906: adds transactionReceipts subscription support, emits receipts when transactions are included in blocks, supports optional filtering by transaction hashes, and aligns with Geth v1.16.5.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the transactionReceipts subscription: API wiring, publisher setup, filtering logic, error handling, and WebSocket tests. No unrelated modifications detected.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch mpeter/implement-tx-receipt-subscriptions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@m-Peter m-Peter changed the title Implement the new 'transactionReceipts' subscription API Implement the new transactionReceipts subscription API Nov 4, 2025
@m-Peter m-Peter force-pushed the mpeter/enable-fusaka-hard-fork branch 2 times, most recently from ee75cc9 to f9e2865 Compare November 7, 2025 11:34
Base automatically changed from mpeter/enable-fusaka-hard-fork to main November 7, 2025 16:11
@m-Peter m-Peter force-pushed the mpeter/implement-tx-receipt-subscriptions branch from 4925450 to 0bd1756 Compare November 12, 2025 11:05
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
api/stream.go (1)

158-176: Consider precomputing hash lookups.

Right now each receipt does an O(len(txHashes)) slices.Contains. With the 200-hash cap that’s fine, but a tiny helper that builds a map[common.Hash]struct{} up front would make the loop constant time and future-proof us if the cap ever grows.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 404c556 and 0bd1756.

📒 Files selected for processing (7)
  • api/stream.go (6 hunks)
  • bootstrap/bootstrap.go (4 hunks)
  • models/errors/errors.go (1 hunks)
  • models/events.go (1 hunks)
  • services/ingestion/engine.go (4 hunks)
  • services/ingestion/engine_test.go (5 hunks)
  • tests/web3js/eth_streaming_test.js (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T11:36:25.478Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/batch_tx_pool.go:0-0
Timestamp: 2025-06-19T11:36:25.478Z
Learning: In Go, when copying maps that contain slices (like `map[gethCommon.Address][]pooledEvmTx`), perform deep copies by iterating over the map and copying each slice individually using `make()` and `copy()` to avoid shared references that could lead to race conditions and data corruption.

Applied to files:

  • api/stream.go
🧬 Code graph analysis (5)
tests/web3js/eth_streaming_test.js (2)
tests/web3js/config.js (2)
  • require (1-1)
  • web3 (2-2)
tests/web3js/helpers.js (4)
  • signedTx (66-66)
  • signedTx (79-79)
  • conf (2-2)
  • web3 (5-5)
services/ingestion/engine_test.go (2)
models/stream.go (1)
  • NewPublisher (14-19)
models/receipt.go (1)
  • Receipt (18-36)
api/stream.go (4)
models/stream.go (2)
  • Publisher (9-12)
  • Subscription (49-53)
models/receipt.go (1)
  • Receipt (18-36)
models/errors/errors.go (1)
  • ErrExceedMaxTxHashes (20-20)
eth/types/types.go (1)
  • MarshalReceipt (514-578)
services/ingestion/engine.go (4)
storage/index.go (1)
  • ReceiptIndexer (61-82)
models/stream.go (1)
  • Publisher (9-12)
models/receipt.go (1)
  • Receipt (18-36)
storage/pebble/receipts.go (1)
  • Receipts (20-22)
bootstrap/bootstrap.go (3)
storage/pebble/receipts.go (1)
  • Receipts (20-22)
models/stream.go (2)
  • Publisher (9-12)
  • NewPublisher (14-19)
models/receipt.go (1)
  • Receipt (18-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test
🔇 Additional comments (6)
models/events.go (1)

80-83: Nice alignment with upstream log schema.

Adding BlockTimestamp to each log keeps our receipts consistent with go-ethereum’s v1.16.x payloads that already ship blockTimestamp, so downstream consumers don’t need an extra block lookup. (chainrelease.info)

models/errors/errors.go (1)

19-21: Error surface matches the new subscription contract.

Exporting ErrExceedMaxTxHashes gives the API layer a precise failure mode when enforcing the hash cap.

services/ingestion/engine_test.go (1)

74-83: Tests updated for the extra publisher.

Thanks for threading the receipts publisher through the test harness—this guards the constructor signature change right away.

services/ingestion/engine.go (1)

208-214: Receipts published post-commit.

Emitting through receiptsPublisher only after indexEvents (and the batch commit) keeps subscribers from racing partially persisted data—good call.

tests/web3js/eth_streaming_test.js (1)

166-204: Coverage for hash-filtered subscriptions.

Great to see the second leg asserting that only the requested hashes surface—this will catch regressions in the filtering logic quickly.

bootstrap/bootstrap.go (1)

58-58: LGTM! Clean wiring of receipts publisher throughout the bootstrap.

The receipts support is correctly threaded through all layers:

  • Storage and publisher infrastructure added to core structs
  • Publisher initialized in the constructor
  • Properly wired to both ingestion engine (for publishing) and stream API (for subscriptions)

The implementation follows the established pattern for blocks, transactions, and logs, ensuring consistency across the codebase.

Also applies to: 65-65, 109-109, 204-204, 344-344

@m-Peter m-Peter force-pushed the mpeter/implement-tx-receipt-subscriptions branch from 0bd1756 to db4c441 Compare November 24, 2025 09:45
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/web3js/eth_streaming_test.js (1)

73-165: Strengthen receipt validation and reduce flakiness in the first transactionReceipts scenario

The review comment is correct. Issues verified:

  1. Missing receipt match assertion (lines 167–180): The inner loop iterates over receipts but never asserts that a matching receipt was found for each txHash. If the stream omits or mislabels receipts but still returns 10 items, the test silently passes. Add a found flag and assertion:
     for (let txHash of sentHashes) {
         let txReceipt = await helpers.callRPCMethod(
             'eth_getTransactionReceipt',
             [txHash]
         )

+        let found = false
         for (let rcp of receipts) {
             if (rcp.transactionHash == txHash) {
                 assert.deepEqual(rcp, txReceipt.body['result'])
+                found = true
+                break
             }
         }
+        assert.isTrue(found, `expected streamed receipt for tx ${txHash}`)
     }
  1. Fixed setTimeout timing (line 116): Relying on a hardcoded 1-second delay before asserting on receipts can be brittle. Replace with a polling approach that waits until receipts.length reaches the expected count (with timeout protection).

  2. Magic number 10 (line 164): Express as 2 * testValues.length to clarify the "two receipts per transaction" invariant.

🧹 Nitpick comments (1)
tests/web3js/eth_streaming_test.js (1)

166-220: Filtered transactionReceipts subscription scenario looks solid, minor clean‑up optional

Using a second subscription with an explicit transactionHashes set and asserting a single streamed receipt that matches eth_getTransactionReceipt exercises the filter path well.

If you want to tighten things further, you could:

  • Unsubscribe or otherwise retire the first transactionReceipts subscription to avoid extra unseen traffic on the same socket.
  • Optionally wait for subID (the subscription ack) before sending the signed transaction, to rule out any theoretical race between subscription activation and first notification.

Functionally, though, this block is correct as-is.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0bd1756 and db4c441.

📒 Files selected for processing (7)
  • api/stream.go (6 hunks)
  • bootstrap/bootstrap.go (4 hunks)
  • models/errors/errors.go (1 hunks)
  • models/events.go (1 hunks)
  • services/ingestion/engine.go (4 hunks)
  • services/ingestion/engine_test.go (5 hunks)
  • tests/web3js/eth_streaming_test.js (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • services/ingestion/engine.go
  • services/ingestion/engine_test.go
  • models/events.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-19T11:36:25.478Z
Learnt from: m-Peter
Repo: onflow/flow-evm-gateway PR: 831
File: services/requester/batch_tx_pool.go:0-0
Timestamp: 2025-06-19T11:36:25.478Z
Learning: In Go, when copying maps that contain slices (like `map[gethCommon.Address][]pooledEvmTx`), perform deep copies by iterating over the map and copying each slice individually using `make()` and `copy()` to avoid shared references that could lead to race conditions and data corruption.

Applied to files:

  • api/stream.go
🧬 Code graph analysis (3)
models/errors/errors.go (2)
bootstrap/bootstrap.go (1)
  • New (84-119)
storage/pebble/storage.go (1)
  • New (19-24)
api/stream.go (4)
models/stream.go (2)
  • Publisher (9-12)
  • Subscription (49-53)
models/receipt.go (1)
  • Receipt (18-36)
models/errors/errors.go (1)
  • ErrExceedMaxTxHashes (20-20)
eth/types/types.go (1)
  • MarshalReceipt (514-578)
bootstrap/bootstrap.go (3)
storage/pebble/receipts.go (1)
  • Receipts (20-22)
models/stream.go (2)
  • Publisher (9-12)
  • NewPublisher (14-19)
models/receipt.go (1)
  • Receipt (18-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test
🔇 Additional comments (5)
models/errors/errors.go (1)

16-21: New ErrExceedMaxTxHashes API error is consistent and well-scoped

Name, visibility, and message align with the existing API-specific error set and cleanly support the new subscription limit check in StreamAPI.TransactionReceipts.

api/stream.go (2)

24-61: Receipts publisher wiring into StreamAPI looks correct

Adding receiptsPublisher *models.Publisher[[]*models.Receipt] to StreamAPI and threading it through NewStreamAPI cleanly mirrors the existing block/tx/log publishers and matches the bootstrap wiring. No issues here.


132-183: Tighten TransactionReceipts implementation: avoid empty notifications and improve hash filtering structure

The overall shape looks good (limit check, optional hash filter, reuse of ethTypes.MarshalReceipt), but a couple of refinements would make it more robust and efficient:

  • Skip notifications when no receipts match the filter. Right now a batch is sent even when marshaledReceipts is empty. To reduce noise and better align with typical subscription semantics, guard the notify call:
-            // Send a batch of tx receipts in one notification
-            return notifier.Notify(sub.ID, marshaledReceipts)
+            // Send a batch of tx receipts in one notification (only if non-empty)
+            if len(marshaledReceipts) == 0 {
+                return nil
+            }
+            return notifier.Notify(sub.ID, marshaledReceipts)
  • Precompute a hash set for filtering instead of using slices.Contains in the hot loop. With maxTxHashes = 200 this is not critical, but using a map[gethCommon.Hash]struct{} avoids repeated O(m) scans and is more idiomatic for membership tests:
 func (s *StreamAPI) TransactionReceipts(
     ctx context.Context,
     filter *filters.TransactionReceiptsQuery,
 ) (*rpc.Subscription, error) {
@@
-    var txHashes []gethCommon.Hash
-
-    if filter != nil {
-        txHashes = filter.TransactionHashes
-    }
+    var txHashes []gethCommon.Hash
+    var txHashSet map[gethCommon.Hash]struct{}
+    if filter != nil && len(filter.TransactionHashes) > 0 {
+        txHashes = filter.TransactionHashes
+        txHashSet = make(map[gethCommon.Hash]struct{}, len(txHashes))
+        for _, h := range txHashes {
+            txHashSet[h] = struct{}{}
+        }
+    }
@@
-                for _, receipt := range receipts {
+                for _, receipt := range receipts {
                     // Check if the subscription is only interested for a given
                     // set of tx receipts.
-                    if len(txHashes) > 0 && !slices.Contains(txHashes, receipt.TxHash) {
+                    if txHashSet != nil {
+                        if _, ok := txHashSet[receipt.TxHash]; !ok {
+                            continue
+                        }
+                    }
                         continue
                     }
  • Go version note: importing slices assumes the repo is on Go ≥ 1.21. Please double‑check the toolchain and go.mod to ensure this is intentional and supported.

[ suggest_recommended_refactor ]

bootstrap/bootstrap.go (2)

62-112: Receipts publisher addition to Publishers and New is consistent with existing patterns

Defining Receipts *models.Publisher[[]*models.Receipt] alongside Block/Transaction/Logs and initializing it in New via models.NewPublisher[[]*models.Receipt]() fits the existing publisher pattern and keeps bootstrap responsibilities clear.


192-208: Ingestion and API wiring for receipts publisher is coherent end‑to‑end

Passing b.publishers.Receipts into ingestion.NewEventIngestionEngine and then into api.NewStreamAPI ensures that newly indexed receipts are streamed out via the transactionReceipts subscription without introducing extra coupling. The argument ordering remains readable and consistent with other publishers.

Also applies to: 336-346

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.

Implement the new TransactionReceipts subscription type

2 participants