2 Commits

Author SHA1 Message Date
3b2aa866aa rpc: honor a command-line rpcpassword across restarts; fix dead -rpcusername key
hush_configfile(), on any restart where the auto-generated DRAGONX.conf
already exists, hard-assigned mapArgs["-rpcpassword"] from the conf and
wrote the username to mapArgs["-rpcusername"] -- a key nothing reads
(InitRPCAuthentication in httprpc.cpp and bitcoin-cli.cpp both read
"-rpcuser"). The hard assignment silently overwrote a -rpcpassword passed
on the command line, so after the first run the effective RPC credentials
became {cmdline-user}:{conf-password}, matching neither the command-line
pair the operator passed nor the full conf pair. Automation or external
clients that connect with the known command-line password broke on every
restart.

Use SoftSetArg for both, so a command-line (or explicitly configured)
value wins and the conf-derived credential is only a fallback. Verified on
regtest: after a restart an external client with the command-line password
gets HTTP 200 and the conf's random password gets 401; the conf-only
operator path (no command-line creds) still authenticates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGhWvdBSgt6UxxHANr7gfN
2026-08-30 16:32:20 -05:00
2d6359ea74 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
2026-08-30 16:24:55 -05:00
3 changed files with 108 additions and 3 deletions

View File

@@ -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)

View 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);
}

View File

@@ -1383,8 +1383,14 @@ void hush_configfile(char *symbol,uint16_t rpcport)
#endif #endif
} else { } else {
_hush_userpass(myusername,mypassword,fp); _hush_userpass(myusername,mypassword,fp);
mapArgs["-rpcpassword"] = mypassword; // Feed the credentials read by InitRPCAuthentication (httprpc.cpp) and the
mapArgs["-rpcusername"] = myusername; // CLI (bitcoin-cli.cpp) -- both read "-rpcuser"/"-rpcpassword". Use SoftSetArg
// so a value passed on the command line (or an explicit -rpcuser/-rpcpassword)
// still wins: the old direct assignment silently overwrote a command-line
// -rpcpassword on every restart once this conf existed, and the username was
// written to a misspelled "-rpcusername" key that nothing ever reads.
SoftSetArg("-rpcpassword", mypassword);
SoftSetArg("-rpcuser", myusername);
fclose(fp); fclose(fp);
} }
} }