Skip to content

feature: publish new best-block-hash notifications over ZMQΒ #10881

Description

@vincentlli

Context

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
  • book/src/user/mining.md, zebrad config docs, CHANGELOG.md

Out of Scope

  • 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.
  • Advertising the longpoll capability in getblocktemplate (a separate gap noted in zebra-rpc/src/methods/types/get_block_template.rs).

Blockers

  • None

Related Issues / Advisories

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-compatibilityArea: Compatibility with other nodes or wallets, or standard rulesA-rpcArea: Remote Procedure Call interfacesC-enhancementCategory: This is an improvementC-featureCategory: New featuresS-needs-designStatus: Needs a design decisionS-needs-triageStatus: A bug report needs triageexternal-contribution

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions