gtest: add job_id parser regression test for the 63-char abort fix
Covers b3e81f1ed: a 63-character mining.submit job_id containing any
non-hex byte must never complete to a 32-byte vector, so the EWBF
completion loop skips it instead of constructing uint256() (which
asserts vch.size()==32 and would abort the daemon from an
unauthenticated client).
Five cases pin the invariant using the real ParseHex/uint256 primitives:
all-spaces, a non-hex byte mid-string, and a non-hex byte at the end
never yield 32 bytes; a genuine 63-hex job_id completes to exactly 32
bytes for all 16 digits; plus ParseHex odd/even-length anchors. Wired
gtest/test_stratum_jobid.cpp into Makefile.gtest.include (5 tests, all
pass; full hush-gtest suite now 18/18).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGhWvdBSgt6UxxHANr7gfN
This commit is contained in:
@@ -13,7 +13,8 @@ hush_gtest_SOURCES = \
|
|||||||
gtest/utils.cpp \
|
gtest/utils.cpp \
|
||||||
gtest/test_randomx_preverify.cpp \
|
gtest/test_randomx_preverify.cpp \
|
||||||
gtest/test_hdtransparent.cpp \
|
gtest/test_hdtransparent.cpp \
|
||||||
gtest/test_mnemonic_compat.cpp
|
gtest/test_mnemonic_compat.cpp \
|
||||||
|
gtest/test_stratum_jobid.cpp
|
||||||
|
|
||||||
hush_gtest_CPPFLAGS = $(AM_CPPFLAGS) -DMULTICORE -fopenmp -DBINARY_OUTPUT -DCURVE_ALT_BN128 -DSTATIC $(BITCOIN_INCLUDES)
|
hush_gtest_CPPFLAGS = $(AM_CPPFLAGS) -DMULTICORE -fopenmp -DBINARY_OUTPUT -DCURVE_ALT_BN128 -DSTATIC $(BITCOIN_INCLUDES)
|
||||||
hush_gtest_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
|
hush_gtest_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
|
||||||
|
|||||||
98
src/gtest/test_stratum_jobid.cpp
Normal file
98
src/gtest/test_stratum_jobid.cpp
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (c) 2016-2026 The Hush developers
|
||||||
|
// Distributed under the GPLv3 software license, see the accompanying
|
||||||
|
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||||
|
//
|
||||||
|
// Regression coverage for the b3e81f1ed fix:
|
||||||
|
// "stratum: do not abort the daemon on a malformed 63-character job_id"
|
||||||
|
//
|
||||||
|
// The EWBF "31 bytes job_id" path in stratum_mining_submit() completes a
|
||||||
|
// 63-character job_id with each hex digit in turn and feeds the result to
|
||||||
|
// uint256(). ParseHex() stops at the first non-hex character and returns a
|
||||||
|
// SHORT vector without signalling an error, and base_blob's vector ctor
|
||||||
|
// asserts vch.size() == 32. asserts are live in release builds here, and the
|
||||||
|
// job_id arrives from an unauthenticated client -- so a single mining.submit
|
||||||
|
// whose 63-character job_id contains any non-hex byte (63 spaces will do)
|
||||||
|
// used to abort the node.
|
||||||
|
//
|
||||||
|
// The fix size-checks each candidate before constructing uint256. These tests
|
||||||
|
// pin the exact invariant that guard relies on, using the real ParseHex and
|
||||||
|
// uint256 primitives, without ever constructing a uint256 from a short vector
|
||||||
|
// (which would still abort under the guard we are protecting).
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "uint256.h"
|
||||||
|
#include "utilstrencodings.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const std::string HEXDIGITS = "0123456789abcdef";
|
||||||
|
|
||||||
|
// Mirrors the guarded completion loop in stratum_mining_submit(): try every
|
||||||
|
// single-hex-digit completion and report whether ANY of them parses to a
|
||||||
|
// whole 32-byte value. Only then is uint256() construction reached.
|
||||||
|
bool AnyCompletionParsesTo32(const std::string& jobid) {
|
||||||
|
for (char d : HEXDIGITS) {
|
||||||
|
std::vector<unsigned char> vch = ParseHex(jobid + d);
|
||||||
|
if (vch.size() == 32) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The exact example from the fix commit: a 63-character job_id of spaces.
|
||||||
|
// No completion may reach a 32-byte vector, so the daemon never constructs
|
||||||
|
// uint256() and never aborts.
|
||||||
|
TEST(StratumJobId, AllSpacesNeverYields32Bytes) {
|
||||||
|
std::string spaces(63, ' ');
|
||||||
|
ASSERT_EQ(spaces.size(), 63u);
|
||||||
|
for (char d : HEXDIGITS) {
|
||||||
|
EXPECT_NE(ParseHex(spaces + d).size(), 32u)
|
||||||
|
<< "completion '" << d << "' unexpectedly produced 32 bytes";
|
||||||
|
}
|
||||||
|
EXPECT_FALSE(AnyCompletionParsesTo32(spaces));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single non-hex byte embedded in an otherwise-hex 63-char job_id is enough:
|
||||||
|
// ParseHex stops at it, so every completion is short.
|
||||||
|
TEST(StratumJobId, SingleNonHexByteInMiddleIsRejected) {
|
||||||
|
std::string jobid(63, 'a');
|
||||||
|
jobid[30] = 'g'; // 'g' is not a hex digit
|
||||||
|
ASSERT_EQ(jobid.size(), 63u);
|
||||||
|
EXPECT_FALSE(AnyCompletionParsesTo32(jobid));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A non-hex byte at the very end (position 62) is likewise rejected: the last
|
||||||
|
// hex pair can never complete to a whole byte.
|
||||||
|
TEST(StratumJobId, NonHexByteAtEndIsRejected) {
|
||||||
|
std::string jobid(62, 'a');
|
||||||
|
jobid.push_back('z'); // length 63, last char non-hex
|
||||||
|
ASSERT_EQ(jobid.size(), 63u);
|
||||||
|
EXPECT_FALSE(AnyCompletionParsesTo32(jobid));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A genuine truncated EWBF job_id -- 63 real hex characters -- must complete
|
||||||
|
// to exactly 32 bytes for every digit, so uint256() construction is safe.
|
||||||
|
TEST(StratumJobId, ValidSixtyThreeHexCompletesToExactly32Bytes) {
|
||||||
|
std::string jobid(63, 'a');
|
||||||
|
ASSERT_EQ(jobid.size(), 63u);
|
||||||
|
for (char d : HEXDIGITS) {
|
||||||
|
std::vector<unsigned char> vch = ParseHex(jobid + d);
|
||||||
|
ASSERT_EQ(vch.size(), 32u) << "completion '" << d << "'";
|
||||||
|
// 32-byte vector: construction must not trip the size assertion.
|
||||||
|
uint256 h(vch);
|
||||||
|
EXPECT_EQ(h.size(), 32u);
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(AnyCompletionParsesTo32(jobid));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity anchors for ParseHex's odd/even handling that the loop depends on:
|
||||||
|
// an odd hex length drops the trailing nibble (63 hex -> 31 bytes), and one
|
||||||
|
// more hex char fills the 32nd byte (64 hex -> 32 bytes).
|
||||||
|
TEST(StratumJobId, ParseHexOddLengthDropsTrailingNibble) {
|
||||||
|
EXPECT_EQ(ParseHex(std::string(63, 'a')).size(), 31u);
|
||||||
|
EXPECT_EQ(ParseHex(std::string(64, 'a')).size(), 32u);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user