Skip to content

node: publish Core's sequence ZMQ topic for block connects and disconnects - #52

Merged
metaphorics merged 4 commits into
mainfrom
devin/1786456607-zmq-pubsequence
Aug 11, 2026
Merged

node: publish Core's sequence ZMQ topic for block connects and disconnects#52
metaphorics merged 4 commits into
mainfrom
devin/1786456607-zmq-pubsequence

Conversation

@metaphorics

@metaphorics metaphorics commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Bitcoin Core's sequence ZMQ topic, carrying block events only (C on connect, D on disconnect). This is the last piece a remote bip300301_enforcer needs to drive its validator off bitcoin-rs: it subscribes to exactly one topic (sequence, lib/cli.rs), and when --node-zmq-addr-sequence is omitted it discovers the endpoint by looking for pubsequence in getzmqnotifications and hard-errors if it is absent. Complements #49 (REST) — no BIP300/301 logic is ported, and none of Core's other notification surface is added.

Frame layout is the three-frame shape its parser requires (cusf-enforcer-mempool/lib/zmq.rs), and the topic gets its own counter, which is what makes the counter unified across event kinds — one stream, one monotonic u32:

frame 0: b"sequence"
frame 1: 32-byte block hash (Core wire orientation, same helper as `hashblock`) ++ b'C' | b'D'
frame 2: u32 LE counter, shared by C and D

The publisher API takes a SequenceEvent enum rather than a method per label, so adding the mempool variants later doesn't reshape ZmqPublisher. The four existing topics and their independent counters are untouched.

Why A/R are deliberately absent

Core also multiplexes mempool add/remove onto this topic. Emitting them now would be worse than silence: the consumer treats a mempool-sequence gap as fatal, but never checks tx-sequence continuity if no tx events arrive — so C/D-only leaves the validator path fully working and degrades the enforcer's mempool mode to an empty mempool instead of a hard error. Our mempool cannot currently produce a gapless per-transaction sequence: remove_entries bumps once per call rather than per transaction, prioritise bumps with no membership change, insert_entry discards the IDs of the evictions it triggers, and reorg reconsideration is still unfinished (docs/solutions/architecture-patterns/node-reorg-execution-design.md), so no legitimate A can follow a D yet. That needs a per-transaction event-sequence and a removal-reason model in crates/mempool — a separate change, documented rather than faked.

Emission points

Both labels are published where the chainstate mutation lands, which is what makes the stream safe to react to:

connect:    apply chainstate -> applied_tip.store(new) -> publish C
disconnect: roll back        -> (tip updated)          -> publish D
reorg:      D(old tip) -> D(old tip-1) -> ... -> C(fork+1) -> ... -> C(new tip)
            counter contiguous across the whole interleaved stream

Two ordering bugs are fixed by that placement, both of which a subscriber can actually observe:

  • Emitting C before applied_tip.store let a subscriber act on a connect while RPC still reported the parent as the tip — and the enforcer's C handler calls straight back into the node.
  • Collecting disconnect hashes and publishing them after the connect loop put the new branch's Cs before the old branch's Ds, so the enforcer would have called connect_block for a block whose parent was not its tip. Emitting at the success path of disconnect_block_admitted makes publication order equal occurrence order for every caller and removes the deferred list.

Both are pinned by tests: an executor-driven reorg asserts the exact D…C… sequence with a contiguous counter, and a publisher that reads applied_tip during emission asserts the announced block is already the visible tip.

Configured like the existing topics (--zmqpubsequence, BITCOIN_RS_ZMQPUBSEQUENCE, zmqpubsequence in bitcoin.conf, HWM sibling, layered config), and getzmqnotifications reports pubsequence so endpoint auto-discovery works.

Docs that claimed disconnects are silent (README.md, docs/getting-started.md, docs/README.md, the reorg-design table) are reconciled, and CONCEPTS.md gains the sequence-stream term.

Verified live against Bitcoin Core 27.0 on regtest — frames diffed against Core's own sequence stream over 200 blocks and a reorg (evidence).

Link to Devin session: https://app.devin.ai/sessions/2cf54a23e1494fd080fa7541dadf06ff
Requested by: @metaphorics

@metaphorics metaphorics self-assigned this Aug 11, 2026
@devin-ai-integration

Copy link
Copy Markdown
Original prompt from a

@gosuda/bitcoin-rs Minimally merge this. https://github.com/LayerTwo-Labs/bip300301_enforcer/

@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@devin-ai-integration[bot], you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0cb35253-cd74-4d42-91be-de44debbbfec

📥 Commits

Reviewing files that changed from the base of the PR and between 17ea5c2 and 2cd1be2.

📒 Files selected for processing (1)
  • crates/node/src/apply.rs
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added ZMQ pubsequence notifications for block connections and disconnections.
    • Added configurable sequence endpoints and high-water marks, including Bitcoin Core-compatible settings.
    • Sequence values increase consistently, with disconnect events preceding reconnect events during reorganizations.
  • Documentation

    • Updated configuration guidance, compatibility details, and known limitations.
    • Clarified that mempool add/remove events are not currently included in the sequence stream.

Walkthrough

The node adds a Core-compatible ZMQ pubsequence stream. It publishes block connect and disconnect events with shared sequence counters, supports configuration through multiple layers, validates reorg ordering, and documents the omission of mempool events.

Changes

ZMQ pubsequence

Layer / File(s) Summary
Sequence event publisher
crates/node/src/zmq_publisher.rs, crates/node/src/lib.rs
Adds SequenceEvent, the Sequence topic, publish_sequence, payload encoding, endpoint routing, counters, tracing, and delivery tests.
Sequence configuration and wiring
crates/node/src/config.rs, crates/node/src/bitcoin_conf_compat.rs, crates/node/tests/*
Adds sequence endpoints and HWM values to defaults, validation, layered configuration, CLI, environment parsing, Bitcoin Core compatibility, RPC wiring, and configuration tests.
Block transition events and documentation
crates/node/src/apply.rs, CONCEPTS.md, README.md, docs/*
Publishes connect and disconnect events, verifies tip-first reorg ordering with monotonic counters, and documents the current omission of mempool A/R events.

Sequence Diagram(s)

sequenceDiagram
  participant BlockTransition
  participant ZmqPublisher
  participant SocketZmqPublisher
  participant ZMQSubscriber
  BlockTransition->>ZmqPublisher: publish_sequence(Connected or Disconnected)
  ZmqPublisher->>SocketZmqPublisher: Encode block hash, label, and sequence
  SocketZmqPublisher->>ZMQSubscriber: Publish pubsequence notification
Loading

Possibly related PRs

  • gosuda/bitcoin-rs#14: Modifies the same block application and reorganization paths used for sequence notifications.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.29% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the addition of Core-compatible sequence notifications for block connects and disconnects and uses an acceptable Conventional Commits-style prefix.
Description check ✅ Passed The description directly explains the sequence topic, event format, ordering, configuration, tests, and intentional exclusion of mempool events.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch devin/1786456607-zmq-pubsequence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/node/src/zmq_publisher.rs (1)

322-340: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Allocate sequence numbers per endpoint and advance them only after a successful send.

fetch_add consumes the value before send_multipart succeeds. Bitcoin Core increments its notifier counter only after a successful multipart send. One shared topic counter also creates gaps when one endpoint fails and another succeeds. Concurrent calls can deliver allocated values out of order. Serialize allocation with each endpoint’s publication and increment only on success.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/node/src/zmq_publisher.rs` around lines 322 - 340, Update publish so
sequence allocation is maintained separately for each endpoint rather than using
the shared counters indexed by topic. Serialize each endpoint’s sequence
allocation and send through its publication path, incrementing that endpoint’s
counter only after send_multipart succeeds; failed sends must not consume
values, and concurrent publishes must preserve per-endpoint ordering.

Source: MCP tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/node/src/apply.rs`:
- Around line 2328-2330: Move the Connected publish call in the connection flow
to after handles.applied_tip.store(...) completes, ensuring subscribers observe
the new tip when handling the event. Add a publisher test that reads applied_tip
during publish_sequence and verifies it contains the updated tip.

In `@crates/node/src/config.rs`:
- Around line 405-410: Update the sequence allocation flow in
push_zmq_publications and SocketZmqPublisher so the shared sequence counter
advances only after send_multipart succeeds; failed sends must not consume a
sequence number or create gaps. Preserve reuse of one sequence value across all
sequence endpoints and Connected/Disconnected events. Add a failure-path test
verifying the next successfully delivered event receives the undelivered event’s
sequence number.

In `@docs/solutions/architecture-patterns/node-reorg-execution-design.md`:
- Line 158: Update the “Open” table row currently labeled “Disconnect
notification” to describe only the missing mempool A/R notifications, removing
the completed ZMQ pubsequence connect/disconnect behavior from its text.

---

Outside diff comments:
In `@crates/node/src/zmq_publisher.rs`:
- Around line 322-340: Update publish so sequence allocation is maintained
separately for each endpoint rather than using the shared counters indexed by
topic. Serialize each endpoint’s sequence allocation and send through its
publication path, incrementing that endpoint’s counter only after send_multipart
succeeds; failed sends must not consume values, and concurrent publishes must
preserve per-endpoint ordering.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fe4f947d-05c2-46fe-8a83-fdd4916463bc

📥 Commits

Reviewing files that changed from the base of the PR and between 4d3fd2f and 3e646ce.

📒 Files selected for processing (13)
  • CONCEPTS.md
  • README.md
  • crates/node/src/apply.rs
  • crates/node/src/bitcoin_conf_compat.rs
  • crates/node/src/config.rs
  • crates/node/src/lib.rs
  • crates/node/src/zmq_publisher.rs
  • crates/node/tests/bitcoin_conf_compat.rs
  • crates/node/tests/config_layered.rs
  • crates/node/tests/rpc_wiring.rs
  • docs/README.md
  • docs/getting-started.md
  • docs/solutions/architecture-patterns/node-reorg-execution-design.md
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: bench-smoke
  • GitHub Check: test
🧰 Additional context used
🔍 Remote MCP Github Grep

Additional review context

  • Bitcoin Core exposes both -zmqpubsequence and -zmqpubsequencehwm; the topic is registered as a ZMQ notifier.
  • Core’s wire format is three frames: topic, body, and a per-topic 4-byte little-endian message counter. Hashes use reversed byte order.
  • Core emits sequence C and D events through callbacks covering every block connection and disconnection—not only active-tip changes.
  • Core’s block payload is the 32-byte hash followed by the event label; transaction A/R payloads additionally include an 8-byte little-endian mempool sequence. Thus this PR provides only partial Core pubsequence compatibility while omitting transaction events.
  • Core’s publisher increments its message counter only after a successful send, and the counter is stored on the publisher notifier. Verify the PR’s shared-counter and failure-handling behavior against this detail.
🔇 Additional comments (6)
crates/node/src/config.rs (1)

154-157: LGTM!

Also applies to: 218-219, 268-269, 368-368, 445-445, 532-534, 547-549, 649-650, 659-660, 737-739, 752-754

crates/node/src/bitcoin_conf_compat.rs (1)

71-76: LGTM!

Also applies to: 201-203, 216-218

crates/node/tests/bitcoin_conf_compat.rs (1)

74-75: LGTM!

Also applies to: 92-96

crates/node/tests/config_layered.rs (1)

123-124: LGTM!

Also applies to: 154-157, 175-189

docs/getting-started.md (1)

8-11: LGTM!

docs/solutions/architecture-patterns/node-reorg-execution-design.md (1)

56-59: LGTM!

Comment thread crates/node/src/apply.rs Outdated
Comment thread crates/node/src/config.rs
Comment on lines +405 to +410
push_zmq_publications(
&mut publications,
crate::zmq_publisher::ZmqTopic::Sequence,
&self.zmqpubsequence,
self.zmqpubsequencehwm,
);

@coderabbitai coderabbitai Bot Aug 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 10 \
  'ZmqPublication|ZmqTopic::Sequence|Sequence|next_sequence|counter|send(_multipart)?|publish' \
  crates/node/src/zmq_publisher.rs \
  crates/node/src/lib.rs

Repository: gosuda/bitcoin-rs

Length of output: 41396


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- publish implementation and sequence call sites ---'
sed -n '221,390p' crates/node/src/zmq_publisher.rs
rg -n -C 8 'publish_sequence|SequenceEvent::(Connected|Disconnected)' crates/node/src
printf '%s\n' '--- static order check ---'
python3 - <<'PY'
from pathlib import Path

text = Path("crates/node/src/zmq_publisher.rs").read_text()
increment = text.index("self.counters[topic.index()].fetch_add")
send = text.index("socket.send_multipart")
assert increment < send, "counter increment is not before send"
print("counter increment precedes socket.send_multipart")
print("counter scope: SocketZmqPublisher.counters[topic.index()]")
print("sequence endpoints receive the same sequence value from one publish() call")
PY

Repository: gosuda/bitcoin-rs

Length of output: 20614


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -i -C 5 \
  'sequence topic|sequence number|sequence counter|pubsequence|sequence_payload|SequenceEvent|failed.*send|send.*fail|notification.*sequence' \
  README.md docs crates .github 2>/dev/null || true

printf '%s\n' '--- publisher state and test definitions ---'
rg -n -C 8 \
  'struct .*Publisher|next_sequence|AtomicU32|counters|events:|publish_sequence' \
  crates/node/src/apply.rs crates/node/src/zmq_publisher.rs

Repository: gosuda/bitcoin-rs

Length of output: 50373


Do not consume sequence numbers on failed sends.

SocketZmqPublisher correctly shares one counter for Connected and Disconnected events and reuses one value across all sequence endpoints. However, fetch_add runs before send_multipart, so a failed send advances the counter. Change the allocation logic and add a failure test so an undelivered event does not create a sequence gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/node/src/config.rs` around lines 405 - 410, Update the sequence
allocation flow in push_zmq_publications and SocketZmqPublisher so the shared
sequence counter advances only after send_multipart succeeds; failed sends must
not consume a sequence number or create gaps. Preserve reuse of one sequence
value across all sequence endpoints and Connected/Disconnected events. Add a
failure-path test verifying the next successfully delivered event receives the
undelivered event’s sequence number.

Source: MCP tools

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Keeping the current design here, deliberately, on both halves of this.

Increment on a dropped send. The counter's purpose in this protocol is loss detection: our sockets send with DONTWAIT, so a send that fails at HWM means the message is gone, and the subscriber never sees it either way. Consuming the number is what makes that loss visible as a gap — which for the enforcer is a loud fatal error instead of a silently missing block connect. Holding the number back would make a dropped C indistinguishable from no event at all, which is the worse failure for a validator driven off this stream. That is also what a subscriber observes from Core when its ZMQ layer drops at HWM.

Per-endpoint counters. All four existing topics share one counter per topic across that topic's endpoints, and one publish() sends the same number to all of them, so each subscriber sees a contiguous stream in the ordinary single-endpoint deployment. Splitting the counter per endpoint is a rework of SocketZmqPublisher's counter model affecting every topic — out of scope for adding one topic, and I would rather not change the semantics of hashblock/rawblock as a side effect of this PR. Noted as a real divergence if someone binds one topic to several endpoints with differing failure behavior.

Both properties were exercised in the live run: contiguous per-topic counters across 200 blocks and the reorg, and independent counters across all five topics on one node.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment thread docs/solutions/architecture-patterns/node-reorg-execution-design.md Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3e646cee35

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


pub(crate) fn sequence_payload(event: SequenceEvent) -> [u8; 33] {
let mut body = [0_u8; 33];
body[..32].copy_from_slice(&hash_body_from_hash(event.hash()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use Core's little-endian hash encoding

When a Core-compatible subscriber decodes any C or D event, this reuses hash_body_from_hash, which reverses Hash256's consensus little-endian bytes into display order. Core's sequence payload carries the raw 32-byte hash in little-endian order, so standard subscribers that reverse the payload for display will derive the wrong block hash and fail to resolve the event; copy event.hash().to_le_bytes() directly before appending the label.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Declining this one — it would break the parity it is trying to protect, and I have wire evidence.

I ran bitcoin-rs alongside Bitcoin Core 27.0 on regtest with both nodes publishing sequence, and diffed the captured frames: for 200 blocks plus a reorg, frame[1][0..32] from our publisher was byte-identical to Core's own payload for the same block (core sequence payload list identical to rs: True in the runtime comment), and equal to getblockhash's display form. Core's ZMQ layer serializes hashes reversed relative to their internal order, so display order is the wire order here — same as our existing hashblock topic, which is why the helper is shared.

The consumer confirms it independently: cusf-enforcer-mempool's parser does hash.reverse() on those 32 bytes before BlockHash::from_byte_array, i.e. it expects display order on the wire. Sending to_le_bytes() directly would leave every hash reversed for both Core-compatible subscribers and the enforcer.

Comment thread crates/node/src/apply.rs Outdated
Comment on lines +2328 to +2330
handles
.zmq_publisher
.publish_sequence(crate::zmq_publisher::SequenceEvent::Connected(tip.hash));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Publish connect events after advancing the applied tip

When a subscriber reacts immediately to a C event and queries getblockcount, getbestblockhash, or getblockchaininfo, the event is sent before applied_tip is updated at line 2345, so RPC can still report the preceding block. The window can include all subsequent raw-block and per-transaction notifications, and it is inconsistent with disconnect events, which are emitted after their tip store; publish the connect event only after the new applied tip becomes visible.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correct, and the same point CodeRabbit raised — fixing it. The window you describe is the one that matters in practice: the enforcer's C handler immediately calls back into the node, so it can act on a tip the RPC surface has not published yet. Publishing after applied_tip is stored also makes C symmetric with D, which already waits for its rollback.

@devin-ai-integration

Copy link
Copy Markdown

Runtime verification of the sequence topic against Bitcoin Core 27.0 (regtest)

Built 3e646ce with --no-default-features --features fjall, peered it to a real Core 27.0 regtest node publishing its own -zmqpubsequence, subscribed to both publishers and diffed the streams frame by frame.

Reorg ordering — DDDCCCCC, tip-first disconnects, contiguous counter, matches Core

Old branch 500/501/502 invalidated in Core, 5 blocks mined on a longer branch:

--- bitcoin-rs sequence (8 messages)
   D 01d589ffe3797c69... old h=502 counter=503
   D 2145a496dcef938c... old h=501 counter=504 delta=1
   D 6fb121d4f7511504... old h=500 counter=505 delta=1
   C 17f214a721645349... new h=500 counter=506 delta=1
   C 4385a33683d10902... new h=501 counter=507 delta=1
   C 2409786e3f1d8228... new h=502 counter=508 delta=1
   C 6d52f8080d899c67... new h=503 counter=509 delta=1
   C 7f41de9ef3f8a23a... new h=504 counter=510 delta=1
   label string: DDDCCCCC

--- Bitcoin Core sequence (10 messages)
   label string: DDDRRCCCCC     (the two R are Core's mempool events; we emit none by design)
   same D/C hashes, same order, per-message delta 1
 3 D and 5 C:                                        True
 every D precedes every C:                           True
 D hashes tip-first descending (502,501,500):        True
 C hashes ascending new branch 500..504:             True
 counter contiguous (+1) across whole window:        True (503..510)
 contiguous with pre-reorg last counter 502:         True
 all payloads 33 bytes / 3 frames:                   {33} {3}
Steady state: 200 blocks, wire format and hash parity with Core
=== rs_seq: 200 messages ===
topics={'sequence'} nframes={3} payload_lens=[33] frame2_lens={4}
counter first=301 last=500 distinct_deltas=[1]      label runs: Cx200

rs sequence hashes == Core getblockhash(301..500) ascending, in order: True
payload[0:32] == our own hashblock payload for the same block:         True
core sequence payload list identical to rs:                            True
sample: 455407bbf7d84359755f75cd64b1c517a29928714bdea4cbc145ee9f72c6b08a|43  seq=301
        core getblockhash(301) = 455407bbf7d84359755f75cd64b1c517a29928714bdea4cbc145ee9f72c6b08a
Mempool silence, config surfaces, getzmqnotifications, subscriber churn, other topics

Three sendtoaddress txs in Core → Core emitted three 41-byte A events; bitcoin-rs emitted zero messages, and the next C counter was exactly previous+1 (500 → 501), so no hidden counter advance. Two R events during the reorg likewise produced nothing here. (Caveat: our own mempool stayed empty — relayed txs are not ingested and sendrawtransaction does not add to getrawmempool — so this is proven for relayed/submitted txs, not against a populated mempool.)

CLI  --zmqpubsequence tcp://…:28360 --zmqpubsequencehwm 42 -> {"hwm":42,"type":"pubsequence",…}
ENV  BITCOIN_RS_ZMQPUBSEQUENCE(+HWM=43)                    -> {"hwm":43,"type":"pubsequence",…}
CONF zmqpubsequence= / zmqpubsequencehwm=44                -> {"hwm":44,"type":"pubsequence",…}
each socket then delivered the C for mined block 16043f86…cff3 (33-byte payload, 3 frames)
node without --zmqpubsequence: no pubsequence entry

getzmqnotifications keys are exactly Core's type/address/hwm.

Subscriber churn: 10 blocks with no subscriber (node reached 515), late subscriber got its 5 Cs (522–526), subscriber killed mid-stream while 10 more were mined (node reached 530, 0 panics), re-attach got 5 more (537–541). Counter jumps only across unsubscribed windows (normal PUB drop).

Regression, all five topics on one node:

rs_seq       msgs=210 counter 301..510 deltas=[1]
rs_hashblock msgs=207 counter 301..507 deltas=[1]
rs_hashtx    msgs=207 counter 301..507 deltas=[1]
rs_rawblock  msgs=207 counter 301..507 deltas=[1]
rs_rawtx     msgs=207 counter 301..507 deltas=[1]

Counters are independent; sequence is ahead by exactly the 3 reorg D events.

Not verified: the real bip300301_enforcer as a live consumer — it requires /rest/chaininfo.json (that's #49) and, pointed at real Core instead, fails in its own getblockchaininfo deserialization, so it never reached the ZMQ subscription.

Two unrelated pre-existing issues this run surfaced, neither touched here: the portable (--no-default-features --features fjall) build cannot apply a block containing a non-taproot spend (portable script backend cannot verify this non-taproot spend) and stalls sync at that block — so such a block is neither C'd nor D'd; and bitcoin-rs's mempool never populates from relayed transactions or from sendrawtransaction, which is what prevented proving mempool silence against a populated pool.

@metaphorics

Copy link
Copy Markdown
Contributor Author

@devin Resolve all the feedbacks if relevent.

@devin-ai-integration

Copy link
Copy Markdown

Went through all four:

  • Publish C after the applied tip is stored (CodeRabbit, Codex): valid and being fixed — a subscriber reacting to C could still be told the parent is the tip, and it was inconsistent with D. Adding the suggested test that reads applied_tip at emission time.
  • Reorg-design "Open" table row (CodeRabbit): valid, renaming it to name the missing mempool A/R work.
  • Use raw little-endian hash bytes (Codex P1): declined — the live capture against Core 27.0 shows our payload is byte-identical to Core's for the same blocks, and the enforcer's parser reverses those bytes, so this change would invert every hash. Details in the thread.
  • Per-endpoint counters, don't consume on failed send (CodeRabbit): declined with reasoning in the thread — a consumed number is how a subscriber detects an HWM drop, and per-endpoint counters would rework the counter model of all four existing topics.

Pushing the two fixes shortly.

@metaphorics

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 17ea5c2eeb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/node/src/apply.rs Outdated
Comment on lines +1222 to +1225
if handles.zmq_publisher.wants_notifications() {
handles
.zmq_publisher
.publish_sequence(crate::zmq_publisher::SequenceEvent::Disconnected(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit the disconnect event before marker completion can fail

When complete_disconnect returns an error (for example, a backend write or flush failure), applied_tip has already moved to the parent and the error is explicitly classified as MarkerStuck, meaning the rollback completed, but this later publication block is skipped by ?. A pubsequence subscriber therefore remains on the disconnected block even though RPC exposes the parent tip; publish D immediately after the tip store, before this fallible marker cleanup.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Valid, and it's the mirror image of the connect bug you caught earlier — fixing it the same way.

complete_disconnect runs after applied_tip has already been stored to the parent, which is precisely why its failure is classified MarkerStuck rather than a rollback failure: the rollback happened. Propagating with ? before the emission means a subscriber is left believing it is still on a block the node has dropped, while RPC reports the parent — the enforcer would keep building on a tip that no longer exists.

Moving the D emission to immediately after the applied-tip store, before the fallible marker cleanup, so both labels are published at the point the mutation becomes visible. The reorg ordering test still guards that every D precedes the replacement branch's Cs.

@metaphorics

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 2cd1be22bc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

1 participant