22#include " node.hpp"
33#include " share.hpp"
44#include " mint_runloop.hpp" // dash::mint::elect_best_share (election policy SSOT)
5+ #include " known_txs_retention.hpp" // dash::retain_template_txs / all_txs_backable (F1/F3)
56
67#include < core/uint256.hpp>
78#include < core/common.hpp>
@@ -1140,27 +1141,43 @@ void NodeImpl::broadcast_share(const uint256& share_hash)
11401141 if (!m_chain->contains (share_hash))
11411142 return ;
11421143
1143- // Walk back up to 5 shares, collecting the not-yet-broadcast ones.
1144+ // Walk back up to 5 shares, collecting the not-yet-broadcast ones. code
1145+ // review F2: do NOT mark them shared here — the tx-completeness gate in
1146+ // send_shares can skip a share for ALL peers (unbacked now, or zero peers),
1147+ // and marking before the send would leave a skipped chain TIP permanently
1148+ // "already shared" and never re-pushed → silent PPLNS-credit loss. We mark
1149+ // only what actually reached >= 1 peer, below.
11441150 std::vector<uint256> to_send;
11451151 int32_t height = m_chain->get_height (share_hash);
11461152 int32_t walk = std::min (height, 5 );
11471153 for (auto [hash, data] : m_chain->get_chain (share_hash, walk)) {
11481154 if (m_shared_share_hashes.count (hash))
11491155 break ;
1150- m_shared_share_hashes.insert (hash);
11511156 to_send.push_back (hash);
11521157 }
11531158 if (to_send.empty ())
11541159 return ;
11551160
1156- for (auto & [nonce, peer] : m_peers)
1157- send_shares (peer, to_send);
1161+ std::set<uint256> actually_sent;
1162+ for (auto & [nonce, peer] : m_peers) {
1163+ auto sent = send_shares (peer, to_send);
1164+ actually_sent.insert (sent.begin (), sent.end ());
1165+ }
1166+ // Mark ONLY shares that reached a peer; anything skipped for every peer (or
1167+ // with no peers connected) stays un-marked and is retried on the next
1168+ // broadcast — once its txs are retained or a peer appears.
1169+ for (const auto & h : actually_sent)
1170+ m_shared_share_hashes.insert (h);
11581171}
11591172
1160- void NodeImpl::send_shares (peer_ptr peer, const std::vector<uint256>& share_hashes)
1173+ std::vector<uint256> NodeImpl::send_shares (peer_ptr peer,
1174+ const std::vector<uint256>& share_hashes)
11611175{
11621176 // Caller (broadcast_share) already holds a shared tracker lock; this
11631177 // method only READS chain + m_known_txs and writes to the peer socket.
1178+ // Returns the hashes of the shares actually SENT to this peer (F2): the
1179+ // caller marks only those as broadcast, so a share skipped for all peers is
1180+ // retried rather than silently withheld.
11641181 std::vector<ShareType> shares;
11651182 for (const auto & hash : share_hashes) {
11661183 if (!m_chain->contains (hash))
@@ -1171,7 +1188,58 @@ void NodeImpl::send_shares(peer_ptr peer, const std::vector<uint256>& share_hash
11711188 }
11721189 }
11731190 if (shares.empty ())
1174- return ;
1191+ return {};
1192+
1193+ // ── Tx-completeness gate (canonical p2pool p2p.py:404 parity) ───────────
1194+ // A v13+ share references its new transactions by hash
1195+ // (new_transaction_hashes); a canonical peer that cannot resolve those txs
1196+ // DISCONNECTS us with "referenced unknown transaction" (p2p.py:403-404,
1197+ // handle_shares / handle_remember_tx). We re-announce the best chain's head
1198+ // and up to 5 ancestors (ROOT-2 re-announce, broadcast_share), and shares
1199+ // pulled via sharereply arrive WITHOUT their txs (the reply carries no tx
1200+ // bytes), so their tx bytes are absent from m_known_txs. Sending such a
1201+ // share unbacked is exactly what makes the public canonical seeds drop the
1202+ // connection ~300ms after handshake — the isolation root cause.
1203+ //
1204+ // So only broadcast a share when we HOLD the bytes of EVERY referenced new
1205+ // tx (m_known_txs), so remember_tx below can always forward them. code
1206+ // review F3: gating on m_remote_txs (the peer's have_tx advert) alone is
1207+ // weaker than canonical — that advert can be stale (the peer may have dropped
1208+ // the tx), and sending hash-only for a tx the peer no longer holds is exactly
1209+ // the disconnect. Requiring the bytes is the canonical invariant
1210+ // (sendShares broadcasts only txs in known_txs); m_remote_txs is used ONLY
1211+ // below to choose hash-vs-bytes encoding. Skipping the rest costs no
1212+ // propagation — a downloaded share is by definition already on the network we
1213+ // fetched it from, and our own minted shares are backable by construction
1214+ // (add_local_share declines a solve whose txs are not retained).
1215+ {
1216+ std::vector<ShareType> sendable;
1217+ sendable.reserve (shares.size ());
1218+ size_t skipped_incomplete = 0 ;
1219+ for (auto & share : shares) {
1220+ bool backable = true ;
1221+ share.invoke ([&](auto * obj) {
1222+ backable = dash::all_txs_backable (obj->m_new_transaction_hashes ,
1223+ m_known_txs);
1224+ });
1225+ if (backable)
1226+ sendable.push_back (std::move (share));
1227+ else
1228+ ++skipped_incomplete;
1229+ }
1230+ if (skipped_incomplete > 0 ) {
1231+ static int skip_log = 0 ;
1232+ if (skip_log++ % 20 == 0 )
1233+ LOG_INFO << " [send_shares] skipped " << skipped_incomplete
1234+ << " share(s) whose new-tx bytes we do not hold "
1235+ " (downloaded via sharereply / template-evicted) to "
1236+ << peer->addr ().to_string ()
1237+ << " — avoids canonical 'unknown transaction' disconnect" ;
1238+ }
1239+ if (sendable.empty ())
1240+ return {};
1241+ shares.swap (sendable);
1242+ }
11751243
11761244 // Forward the txs the peer needs BEFORE the shares (p2pool remember_tx
11771245 // protocol) — a share referencing unknown txs makes the receiving oracle
@@ -1225,6 +1293,14 @@ void NodeImpl::send_shares(peer_ptr peer, const std::vector<uint256>& share_hash
12251293
12261294 LOG_INFO << " [Pool] Sent " << shares.size () << " share(s) (+"
12271295 << needed_txs.size () << " tx refs) to " << peer->addr ().to_string ();
1296+
1297+ // F2: report exactly which shares reached this peer so the caller marks only
1298+ // those broadcast (a share skipped for all peers stays un-marked → retried).
1299+ std::vector<uint256> sent;
1300+ sent.reserve (shares.size ());
1301+ for (auto & share : shares)
1302+ sent.push_back (share.hash ());
1303+ return sent;
12281304}
12291305
12301306uint256 NodeImpl::add_local_share (ShareType share)
@@ -1248,6 +1324,31 @@ uint256 NodeImpl::add_local_share(ShareType share)
12481324 return uint256::ZERO ;
12491325 }
12501326
1327+ // F1-sub (by-construction backability): DECLINE a solve whose template
1328+ // txs are no longer retained in m_known_txs, rather than mint a share
1329+ // send_shares would then have to withhold. FrozenJobRegistry
1330+ // (mint_runloop.hpp) is a FIFO cap-512 with NO TTL and re-put refreshes,
1331+ // so a solve can arrive from an arbitrarily-old job — no finite retention
1332+ // window is a provable bound, so the only by-construction guarantee is to
1333+ // fail closed here: every share we add is fully forwardable. The miner
1334+ // keeps the pseudoshare credit; the next solve on a fresh job mints. The
1335+ // deduped rolling template window (register_template_txs) keeps declines
1336+ // rare — this fires only for genuinely stale solves past the window.
1337+ {
1338+ bool backable = true ;
1339+ share.invoke ([&](auto * obj) {
1340+ backable = dash::all_txs_backable (obj->m_new_transaction_hashes ,
1341+ m_known_txs);
1342+ });
1343+ if (!backable) {
1344+ LOG_WARNING << " [MINT] local share " << hash.GetHex ().substr (0 , 16 )
1345+ << " references template tx(s) no longer retained — "
1346+ " declined (stale job past retention window; retry "
1347+ " on next solve)" ;
1348+ return uint256::ZERO ;
1349+ }
1350+ }
1351+
12511352 m_tracker.add (share);
12521353 // Local share: verify inline (p2pool: set_best_share → think verifies;
12531354 // the inline attempt keeps the just-mint tip usable immediately).
@@ -1274,17 +1375,22 @@ void NodeImpl::register_template_txs(const std::vector<coin::Transaction>& txs,
12741375 << " hashes=" << hashes.size () << " — skipped" ;
12751376 return ;
12761377 }
1277- // Drop the PREVIOUS template's leftovers that the new template no longer
1278- // carries, then insert the new set — m_known_txs stays bounded by one
1279- // template (plus reception-protocol remembered txs, which are peer-scoped).
1280- std::set<uint256> new_set (hashes.begin (), hashes.end ());
1281- for (const auto & old_hash : m_template_tx_hashes) {
1282- if (!new_set.count (old_hash))
1283- m_known_txs.erase (old_hash);
1284- }
1285- for (size_t i = 0 ; i < txs.size (); ++i)
1286- m_known_txs.insert_or_assign (hashes[i], txs[i]);
1287- m_template_tx_hashes = std::move (new_set);
1378+ // Retain a rolling window of the last RETAINED_TEMPLATE_TX_SETS DISTINCT
1379+ // templates; a tx leaves m_known_txs only once it has fallen out of EVERY
1380+ // retained set — so a share minted on any recent job (the miner may solve a
1381+ // job whose template has since rotated) and a ROOT-2 re-announce a few
1382+ // rotations later can still forward the referenced tx bytes via remember_tx.
1383+ //
1384+ // code review F1: register_template_txs is called per producer-job cache
1385+ // MISS = per (prev_share_hash, payout_script) (main_dash.cpp:1539, key
1386+ // :1450), NOT per template rotation. A ~50-payout-script cluster re-registers
1387+ // the SAME template ~50 times on one tip, so pushing each unconditionally
1388+ // would collapse the window to a single template. retain_template_txs()
1389+ // dedups by TEMPLATE IDENTITY (the tx-hash set): a set already retained just
1390+ // refreshes recency and consumes no slot, so the window holds N DISTINCT
1391+ // templates (≈ minutes of stale-solve coverage), not N re-registrations.
1392+ dash::retain_template_txs (m_recent_template_tx_sets, m_known_txs,
1393+ hashes, txs, RETAINED_TEMPLATE_TX_SETS );
12881394}
12891395
12901396} // namespace dash
0 commit comments