You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
zcashd exposes ZeroMQ publish notifications, configured with zmqpubhashblock=<endpoint> in zcashd.conf: whenever the best chain tip changes, it publishes the new block hash on the hashblock topic, and mining pool software subscribes to that topic to fetch a fresh block template immediately instead of polling. This was reported in the original form of this issue by @vincentlli: pools migrating from zcashd to Zebra rely on this push notification to start mining on new blocks promptly.
Zebra currently has no ZMQ support anywhere in the workspace. It does have three adjacent mechanisms: the notify.block_notify_command config, getblocktemplate long polling, and the indexer gRPC ChainTipChange streaming method. None of these help existing pool software that already speaks zcashd's ZMQ protocol out of the box: ZMQ parity removes a migration blocker for pools moving off zcashd without requiring them to modify their software.
User Story
As a mining pool operator migrating from zcashd I want Zebra to publish the new best-block hash on the ZMQ hashblock topic so that my existing pool software's ZMQ subscription works unmodified and generates new work immediately when a block arrives, without polling
As a Zebra node operator I want the ZMQ publisher disabled by default and only bound when I configure an explicit endpoint so that running a node opens no new listening sockets unless I opt in
Acceptance Criteria
Design is agreed with the team before implementation starts: ZMQ crate choice (pure-Rust zeromq vs zmq bindings to libzmq), component placement (zebrad/src/components/ beside notify.rs vs zebra-rpc), whether the feature is gated behind a cargo feature (indexer precedent) or config-only, and the config section/field naming.
A new config option (e.g. a zmqpubhashblock-style endpoint, exact naming per design) is added following Zebra's config conventions (#[serde(deny_unknown_fields, default)], documented field, sensible production default), defaulting to disabled β precedent: indexer_listen_addr at zebra-rpc/src/config/rpc.rs:47 and the notify section at zebrad/src/components/notify.rs:27.
A publisher task subscribes to tip changes via ChainTipChange (zebra-state/src/service/chain_tip.rs) and is wired into zebrad/src/commands/start.rs alongside the block notify task; publishing never blocks block validation, and a slow or absent subscriber cannot stall the node or grow memory unboundedly (bounded ZMQ high-water mark; drop, don't queue forever).
The wire format matches zcashd's hashblock notification (src/zmq/zmqpublishnotifier.cpp in zcashd): multipart message with the hashblock topic frame and the 32-byte block hash payload; the payload byte order and whether zcashd appends a trailing 4-byte little-endian sequence number frame are verified against zcashd (live node or captured fixture) and replicated exactly.
Delivery semantics are decided and documented: notifications are best-effort (consistent with ZMQ pub/sub β subscribers must reconcile via RPC after gaps), intermediate tips coalesced by ChainTipChange resets may be skipped, and the behavior during initial sync is explicitly chosen (zcashd publishes during initial block download; Zebra's block_notify_command gates on SyncStatus::wait_until_close_to_tip β pick one and document why).
Unit tests for message encoding are colocated per convention (src/**/tests/), and an integration test starts the publisher, subscribes with a ZMQ client, drives a chain tip change, and asserts the received topic and payload.
book/src/user/mining.md documents the new option (including that ZMQ has no authentication, so the endpoint should only be exposed on trusted networks β same caveat as zcashd), and the zebrad config docs are updated.
CHANGELOG.md gets an [Unreleased] entry (user-visible feature).
cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test --workspace all pass.
Implementation Details
The natural model is the block notify task: run_block_notify at zebrad/src/components/notify.rs:76-110 loops on chain_tip_change.wait_for_tip_change(), then reads last_tip_change() to publish the freshest tip hash. A ZMQ publisher is the same loop with a socket send in place of the command spawn, spawned from start.rs:352 where the notify task is wired, receiving a ChainTipChange clone the same way.
The main design decisions, to settle with the team before the mechanical work:
Crate: the pure-Rust async zeromq crate avoids a libzmq C dependency (which would touch release builds, Docker images, and cross-compilation) but is younger; the zmq crate binds battle-tested libzmq but adds an FFI surface and packaging cost. Assess zeromq crate maturity for the pub socket specifically β publishing is the simplest ZMQ role, which favors the pure-Rust option.
Placement and gating: a zebrad component beside notify.rs keeps zebra-rpc free of a new transport; alternatively zebra-rpc groups it with the other externally-facing endpoints and their config. If the dependency is heavy, gate it behind a cargo feature like the indexer gRPC server; if it is light and pure-Rust, config-presence gating alone (like indexer_listen_addr) may be enough.
IBD behavior: zcashd fires ZMQ notifications throughout initial block download; Zebra's ChainTipChange coalesces bulk commits into resets, so even ungated it would not emit per-block spam during sync. Decide whether to gate on SyncStatus::wait_until_close_to_tip for consistency with block_notify_command or match zcashd; document either way.
Security posture: this adds a new listening socket with no authentication (ZMQ pub sockets have none, matching zcashd). It must be off by default, and the docs must carry the trusted-network caveat. Validate the configured endpoint at startup and fail loudly rather than silently not publishing.
In Scope
New publisher component (likely zebrad/src/components/ beside notify.rs; exact placement per design) and its wiring in zebrad/src/commands/start.rs
Config addition (zebrad/src/config.rs or zebra-rpc/src/config/rpc.rs, per placement decision)
ZMQ crate dependency in the relevant Cargo.toml (plus workspace Cargo.toml/deny.toml updates), optionally behind a cargo feature
Other zcashd ZMQ topics β zmqpubrawblock, zmqpubhashtx, zmqpubrawtx β and Bitcoin-Core-style zmqpubsequence; file follow-ups if pools ask for them, but hashblock is what the request needs.
Any mining-pool-side functionality (stratum, share accounting, payout logic) β Zebra is a validator node; pool software is external, and wallet-adjacent features belong in Zaino/Zallet.
Changes to the existing notify.block_notify_command, getblocktemplate long polling, or the indexer gRPC ChainTipChange stream β they remain the non-ZMQ alternatives.
Context
zcashd exposes ZeroMQ publish notifications, configured with
zmqpubhashblock=<endpoint>inzcashd.conf: whenever the best chain tip changes, it publishes the new block hash on thehashblocktopic, and mining pool software subscribes to that topic to fetch a fresh block template immediately instead of polling. This was reported in the original form of this issue by @vincentlli: pools migrating from zcashd to Zebra rely on this push notification to start mining on new blocks promptly.Zebra currently has no ZMQ support anywhere in the workspace. It does have three adjacent mechanisms: the
notify.block_notify_commandconfig,getblocktemplatelong polling, and the indexer gRPCChainTipChangestreaming method. None of these help existing pool software that already speaks zcashd's ZMQ protocol out of the box: ZMQ parity removes a migration blocker for pools moving off zcashd without requiring them to modify their software.User Story
As a mining pool operator migrating from zcashd
I want Zebra to publish the new best-block hash on the ZMQ
hashblocktopicso that my existing pool software's ZMQ subscription works unmodified and generates new work immediately when a block arrives, without polling
As a Zebra node operator
I want the ZMQ publisher disabled by default and only bound when I configure an explicit endpoint
so that running a node opens no new listening sockets unless I opt in
Acceptance Criteria
zeromqvszmqbindings to libzmq), component placement (zebrad/src/components/besidenotify.rsvszebra-rpc), whether the feature is gated behind a cargo feature (indexer precedent) or config-only, and the config section/field naming.zmqpubhashblock-style endpoint, exact naming per design) is added following Zebra's config conventions (#[serde(deny_unknown_fields, default)], documented field, sensible production default), defaulting to disabled β precedent:indexer_listen_addratzebra-rpc/src/config/rpc.rs:47and thenotifysection atzebrad/src/components/notify.rs:27.ChainTipChange(zebra-state/src/service/chain_tip.rs) and is wired intozebrad/src/commands/start.rsalongside the block notify task; publishing never blocks block validation, and a slow or absent subscriber cannot stall the node or grow memory unboundedly (bounded ZMQ high-water mark; drop, don't queue forever).hashblocknotification (src/zmq/zmqpublishnotifier.cppin zcashd): multipart message with thehashblocktopic frame and the 32-byte block hash payload; the payload byte order and whether zcashd appends a trailing 4-byte little-endian sequence number frame are verified against zcashd (live node or captured fixture) and replicated exactly.ChainTipChangeresets may be skipped, and the behavior during initial sync is explicitly chosen (zcashd publishes during initial block download; Zebra'sblock_notify_commandgates onSyncStatus::wait_until_close_to_tipβ pick one and document why).src/**/tests/), and an integration test starts the publisher, subscribes with a ZMQ client, drives a chain tip change, and asserts the received topic and payload.book/src/user/mining.mddocuments the new option (including that ZMQ has no authentication, so the endpoint should only be exposed on trusted networks β same caveat as zcashd), and the zebrad config docs are updated.CHANGELOG.mdgets an[Unreleased]entry (user-visible feature).cargo fmt --all -- --check,cargo clippy --workspace --all-targets -- -D warnings, andcargo test --workspaceall pass.Implementation Details
The natural model is the block notify task:
run_block_notifyatzebrad/src/components/notify.rs:76-110loops onchain_tip_change.wait_for_tip_change(), then readslast_tip_change()to publish the freshest tip hash. A ZMQ publisher is the same loop with a socket send in place of the command spawn, spawned fromstart.rs:352where the notify task is wired, receiving aChainTipChangeclone the same way.The main design decisions, to settle with the team before the mechanical work:
zeromqcrate avoids a libzmq C dependency (which would touch release builds, Docker images, and cross-compilation) but is younger; thezmqcrate binds battle-tested libzmq but adds an FFI surface and packaging cost. Assesszeromqcrate maturity for the pub socket specifically β publishing is the simplest ZMQ role, which favors the pure-Rust option.zebradcomponent besidenotify.rskeepszebra-rpcfree of a new transport; alternativelyzebra-rpcgroups it with the other externally-facing endpoints and their config. If the dependency is heavy, gate it behind a cargo feature like the indexer gRPC server; if it is light and pure-Rust, config-presence gating alone (likeindexer_listen_addr) may be enough.ChainTipChangecoalesces bulk commits into resets, so even ungated it would not emit per-block spam during sync. Decide whether to gate onSyncStatus::wait_until_close_to_tipfor consistency withblock_notify_commandor match zcashd; document either way.Security posture: this adds a new listening socket with no authentication (ZMQ pub sockets have none, matching zcashd). It must be off by default, and the docs must carry the trusted-network caveat. Validate the configured endpoint at startup and fail loudly rather than silently not publishing.
In Scope
zebrad/src/components/besidenotify.rs; exact placement per design) and its wiring inzebrad/src/commands/start.rszebrad/src/config.rsorzebra-rpc/src/config/rpc.rs, per placement decision)Cargo.toml(plus workspaceCargo.toml/deny.tomlupdates), optionally behind a cargo featurebook/src/user/mining.md, zebrad config docs,CHANGELOG.mdOut of Scope
zmqpubrawblock,zmqpubhashtx,zmqpubrawtxβ and Bitcoin-Core-stylezmqpubsequence; file follow-ups if pools ask for them, buthashblockis what the request needs.notify.block_notify_command,getblocktemplatelong polling, or the indexer gRPCChainTipChangestream β they remain the non-ZMQ alternatives.longpollcapability ingetblocktemplate(a separate gap noted inzebra-rpc/src/methods/types/get_block_template.rs).Blockers
Related Issues / Advisories
-blocknotify(notify.block_notify_command), the closest existing mechanism and the implementation template.src/zmq/zmqpublishnotifier.cppanddoc/zmq.mdin zcash/zcash β the wire format and operational caveats to match.