Skip to content

Commit eb8616b

Browse files
authored
Merge pull request #913 from frstrtr/bch/tx-forward-probe-fix
bch: forward txs on the send path via bch::new_tx_hashes accessor (#905)
2 parents f068a23 + d83f539 commit eb8616b

5 files changed

Lines changed: 222 additions & 7 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ jobs:
478478
bch_donation_flip_kat_test \
479479
bch_cashtokens_transparency_test \
480480
bch_cashaddr_kat_test \
481+
bch_share_tx_refs_kat_test \
481482
bch_block_connector_test bch_block_ordering_test \
482483
bch_utxo_connect_test bch_reorg_connect_test bch_reorg_rerequest_test \
483484
bch_compact_block_connector_test \
@@ -613,6 +614,7 @@ jobs:
613614
bch_donation_flip_kat_test \
614615
bch_cashtokens_transparency_test \
615616
bch_cashaddr_kat_test \
617+
bch_share_tx_refs_kat_test \
616618
bch_block_connector_test bch_block_ordering_test \
617619
bch_utxo_connect_test bch_reorg_connect_test bch_reorg_rerequest_test \
618620
bch_compact_block_connector_test \

src/impl/bch/node.cpp

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-License-Identifier: AGPL-3.0-or-later
22
#include "node.hpp"
33

4+
#include "share_tx_refs.hpp" // bch::new_tx_hashes -- uniform send-side probe (#905)
5+
46
#include <core/common.hpp>
57
#include <core/hash.hpp>
68
#include <core/random.hpp>
@@ -722,14 +724,21 @@ void NodeImpl::send_shares(peer_ptr peer, const std::vector<uint256>& share_hash
722724
for (auto& share : shares)
723725
{
724726
share.invoke([&](auto* obj) {
725-
if constexpr (requires { obj->m_new_transaction_hashes; })
727+
// Route through the accessor instead of probing
728+
// obj->m_new_transaction_hashes directly (as this used to). That flat
729+
// probe is FALSE for every BCH variant -- v17/v33 nest the list inside
730+
// m_tx_info, v34+ carry no list -- so this whole forwarding block was
731+
// unreachable and BCH forwarded no tx bytes (#905). new_tx_hashes()
732+
// returns the nested list for v17/v33 and nullptr for v34+ (nothing to
733+
// forward), never a silent skip.
734+
const auto* new_txs = bch::new_tx_hashes(obj);
735+
if (!new_txs)
736+
return;
737+
for (const auto& th : *new_txs)
726738
{
727-
for (const auto& th : obj->m_new_transaction_hashes)
728-
{
729-
if (!peer->m_remote_txs.count(th) &&
730-
!peer->m_remembered_txs.count(th))
731-
needed_txs.insert(th);
732-
}
739+
if (!peer->m_remote_txs.count(th) &&
740+
!peer->m_remembered_txs.count(th))
741+
needed_txs.insert(th);
733742
}
734743
});
735744
}

src/impl/bch/share_tx_refs.hpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
//
3+
// Uniform access to a share's "new transaction hashes" list across the BCH
4+
// share variants.
5+
//
6+
// WHY THIS EXISTS. The BCH share variants do NOT all spell this list the same
7+
// way, and they do not all HAVE it:
8+
//
9+
// bch::Share (v17) -> obj->m_tx_info.m_new_transaction_hashes
10+
// bch::NewShare (v33) -> obj->m_tx_info.m_new_transaction_hashes
11+
// bch::SegwitMiningShare (v34) -> none (carries types::DataSegwitShare, no
12+
// bch::PaddingBugfixShare (v35) m_tx_info member -- BCH has no SegWit so
13+
// bch::MergedMiningShare (v36) v34+ never populate a new-tx list at all)
14+
//
15+
// node.cpp's tx-forwarding SEND path (send_shares) probed
16+
// `requires { obj->m_new_transaction_hashes; }` directly on the share object.
17+
// That expression is FALSE for every one of the five variants -- v17/v33 nest
18+
// the list inside m_tx_info, v34+ have no list -- so the whole needed_txs /
19+
// remember_tx forwarding block was unreachable: BCH never forwarded a single tx
20+
// byte, and a relayed v17/v33 share referencing a tx the peer lacked was exactly
21+
// the canonical "referenced unknown transaction" disconnect (#905). The RECEIVE
22+
// side (protocol_actual.cpp:167 / protocol_legacy.cpp:173 / node.hpp:291) spells
23+
// the nested list correctly, which is why receive worked and only send was dead.
24+
//
25+
// Probing through one accessor removes the class of bug: a new share variant
26+
// either exposes the list in one of the two known spellings and is handled, or
27+
// exposes none and is reported as "no list" (nothing to forward) -- never
28+
// silently skipped because of a member-name mismatch. Mirrors ltc::new_tx_hashes
29+
// (src/impl/ltc/share_tx_refs.hpp, #873); BCH's variant topology is identical.
30+
31+
#pragma once
32+
33+
#include <vector>
34+
35+
#include <core/uint256.hpp>
36+
37+
namespace bch {
38+
39+
// Pointer to the share's new-transaction-hash list, or nullptr when this share
40+
// variant carries none. A pointer (not a copy) so the broadcast hot path does no
41+
// per-share allocation. Lifetime is the share object's.
42+
template <typename ShareObj>
43+
inline auto new_tx_hashes(ShareObj* obj)
44+
{
45+
if constexpr (requires { obj->m_tx_info.m_new_transaction_hashes; })
46+
return &obj->m_tx_info.m_new_transaction_hashes;
47+
else if constexpr (requires { obj->m_new_transaction_hashes; })
48+
return &obj->m_new_transaction_hashes;
49+
else
50+
return static_cast<const std::vector<uint256>*>(nullptr);
51+
}
52+
53+
// Compile-time pin of the fact above: true iff the share variant exposes the
54+
// list under the FLAT spelling that the old probe assumed. Held false by the KAT
55+
// for every BCH variant -- if this ever becomes true, the flat branch of
56+
// new_tx_hashes() is what handles it, not a silent skip.
57+
template <typename ShareObj>
58+
inline constexpr bool has_flat_new_tx_hashes =
59+
requires(ShareObj* o) { o->m_new_transaction_hashes; };
60+
61+
// True iff the share variant nests the list inside m_tx_info (v17 / v33).
62+
template <typename ShareObj>
63+
inline constexpr bool has_nested_new_tx_hashes =
64+
requires(ShareObj* o) { o->m_tx_info.m_new_transaction_hashes; };
65+
66+
} // namespace bch

src/impl/bch/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ if(BUILD_TESTING)
2828
coinbase_author_kat_test # KAT G2: v36 coinbase output-assembly byte-parity vs p2pool-merged-v36 oracle (pure)
2929
donation_flip_kat_test # KAT G2-C3: donation P2PK<->P2SH byte-correctness across the v35<->v36 flip (pure)
3030
cashaddr_kat_test # M4: CashAddr codec KAT -- encode/decode roundtrip + PolyMod/version-byte parity vs BCHN vectors, prefix/checksum rejection (pure)
31+
share_tx_refs_kat_test # #905: bch::new_tx_hashes accessor -- send-side flat-probe regression pin (v17/v33 nested, v34+ none) (pure)
3132
)
3233
foreach(t IN LISTS BCH_ABLA_TESTS)
3334
add_executable(bch_${t} ${t}.cpp)
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// ---------------------------------------------------------------------------
2+
// bch::new_tx_hashes accessor KAT -- the guard for the #905 send-side fix.
3+
//
4+
// #905: BCH's tx-forwarding SEND path (node.cpp send_shares) probed
5+
// `requires { obj->m_new_transaction_hashes; }` directly on the share object.
6+
// That FLAT probe is false for EVERY BCH share variant -- v17/v33 nest the list
7+
// inside m_tx_info, v34/v35/v36 carry no list at all -- so the whole needed_txs
8+
// forwarding block was dead code and BCH forwarded no tx bytes. share_tx_refs.hpp
9+
// replaces the flat probe with bch::new_tx_hashes(), which returns the nested
10+
// list for v17/v33 and nullptr for v34+.
11+
//
12+
// This test pins two things so the regression cannot silently return:
13+
// A. Compile-time topology (static_assert): the FLAT spelling is absent on
14+
// every variant (== the old probe was dead), the NESTED spelling is present
15+
// exactly on v17/v33, and new_tx_hashes() selects the right branch.
16+
// B. Runtime behaviour: the accessor delivers the v17/v33 list by reference
17+
// (send loop sees every referenced hash) and returns nullptr for v34+.
18+
//
19+
// Build-INERT wrt consensus surface: pure header-only over share.hpp +
20+
// share_tx_refs.hpp, no node/RPC. p2pool-merged-v36 surface: NONE (this is a
21+
// LOCAL relay-plumbing accessor; the share wire-format is unchanged).
22+
// ---------------------------------------------------------------------------
23+
24+
#include <iostream>
25+
#include <type_traits>
26+
#include <vector>
27+
28+
#include <core/uint256.hpp>
29+
30+
#include "../share.hpp"
31+
#include "../share_tx_refs.hpp"
32+
33+
namespace {
34+
35+
int failures = 0;
36+
#define CHECK(cond) do { if (!(cond)) { \
37+
std::cerr << "FAIL: " #cond " @ line " << __LINE__ << "\n"; ++failures; } } while (0)
38+
39+
// ---- A. Compile-time topology --------------------------------------------
40+
// The FLAT spelling the old probe assumed is absent on EVERY variant: that is
41+
// precisely why the old send path was dead. Held here so it stays absent.
42+
static_assert(!bch::has_flat_new_tx_hashes<bch::Share>, "v17 must NOT expose the flat list");
43+
static_assert(!bch::has_flat_new_tx_hashes<bch::NewShare>, "v33 must NOT expose the flat list");
44+
static_assert(!bch::has_flat_new_tx_hashes<bch::SegwitMiningShare>, "v34 must NOT expose the flat list");
45+
static_assert(!bch::has_flat_new_tx_hashes<bch::PaddingBugfixShare>, "v35 must NOT expose the flat list");
46+
static_assert(!bch::has_flat_new_tx_hashes<bch::MergedMiningShare>, "v36 must NOT expose the flat list");
47+
48+
// The NESTED spelling is present exactly on v17/v33 (which carry m_tx_info) and
49+
// absent on v34+ (SegWit-less variants carry no new-tx list).
50+
static_assert(bch::has_nested_new_tx_hashes<bch::Share>, "v17 must nest the list in m_tx_info");
51+
static_assert(bch::has_nested_new_tx_hashes<bch::NewShare>, "v33 must nest the list in m_tx_info");
52+
static_assert(!bch::has_nested_new_tx_hashes<bch::SegwitMiningShare>, "v34 carries no new-tx list");
53+
static_assert(!bch::has_nested_new_tx_hashes<bch::PaddingBugfixShare>, "v35 carries no new-tx list");
54+
static_assert(!bch::has_nested_new_tx_hashes<bch::MergedMiningShare>, "v36 carries no new-tx list");
55+
56+
// new_tx_hashes() selects a non-null pointer branch for v17/v33 and the null
57+
// pointer branch for v34+ -- checked at compile time on the return type / at run
58+
// time on the value below.
59+
template <typename S>
60+
constexpr bool returns_null_branch =
61+
std::is_same_v<decltype(bch::new_tx_hashes(std::declval<S*>())),
62+
const std::vector<uint256>*>;
63+
static_assert(returns_null_branch<bch::SegwitMiningShare>, "v34 accessor must be the null branch");
64+
static_assert(returns_null_branch<bch::PaddingBugfixShare>, "v35 accessor must be the null branch");
65+
static_assert(returns_null_branch<bch::MergedMiningShare>, "v36 accessor must be the null branch");
66+
67+
// ---- B. Runtime behaviour -------------------------------------------------
68+
69+
// Reproduce the send-side loop over the accessor result and count the hashes it
70+
// would forward -- this is exactly what node.cpp send_shares now does.
71+
template <typename ShareVariant>
72+
std::size_t forwardable_count(ShareVariant& s)
73+
{
74+
std::size_t n = 0;
75+
const auto* new_txs = bch::new_tx_hashes(&s);
76+
if (!new_txs)
77+
return 0;
78+
for (const auto& th : *new_txs)
79+
{
80+
(void)th;
81+
++n;
82+
}
83+
return n;
84+
}
85+
86+
void test_v17_v33_deliver_nested_list()
87+
{
88+
bch::Share s17;
89+
s17.m_tx_info.m_new_transaction_hashes = {uint256(1), uint256(2), uint256(3)};
90+
91+
const auto* p = bch::new_tx_hashes(&s17);
92+
CHECK(p != nullptr);
93+
// Same object, by reference -- no copy: the send path reads the live list.
94+
CHECK(p == &s17.m_tx_info.m_new_transaction_hashes);
95+
CHECK(p->size() == 3);
96+
CHECK(forwardable_count(s17) == 3);
97+
98+
bch::NewShare s33;
99+
s33.m_tx_info.m_new_transaction_hashes = {uint256(7)};
100+
CHECK(bch::new_tx_hashes(&s33) != nullptr);
101+
CHECK(forwardable_count(s33) == 1);
102+
103+
// Empty nested list: accessor non-null, nothing to forward (not the same as
104+
// "no list" -- a v17 share with no new txs still probes cleanly).
105+
bch::Share s17_empty;
106+
CHECK(bch::new_tx_hashes(&s17_empty) != nullptr);
107+
CHECK(forwardable_count(s17_empty) == 0);
108+
}
109+
110+
void test_v34plus_have_no_list()
111+
{
112+
bch::SegwitMiningShare s34;
113+
bch::PaddingBugfixShare s35;
114+
bch::MergedMiningShare s36;
115+
CHECK(bch::new_tx_hashes(&s34) == nullptr);
116+
CHECK(bch::new_tx_hashes(&s35) == nullptr);
117+
CHECK(bch::new_tx_hashes(&s36) == nullptr);
118+
CHECK(forwardable_count(s34) == 0);
119+
CHECK(forwardable_count(s35) == 0);
120+
CHECK(forwardable_count(s36) == 0);
121+
}
122+
123+
} // namespace
124+
125+
int main()
126+
{
127+
test_v17_v33_deliver_nested_list();
128+
test_v34plus_have_no_list();
129+
130+
if (failures)
131+
{
132+
std::cerr << failures << " CHECK(s) failed\n";
133+
return 1;
134+
}
135+
std::cout << "bch_share_tx_refs_kat_test: OK\n";
136+
return 0;
137+
}

0 commit comments

Comments
 (0)