Skip to content

Commit c9a1944

Browse files
committed
llmq: cover QBSIGSHARES aggregate sig-share bound with a testable decoder
The self-review of the batched sig-share intake bounds flagged one gap: the running-total abort in the QBSIGSHARES handler — where many individually-valid batches together exceed MAX_MSGS_TOTAL_BATCHED_SIGS — had no direct unit coverage. The check lived inline in NetSigning::ProcessMessage, so the only way to reach it was to drive a full P2P message through the handler. Extract the QBSIGSHARES decode loop into a free function, llmq::UnserializeBatchedSigShares(CDataStream&), that production calls inside the existing try/catch. Wire format, peer banning, and logging semantics are unchanged — the callsite still logs, bans, and rethrows on any ios_base::failure. The extracted decoder is the exact code that runs in production, so the new tests exercise the real path rather than a mirror. Add three targeted cases in llmq_utils_tests: - oversized outer batch count (built from empty batches so only the outer-count guard, not an end-of-stream artifact, can reject it), - oversized running aggregate across batches each within the per-batch cap, - an aggregate exactly at the cap, which must decode intact. Both count guards were mutation-verified: neutralizing either guard makes its corresponding test fail and no other. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 89c0933 commit c9a1944

3 files changed

Lines changed: 89 additions & 19 deletions

File tree

src/llmq/net_signing.cpp

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,26 @@
2323
#include <unordered_map>
2424

2525
namespace llmq {
26+
std::vector<CBatchedSigShares> UnserializeBatchedSigShares(CDataStream& vRecv)
27+
{
28+
std::vector<CBatchedSigShares> msgs;
29+
const uint64_t msgs_size{ReadCompactSize(vRecv, /*range_check=*/false)};
30+
if (msgs_size > MAX_MSGS_TOTAL_BATCHED_SIGS) {
31+
throw std::ios_base::failure("QBSIGSHARES batch count too large");
32+
}
33+
msgs.reserve(msgs_size);
34+
size_t total_sigs_count{0};
35+
while (msgs.size() < msgs_size) {
36+
msgs.emplace_back();
37+
vRecv >> msgs.back();
38+
total_sigs_count += msgs.back().sigShares.size();
39+
if (total_sigs_count > MAX_MSGS_TOTAL_BATCHED_SIGS) {
40+
throw std::ios_base::failure("QBSIGSHARES sig share count too large");
41+
}
42+
}
43+
return msgs;
44+
}
45+
2646
void NetSigning::ProcessMessage(CNode& pfrom, const std::string& msg_type, CDataStream& vRecv)
2747
{
2848
if (msg_type == NetMsgType::QSIGREC) {
@@ -99,27 +119,9 @@ void NetSigning::ProcessMessage(CNode& pfrom, const std::string& msg_type, CData
99119
return;
100120
}
101121
} else if (msg_type == NetMsgType::QBSIGSHARES) {
102-
// The inner sigShares vector is bounded by CBatchedSigShares's SERIALIZE_METHODS
103-
// via LIMITED_VECTOR, but many individually-valid batches could still exceed the
104-
// total sig-share cap, so bound the outer count and check the running total as we
105-
// decode so we stop reading before an attacker forces us through the full cross
106-
// product of the per-vector limits.
107122
std::vector<CBatchedSigShares> msgs;
108123
try {
109-
const uint64_t msgs_size{ReadCompactSize(vRecv, /*range_check=*/false)};
110-
if (msgs_size > MAX_MSGS_TOTAL_BATCHED_SIGS) {
111-
throw std::ios_base::failure("QBSIGSHARES batch count too large");
112-
}
113-
msgs.reserve(msgs_size);
114-
size_t total_sigs_count{0};
115-
while (msgs.size() < msgs_size) {
116-
msgs.emplace_back();
117-
vRecv >> msgs.back();
118-
total_sigs_count += msgs.back().sigShares.size();
119-
if (total_sigs_count > MAX_MSGS_TOTAL_BATCHED_SIGS) {
120-
throw std::ios_base::failure("QBSIGSHARES sig share count too large");
121-
}
122-
}
124+
msgs = UnserializeBatchedSigShares(vRecv);
123125
} catch (const std::ios_base::failure& e) {
124126
LogPrint(BCLog::LLMQ_SIGS, "NetSigning::%s -- rejected %s from peer=%d: %s\n",
125127
__func__, msg_type, pfrom.GetId(), e.what());

src/llmq/net_signing.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ namespace llmq {
2424
class CSigSharesManager;
2525
class CSigningManager;
2626

27+
//! Decode a QBSIGSHARES payload into its vector of CBatchedSigShares.
28+
//!
29+
//! The inner sigShares vector of each batch is bounded by CBatchedSigShares's
30+
//! SERIALIZE_METHODS via LIMITED_VECTOR, but many individually-valid batches
31+
//! could still exceed the total sig-share cap, so this bounds the outer batch
32+
//! count and checks the running total of inner sig shares as it decodes,
33+
//! stopping before an attacker forces us through the full cross product of the
34+
//! per-vector limits. A wire count above the cap (outer batch count or running
35+
//! inner total) throws std::ios_base::failure before the offending element is
36+
//! decoded, leaving the caller to log, ban, and rethrow uniformly.
37+
std::vector<CBatchedSigShares> UnserializeBatchedSigShares(CDataStream& vRecv);
38+
2739
class NetSigning final : public NetHandler, public CValidationInterface
2840
{
2941
public:

src/test/llmq_utils_tests.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <test/util/setup_common.h>
77

88
#include <consensus/params.h>
9+
#include <llmq/net_signing.h>
910
#include <llmq/params.h>
1011
#include <llmq/signing_shares.h>
1112
#include <llmq/utils.h>
@@ -197,6 +198,61 @@ BOOST_AUTO_TEST_CASE(batched_sig_shares_accepts_max_inner_vector)
197198
BOOST_CHECK_EQUAL(roundtripped.sigShares.size(), MAX_MSGS_TOTAL_BATCHED_SIGS);
198199
}
199200

201+
static CBatchedSigShares MakeBatch(uint32_t session_id, size_t share_count)
202+
{
203+
CBatchedSigShares batch;
204+
batch.sessionId = session_id;
205+
batch.sigShares.resize(share_count); // default pairs are enough to exercise the count invariant
206+
return batch;
207+
}
208+
209+
// Exercise the production QBSIGSHARES decoder directly: the outer batch count is
210+
// bounded regardless of the inner contents. Every batch here is empty, so the
211+
// aggregate running-total guard never fires; only the outer-count guard can
212+
// reject the stream, which keeps this case a genuine test of that guard rather
213+
// than an end-of-stream artifact.
214+
BOOST_AUTO_TEST_CASE(qbsigshares_rejects_oversized_batch_count)
215+
{
216+
std::vector<CBatchedSigShares> msgs(MAX_MSGS_TOTAL_BATCHED_SIGS + 1); // count over cap, 0 inner shares each
217+
218+
CDataStream stream{SER_NETWORK, PROTOCOL_VERSION};
219+
stream << msgs;
220+
221+
BOOST_CHECK_THROW(UnserializeBatchedSigShares(stream), std::ios_base::failure);
222+
}
223+
224+
// The regression this targets: each batch is individually within the per-batch
225+
// LIMITED_VECTOR cap, but their running aggregate exceeds MAX_MSGS_TOTAL_BATCHED_SIGS,
226+
// so the decoder must abort mid-stream rather than accept the cross product.
227+
BOOST_AUTO_TEST_CASE(qbsigshares_rejects_oversized_aggregate_total)
228+
{
229+
std::vector<CBatchedSigShares> msgs;
230+
msgs.push_back(MakeBatch(1, 300));
231+
msgs.push_back(MakeBatch(2, 200)); // 300 + 200 = 500 > 400, though each batch is <= the cap
232+
233+
CDataStream stream{SER_NETWORK, PROTOCOL_VERSION};
234+
stream << msgs;
235+
236+
BOOST_CHECK_THROW(UnserializeBatchedSigShares(stream), std::ios_base::failure);
237+
}
238+
239+
// Batches whose aggregate is exactly at the cap must be accepted and decoded intact.
240+
BOOST_AUTO_TEST_CASE(qbsigshares_accepts_aggregate_at_cap)
241+
{
242+
std::vector<CBatchedSigShares> msgs;
243+
msgs.push_back(MakeBatch(1, 200));
244+
msgs.push_back(MakeBatch(2, MAX_MSGS_TOTAL_BATCHED_SIGS - 200)); // aggregate == cap
245+
246+
CDataStream stream{SER_NETWORK, PROTOCOL_VERSION};
247+
stream << msgs;
248+
249+
std::vector<CBatchedSigShares> decoded;
250+
BOOST_CHECK_NO_THROW(decoded = UnserializeBatchedSigShares(stream));
251+
BOOST_CHECK_EQUAL(decoded.size(), 2U);
252+
BOOST_CHECK_EQUAL(decoded[0].sigShares.size(), 200U);
253+
BOOST_CHECK_EQUAL(decoded[1].sigShares.size(), MAX_MSGS_TOTAL_BATCHED_SIGS - 200);
254+
}
255+
200256
BOOST_AUTO_TEST_CASE(deterministic_outbound_connection_test)
201257
{
202258
// Test deterministic behavior

0 commit comments

Comments
 (0)