Skip to content

btc: F3 known-txs retention/backable primitives + unit KAT - #868

Merged
frstrtr merged 1 commit into
masterfrom
btc/f3-known-txs-retention-header
Jul 26, 2026
Merged

btc: F3 known-txs retention/backable primitives + unit KAT#868
frstrtr merged 1 commit into
masterfrom
btc/f3-known-txs-retention-header

Conversation

@frstrtr

@frstrtr frstrtr commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Ports the pure known-txs bookkeeping from the DASH variant to src/impl/btc/known_txs_retention.hpp:

  • btc::retain_template_txs() — rolling-window retention with F1 template-identity (tx-hash SET) dedup.
  • btc::all_txs_backable() — the F3 broadcast gate (hold the bytes of every referenced new-tx).

New src/impl/btc/test/known_txs_retention_test.cpp (rides btc_share_test, no new target): dedup + eviction correctness + the backable gate. Defeating the gate flips BtcKnownTxsBackable.UnheldTxIsNotBackable red. btc_share_test 112/112 green.

Scope: src/impl/btc/ only; nothing lifted to bitcoin_family/ or src/core/. Header primitive + coverage only — call-site wiring at send_shares/mint is deferred pending a structural decision (BTC v35 shares carry no per-share tx refs).

Port the pure, unit-testable known-txs bookkeeping from the DASH variant to
src/impl/btc/known_txs_retention.hpp:

  - retain_template_txs(): rolling-window retention with F1 template-identity
    (tx-hash SET) dedup — N re-registrations of one template consume one slot
    while the window still holds N distinct templates; a tx leaves known_txs
    only once absent from every retained set.
  - all_txs_backable(): the F3 broadcast gate — a share is backable only when
    we hold the bytes of every referenced new-tx.

Adds src/impl/btc/test/known_txs_retention_test.cpp (rides btc_share_test):
dedup + eviction correctness, and the backable gate (defeating the gate flips
BtcKnownTxsBackable.UnheldTxIsNotBackable red). btc_share_test 112/112 green.

This is the header primitive + coverage only. Call-site wiring at send_shares
and the mint path is deferred: BTC v35 shares carry no per-share tx refs, so
that wiring is a structural design decision tracked with the integrator.
@frstrtr
frstrtr merged commit c77dc03 into master Jul 26, 2026
26 checks passed
frstrtr added a commit that referenced this pull request Jul 26, 2026
… was sent

Two reward-critical share-loss fixes, ported to BTC, DGB and BCH from the
already-correct DASH implementation (src/impl/dash/node.cpp).

F3 -- tx-completeness broadcast gate. send_shares() collected the referenced
new-tx bytes with `if (it != m_known_txs.end()) full_txs.emplace_back(...)`
and no else branch: a tx we do not hold was silently omitted and the share was
written to the peer anyway. Canonical p2pool then drops the connection on
"referenced unknown transaction" (p2p.py), which isolates us from the
sharechain and orphans our shares. Shares pulled via sharereply arrive without
their tx bytes, so this is reachable in normal operation. send_shares now
filters the outgoing batch through partition_backable() (gate on the BYTES in
m_known_txs, not on the peer's possibly-stale have_tx advert) and the
now-unreachable omission path increments a counter that logs a warning.

F2 -- mark only what was sent. broadcast_share() inserted each walked hash
into m_shared_share_hashes INSIDE the chain-walk loop, before send_shares ran.
send_shares returned void and has abandon paths (tracker try_to_lock miss,
empty batch, and now the F3 gate), and zero connected peers abandons the batch
too. A marked-but-unsent share is lost permanently: the next walk breaks on
the first marked hash, so nothing ever re-pushes it -- silent PPLNS-credit
loss with no retry path. send_shares now returns the hashes it actually wrote
and broadcast_share marks only those, after the peer loop, via
broadcast_and_mark(). m_last_broadcast_to likewise records what was written
rather than what was offered, so a withheld share is never blamed for a peer
disconnect (which would mark it rejected and block all future re-broadcast).

Consensus surface: NONE. Both changes decide only WHETHER a share goes on the
wire. Share bytes, minting, PPLNS weighting and payout are untouched, and a
withheld share stays in the chain and is re-offered once its txs arrive.

Primitives follow the per-coin header pattern established for BTC in #868:
src/impl/{btc,dgb,bch}/known_txs_retention.hpp. The LTC lane is concurrently
hoisting all_txs_backable into src/core/; these four per-coin headers
(including dash) should be collapsed onto that hoist once both land.

Tests ride existing allowlisted CI targets -- no new add_executable, no
build.yml change:
  btc  -> btc_share_test  (broadcast_gate_test.cpp)
  dgb  -> dgb_share_test  (broadcast_gate_test.cpp, known_txs_retention_test.cpp)
  bch  -> bch_embedded_block_broadcast_test (broadcast_gate_test.cpp; the bch
          test tree has no GTest harness, so the TU exposes a checks function
          the host main() calls)

Mutation-verified red: deleting the gate reddens 2 btc / 4 bch assertions;
restoring mark-before-send reddens 4 btc / 5 bch assertions.
frstrtr added a commit that referenced this pull request Jul 26, 2026
… was sent

Two reward-critical share-loss fixes, ported to BTC, DGB and BCH from the
already-correct DASH implementation (src/impl/dash/node.cpp).

F3 -- tx-completeness broadcast gate. send_shares() collected the referenced
new-tx bytes with `if (it != m_known_txs.end()) full_txs.emplace_back(...)`
and no else branch: a tx we do not hold was silently omitted and the share was
written to the peer anyway. Canonical p2pool then drops the connection on
"referenced unknown transaction" (p2p.py), which isolates us from the
sharechain and orphans our shares. Shares pulled via sharereply arrive without
their tx bytes, so this is reachable in normal operation. send_shares now
filters the outgoing batch through partition_backable() (gate on the BYTES in
m_known_txs, not on the peer's possibly-stale have_tx advert) and the
now-unreachable omission path increments a counter that logs a warning.

F2 -- mark only what was sent. broadcast_share() inserted each walked hash
into m_shared_share_hashes INSIDE the chain-walk loop, before send_shares ran.
send_shares returned void and has abandon paths (tracker try_to_lock miss,
empty batch, and now the F3 gate), and zero connected peers abandons the batch
too. A marked-but-unsent share is lost permanently: the next walk breaks on
the first marked hash, so nothing ever re-pushes it -- silent PPLNS-credit
loss with no retry path. send_shares now returns the hashes it actually wrote
and broadcast_share marks only those, after the peer loop, via
broadcast_and_mark(). m_last_broadcast_to likewise records what was written
rather than what was offered, so a withheld share is never blamed for a peer
disconnect (which would mark it rejected and block all future re-broadcast).

Consensus surface: NONE. Both changes decide only WHETHER a share goes on the
wire. Share bytes, minting, PPLNS weighting and payout are untouched, and a
withheld share stays in the chain and is re-offered once its txs arrive.

Primitives follow the per-coin header pattern established for BTC in #868:
src/impl/{btc,dgb,bch}/known_txs_retention.hpp. The LTC lane is concurrently
hoisting all_txs_backable into src/core/; these four per-coin headers
(including dash) should be collapsed onto that hoist once both land.

Tests ride existing allowlisted CI targets -- no new add_executable, no
build.yml change:
  btc  -> btc_share_test  (broadcast_gate_test.cpp)
  dgb  -> dgb_share_test  (broadcast_gate_test.cpp, known_txs_retention_test.cpp)
  bch  -> bch_embedded_block_broadcast_test (broadcast_gate_test.cpp; the bch
          test tree has no GTest harness, so the TU exposes a checks function
          the host main() calls)

Mutation-verified red: deleting the gate reddens 2 btc / 4 bch assertions;
restoring mark-before-send reddens 4 btc / 5 bch assertions.
frstrtr added a commit that referenced this pull request Jul 27, 2026
… was sent

Two reward-critical share-loss fixes, ported to BTC, DGB and BCH from the
already-correct DASH implementation (src/impl/dash/node.cpp).

F3 -- tx-completeness broadcast gate. send_shares() collected the referenced
new-tx bytes with `if (it != m_known_txs.end()) full_txs.emplace_back(...)`
and no else branch: a tx we do not hold was silently omitted and the share was
written to the peer anyway. Canonical p2pool then drops the connection on
"referenced unknown transaction" (p2p.py), which isolates us from the
sharechain and orphans our shares. Shares pulled via sharereply arrive without
their tx bytes, so this is reachable in normal operation. send_shares now
filters the outgoing batch through partition_backable() (gate on the BYTES in
m_known_txs, not on the peer's possibly-stale have_tx advert) and the
now-unreachable omission path increments a counter that logs a warning.

F2 -- mark only what was sent. broadcast_share() inserted each walked hash
into m_shared_share_hashes INSIDE the chain-walk loop, before send_shares ran.
send_shares returned void and has abandon paths (tracker try_to_lock miss,
empty batch, and now the F3 gate), and zero connected peers abandons the batch
too. A marked-but-unsent share is lost permanently: the next walk breaks on
the first marked hash, so nothing ever re-pushes it -- silent PPLNS-credit
loss with no retry path. send_shares now returns the hashes it actually wrote
and broadcast_share marks only those, after the peer loop, via
broadcast_and_mark(). m_last_broadcast_to likewise records what was written
rather than what was offered, so a withheld share is never blamed for a peer
disconnect (which would mark it rejected and block all future re-broadcast).

Consensus surface: NONE. Both changes decide only WHETHER a share goes on the
wire. Share bytes, minting, PPLNS weighting and payout are untouched, and a
withheld share stays in the chain and is re-offered once its txs arrive.

Primitives follow the per-coin header pattern established for BTC in #868:
src/impl/{btc,dgb,bch}/known_txs_retention.hpp. The LTC lane is concurrently
hoisting all_txs_backable into src/core/; these four per-coin headers
(including dash) should be collapsed onto that hoist once both land.

Tests ride existing allowlisted CI targets -- no new add_executable, no
build.yml change:
  btc  -> btc_share_test  (broadcast_gate_test.cpp)
  dgb  -> dgb_share_test  (broadcast_gate_test.cpp, known_txs_retention_test.cpp)
  bch  -> bch_embedded_block_broadcast_test (broadcast_gate_test.cpp; the bch
          test tree has no GTest harness, so the TU exposes a checks function
          the host main() calls)

Mutation-verified red: deleting the gate reddens 2 btc / 4 bch assertions;
restoring mark-before-send reddens 4 btc / 5 bch assertions.
frstrtr added a commit that referenced this pull request Jul 29, 2026
… was sent (#880)

* btc/dgb/bch: gate share broadcast on tx completeness + mark only what was sent

Two reward-critical share-loss fixes, ported to BTC, DGB and BCH from the
already-correct DASH implementation (src/impl/dash/node.cpp).

F3 -- tx-completeness broadcast gate. send_shares() collected the referenced
new-tx bytes with `if (it != m_known_txs.end()) full_txs.emplace_back(...)`
and no else branch: a tx we do not hold was silently omitted and the share was
written to the peer anyway. Canonical p2pool then drops the connection on
"referenced unknown transaction" (p2p.py), which isolates us from the
sharechain and orphans our shares. Shares pulled via sharereply arrive without
their tx bytes, so this is reachable in normal operation. send_shares now
filters the outgoing batch through partition_backable() (gate on the BYTES in
m_known_txs, not on the peer's possibly-stale have_tx advert) and the
now-unreachable omission path increments a counter that logs a warning.

F2 -- mark only what was sent. broadcast_share() inserted each walked hash
into m_shared_share_hashes INSIDE the chain-walk loop, before send_shares ran.
send_shares returned void and has abandon paths (tracker try_to_lock miss,
empty batch, and now the F3 gate), and zero connected peers abandons the batch
too. A marked-but-unsent share is lost permanently: the next walk breaks on
the first marked hash, so nothing ever re-pushes it -- silent PPLNS-credit
loss with no retry path. send_shares now returns the hashes it actually wrote
and broadcast_share marks only those, after the peer loop, via
broadcast_and_mark(). m_last_broadcast_to likewise records what was written
rather than what was offered, so a withheld share is never blamed for a peer
disconnect (which would mark it rejected and block all future re-broadcast).

Consensus surface: NONE. Both changes decide only WHETHER a share goes on the
wire. Share bytes, minting, PPLNS weighting and payout are untouched, and a
withheld share stays in the chain and is re-offered once its txs arrive.

Primitives follow the per-coin header pattern established for BTC in #868:
src/impl/{btc,dgb,bch}/known_txs_retention.hpp. The LTC lane is concurrently
hoisting all_txs_backable into src/core/; these four per-coin headers
(including dash) should be collapsed onto that hoist once both land.

Tests ride existing allowlisted CI targets -- no new add_executable, no
build.yml change:
  btc  -> btc_share_test  (broadcast_gate_test.cpp)
  dgb  -> dgb_share_test  (broadcast_gate_test.cpp, known_txs_retention_test.cpp)
  bch  -> bch_embedded_block_broadcast_test (broadcast_gate_test.cpp; the bch
          test tree has no GTest harness, so the TU exposes a checks function
          the host main() calls)

Mutation-verified red: deleting the gate reddens 2 btc / 4 bch assertions;
restoring mark-before-send reddens 4 btc / 5 bch assertions.

* dgb: rescope to the reachable readvertise path; label broadcast_share pre-wiring

A caller-side reachability trace shows DGB is not symmetric with btc/bch:
main_dgb.cpp binds no create/mint-share fn, so dgb::NodeImpl::broadcast_share
and notify_local_share have zero callers repo-wide -- DGB never mints a local
share today (#884). DGB's live path into send_shares is readvertise_best_share()
(ROOT-2 re-advert on best-change plus a 10s one-shot timer), which re-pushes
PEER-RECEIVED shares and deliberately bypasses the de-dup set.

Verified independently: set_mint_share_fn / set_create_share_fn / broadcast_share
/ notify_local_share have zero occurrences in main_dgb.cpp; the only
set_mint_share_fn binding in the tree is main_dash.cpp:1854 plus dgb's own
work_source_test. readvertise_best_share is called from node.cpp:1767 and 1774.
BTC (main_btc.cpp:1336 after lk.unlock() at :1323) and BCH
(pool_entrypoint.hpp:564 after lk.unlock() at :562) are both live and unchanged.

Changes:

- dgb: extract the readvertise recording rule into readvertise_and_record() and
  wire readvertise_best_share() through it. This is DGB's live reward-critical
  marking discipline: m_last_broadcast_to is what a peer disconnect within 10s
  converts into m_rejected_share_hashes, and the readvertise walk `continue`s
  past rejected hashes permanently. Recording the OFFERED batch would let a
  share the F3 gate merely withheld be blamed for someone else's drop and
  excluded from every future re-advert -- a propagation loss for the whole
  sharechain, since DGB re-advertises peer-received shares.

- dgb: rescope the KATs onto that live path. The gate cases (partition_backable)
  and the new DgbReadvertiseRecording cases drive code production executes; the
  broadcast_and_mark cases are renamed
  DgbBroadcastMarkingPrewired_NotReachedToday so no reader mistakes them for
  live protection. Removes the broadcast_share de-dup-walk KAT, which modelled
  an unreachable function -- the shape of the LTC #873 miss.

- dgb: label NodeImpl::broadcast_share in source as pre-wiring for #884. The F2
  fix stays so the seam is correct when the mint path is bound and so the three
  coins remain symmetric, but it protects nothing at runtime today.

- btc/bch: document the ROOT-2 interaction at readvertise_best(). Marking is now
  a strict subset of what the pre-fix code marked, so the walk breaks later or
  in the same place, never earlier: the re-advert re-pushes a superset of what
  it used to. Monotonically better, strictly better for head shares minted while
  no peer was connected. It does not fully close ROOT-2 here -- shares already
  accepted by another peer stay marked and the walk still breaks, as before.
  That needs the ltc/dgb-style de-dup-bypass readvertise; pre-existing, out of
  scope. No behaviour change in this commit for btc/bch.

Mutation-verified red on the DGB live path: recording the offered batch instead
of what was written reddens WithheldShareIsNeverBlamedForAPeerDrop; dropping the
empty-send skip reddens PeerThatReceivedNothingIsNotRecorded; deleting the gate
reddens 2 DgbBroadcastGate cases.

* btc/bch/dgb: route F3 send-path tx-completeness gate through the per-coin new_tx_hashes SSOT

The old flat m_new_transaction_hashes probe was FALSE for every share
variant (v17/v33 nest the refs in m_tx_info; v34+ carry none), so the F3
gate collected empty refs and treated every share as vacuously backable
-- it never withheld an incomplete share on any version. Route each
coin through its SSOT accessor from #913/#914 (btc/bch::new_tx_hashes,
dgb::append_share_tx_refs) so the gate sees the real refs.

Adds BtcBroadcastGate.RealShareTxInfoRefsWithheld driving a REAL v17/v33
share (not a top-level mock); mutation-verify confirms it goes red when
the probe is reverted to the dead flat form.

* btc: mirror ltc broadcast lock-discipline trace; pin send-path reachability

Instrument btc::NodeImpl::broadcast_share with the deferred/acquired/
reached-send counters and IsNull/contains guards that ltc already
carries, and add broadcast_lock_discipline_test.cpp driving a REAL
btc::NodeImpl against a populated sharechain. Proves the F3 send path
(tx-completeness gate + send_shares + mark-after-send) is reachable, not
dead code behind an always-held lock: reached_send is 0 when the caller
holds the tracker mutex EXCLUSIVELY on the calling thread and 1 when it
does not -- the shape main_btc create_share_fn guarantees by dropping
the exclusive lock (lk.unlock) before calling broadcast_share. Rides the
allowlisted btc_share_test target (no new add_executable, #769).

---------

Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
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