feat: async eth_sendRawTransaction#281
Conversation
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.
There was a problem hiding this comment.
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:
- In
submitter.go, database updates (CompleteMempoolEntryandFailMempoolEntry) 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. - The
Newconstructor in the submitter should validate that theintervalis positive to prevent startup panics intime.NewTicker.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #281 +/- ##
=======================================
Coverage ? 31.21%
=======================================
Files ? 131
Lines ? 10179
Branches ? 0
=======================================
Hits ? 3177
Misses ? 6732
Partials ? 270
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
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.
dhyaniarun1993
left a comment
There was a problem hiding this comment.
This is functionally correct, but it has the drawbacks mentioned in issue #282, which can be addressed in follow-up PRs.
Closes #278.
Summary
eth_sendRawTransactionpreviously 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 pollseth_getTransactionReceipt. PR #280 raised the HTTP timeout as a stopgap; this PR is the real fix.What changed
SendRawTransactionis now async. It validates the signed tx (signature, contract whitelist, calldata), inserts apendingmempool row, returns the hash. No Canton call on the request path.pkg/ethrpc/submitterworker. Pollspendingentries on a ticker (default 500ms), runs Canton transfers in parallel (default concurrency = 10, configurable), transitions each entry tocompletedorfailed. 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.completedandfailedentries are claimed and sealed in the same block. Failed entries becomestatus=0EVM transactions with no Transfer log, soeth_getTransactionReceiptsurfaces them naturally.LogIndexis now tracked separately fromTxIndexso it stays block-relative contiguous when failed txs contribute zero logs.RPCReceipt.revertReason(omitempty, non-standard) carries the Canton error message forstatus=0receipts. Standard clients ignore it; our scripts/Loop wallet can render the cause.GetMempoolEntriesByStatustakes alimitpushed into SQL — submitter passes its batch size (default 100) so a backlog never loads the whole queue into memory.Config (all overridable, sensible defaults)
submitter_interval500mssubmitter_batch_size100submitter_concurrency10Canton call timeout is hardcoded at 60s — it's a property of the Canton SLO, not per-deployment tuning.
Errors
apperr.ServiceError: data, not-supported, forbidden, not-found, conflict, gone) → entry markedfailed, mined asstatus=0, reason surfaced viarevertReason.Dependency, generic, ctx deadline) → entry stayspending, retried on next tick.E2E
Added
DSL.WaitForAPITxReceipt(ctx, t, txHash)for tests that submit signed EVM transactions viasys.APIServer.RPC().SendTransaction(...). It pollseth_getTransactionReceipt, returns onstatus=1, andt.Fatals onstatus=0with therevertReason(the typedethclient.Receiptdrops unknown fields, so we re-query via raw RPC to pull it out). Existing e2e tests don't currently callsendRawTransactiondirectly, 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=5golangci-lint run ./pkg/ethrpc/... ./tests/e2e/devstack/dsl/... --build-tags=e2e/ethwith MetaMask, confirm tx hash returns <100ms; observe receipt status=1 once Canton commits; intentionally insufficient balance returns status=0 + revertReason.Notes / follow-ups (intentionally out of scope)
pendingentries. Safe via Canton idempotency, wasteful. Same single-instance constraint as the miner today.pendingforever. Worth a circuit breaker in a follow-up if we see this in practice.