diff --git a/src/Makefile.gtest.include b/src/Makefile.gtest.include index fa9d47448..2f3100f11 100644 --- a/src/Makefile.gtest.include +++ b/src/Makefile.gtest.include @@ -13,7 +13,8 @@ hush_gtest_SOURCES = \ gtest/utils.cpp \ gtest/test_randomx_preverify.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_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) diff --git a/src/gtest/test_stratum_jobid.cpp b/src/gtest/test_stratum_jobid.cpp new file mode 100644 index 000000000..7c248a6a9 --- /dev/null +++ b/src/gtest/test_stratum_jobid.cpp @@ -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 +#include +#include + +#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 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 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); +}