Skip to content

feat: async eth_sendRawTransaction#281

Merged
dhyaniarun1993 merged 2 commits into
mainfrom
feat/async-send-raw-transaction
May 26, 2026
Merged

feat: async eth_sendRawTransaction#281
dhyaniarun1993 merged 2 commits into
mainfrom
feat/async-send-raw-transaction

Conversation

@sadiq1971
Copy link
Copy Markdown
Member

Closes #278.

Summary

eth_sendRawTransaction previously waited synchronously for Canton to commit before returning the tx hash — 5-15s+ on hosted Canton, which broke MetaMask and any standard Ethereum client that expects the call to return immediately and then polls eth_getTransactionReceipt. PR #280 raised the HTTP timeout as a stopgap; this PR is the real fix.

What changed

  • SendRawTransaction is now async. It validates the signed tx (signature, contract whitelist, calldata), inserts a pending mempool row, returns the hash. No Canton call on the request path.
  • New pkg/ethrpc/submitter worker. Polls pending entries on a ticker (default 500ms), runs Canton transfers in parallel (default concurrency = 10, configurable), transitions each entry to completed or failed. Canton's tx-hash command-id keeps retries idempotent. Each Canton call has a 60s package-level timeout so a hung gRPC call can't park a worker slot indefinitely.
  • Miner mines failed entries too — both completed and failed entries are claimed and sealed in the same block. Failed entries become status=0 EVM transactions with no Transfer log, so eth_getTransactionReceipt surfaces them naturally.
  • LogIndex is now tracked separately from TxIndex so it stays block-relative contiguous when failed txs contribute zero logs.
  • RPCReceipt.revertReason (omitempty, non-standard) carries the Canton error message for status=0 receipts. Standard clients ignore it; our scripts/Loop wallet can render the cause.
  • GetMempoolEntriesByStatus takes a limit pushed into SQL — submitter passes its batch size (default 100) so a backlog never loads the whole queue into memory.

Config (all overridable, sensible defaults)

Key Default Purpose
submitter_interval 500ms drain tick spacing
submitter_batch_size 100 max pending entries fetched per tick (SQL LIMIT)
submitter_concurrency 10 parallel Canton transfers per tick

Canton call timeout is hardcoded at 60s — it's a property of the Canton SLO, not per-deployment tuning.

Errors

  • Permanent (categorized apperr.ServiceError: data, not-supported, forbidden, not-found, conflict, gone) → entry marked failed, mined as status=0, reason surfaced via revertReason.
  • Transient (network, gRPC unavailable, Dependency, generic, ctx deadline) → entry stays pending, retried on next tick.

E2E

Added DSL.WaitForAPITxReceipt(ctx, t, txHash) for tests that submit signed EVM transactions via sys.APIServer.RPC().SendTransaction(...). It polls eth_getTransactionReceipt, returns on status=1, and t.Fatals on status=0 with the revertReason (the typed ethclient.Receipt drops unknown fields, so we re-query via raw RPC to pull it out). Existing e2e tests don't currently call sendRawTransaction directly, so none needed updating — but anything new on that path should use this helper.

Test plan

  • go build ./...
  • go build -tags=e2e ./...
  • go vet ./...
  • go test ./...
  • go test ./pkg/ethrpc/submitter/... -race -count=5
  • golangci-lint run ./pkg/ethrpc/... ./tests/e2e/devstack/dsl/... --build-tags=e2e
  • Manual: hit /eth with MetaMask, confirm tx hash returns <100ms; observe receipt status=1 once Canton commits; intentionally insufficient balance returns status=0 + revertReason.
  • Manual: kill api-server mid-tick; on restart, observe pending entries get drained.

Notes / follow-ups (intentionally out of scope)

  • Multi-replica api-server would have multiple submitters double-submitting the same pending entries. Safe via Canton idempotency, wasteful. Same single-instance constraint as the miner today.
  • No max retry count / max age on transient failures — a permanently-broken Canton call sits pending forever. Worth a circuit breaker in a follow-up if we see this in practice.

Return tx hash immediately on eth_sendRawTransaction and drive the Canton
transfer asynchronously, so MetaMask and other wallets stop timing out
when Canton commits take 5-15s+.

- SendRawTransaction now validates the signed tx (signature, contract
  whitelist, calldata) and inserts a pending mempool row, returning the
  hash without waiting for Canton.
- New pkg/ethrpc/submitter worker polls pending entries on a ticker,
  calls Canton in parallel (concurrency 10 by default, configurable),
  and transitions each entry to completed or failed. Canton's tx-hash
  command-id makes retries idempotent. Each Canton call is bounded by
  a 60s timeout so a hung gRPC call can't drain the pool.
- Miner now claims both completed and failed terminal entries in a
  single block, mining failures as status=0 EVM transactions with no
  Transfer log so eth_getTransactionReceipt surfaces them naturally.
- LogIndex is tracked separately from TxIndex to stay block-relative
  contiguous when failed txs contribute zero logs.
- RPCReceipt grows an optional `revertReason` field, populated from the
  Canton error message so clients see why a tx failed instead of just
  status=0. Standard clients ignore unknown fields.
- pgstore GetMempoolEntriesByStatus takes a limit so a backlog never
  loads the whole pending queue into memory; submitter forwards its
  batch size (default 100).
- New WaitForAPITxReceipt DSL helper for e2e tests that submit signed
  EVM transactions via the /eth facade — polls eth_getTransactionReceipt
  and fails on status=0 with the revertReason rather than timing out.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors SendRawTransaction to be asynchronous by introducing a background submitter worker that processes pending mempool entries in parallel. The miner is updated to handle failed transactions (assigning contiguous log indices and omitting logs for failures), and transaction receipts now surface failure reasons via a non-standard RevertReason field.

Feedback highlights two key improvements:

  1. In submitter.go, database updates (CompleteMempoolEntry and FailMempoolEntry) should use a separate context with its own timeout instead of the Canton call context to prevent database write failures if the Canton call times out or takes too long.
  2. The New constructor in the submitter should validate that the interval is positive to prevent startup panics in time.NewTicker.

Comment thread pkg/ethrpc/submitter/submitter.go
Comment thread pkg/ethrpc/submitter/submitter.go
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 26, 2026

Codecov Report

❌ Patch coverage is 79.11392% with 33 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@ef8a122). Learn more about missing BASE report.

Files with missing lines Patch % Lines
pkg/ethrpc/submitter/submitter.go 83.69% 12 Missing and 3 partials ⚠️
pkg/app/api/server.go 0.00% 11 Missing ⚠️
pkg/ethrpc/store/pg.go 81.57% 3 Missing and 4 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #281   +/-   ##
=======================================
  Coverage        ?   31.21%           
=======================================
  Files           ?      131           
  Lines           ?    10179           
  Branches        ?        0           
=======================================
  Hits            ?     3177           
  Misses          ?     6732           
  Partials        ?      270           
Flag Coverage Δ
unittests 31.21% <79.11%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/ethrpc/miner/miner.go 97.61% <100.00%> (ø)
pkg/ethrpc/service/service.go 80.05% <100.00%> (ø)
pkg/ethrpc/types.go 0.00% <ø> (ø)
pkg/ethrpc/store/pg.go 84.03% <81.57%> (ø)
pkg/app/api/server.go 0.00% <0.00%> (ø)
pkg/ethrpc/submitter/submitter.go 83.69% <83.69%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sadiq1971 sadiq1971 self-assigned this May 26, 2026
Per Gemini review on #281: CompleteMempoolEntry / FailMempoolEntry were
sharing the Canton-scoped ctx (60s deadline). A Canton call that nearly
exhausted its budget would leave the DB write with an expired deadline,
the row would stay pending, and a permanent failure would loop forever
against a Canton that already rejected the transfer.

Derive the DB-write ctx from `parent` instead, with its own
dbWriteTimeout (10s). Extract a completeEntry helper to mirror
failEntry. Two new tests pin the property: the ctx CompleteMempoolEntry
/ FailMempoolEntry receive is distinct from the Canton ctx and has at
least 5s of budget left.
Copy link
Copy Markdown
Member

@dhyaniarun1993 dhyaniarun1993 left a comment

Choose a reason for hiding this comment

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

This is functionally correct, but it has the drawbacks mentioned in issue #282, which can be addressed in follow-up PRs.

@dhyaniarun1993 dhyaniarun1993 merged commit 1431b48 into main May 26, 2026
4 checks passed
@dhyaniarun1993 dhyaniarun1993 deleted the feat/async-send-raw-transaction branch May 26, 2026 16:21
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.

Make eth_sendRawTransaction async (return tx hash immediately)

4 participants