Skip to content

feat(mempool): allow subscribing to unconfirmed_txs via WebSocket - #6035

Open
gomesalexandre wants to merge 3 commits into
cometbft:mainfrom
gomesalexandre:feat_subscribe_unconfirmed_txs
Open

feat(mempool): allow subscribing to unconfirmed_txs via WebSocket#6035
gomesalexandre wants to merge 3 commits into
cometbft:mainfrom
gomesalexandre:feat_subscribe_unconfirmed_txs

Conversation

@gomesalexandre

Copy link
Copy Markdown
Contributor

closes #1770

Adds a MempoolTx event, fired when a tx is admitted into the mempool (passes
CheckTx for the first time), mirroring how the existing Tx event is fired
on block commit. Clients can subscribe with tm.event='MempoolTx' instead of
polling /unconfirmed_txs.

Use case

Answering the maintainer's question on the issue thread: MEV/backrunning
tooling and network diagnostics both want to observe txs the moment they enter
the mempool, not just once committed - polling /unconfirmed_txs on an
interval misses the timing precision either use case needs.

Scope: CListMempool only

AppMempool (mempool.type = "app") is a separate, newer implementation that
doesn't touch the EventBus at all today - wiring it there is left as a
follow-up rather than folded into this change. Documented explicitly in
docs/core/subscription.md: a node running mempool.type = "app" or "nop"
currently receives nothing on a MempoolTx subscription, with no error. Happy
to track that as a separate issue if useful.

Things worth knowing before merging

Publish is synchronous under CheckTx's read lock. PublishEventMempoolTx
follows the exact same pattern as the existing PublishEventTx (unbounded
context, error logged not propagated) - but the underlying pubsub Server
dispatches on an unbuffered channel by default
(EventBusBufferCapacity defaults to 0), and CheckTx holds
updateMtx.RLock() for its entire body with the local ABCI client. So if the
pubsub loop is momentarily busy (e.g. draining a big block's Tx events into
an unbuffered subscriber like the indexer), a MempoolTx publish - which now
fires per admitted tx, not per block - queues behind it, and mempool-admission
latency inherits that delay. No deadlock: the indexer never takes mempool
locks, and it only subscribes to Tx/NewBlockEvents queries by default, so
it never receives MempoolTx itself. Operators who care can raise
event_bus_buffer_capacity. Flagging rather than restructuring the publish
path, since it matches the existing Tx-event pattern exactly.

No zero-subscriber fast path. Every admitted tx now pays a Tx.Hash()
computation, a couple of map allocations, and a pubsub dispatch, even with zero
MempoolTx subscribers - across every gossiped tx network-wide, not just
locally submitted ones. Small relative to the ABCI round-trip per CheckTx,
but nonzero. Happy to add an opt-in mempool.publish_events config flag as a
follow-up if that overhead matters to node operators.

Queries without a tm.event clause now also match MempoolTx. Since
PublishEventMempoolTx indexes the CheckTx response's app events and the tx
hash into the same composite-key space Tx events use, a subscription like
transfer.recipient='X' (no tm.event filter) will now also receive
mempool-admission events if the app emits that event in CheckTx, not just on
commit. Built-in flows are unaffected (EventQueryTxFor always includes
tm.event='Tx', and cosmjs's tx-search/subscribe paths do the same), but a
hand-rolled subscriber without a tm.event clause changes behavior. Keeping
the shared key space since hash-based filtering is a real part of this
feature's value, but calling it out explicitly rather than leaving it for a
reviewer to discover.

Testing

New test in mempool/clist_mempool_test.go: real CListMempool + real
EventBus + an in-memory kvstore app, subscribes via EventQueryMempoolTx,
asserts the subscriber receives EventDataMempoolTx with the right tx bytes
and CheckTx code.

go build ./... clean. gofmt -l clean on all changed files. go vet ./mempool/... ./types/... ./node/... ./rpc/... clean (one pre-existing,
unrelated finding on unmodified main, confirmed not touched by this diff).
go test ./mempool/..., ./types/..., ./node/..., ./rpc/core/... all
pass, plus a targeted rerun of the existing TestTxEventsSent to confirm no
regression on the adjacent, pre-existing Tx-event subscription flow.

@gomesalexandre
gomesalexandre requested a review from a team as a code owner August 19, 2026 10:15
@greptile-apps

greptile-apps Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

PR author is not in the allowed authors list.

gomesalexandre and others added 3 commits September 2, 2026 13:40
Adds a MempoolTx event, fired when a tx is admitted into the mempool
(passes CheckTx for the first time), mirroring how the existing Tx event
is fired on block commit. Clients (MEV/backrunning tooling, network
diagnostics per the issue) can now subscribe with
tm.event='MempoolTx' instead of polling /unconfirmed_txs.

- types/events.go, types/event_bus.go: new EventMempoolTx constant,
  EventDataMempoolTx, MempoolTxEventPublisher interface, and
  EventBus/NopEventBus.PublishEventMempoolTx, following the same shape
  as the existing Tx event plumbing.
- mempool/clist_mempool.go: CListMempool takes an optional
  types.MempoolTxEventPublisher (WithEventBus option, defaults to a
  no-op), and publishes on tx admission in resCbFirstTime.
- node/setup.go, node/node.go: wire the node's EventBus into the
  mempool at construction time.
- docs/core/subscription.md, CHANGELOG.md: document the new event.

Scoped to CListMempool (the default mempool.type = "flood"); AppMempool
(mempool.type = "app") is a separate, newer app-side implementation
that doesn't yet touch the EventBus at all, so wiring it there is left
for a follow-up rather than folded into this change.

closes cometbft#1770

Co-Authored-By: Claude <noreply@anthropic.com>
- events.go: the comment said EventMempoolTx 'carries no execution
  result', but EventDataMempoolTx does carry the CheckTx response.
  Reworded to clarify it's not a block execution result, not that
  there's no payload at all.
- subscription.md: state explicitly that MempoolTx only fires on
  mempool.type = "flood" (the default) - a subscription on an
  app/nop-mempool node currently receives nothing, silently.
@gomesalexandre
gomesalexandre force-pushed the feat_subscribe_unconfirmed_txs branch from 3e612bb to ab5251e Compare September 2, 2026 11:42
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.

[REQ] Possibility to subscribe via ws to unconfirmed_txs

1 participant