security: harden wallet, bootstrap, consensus, and RPC

(cherry picked from commit bed3d72099e04813393561a535b7dec9c0ac5e7f)
This commit is contained in:
Ethan Clay
2026-07-12 01:05:49 -04:00
parent 41e3898ff8
commit e6ae48d4d7
42 changed files with 1694 additions and 902 deletions
+18 -16
View File
@@ -907,46 +907,48 @@ void StartShutdown() {}
# "$@" preservation. We use bash explicitly (not sh) for "$@" array
# semantics — paths may contain spaces, so word-splitting on IFS
# would corrupt them.
file(WRITE "${FUZZ_LINK_WRAPPER}"
"#!/bin/bash
set(FUZZ_LINK_WRAPPER_CONTENT [=[#!/bin/bash
# Auto-generated by CMake (BUILD_FUZZ block). Discovers triangles_common +
# trianglesd .o files at link time and exec's the clang++ link line.
#
# Usage: link.sh clang++ [link-args...]
# Final exec: clang++ <each .o> <each original link-arg>
set -euo pipefail
PROG=\"\$1\"
PROG="$1"
shift
TRIANGLES_COMMON_DIR=\"${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/triangles_common.dir\"
TRIANGLESD_DIR=\"${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/trianglesd_objects.dir\"
TRIANGLES_COMMON_DIR="@CMAKE_CURRENT_BINARY_DIR@/CMakeFiles/triangles_common.dir"
TRIANGLESD_DIR="@CMAKE_CURRENT_BINARY_DIR@/CMakeFiles/trianglesd_objects.dir"
# Discover .o files into a bash array. Exclude script.cpp.o (we have our
# own clang-instrumented copy in fuzz_objs/ that we want to keep separate
# from the main build's copy).
declare -a OBJS=()
for f in \"\$TRIANGLES_COMMON_DIR\"/*.o \"\$TRIANGLES_COMMON_DIR\"/*/*.o; do
[ -f \"\$f\" ] || continue
case \"\$f\" in
for f in "$TRIANGLES_COMMON_DIR"/*.o "$TRIANGLES_COMMON_DIR"/*/*.o; do
[ -f "$f" ] || continue
case "$f" in
*/script.cpp.o) continue ;;
esac
OBJS+=(\"\$f\")
OBJS+=("$f")
done
if [ -d "\$TRIANGLESD_DIR" ]; then
for f in "\$TRIANGLESD_DIR"/*.o; do
[ -f "\$f" ] || continue
if [ -d "$TRIANGLESD_DIR" ]; then
for f in "$TRIANGLESD_DIR"/*.o; do
[ -f "$f" ] || continue
# init.cpp defines the daemon's main(); the fuzz harness has its own
# (libFuzzer's). wallet.cpp, noui.cpp etc. are safe — they don't
# define main and their external references (pwalletMain,
# uiInterface, nDerivationMethodIndex) are satisfied by the stub
# object file we add at the end of the link line.
case "\$f" in
case "$f" in
*/init.cpp.o) continue ;;
esac
OBJS+=("\$f")
OBJS+=("$f")
done
fi
# Final arg list: PROG, then all .o files, then all original link args.
exec \"\$PROG\" \"\${OBJS[@]}\" \"\$@\"
")
exec "$PROG" "${OBJS[@]}" "$@"
]=])
string(CONFIGURE "${FUZZ_LINK_WRAPPER_CONTENT}"
FUZZ_LINK_WRAPPER_CONTENT @ONLY)
file(WRITE "${FUZZ_LINK_WRAPPER}" "${FUZZ_LINK_WRAPPER_CONTENT}")
file(CHMOD "${FUZZ_LINK_WRAPPER}" PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
+70 -57
View File
@@ -14,6 +14,8 @@
#include <openssl/opensslv.h>
#include <algorithm>
#include <cctype>
#include <limits>
#include <stdexcept>
#include <vector>
@@ -69,16 +71,10 @@ public:
throw bignum_error("CBigNum::CBigNum() : BN_new() returned NULL");
}
CBigNum(const CBigNum& b)
CBigNum(const CBigNum& b) : CBigNum()
{
pbn = BN_new();
if (pbn == nullptr)
throw bignum_error("CBigNum::CBigNum(const CBigNum&) : BN_new() returned NULL");
if (!BN_copy(pbn, b.pbn))
{
BN_clear_free(pbn);
throw bignum_error("CBigNum::CBigNum(const CBigNum&) : BN_copy failed");
}
}
CBigNum& operator=(const CBigNum& b)
@@ -99,21 +95,20 @@ public:
const BIGNUM* get() const { return pbn; }
//CBigNum(char n) is not portable. Use 'signed char' or 'unsigned char'.
CBigNum(signed char n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); }
CBigNum(short n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); }
CBigNum(int n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); }
CBigNum(long n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); }
CBigNum(long long n) { pbn = BN_new(); setint64(n); }
CBigNum(unsigned char n) { pbn = BN_new(); setulong(n); }
CBigNum(unsigned short n) { pbn = BN_new(); setulong(n); }
CBigNum(unsigned int n) { pbn = BN_new(); setulong(n); }
CBigNum(unsigned long n) { pbn = BN_new(); setulong(n); }
CBigNum(unsigned long long n) { pbn = BN_new(); setuint64(n); }
explicit CBigNum(uint256 n) { pbn = BN_new(); setuint256(n); }
CBigNum(signed char n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); }
CBigNum(short n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); }
CBigNum(int n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); }
CBigNum(long n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); }
CBigNum(long long n) : CBigNum() { setint64(n); }
CBigNum(unsigned char n) : CBigNum() { setulong(n); }
CBigNum(unsigned short n) : CBigNum() { setulong(n); }
CBigNum(unsigned int n) : CBigNum() { setulong(n); }
CBigNum(unsigned long n) : CBigNum() { setulong(n); }
CBigNum(unsigned long long n) : CBigNum() { setuint64(n); }
explicit CBigNum(uint256 n) : CBigNum() { setuint256(n); }
explicit CBigNum(const std::vector<unsigned char>& vch)
explicit CBigNum(const std::vector<unsigned char>& vch) : CBigNum()
{
pbn = BN_new();
setvch(vch);
}
@@ -216,21 +211,23 @@ public:
pch[1] = (nSize >> 16) & 0xff;
pch[2] = (nSize >> 8) & 0xff;
pch[3] = (nSize) & 0xff;
BN_mpi2bn(pch, p - pch, pbn);
if (BN_mpi2bn(pch, static_cast<int>(p - pch), pbn) == nullptr)
throw bignum_error("CBigNum::setint64() : BN_mpi2bn failed");
}
uint64_t getuint64()
uint64_t getuint64() const
{
unsigned int nSize = BN_bn2mpi(pbn, nullptr);
if (nSize < 4)
const int nSize = BN_bn2mpi(pbn, nullptr);
if (nSize <= 4)
return 0;
std::vector<unsigned char> vch(nSize);
BN_bn2mpi(pbn, &vch[0]);
std::vector<unsigned char> vch(static_cast<size_t>(nSize));
if (BN_bn2mpi(pbn, vch.data()) != nSize)
throw bignum_error("CBigNum::getuint64() : BN_bn2mpi failed");
if (vch.size() > 4)
vch[4] &= 0x7f;
uint64_t n = 0;
for (unsigned int i = 0, j = vch.size()-1; i < sizeof(n) && j >= 4; i++, j--)
((unsigned char*)&n)[i] = vch[j];
for (size_t i = 0; i < sizeof(n) && i + 4 < vch.size(); ++i)
n |= static_cast<uint64_t>(vch[vch.size() - 1 - i]) << (8 * i);
return n;
}
@@ -258,7 +255,8 @@ public:
pch[1] = (nSize >> 16) & 0xff;
pch[2] = (nSize >> 8) & 0xff;
pch[3] = (nSize) & 0xff;
BN_mpi2bn(pch, p - pch, pbn);
if (BN_mpi2bn(pch, static_cast<int>(p - pch), pbn) == nullptr)
throw bignum_error("CBigNum::setuint64() : BN_mpi2bn failed");
}
void setuint256(uint256 n)
@@ -286,29 +284,33 @@ public:
pch[1] = (nSize >> 16) & 0xff;
pch[2] = (nSize >> 8) & 0xff;
pch[3] = (nSize >> 0) & 0xff;
BN_mpi2bn(pch, p - pch, pbn);
if (BN_mpi2bn(pch, static_cast<int>(p - pch), pbn) == nullptr)
throw bignum_error("CBigNum::setuint256() : BN_mpi2bn failed");
}
uint256 getuint256() const
{
unsigned int nSize = BN_bn2mpi(pbn, nullptr);
if (nSize < 4)
const int mpiSize = BN_bn2mpi(pbn, nullptr);
if (mpiSize <= 4)
return 0;
std::vector<unsigned char> vch(nSize);
BN_bn2mpi(pbn, &vch[0]);
if (vch.size() > 4)
vch[4] &= 0x7f;
std::vector<unsigned char> vch(static_cast<size_t>(mpiSize));
if (BN_bn2mpi(pbn, vch.data()) != mpiSize)
throw bignum_error("CBigNum::getuint256() : BN_bn2mpi failed");
vch[4] &= 0x7f;
uint256 n = 0;
for (unsigned int i = 0, j = vch.size()-1; i < sizeof(n) && j >= 4; i++, j--)
((unsigned char*)&n)[i] = vch[j];
for (size_t i = 0; i < sizeof(n) && i + 4 < vch.size(); ++i)
reinterpret_cast<unsigned char*>(&n)[i] = vch[vch.size() - 1 - i];
return n;
}
void setvch(const std::vector<unsigned char>& vch)
{
if (vch.size() > static_cast<size_t>(std::numeric_limits<int>::max() - 4))
throw bignum_error("CBigNum::setvch() : input is too large");
std::vector<unsigned char> vch2(vch.size() + 4);
unsigned int nSize = vch.size();
const uint32_t nSize = static_cast<uint32_t>(vch.size());
// BIGNUM's byte stream format expects 4 bytes of
// big endian size data info at the front
vch2[0] = (nSize >> 24) & 0xff;
@@ -316,20 +318,25 @@ public:
vch2[2] = (nSize >> 8) & 0xff;
vch2[3] = (nSize >> 0) & 0xff;
// swap data to big endian
reverse_copy(vch.begin(), vch.end(), vch2.begin() + 4);
BN_mpi2bn(&vch2[0], vch2.size(), pbn);
for (size_t i = 0; i < vch.size(); ++i)
vch2.at(i + 4) = vch.at(vch.size() - 1 - i);
if (BN_mpi2bn(vch2.data(), static_cast<int>(vch2.size()), pbn) == nullptr)
throw bignum_error("CBigNum::setvch() : BN_mpi2bn failed");
}
std::vector<unsigned char> getvch() const
{
unsigned int nSize = BN_bn2mpi(pbn, nullptr);
if (nSize <= 4)
const int mpiSize = BN_bn2mpi(pbn, nullptr);
if (mpiSize <= 4)
return std::vector<unsigned char>();
std::vector<unsigned char> vch(nSize);
BN_bn2mpi(pbn, &vch[0]);
vch.erase(vch.begin(), vch.begin() + 4);
reverse(vch.begin(), vch.end());
return vch;
std::vector<unsigned char> mpi(static_cast<size_t>(mpiSize));
if (BN_bn2mpi(pbn, mpi.data()) != mpiSize)
throw bignum_error("CBigNum::getvch() : BN_bn2mpi failed");
std::vector<unsigned char> result(static_cast<size_t>(mpiSize - 4));
for (size_t i = 0; i < result.size(); ++i)
result.at(i) = mpi.at(mpi.size() - 1 - i);
return result;
}
CBigNum& SetCompact(unsigned int nCompact)
@@ -340,16 +347,20 @@ public:
if (nSize >= 1) vch[4] = (nCompact >> 16) & 0xff;
if (nSize >= 2) vch[5] = (nCompact >> 8) & 0xff;
if (nSize >= 3) vch[6] = (nCompact >> 0) & 0xff;
BN_mpi2bn(&vch[0], vch.size(), pbn);
if (BN_mpi2bn(vch.data(), static_cast<int>(vch.size()), pbn) == nullptr)
throw bignum_error("CBigNum::SetCompact() : BN_mpi2bn failed");
return *this;
}
unsigned int GetCompact() const
{
unsigned int nSize = BN_bn2mpi(pbn, nullptr);
std::vector<unsigned char> vch(nSize);
nSize -= 4;
BN_bn2mpi(pbn, &vch[0]);
const int mpiSize = BN_bn2mpi(pbn, nullptr);
if (mpiSize <= 4)
return 0;
std::vector<unsigned char> vch(static_cast<size_t>(mpiSize));
if (BN_bn2mpi(pbn, vch.data()) != mpiSize)
throw bignum_error("CBigNum::GetCompact() : BN_bn2mpi failed");
const unsigned int nSize = static_cast<unsigned int>(mpiSize - 4);
unsigned int nCompact = nSize << 24;
if (nSize >= 1) nCompact |= (vch[4] << 16);
if (nSize >= 2) nCompact |= (vch[5] << 8);
@@ -361,7 +372,7 @@ public:
{
// skip 0x
const char* psz = str.c_str();
while (isspace(*psz))
while (isspace(static_cast<unsigned char>(*psz)))
psz++;
bool fNegative = false;
if (*psz == '-')
@@ -369,15 +380,15 @@ public:
fNegative = true;
psz++;
}
if (psz[0] == '0' && tolower(psz[1]) == 'x')
if (psz[0] == '0' && tolower(static_cast<unsigned char>(psz[1])) == 'x')
psz += 2;
while (isspace(*psz))
while (isspace(static_cast<unsigned char>(*psz)))
psz++;
// hex string to bignum
static constexpr signed char phexdigit[256] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,1,2,3,4,5,6,7,8,9,0,0,0,0,0,0, 0,0xa,0xb,0xc,0xd,0xe,0xf,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0xa,0xb,0xc,0xd,0xe,0xf,0,0,0,0,0,0,0,0,0 };
*this = 0;
while (isxdigit(*psz))
while (isxdigit(static_cast<unsigned char>(*psz)))
{
*this <<= 4;
int n = phexdigit[(unsigned char)*psz++];
@@ -389,6 +400,8 @@ public:
std::string ToString(int nBase=10) const
{
if (nBase < 2 || nBase > 16)
throw bignum_error("CBigNum::ToString() : base must be in [2, 16]");
CAutoBN_CTX pctx;
CBigNum bnBase = nBase;
CBigNum bn0 = 0;
+217 -122
View File
@@ -4,6 +4,7 @@
#include "bootstrap.h"
#include "utxosnapshot.h"
#include "txdb.h"
#include "checkpoints.h"
#include <filesystem>
#include <fstream>
@@ -22,6 +23,7 @@
#include "key.h"
#include "base58.h"
#include "util.h"
#include "json/nlohmann_json.hpp"
extern const std::string strMessageMagic;
@@ -30,12 +32,14 @@ extern const std::string strMessageMagic;
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cctype>
#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>
#include <unistd.h>
#endif
@@ -88,6 +92,20 @@ static SOCKET ConnectDirectTCP(const std::string& host, int port, std::string& s
if (hSocket == INVALID_SOCKET)
continue;
// A bootstrap endpoint must not be able to wedge daemon startup by
// accepting a connection and then never sending a response.
#ifdef WIN32
DWORD timeoutMs = 30000;
setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO,
reinterpret_cast<const char*>(&timeoutMs), sizeof(timeoutMs));
setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO,
reinterpret_cast<const char*>(&timeoutMs), sizeof(timeoutMs));
#else
struct timeval timeout = {30, 0};
setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
#endif
if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0)
break; // success
@@ -154,8 +172,11 @@ struct HttpConn {
strError = "Failed to create SSL context";
return false;
}
// Skip cert verification — we verify data integrity via checkpoint hashes
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr);
if (SSL_CTX_set_default_verify_paths(ctx) != 1) {
strError = "Failed to load the system TLS trust store";
return false;
}
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr);
ssl = SSL_new(ctx);
if (!ssl) {
@@ -163,7 +184,11 @@ struct HttpConn {
return false;
}
SSL_set_fd(ssl, (int)sock);
SSL_set_tlsext_host_name(ssl, hostname.c_str()); // SNI
if (SSL_set_tlsext_host_name(ssl, hostname.c_str()) != 1 ||
SSL_set1_host(ssl, hostname.c_str()) != 1) {
strError = "Failed to configure TLS hostname verification for " + hostname;
return false;
}
if (SSL_connect(ssl) != 1) {
unsigned long err = ERR_get_error();
@@ -172,6 +197,10 @@ struct HttpConn {
strError = "TLS handshake failed with " + hostname + ": " + errBuf;
return false;
}
if (SSL_get_verify_result(ssl) != X509_V_OK) {
strError = "TLS certificate verification failed for " + hostname;
return false;
}
return true;
}
};
@@ -229,13 +258,18 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
ProgressCallback progressFn,
std::string& strError,
bool noProxy,
int portOverride)
int portOverride,
int64_t maxDownloadBytes)
{
try {
if (maxDownloadBytes <= 0) {
strError = "Download size limit must be positive";
return false;
}
std::string currentHost = host;
std::string currentPath = urlPath;
int currentPort = (portOverride > 0) ? portOverride : PORT;
bool useSSL = false;
bool useSSL = (currentPort == 443);
std::string headerData;
int redirectCount = 0;
const int MAX_REDIRECTS = 5;
@@ -324,11 +358,23 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
// Parse redirect URL — supports http://, https://, and relative paths
if (location.compare(0, 7, "http://") == 0 ||
location.compare(0, 8, "https://") == 0) {
if (!ParseAbsoluteUrl(location, useSSL, currentHost,
currentPort, currentPath)) {
bool redirectUsesSSL = false;
std::string redirectHost;
std::string redirectPath;
int redirectPort = 0;
if (!ParseAbsoluteUrl(location, redirectUsesSSL, redirectHost,
redirectPort, redirectPath)) {
strError = "Unsupported redirect location: " + location;
return false;
}
if (useSSL && !redirectUsesSSL) {
strError = "Refusing HTTPS downgrade redirect to " + location;
return false;
}
useSSL = redirectUsesSSL;
currentHost = redirectHost;
currentPath = redirectPath;
currentPort = redirectPort;
} else if (!location.empty() && location[0] == '/') {
currentPath = location;
} else {
@@ -362,6 +408,10 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
if (lineEnd != std::string::npos)
content_length = std::stoll(headerData.substr(valStart, lineEnd - valStart));
}
if (content_length < 0 || content_length > maxDownloadBytes) {
strError = "Download response exceeds the configured size limit";
return false;
}
// Open output file
FILE* file = fopen(destPath.string().c_str(), "wb");
@@ -385,7 +435,18 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
}
if (n == 0) break; // EOF
fwrite(chunk, 1, n, file);
if (bytes_written > maxDownloadBytes - n) {
fclose(file);
fs::remove(destPath);
strError = "Download response exceeded the configured size limit";
return false;
}
if (fwrite(chunk, 1, n, file) != static_cast<size_t>(n)) {
fclose(file);
fs::remove(destPath);
strError = "Failed writing bootstrap data to disk";
return false;
}
bytes_written += n;
if (progressFn && (bytes_written - last_progress >= 262144)) {
@@ -422,7 +483,8 @@ bool FetchFileList(const std::string& host,
fs::path tmpPath = fs::temp_directory_path() / "triangles_bootstrap_filelist.txt";
std::string urlPath = std::string(BASE_PATH) + "filelist.txt";
if (!DownloadFile(host, urlPath, tmpPath, nullptr, strError, noProxy))
if (!DownloadFile(host, urlPath, tmpPath, nullptr, strError, noProxy,
-1, 1024 * 1024))
return false;
// Read lines
@@ -452,23 +514,6 @@ bool FetchFileList(const std::string& host,
// --- tar.gz bootstrap support ---
namespace {
// Parse a tar octal field (ASCII octal, null/space terminated)
static int64_t ParseTarOctal(const char* field, size_t len)
{
int64_t result = 0;
for (size_t i = 0; i < len && field[i] != '\0' && field[i] != ' '; i++) {
if (field[i] < '0' || field[i] > '7') continue;
result = (result << 3) | (field[i] - '0');
}
return result;
}
// Extract a tar.gz file to a destination directory
} // anonymous namespace
bool ParseManifest(const fs::path& manifestPath,
SnapshotManifest& manifest,
std::string& strError)
@@ -575,12 +620,9 @@ bool VerifyManifest(const SnapshotManifest& manifest,
}
// ─── Signature verification (#11) ─────────────────────────────────────
// If the manifest includes a signature, verify it against the
// compiled-in snapshot signing key. This prevents MITM attacks
// where an attacker replaces the snapshot file on the bootstrap server.
//
// If no signature is present, print a warning but continue (backward
// compatibility with older snapshots that pre-date signing).
// Legacy pre-built indexes are never accepted without authentication.
// This format is disabled below, but keep its verifier fail-closed so a
// future caller cannot silently revive the old trust behavior.
if (!manifest.signature.empty()) {
// Build the message that was signed: "height||hash" (ASCII)
std::string message = std::to_string(manifest.height) + "||" + manifest.hash;
@@ -656,12 +698,12 @@ bool VerifyManifest(const SnapshotManifest& manifest,
strError = "Snapshot manifest signature INVALID — possible tampering detected";
return false;
} else {
// rc < 0 means error (e.g., placeholder zero pubkey not yet deployed)
printf("WARNING: Snapshot manifest signature verification error (rc=%d). "
"Signing key may not be deployed yet. Proceeding without verification.\n", rc);
strError = "Snapshot manifest signature verification error";
return false;
}
} else {
printf("WARNING: Snapshot manifest has no signature — loading WITHOUT signature verification\n");
strError = "Snapshot manifest has no signature";
return false;
}
return true;
@@ -672,6 +714,13 @@ bool DownloadBootstrap(const std::string& host,
ProgressCallback progressFn,
std::string& strError)
{
(void)host;
(void)dataDir;
(void)progressFn;
strError = "Legacy file-list bootstrap is disabled; use a compiled-hash UTXO snapshot or sync from genesis";
return false;
#if 0
bool gotBlockFile = false;
// FastImport removed (commit bdb7253). v2 UTXO snapshot is the ONLY
@@ -758,8 +807,10 @@ bool DownloadBootstrap(const std::string& host,
fs::remove(manifestPath);
return true;
#endif
}
#if 0
namespace {
// Try to find the canonical UTXO snapshot entry in the bootstrap server's
@@ -892,6 +943,7 @@ bool UnsetTrustedSnapshotPublisher(std::string& strError)
fs::remove(filePath);
return true;
}
#endif
// Re-enter anonymous namespace for the remaining file-private helpers.
// (IsTrustedSnapshotSigner / VerifySignedMessage / ExtractJsonString are
@@ -899,6 +951,7 @@ bool UnsetTrustedSnapshotPublisher(std::string& strError)
namespace {
#if 0
bool IsTrustedSnapshotSigner(const std::string& addr)
{
// 1. Runtime override (set via RPC).
@@ -1072,6 +1125,7 @@ bool FindCanonicalSnapshotInManifest(const std::string& manifestText,
return true;
}
#endif
// Read an entire file into a string. Empty string on error.
std::string ReadFileToString(const fs::path& path)
@@ -1114,6 +1168,80 @@ std::string Sha256OfFile(const fs::path& path)
} // anonymous namespace
namespace {
bool IsHexString(const std::string& value, size_t expectedLength)
{
if (value.size() != expectedLength)
return false;
for (unsigned char c : value) {
if (!std::isxdigit(c))
return false;
}
return true;
}
} // anonymous namespace
bool ParseRemoteSnapshotManifest(const std::string& manifestText,
RemoteSnapshot& snapshot,
std::string& strError)
{
snapshot = RemoteSnapshot{};
try {
const nlohmann::json root = nlohmann::json::parse(manifestText);
if (!root.is_object() || !root.contains("canonical") ||
!root.contains("files") || !root.contains("chain_tip")) {
strError = "manifest.json is missing canonical, files, or chain_tip";
return false;
}
snapshot.filename = root.at("canonical").at("snapshot").get<std::string>();
if (snapshot.filename.empty() || snapshot.filename == "." ||
snapshot.filename == ".." ||
snapshot.filename.find('/') != std::string::npos ||
snapshot.filename.find('\\') != std::string::npos) {
strError = "manifest snapshot filename must be a plain filename";
return false;
}
const nlohmann::json& files = root.at("files");
if (!files.is_object() || !files.contains(snapshot.filename)) {
strError = "canonical snapshot is absent from the files object";
return false;
}
const nlohmann::json& file = files.at(snapshot.filename);
const std::string type = file.at("type").get<std::string>();
if (type.rfind("utxo_snapshot", 0) != 0) {
strError = "canonical file is not a UTXO snapshot";
return false;
}
snapshot.sha256 = file.at("sha256").get<std::string>();
std::transform(snapshot.sha256.begin(), snapshot.sha256.end(),
snapshot.sha256.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
snapshot.height = root.at("chain_tip").at("height").get<int>();
snapshot.blockHash = root.at("chain_tip").at("blockhash").get<std::string>();
std::transform(snapshot.blockHash.begin(), snapshot.blockHash.end(),
snapshot.blockHash.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (snapshot.height <= 0 || !IsHexString(snapshot.sha256, 64) ||
!IsHexString(snapshot.blockHash, 64)) {
strError = "manifest snapshot height or hash fields are invalid";
return false;
}
} catch (const std::exception& e) {
strError = std::string("invalid manifest.json: ") + e.what();
return false;
}
return true;
}
bool DownloadUtxoSnapshot(const std::string& host,
const fs::path& dataDir,
ProgressCallback progressFn,
@@ -1121,79 +1249,51 @@ bool DownloadUtxoSnapshot(const std::string& host,
{
const bool noProxy = true;
// Step 1: discover the canonical snapshot filename + expected SHA256 +
// per-snapshot manifest filename from the big manifest.json. Falls back
// to legacy URL if manifest unavailable.
std::string snapshotFilename = "utxo-snapshot.bin";
std::string expectedSha256;
std::string snapshotManifestFilename;
bool haveManifest = false;
// manifest.json is discovery metadata, not a trust root. The only accepted
// snapshot hash is the one compiled into this release for the same height.
fs::path tmpManifest = dataDir / "manifest.json.tmp";
if (DownloadFile(host, "manifest.json", tmpManifest, nullptr, strError, noProxy)) {
std::string text = ReadFileToString(tmpManifest);
if (!DownloadFile(host, std::string(BASE_PATH) + "manifest.json",
tmpManifest, nullptr, strError, noProxy,
-1, 4 * 1024 * 1024)) {
fs::remove(tmpManifest);
std::string mFile, mSha, mManifest;
std::string mErr;
if (FindCanonicalSnapshotInManifest(text, mFile, mSha, mManifest, mErr)) {
snapshotFilename = mFile;
expectedSha256 = mSha;
snapshotManifestFilename = mManifest;
haveManifest = true;
printf("Bootstrap: manifest declares canonical snapshot %s (sha256=%s)\n",
snapshotFilename.c_str(), expectedSha256.substr(0, 16).c_str());
} else {
printf("Bootstrap: manifest parse failed (%s) — falling back to legacy URL\n",
mErr.c_str());
}
} else {
printf("Bootstrap: no manifest.json available — falling back to legacy URL\n");
strError.clear();
return false;
}
// Step 2: verify the per-snapshot manifest's signature. This is the
// AUTHENTICATION gate — the signature attests that the listed snapshot
// file came from a trusted operator. No checkpoint required; signature
// alone proves authenticity.
if (!snapshotManifestFilename.empty()) {
fs::path tmpSnapManifest = dataDir / "snapshot-manifest.tmp";
if (!DownloadFile(host, snapshotManifestFilename, tmpSnapManifest, nullptr, strError, noProxy)) {
fs::remove(tmpSnapManifest);
return false;
}
std::string snapManifestText = ReadFileToString(tmpSnapManifest);
fs::remove(tmpSnapManifest);
std::string signerAddr = ExtractJsonString(snapManifestText, "signing_address");
std::string message = ExtractJsonString(snapManifestText, "message");
std::string signature = ExtractJsonString(snapManifestText, "signature");
std::string declaredSha = ExtractJsonString(snapManifestText, "snapshot_sha256");
if (signerAddr.empty() || message.empty() || signature.empty()) {
strError = "per-snapshot manifest missing required fields (signing_address/message/signature)";
return false;
}
if (!IsTrustedSnapshotSigner(signerAddr)) {
strError = "snapshot manifest signer " + signerAddr + " is not in trusted signers list";
return false;
}
std::string vErr;
if (!VerifySignedMessage(signerAddr, signature, message, vErr)) {
strError = "snapshot signature verification failed: " + vErr;
return false;
}
if (!declaredSha.empty())
expectedSha256 = declaredSha;
printf("Bootstrap: snapshot signature verified (signer=%s)\n", signerAddr.c_str());
} else {
printf("Bootstrap: WARNING — no per-snapshot manifest available; "
"loading snapshot WITHOUT signature verification\n");
const std::string manifestText = ReadFileToString(tmpManifest);
fs::remove(tmpManifest);
if (manifestText.empty()) {
strError = "Cannot read downloaded manifest.json";
return false;
}
// Step 3: download the canonical snapshot file.
RemoteSnapshot snapshot;
if (!ParseRemoteSnapshotManifest(manifestText, snapshot, strError))
return false;
const uint256 manifestBlockHash(snapshot.blockHash);
if (!Checkpoints::IsKnownCheckpoint(snapshot.height, manifestBlockHash)) {
strError = "Server snapshot tip is not a hardened checkpoint in this release";
return false;
}
uint256 compiledFileHash;
if (!Checkpoints::GetSnapshotHash(snapshot.height, compiledFileHash)) {
strError = "Snapshot height " + std::to_string(snapshot.height) +
" has no file hash compiled into this release";
return false;
}
const std::string compiledSha256 = compiledFileHash.ToString();
if (snapshot.sha256 != compiledSha256) {
strError = "Server snapshot hash does not match the hash compiled into this release";
return false;
}
printf("Bootstrap: manifest selects compiled snapshot %s at height %d (sha256=%s)\n",
snapshot.filename.c_str(), snapshot.height,
compiledSha256.substr(0, 16).c_str());
fs::path tmpPath = dataDir / "utxo-snapshot.bin.tmp";
std::string urlPath = std::string(BASE_PATH) + snapshotFilename;
std::string urlPath = std::string(BASE_PATH) + snapshot.filename;
printf("Bootstrap: downloading UTXO snapshot from %s%s...\n", host.c_str(), urlPath.c_str());
@@ -1202,30 +1302,25 @@ bool DownloadUtxoSnapshot(const std::string& host,
return false;
}
// Step 4: verify the downloaded file's SHA256 against the manifest.
if (!expectedSha256.empty()) {
std::string actualSha = Sha256OfFile(tmpPath);
if (actualSha.empty()) {
strError = "Cannot read downloaded snapshot for SHA256 verification";
fs::remove(tmpPath);
return false;
}
if (actualSha != expectedSha256) {
strError = "Snapshot SHA256 mismatch: expected " + expectedSha256
+ ", got " + actualSha
+ " (manifest/snapshot tampering or server misconfiguration)";
fs::remove(tmpPath);
return false;
}
printf("Bootstrap: snapshot SHA256 verified (%s)\n", actualSha.substr(0, 16).c_str());
const std::string actualSha256 = Sha256OfFile(tmpPath);
if (actualSha256.empty()) {
strError = "Cannot read downloaded snapshot for SHA256 verification";
fs::remove(tmpPath);
return false;
}
if (actualSha256 != compiledSha256) {
strError = "Snapshot SHA256 does not match the hash compiled into this release";
fs::remove(tmpPath);
return false;
}
printf("Bootstrap: compiled snapshot SHA256 verified (%s)\n",
actualSha256.substr(0, 16).c_str());
printf("Bootstrap: UTXO snapshot downloaded, loading into database...\n");
// Step 5: load the snapshot. requireCheckpoint is FALSE — signature is
// the authentication gate; checkpoints would force snapshots only at
// specific heights. Signature alone is sufficient.
if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError, /*requireCheckpoint=*/false)) {
// File hash and tip checkpoint are independent gates. The hash commits to
// the complete serialized UTXO set; the checkpoint commits to chain identity.
if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError, /*requireCheckpoint=*/true)) {
fs::remove(tmpPath);
return false;
}
+23 -25
View File
@@ -8,13 +8,14 @@
#include <vector>
#include <functional>
#include <filesystem>
#include <cstdint>
namespace Bootstrap {
// Bootstrap server configuration
static const char* DEFAULT_HOST = "bootstrap.cryptographic-triangles.org";
static const char* BASE_PATH = "/";
static const int PORT = 80;
inline constexpr const char* DEFAULT_HOST = "bootstrap.cryptographic-triangles.org";
inline constexpr const char* BASE_PATH = "/";
inline constexpr int PORT = 443;
// Progress callback: (bytesDownloaded, totalBytes)
typedef std::function<void(int64_t, int64_t)> ProgressCallback;
@@ -31,7 +32,8 @@ namespace Bootstrap {
ProgressCallback progressFn,
std::string& strError,
bool noProxy = false,
int portOverride = -1);
int portOverride = -1,
int64_t maxDownloadBytes = 4LL * 1024 * 1024 * 1024);
// Fetch the file manifest (list of relative paths to download)
bool FetchFileList(const std::string& host,
@@ -46,6 +48,23 @@ namespace Bootstrap {
ProgressCallback progressFn,
std::string& strError);
// Advertised identity of a snapshot listed by manifest.json.
// The advertised SHA256 is accepted only when it matches the hash compiled
// into checkpoints.cpp for the same height.
struct RemoteSnapshot {
std::string filename;
std::string sha256;
int height;
std::string blockHash;
};
// Parse and validate the small, untrusted bootstrap manifest. This routine
// performs no network I/O and is exposed so malformed-input behavior can be
// covered by unit tests.
bool ParseRemoteSnapshotManifest(const std::string& manifestText,
RemoteSnapshot& snapshot,
std::string& strError);
// Snapshot manifest (parsed from snapshot.manifest in bootstrap archive)
struct SnapshotManifest {
int format; // format version, must be 1
@@ -73,27 +92,6 @@ namespace Bootstrap {
ProgressCallback progressFn,
std::string& strError);
// ===================================================================
// Trusted snapshot publisher — RPC-driven single-slot rotation
// ===================================================================
// Returns the currently active trusted publisher, or empty string if
// only the built-in fallback is in effect.
std::string GetActiveTrustedSnapshotPublisher();
// Atomically replaces the active publisher. The previous one is dropped
// immediately (Design A: single-slot, no grace period). Persists to
// <datadir>/snapshot-publisher.json so the choice survives restarts.
bool SetTrustedSnapshotPublisher(const std::string& addr,
std::string& strError);
// Clears the runtime override and reverts to the built-in fallback
// list. Also removes snapshot-publisher.json from disk.
bool UnsetTrustedSnapshotPublisher(std::string& strError);
// Called once at daemon startup (from init.cpp) to load any persisted
// runtime override.
void LoadTrustedSnapshotPublisher();
} // namespace Bootstrap
#endif // TRIANGLES_BOOTSTRAP_H
+2 -1
View File
@@ -425,7 +425,8 @@ bool LoadSignedCheckpoints(
if (Bootstrap::DownloadFile(host, "signed-checkpoints.json",
std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp",
nullptr, strError,
/*noProxy=*/true)) {
/*noProxy=*/true, /*portOverride=*/-1,
/*maxDownloadBytes=*/10 * 1024 * 1024)) {
std::filesystem::path tmp = std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp";
FILE* f = fopen(tmp.string().c_str(), "rb");
if (f) {
+7 -48
View File
@@ -353,53 +353,14 @@ namespace Checkpoints
bool SetCheckpointPrivKey(std::string strPrivKey)
{
// Test signing a sync-checkpoint with genesis block
CSyncCheckpoint checkpoint;
checkpoint.hashCheckpoint = !fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet;
CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION);
sMsg << (CUnsignedSyncCheckpoint)checkpoint;
checkpoint.vchMsg = std::vector<unsigned char>(sMsg.begin(), sMsg.end());
std::vector<unsigned char> vchPrivKey = ParseHex(strPrivKey);
CKey key;
key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash
if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig))
return false;
// Test signing successful, proceed
CSyncCheckpoint::strMasterPrivKey = strPrivKey;
return true;
(void)strPrivKey;
return error("SetCheckpointPrivKey: synchronized checkpoints are disabled");
}
bool SendSyncCheckpoint(uint256 hashCheckpoint)
{
CSyncCheckpoint checkpoint;
checkpoint.hashCheckpoint = hashCheckpoint;
CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION);
sMsg << (CUnsignedSyncCheckpoint)checkpoint;
checkpoint.vchMsg = std::vector<unsigned char>(sMsg.begin(), sMsg.end());
if (CSyncCheckpoint::strMasterPrivKey.empty())
return error("SendSyncCheckpoint: Checkpoint master key unavailable.");
std::vector<unsigned char> vchPrivKey = ParseHex(CSyncCheckpoint::strMasterPrivKey);
CKey key;
key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash
if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig))
return error("SendSyncCheckpoint: Unable to sign checkpoint, check private key?");
if(!checkpoint.ProcessSyncCheckpoint(nullptr))
{
printf("WARNING: SendSyncCheckpoint: Failed to process checkpoint.\n");
return false;
}
// Relay checkpoint
{
LOCK(cs_vNodes);
for (CNode* pnode : vNodes)
checkpoint.RelayTo(pnode);
}
return true;
(void)hashCheckpoint;
return error("SendSyncCheckpoint: synchronized checkpoints are disabled");
}
// Is the sync-checkpoint outside maturity window?
@@ -420,13 +381,11 @@ const std::string CSyncCheckpoint::strMasterPubKey = "";
std::string CSyncCheckpoint::strMasterPrivKey = "";
// triangles: verify signature of sync-checkpoint message
// Master key system disabled - checkpoint signatures are no longer required
// The master-key system is disabled. Reject these legacy messages instead of
// treating unsigned data as authenticated if a dispatcher is added later.
bool CSyncCheckpoint::CheckSignature()
{
// Deserialize the checkpoint data without signature verification
CDataStream sMsg(vchMsg, SER_NETWORK, PROTOCOL_VERSION);
sMsg >> *(CUnsignedSyncCheckpoint*)this;
return true;
return error("CSyncCheckpoint::CheckSignature: synchronized checkpoints are disabled");
}
// triangles: process synchronized checkpoint
+92 -61
View File
@@ -30,8 +30,11 @@
#include "addressindex.h"
#include "chaindb_migrate.h"
#include <memory>
#include <atomic>
#include <cstdlib>
#include <thread>
#include <vector>
#include <cerrno>
// Forward declaration: InitError / InitWarning are defined further down
// in this file but referenced by AppInit (line ~423) before the definition.
@@ -66,6 +69,8 @@ using namespace std;
namespace fs = std::filesystem;
namespace {
std::atomic<int> g_shutdownExitCode{EXIT_SUCCESS};
// Acquire an exclusive, non-blocking advisory lock on the datadir .lock file
// and hold it for the lifetime of the process. Replaces
// boost::interprocess::file_lock. The descriptor/handle is intentionally never
@@ -96,8 +101,32 @@ bool LockDataDirectory(const std::filesystem::path& pathLockFile)
return true; // fd held until process exit
#endif
}
#ifndef WIN32
bool EnsureOwnerOnlyFile(const std::filesystem::path& path, std::string& error)
{
struct stat fileStat;
if (::lstat(path.string().c_str(), &fileStat) != 0)
return errno == ENOENT;
if (!S_ISREG(fileStat.st_mode) || fileStat.st_uid != geteuid()) {
error = path.string() + " must be a regular file owned by the daemon user";
return false;
}
if ((fileStat.st_mode & (S_IRWXG | S_IRWXO)) != 0 &&
::chmod(path.string().c_str(), S_IRUSR | S_IWUSR) != 0) {
error = "could not restrict permissions on " + path.string();
return false;
}
return true;
}
#endif
} // namespace
void MarkShutdownFailure()
{
g_shutdownExitCode.store(EXIT_FAILURE, std::memory_order_relaxed);
}
std::unique_ptr<CWallet> pwalletMain;
CClientUIInterface uiInterface;
std::string strWalletFileName;
@@ -144,7 +173,8 @@ void ExitTimeout(void* parg)
{
#ifdef WIN32
MilliSleep(5000);
ExitProcess(0);
ExitProcess(static_cast<UINT>(
g_shutdownExitCode.load(std::memory_order_relaxed)));
#endif
}
@@ -421,7 +451,11 @@ void Shutdown(void* parg)
// MakeChainDB()->Close();
bitdb.Flush(false);
bitdb.Flush(true);
fs::remove(GetPidFile());
std::error_code pidFileError;
fs::remove(GetPidFile(), pidFileError);
if (pidFileError)
printf("Warning: could not remove PID file: %s\n",
pidFileError.message().c_str());
UnregisterWallet(pwalletMain.get());
pwalletMain.reset();
// DB is flushed and wallet saved - safe to force-exit if something hangs
@@ -431,7 +465,7 @@ void Shutdown(void* parg)
fExit = true;
#ifndef QT_GUI
// ensure non-UI client gets exited here, but let Triangles-Qt reach 'return 0;' in triangles.cpp
exit(0);
exit(g_shutdownExitCode.load(std::memory_order_relaxed));
#endif
}
else
@@ -528,8 +562,10 @@ bool AppInit(int argc, char* argv[])
} catch (...) {
PrintException(nullptr, "AppInit()");
}
if (!fRet)
if (!fRet) {
MarkShutdownFailure();
Shutdown(nullptr);
}
return fRet;
}
@@ -610,8 +646,8 @@ std::string HelpMessage()
//" -onlynet=<net> " + _("Only connect to nodes in network <net> (IPv4, IPv6 or Tor)") + "\n" +
//" -discover " + _("Discover own IP address (default: 1 when listening and no -externalip)") + "\n" +
//" -irc " + _("Find peers using internet relay chat (default: 0)") + "\n" +
//" -listen " + _("Accept connections from outside (default: 1 if no -proxy or -connect)") + "\n" +
//" -bind=<addr> " + _("Bind to given address. Use [host]:port notation for IPv6") + "\n" +
" -listen " + _("Accept inbound peer connections (default: 1 unless -proxy or -connect is set)") + "\n" +
" -bind=<addr> " + _("Bind inbound peers to this address. Use [host]:port notation for IPv6") + "\n" +
// -dnsseed " + _("Find peers using DNS lookup (default: 1)") + "\n" +
" -staking " + _("Stake your coins to support network and gain reward (default: 1)") + "\n" +
" -synctime " + _("Sync time with other nodes. Disable if time on your system is precise e.g. syncing with NTP (default: 1)") + "\n" +
@@ -652,8 +688,11 @@ std::string HelpMessage()
#endif
" -rpcuser=<user> " + _("Username for JSON-RPC connections") + "\n" +
" -rpcpassword=<pw> " + _("Password for JSON-RPC connections") + "\n" +
" -rpcport=<port> " + _("Listen for JSON-RPC connections on <port> (default: 19111 or testnet: 19112)") + "\n" +
" -rpcallowip=<ip> " + _("Allow JSON-RPC connections from specified IP address") + "\n" +
" -rpcport=<port> " + _("Listen for JSON-RPC connections on <port> (default: 19112 or testnet: 19111)") + "\n" +
" -rpcbind=<addr> " + _("Bind JSON-RPC to this address (default: loopback only; use * explicitly for all interfaces)") + "\n" +
" -rpcallowip=<ip> " + _("Allow JSON-RPC clients matching this address pattern; does not change the bind address") + "\n" +
" -rpcallowmethod=<name> " + _("Allow only this JSON-RPC method (repeat for each method; default: all)") + "\n" +
" -rpcservertimeout=<n> " + _("RPC socket read/write timeout in seconds (default: 30, range: 1-600)") + "\n" +
" -rpcconnect=<ip> " + _("Send commands to node running on <ip> (default: 127.0.0.1)") + "\n" +
" -blocknotify=<cmd> " + _("Execute command when the best block changes (%s in cmd is replaced by block hash)") + "\n" +
" -walletnotify=<cmd> " + _("Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)") + "\n" +
@@ -1120,24 +1159,19 @@ bool AppInit2()
fUseUPnP = GetBoolArg("-upnp", USE_UPNP);
#endif
bool fBound = false;
if (true) {
if (true) {
do {
// W1: Bind to all interfaces so external peers can connect.
//
// The previous code went through Lookup("0.0.0.0", ...) which
// hands the literal string to getaddrinfo(). On Windows that
// resolver can fail to map "0.0.0.0" to INADDR_ANY and the
// daemon would abort at startup with "Cannot resolve binding
// address". Construct the CService directly from INADDR_ANY
// instead — this is the canonical "any-address" binding and
// works on every platform without consulting the resolver.
if (!fNoListen) {
if (mapArgs.count("-bind")) {
for (const std::string& bindAddress : mapMultiArgs["-bind"]) {
CService addrBind;
struct in_addr any;
any.s_addr = htonl(INADDR_ANY);
addrBind = CService(any, GetListenPort());
if (!Lookup(bindAddress.c_str(), addrBind, GetListenPort(), false))
return InitError(strprintf(_("Cannot resolve -bind address: '%s'"),
bindAddress.c_str()));
fBound |= Bind(addrBind);
} while (false);
}
} else {
struct in_addr any;
any.s_addr = htonl(INADDR_ANY);
fBound = Bind(CService(any, GetListenPort()));
}
if (!fBound)
return InitError(_("Failed to listen on any port."));
@@ -1167,10 +1201,9 @@ bool AppInit2()
}
}
if (mapArgs.count("-checkpointkey")) // triangles: checkpoint master priv key
if (mapArgs.count("-checkpointkey"))
{
if (!Checkpoints::SetCheckpointPrivKey(GetArg(std::string_view{"-checkpointkey"}, std::string_view{""})))
InitError(_("Unable to sign checkpoint, wrong checkpointkey?\n"));
return InitError(_("Synchronized checkpoint signing is disabled."));
}
for (string strDest : mapMultiArgs["-seednode"])
@@ -1178,8 +1211,8 @@ bool AppInit2()
StartupPerfLog("network_init", GetTimeMillis() - nStart, strprintf("listen=%d seednodes=%" PRIszu, !fNoListen, mapMultiArgs["-seednode"].size()));
// ********************************************************* Step 6b: bootstrap download (daemon)
// Automatic: if data dir has no blockchain, bootstrap without asking.
// Can also be forced with -bootstrap flag, or disabled with -nobootstrap.
// Remote HTTP bootstrap is opt-in via -bootstrap. Fresh nodes otherwise
// use the compiled-hash P2P snapshot path or sync from genesis.
//
// v5.9.5: P2P UTXO snapshot fetch is the default for fresh installs (Step 11.6).
// The legacy clearnet HTTP bootstrap only runs when the user explicitly requests
@@ -1187,20 +1220,13 @@ bool AppInit2()
// Bootstrap auto-download works for both GUI and daemon.
// GUI users get the same automatic bootstrap on fresh installs.
{
bool wantsBootstrap = GetBoolArg("-bootstrap", false);
bool noBootstrap = GetBoolArg("-nobootstrap", false);
bool snapshotMode = GetBoolArg("-snapshot", true);
bool wantsBootstrap = GetBoolArg("-bootstrap", false) && !noBootstrap;
fs::path dataPath = GetDataDir();
// Load any runtime trusted snapshot publisher override that was
// persisted by a previous settrustedv2snapshotpublisher call.
Bootstrap::LoadTrustedSnapshotPublisher();
bool needsBootstrap = Bootstrap::NeedsBootstrap(dataPath);
if (needsBootstrap && !noBootstrap) {
printf("Bootstrap: no blockchain data found — downloading UTXO snapshot automatically.\n");
printf("Bootstrap: (use -nobootstrap to skip)\n");
uiInterface.InitMessage(_("Downloading UTXO snapshot..."));
wantsBootstrap = true;
if (needsBootstrap && !wantsBootstrap) {
printf("Bootstrap: no blockchain data found; remote bootstrap is disabled unless -bootstrap is set.\n");
}
if (wantsBootstrap)
@@ -1208,7 +1234,6 @@ bool AppInit2()
int64_t nBootstrapStart = GetTimeMillis();
fs::path dataPath = GetDataDir();
std::string host = Bootstrap::DEFAULT_HOST;
std::string strError;
int64_t lastGuiUpdate = 0;
auto progressFn = [&lastGuiUpdate](int64_t bytesDownloaded, int64_t totalBytes) {
@@ -1250,19 +1275,10 @@ bool AppInit2()
triedUtxoSnapshot = true;
}
// Fall back to full bootstrap.tar.gz if UTXO snapshot failed
// Never consume a server-directed file list. If the authenticated
// snapshot is unavailable, normal peer-to-peer sync is the safe fallback.
if (!success) {
uiInterface.InitMessage(_("Downloading blockchain snapshot..."));
printf("Bootstrap: contacting %s...\n", host.c_str());
success = Bootstrap::DownloadBootstrap(host, dataPath, progressFn, strError);
if (!success) {
printf("\nBootstrap: failed: %s\n", strError.c_str());
printf("Bootstrap: skipping, will sync from network.\n");
} else {
printf("\nBootstrap: done.\n");
}
printf("Bootstrap: no trusted compiled-hash snapshot available; syncing from peers.\n");
}
StartupPerfLog("bootstrap_download", GetTimeMillis() - nBootstrapStart,
@@ -1282,13 +1298,23 @@ bool AppInit2()
printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n");
uiInterface.InitMessage(_("Loading UTXO snapshot..."));
// Local file load: skip the checkpoint gate. The operator has
// filesystem access, so the trust model is already equivalent
// to direct chain state modification — a malicious local file
// is no worse than a malicious chain DB. P2P-delivered
// snapshots (SnapshotNet) keep the checkpoint gate on.
std::string strError;
if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError, /*requireCheckpoint=*/false)) {
const int snapshotHeight = Checkpoints::GetBestSnapshotHeight();
uint256 compiledHash;
uint256 actualHash;
const bool hasCompiledHash = snapshotHeight > 0 &&
Checkpoints::GetSnapshotHash(snapshotHeight, compiledHash);
const bool hashVerified = hasCompiledHash &&
SnapshotNet::ComputeSnapshotFileHash(snapshotFile, actualHash, strError) &&
actualHash == compiledHash;
if (!hashVerified) {
if (strError.empty())
strError = "snapshot SHA256 is not compiled into this release";
printf("UTXO snapshot rejected before import: %s\n", strError.c_str());
printf("Will proceed with normal sync.\n");
} else if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError,
/*requireCheckpoint=*/true)) {
printf("UTXO snapshot loaded successfully.\n");
} else {
printf("UTXO snapshot load failed: %s\n", strError.c_str());
@@ -1501,6 +1527,11 @@ bool AppInit2()
{
fs::path walletPath = GetDataDir() / strWalletFileName;
if (fs::exists(walletPath)) {
#ifndef WIN32
std::string permissionError;
if (!EnsureOwnerOnlyFile(walletPath, permissionError))
return InitError(permissionError);
#endif
uintmax_t wsize = fs::file_size(walletPath);
printf("Wallet file size: %llu bytes\n", (unsigned long long)wsize);
if (wsize < 1024) {
@@ -1933,10 +1964,10 @@ bool AppInit2()
printf("mapAddressBook.size() = %" PRIszu "\n", pwalletMain->mapAddressBook.size());
if (!NewThread(StartNode, nullptr))
InitError(_("Error: could not start node"));
return InitError(_("Error: could not start node"));
if (fServer)
NewThread(ThreadRPCServer, nullptr);
if (fServer && !NewThread(ThreadRPCServer, nullptr))
return InitError(_("Error: could not start the RPC server"));
// ********************************************************* Step 11.6: P2P UTXO snapshot fetch
// If the chain is empty and snapshot mode is enabled (default), spawn a
+1 -1
View File
@@ -12,6 +12,7 @@
extern std::unique_ptr<CWallet> pwalletMain;
extern std::string strWalletFileName;
void StartShutdown();
void MarkShutdownFailure();
bool ShutdownRequested();
void Shutdown(void* parg);
bool AppInit2();
@@ -19,4 +20,3 @@ std::string HelpMessage();
#endif
+41 -20
View File
@@ -190,28 +190,49 @@ bool CCryptoKeyStore::GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) co
return false;
}
bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn)
bool CCryptoKeyStore::PrepareKeyEncryption(CKeyingMaterial& vMasterKeyIn,
CryptedKeyMap& cryptedKeysOut) const
{
{
LOCK(cs_KeyStore);
if (!mapCryptedKeys.empty() || IsCrypted())
return false;
LOCK(cs_KeyStore);
if (!mapCryptedKeys.empty() || IsCrypted())
return false;
fUseCrypto = true;
for (KeyMap::value_type& mKey : mapKeys)
{
CKey key;
if (!key.SetSecret(mKey.second.first, mKey.second.second))
return false;
const CPubKey vchPubKey = key.GetPubKey();
std::vector<unsigned char> vchCryptedSecret;
bool fCompressed;
if (!EncryptSecret(vMasterKeyIn, key.GetSecret(fCompressed), vchPubKey.GetHash(), vchCryptedSecret))
return false;
if (!AddCryptedKey(vchPubKey, vchCryptedSecret))
return false;
}
mapKeys.clear();
cryptedKeysOut.clear();
for (const KeyMap::value_type& mKey : mapKeys)
{
CKey key;
if (!key.SetSecret(mKey.second.first, mKey.second.second))
return false;
const CPubKey vchPubKey = key.GetPubKey();
std::vector<unsigned char> vchCryptedSecret;
bool fCompressed;
if (!EncryptSecret(vMasterKeyIn, key.GetSecret(fCompressed),
vchPubKey.GetHash(), vchCryptedSecret))
return false;
if (!cryptedKeysOut.emplace(vchPubKey.GetID(),
std::make_pair(vchPubKey,
std::move(vchCryptedSecret))).second)
return false;
}
return cryptedKeysOut.size() == mapKeys.size();
}
bool CCryptoKeyStore::CommitKeyEncryption(CryptedKeyMap&& cryptedKeys)
{
LOCK(cs_KeyStore);
if (!mapCryptedKeys.empty() || IsCrypted() || cryptedKeys.size() != mapKeys.size())
return false;
mapCryptedKeys = std::move(cryptedKeys);
mapKeys.clear();
fUseCrypto = true;
return true;
}
bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn)
{
CryptedKeyMap cryptedKeys;
if (!PrepareKeyEncryption(vMasterKeyIn, cryptedKeys))
return false;
return CommitKeyEncryption(std::move(cryptedKeys));
}
+9 -1
View File
@@ -9,6 +9,8 @@
#include "util_signal.h"
#include "sync.h"
#include <utility>
class CScript;
/** A virtual base class for key stores */
@@ -112,7 +114,13 @@ protected:
bool SetCrypted();
// will encrypt previously unencrypted keys
// Stage and commit wallet-key encryption separately so callers can make
// the on-disk update atomic before discarding plaintext keys in memory.
bool PrepareKeyEncryption(CKeyingMaterial& vMasterKeyIn,
CryptedKeyMap& cryptedKeysOut) const;
bool CommitKeyEncryption(CryptedKeyMap&& cryptedKeys);
// Encrypt previously unencrypted keys in memory.
bool EncryptKeys(CKeyingMaterial& vMasterKeyIn);
bool Unlock(const CKeyingMaterial& vMasterKeyIn);
+22 -6
View File
@@ -1747,6 +1747,11 @@ bool IsConsensusAssumeValidHeight(int nHeight)
|| (nHeight <= nAssumeValidThreshold);
}
bool IsBlockSignatureRequiredAtHeight(int nHeight)
{
return nHeight > Checkpoints::GetTotalBlocksEstimate();
}
void static InvalidChainFound(CBlockIndex* pindexNew)
{
if (pindexNew->nChainTrust > nBestInvalidTrust)
@@ -3401,6 +3406,12 @@ bool CBlock::AcceptBlock()
uint256 hashProofOfStake = 0, targetProofOfStake = 0;
if (IsProofOfStake())
{
// The rolling validation optimization is not a signature trust root.
// Every PoS block above the compiled checkpoint must authorize its
// exact block contents, including while the local tip is stale.
if (IsBlockSignatureRequiredAtHeight(nHeight) && !CheckBlockSignature())
return DoS(100, error("AcceptBlock() : bad proof-of-stake block signature at height %d", nHeight));
if (IsConsensusAssumeValidHeight(nHeight))
{
// Historical fast path: blocks at/below hardcoded checkpoint or
@@ -3539,10 +3550,16 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
if (pblock->IsProofOfStake() && !GetBoolArg("-ignoredupstake", false) && setStakeSeen.count(pblock->GetProofOfStake()) && !mapOrphanBlocksByPrev.count(hash))
return error("ProcessBlock() : duplicate proof-of-stake (%s, %d) for block %s", pblock->GetProofOfStake().first.ToString().c_str(), pblock->GetProofOfStake().second, hash.ToString().c_str());
// Preliminary checks
// Skip block signature verification during initial block download (below checkpoint).
// The hardcoded checkpoint guarantees historical chain integrity.
if (!pblock->CheckBlock(true, true, !IsInitialBlockDownload()))
// Operational IBD state is never permission to skip a live proof-of-stake
// block signature. Only a candidate height committed by the latest
// hardened checkpoint uses the historical fast path.
bool checkBlockSignature = true;
const auto prevIt = mapBlockIndex.find(pblock->hashPrevBlock);
if (prevIt != mapBlockIndex.end()) {
const int candidateHeight = prevIt->second->nHeight + 1;
checkBlockSignature = IsBlockSignatureRequiredAtHeight(candidateHeight);
}
if (!pblock->CheckBlock(true, true, checkBlockSignature))
{
printf("IBD-DIAG: CheckBlock FAILED for %s (PoS=%d, IBD=%d)\n",
hash.ToString().substr(0,20).c_str(), pblock->IsProofOfStake(), IsInitialBlockDownload());
@@ -3786,6 +3803,7 @@ bool CheckDiskSpace(uint64_t nAdditionalBytes)
strMiscWarning = strMessage;
printf("*** %s\n", strMessage.c_str());
uiInterface.ThreadSafeMessageBox(strMessage, "Triangles", CClientUIInterface::OK | CClientUIInterface::ICON_EXCLAMATION | CClientUIInterface::MODAL);
MarkShutdownFailure();
StartShutdown();
return false;
}
@@ -6039,5 +6057,3 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
}
return true;
}
+1
View File
@@ -176,6 +176,7 @@ bool IsStakingSafe(const CWallet* pwallet, const std::vector<CNode*>& vNodesSnap
// assume-valid validation. This intentionally excludes operational IBD states
// such as a stale tip; stale-tip IBD must not disable live PoS checks.
[[nodiscard]] bool IsConsensusAssumeValidHeight(int nHeight);
[[nodiscard]] bool IsBlockSignatureRequiredAtHeight(int nHeight);
std::string GetWarnings(std::string strFor);
bool GetTransaction(const uint256 &hash, CTransaction &tx, uint256 &hashBlock);
uint256 WantedByOrphan(const CBlock* pblockOrphan);
+39 -5
View File
@@ -605,7 +605,8 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest)
}
if (fDebug) {
printf("ConnectNode(): pszDest: %s\n", pszDest);
printf("ConnectNode(): destination: %s\n",
pszDest ? pszDest : addrConnect.ToString().c_str());
}
/// debug print
@@ -1738,7 +1739,7 @@ void ThreadOnionSeed(void* parg)
// Fetch dynamic seeds with retry — up to 4 attempts with increasing backoff.
// This is the primary discovery mechanism — seeds.cryptographic-triangles.org
{
if (!GetBoolArg("-noseedurl", false)) {
bool ok = false;
int delays[] = {0, 30, 60, 120};
for (int attempt = 0; attempt < 4 && !ok && !fShutdown; attempt++) {
@@ -1806,7 +1807,8 @@ void ThreadOnionSeed(void* parg)
else
printf("ThreadOnionSeed: low outbound peers (%d), re-seeding...\n", nOutbound);
ThreadHTTPSeedFetch2(nullptr);
if (!GetBoolArg("-noseedurl", false))
ThreadHTTPSeedFetch2(nullptr);
// Re-queue hardcoded seeds for direct connection
for (unsigned int seed_idx = 0; strOnionSeed[seed_idx][0] != nullptr; seed_idx++) {
@@ -1891,6 +1893,12 @@ bool ThreadHTTPSeedFetch2(void* parg)
seedPath = seedHost.substr(slashPos);
seedHost = seedHost.substr(0, slashPos);
}
if (seedHost.empty() || seedHost.find_first_of("\r\n") != std::string::npos ||
seedPath.empty() || seedPath[0] != '/' ||
seedPath.find_first_of("\r\n") != std::string::npos) {
printf("HTTPS seed fetch: invalid -seedurl value\n");
return false;
}
printf("Fetching seed list from https://%s%s (via Tor)...\n", seedHost.c_str(), seedPath.c_str());
@@ -1929,7 +1937,14 @@ bool ThreadHTTPSeedFetch2(void* parg)
}
// Set SNI hostname (required for Caddy/Let's Encrypt)
SSL_set_tlsext_host_name(ssl, seedHost.c_str());
if (SSL_set_tlsext_host_name(ssl, seedHost.c_str()) != 1 ||
SSL_set1_host(ssl, seedHost.c_str()) != 1) {
printf("HTTPS seed fetch: failed to configure TLS hostname verification\n");
SSL_free(ssl);
SSL_CTX_free(ctx);
closesocket(hSocket);
return false;
}
SSL_set_fd(ssl, (int)hSocket);
int ret = SSL_connect(ssl);
@@ -1944,6 +1959,15 @@ bool ThreadHTTPSeedFetch2(void* parg)
closesocket(hSocket);
return false;
}
if (SSL_get_verify_result(ssl) != X509_V_OK) {
printf("HTTPS seed fetch: certificate verification failed for %s\n",
seedHost.c_str());
SSL_shutdown(ssl);
SSL_free(ssl);
SSL_CTX_free(ctx);
closesocket(hSocket);
return false;
}
printf("HTTPS seed fetch: TLS connection established to %s\n", seedHost.c_str());
@@ -1973,10 +1997,19 @@ bool ThreadHTTPSeedFetch2(void* parg)
// Read response over TLS
std::string response;
char buf[4096];
static constexpr size_t MAX_SEED_RESPONSE_SIZE = 1024 * 1024;
while (true) {
int nBytes = SSL_read(ssl, buf, sizeof(buf));
if (nBytes <= 0)
break;
if (response.size() + static_cast<size_t>(nBytes) > MAX_SEED_RESPONSE_SIZE) {
printf("HTTPS seed fetch: response exceeds 1 MiB limit\n");
SSL_shutdown(ssl);
SSL_free(ssl);
SSL_CTX_free(ctx);
closesocket(hSocket);
return false;
}
response.append(buf, nBytes);
}
@@ -2002,7 +2035,8 @@ bool ThreadHTTPSeedFetch2(void* parg)
// Check status code
std::string statusLine = response.substr(0, response.find("\r\n"));
if (statusLine.find("200") == std::string::npos) {
if (statusLine.size() < 12 || statusLine.compare(0, 7, "HTTP/1.") != 0 ||
statusLine.compare(9, 3, "200") != 0) {
printf("HTTPS seed fetch: %s from %s\n", statusLine.c_str(), seedHost.c_str());
return false;
}
+27 -10
View File
@@ -141,12 +141,27 @@ inline SOCKET ConnectRPCSocket(const std::string& host, int port)
return hSocket;
}
// Create listening sockets for the RPC server. When loopbackOnly is true the
// server binds the loopback interface(s) only; otherwise it binds the wildcard
// address(es). IPv4 and IPv6 are bound on separate sockets (IPV6_V6ONLY) so the
// two never conflict. Returns the bound, listening sockets; empty + strError on
// total failure (partial success — e.g. only IPv4 — is returned as success).
inline std::vector<SOCKET> BindRPCSockets(int port, bool loopbackOnly, std::string& strError)
inline bool SetRPCSocketTimeouts(SOCKET socket, int timeoutSeconds)
{
#ifdef WIN32
DWORD timeout = static_cast<DWORD>(timeoutSeconds * 1000);
#else
struct timeval timeout;
timeout.tv_sec = timeoutSeconds;
timeout.tv_usec = 0;
#endif
const char* value = reinterpret_cast<const char*>(&timeout);
const socklen_t valueSize = sizeof(timeout);
return ::setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, value, valueSize) == 0 &&
::setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, value, valueSize) == 0;
}
// Create listening sockets for the RPC server. An empty bindAddress binds only
// localhost. A non-empty value binds exactly that address; "*" explicitly
// requests wildcard addresses. IPv4 and IPv6 use separate sockets when the
// selected name resolves to both families.
inline std::vector<SOCKET> BindRPCSockets(int port, const std::string& bindAddress,
std::string& strError)
{
std::vector<SOCKET> vListen;
@@ -154,13 +169,15 @@ inline std::vector<SOCKET> BindRPCSockets(int port, bool loopbackOnly, std::stri
std::memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; // wildcard when node == nullptr
const bool wildcard = bindAddress == "*";
if (wildcard)
hints.ai_flags = AI_PASSIVE;
struct addrinfo* res = nullptr;
const std::string portStr = std::to_string(port);
// "localhost" resolves to the loopback addresses (127.0.0.1 and ::1);
// nullptr + AI_PASSIVE yields the wildcard addresses.
const char* node = loopbackOnly ? "localhost" : nullptr;
const char* node = wildcard ? nullptr :
(bindAddress.empty() ? "localhost" : bindAddress.c_str());
int gai = ::getaddrinfo(node, portStr.c_str(), &hints, &res);
if (gai != 0) {
strError = std::string("RPC bind: getaddrinfo failed: ") + gai_strerror(gai);
-88
View File
@@ -1323,91 +1323,3 @@ Value dumputxoset(const Array& params, bool fHelp)
return result;
}
// ============================================================================
// Trusted snapshot publisher RPCs (Design A: single-slot rotation)
// ============================================================================
//
// settrustedv2snapshotpublisher <address>
// - Atomically replaces the active trusted snapshot publisher.
// - The previous publisher is dropped immediately (no grace period).
// - The new publisher is persisted to <datadir>/snapshot-publisher.json
// so the choice survives daemon restarts.
//
// gettrustedv2snapshotpublisher
// - Returns the currently active runtime override.
// - Empty string means no runtime override; built-in fallback list is
// the source of truth (which contains "TG8f76ykt...").
//
// unsettrustedv2snapshotpublisher
// - Clears the runtime override.
// - The built-in fallback list (read-only, compiled in) becomes the
// source of truth again.
// - Removes <datadir>/snapshot-publisher.json.
Value settrustedv2snapshotpublisher(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 1)
throw runtime_error(
"settrustedv2snapshotpublisher <address>\n"
"Atomically replace the trusted snapshot publisher.\n"
"The previous publisher is dropped immediately (no grace period).\n"
"The new publisher is persisted to <datadir>/snapshot-publisher.json.\n"
"\nArguments:\n"
"1. address (string, required) Triangles T-address (34 chars, starts with 'T')\n"
"\nResult:\n"
"{ previous: 'T...', current: 'T...' } (previous is empty if first set)\n"
"\nExample:\n"
" triangles-cli settrustedv2snapshotpublisher TGotWuftzH7rD9tXC7whE8EXiyC3mr1CrH");
std::string addr = params[0].get_str();
std::string previous = Bootstrap::GetActiveTrustedSnapshotPublisher();
std::string err;
if (!Bootstrap::SetTrustedSnapshotPublisher(addr, err)) {
throw JSONRPCError(RPC_INVALID_PARAMETER, err);
}
Object result;
result.push_back(Pair("previous", previous));
result.push_back(Pair("current", addr));
if (!err.empty())
result.push_back(Pair("warning", err));
return result;
}
Value gettrustedv2snapshotpublisher(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 0)
throw runtime_error(
"gettrustedv2snapshotpublisher\n"
"Returns the currently active trusted snapshot publisher.\n"
"Empty string means no runtime override is set; the built-in\n"
"fallback list (compiled in) is the source of truth.\n"
"\nResult:\n"
"{ active: 'T...', has_runtime_override: true|false }");
std::string active = Bootstrap::GetActiveTrustedSnapshotPublisher();
Object result;
result.push_back(Pair("active", active));
result.push_back(Pair("has_runtime_override", !active.empty()));
return result;
}
Value unsettrustedv2snapshotpublisher(const Array& params, bool fHelp)
{
if (fHelp || params.size() != 0)
throw runtime_error(
"unsettrustedv2snapshotpublisher\n"
"Clear the runtime trusted snapshot publisher override.\n"
"The built-in fallback list (compiled in) becomes the source of truth again.\n"
"Removes <datadir>/snapshot-publisher.json.\n"
"\nResult:\n"
"{ unset: true, fallback_in_effect: true }");
std::string err;
if (!Bootstrap::UnsetTrustedSnapshotPublisher(err)) {
throw JSONRPCError(RPC_INTERNAL_ERROR, err);
}
Object result;
result.push_back(Pair("unset", true));
result.push_back(Pair("fallback_in_effect", true));
return result;
}
+21 -8
View File
@@ -13,6 +13,8 @@
#include "tor/onion_v3.h"
#include "tor/tor_embedded.h"
#include <memory>
using namespace json_spirit;
using namespace std;
@@ -1519,7 +1521,7 @@ Value walletpassphrase(const Array& params, bool fHelp)
if (pwalletMain->IsCrypted() && (fHelp || params.size() < 2 || params.size() > 3))
throw runtime_error(
"walletpassphrase <passphrase> <timeout> [stakingonly]\n"
"Stores the wallet decryption key in memory for <timeout> seconds.\n"
"Stores the wallet decryption key in memory for <timeout> seconds (1-604800).\n"
"if [stakingonly] is true sending functions are disabled.");
if (fHelp)
return true;
@@ -1530,6 +1532,14 @@ Value walletpassphrase(const Array& params, bool fHelp)
if (!pwalletMain->IsLocked())
throw JSONRPCError(RPC_WALLET_ALREADY_UNLOCKED, "Error: Wallet is already unlocked, use walletlock first if need to change unlock settings.");
const int64_t timeoutSeconds = params[1].get_int64();
if (timeoutSeconds < 1 || timeoutSeconds > 7 * 24 * 60 * 60)
throw JSONRPCError(RPC_INVALID_PARAMETER,
"Wallet unlock timeout must be between 1 and 604800 seconds.");
const bool stakingOnly = params.size() > 2 ? params[2].get_bool() : false;
std::unique_ptr<int64_t> sleepTime(new int64_t(timeoutSeconds));
// Note that the walletpassphrase is stored in params[0] which is not mlock()ed
SecureString strWalletPass;
strWalletPass.reserve(100);
@@ -1545,15 +1555,18 @@ Value walletpassphrase(const Array& params, bool fHelp)
"walletpassphrase <passphrase> <timeout>\n"
"Stores the wallet decryption key in memory for <timeout> seconds.");
NewThread(ThreadTopUpKeyPool, nullptr);
int64_t* pnSleepTime = new int64_t(params[1].get_int64());
NewThread(ThreadCleanWalletPassphrase, pnSleepTime);
// triangles: if user OS account compromised prevent trivial sendmoney commands
if (params.size() > 2)
fWalletUnlockStakingOnly = params[2].get_bool();
else
fWalletUnlockStakingOnly = stakingOnly;
if (!NewThread(ThreadCleanWalletPassphrase, sleepTime.get())) {
pwalletMain->Lock();
fWalletUnlockStakingOnly = false;
throw JSONRPCError(RPC_WALLET_ERROR,
"Could not start the wallet relock timer; wallet was locked again.");
}
sleepTime.release();
if (!NewThread(ThreadTopUpKeyPool, nullptr))
printf("walletpassphrase: could not start background keypool refill\n");
return Value::null;
}
+5
View File
@@ -553,3 +553,8 @@ scrypt_core_loop2:
#endif
#endif
#if defined(__ELF__)
.section .note.GNU-stack,"",%progbits
#endif
+4
View File
@@ -910,3 +910,7 @@ xmm_scrypt_core_loop2:
ret
#endif
#if defined(__ELF__)
.section .note.GNU-stack,"",@progbits
#endif
+5 -1
View File
@@ -856,4 +856,8 @@ xmm_scrypt_core_loop2:
popq %rbx
ret
#endif
#endif
#if defined(__ELF__)
.section .note.GNU-stack,"",@progbits
#endif
+18 -8
View File
@@ -3555,6 +3555,8 @@ int SecureMsgValidate(unsigned char *pHeader, unsigned char *pPayload, uint32_t
memcpy(civ+i, &nonse, 4);
HMAC_CTX *ctx = HMAC_CTX_new();
if (ctx == nullptr)
return 1;
unsigned int nBytes;
if (!HMAC_Init_ex(ctx, &civ[0], 32, EVP_sha256(), nullptr)
@@ -3571,7 +3573,7 @@ int SecureMsgValidate(unsigned char *pHeader, unsigned char *pPayload, uint32_t
{
if (sha256Hash[31] == 0
&& sha256Hash[30] == 0
&& (~(sha256Hash[29]) & ((1<<0) || (1<<1) || (1<<2)) ))
&& (sha256Hash[29] & 1U) == 0)
{
if (fDebugSmsg)
printf("Hash Valid.\n");
@@ -3614,6 +3616,8 @@ int SecureMsgSetHash(unsigned char *pHeader, unsigned char *pPayload, uint32_t n
bool found = false;
HMAC_CTX *ctx = HMAC_CTX_new();
if (ctx == nullptr)
return 1;
uint32_t nonse = 0;
@@ -3655,7 +3659,7 @@ int SecureMsgSetHash(unsigned char *pHeader, unsigned char *pPayload, uint32_t n
if (sha256Hash[31] == 0
&& sha256Hash[30] == 0
&& (~(sha256Hash[29]) & ((1<<0) || (1<<1) || (1<<2)) ))
&& (sha256Hash[29] & 1U) == 0)
// && sha256Hash[29] == 0)
{
found = true;
@@ -3794,7 +3798,10 @@ int SecureMsgEncrypt(SecureMessage& smsg, std::string& addressFrom, std::string&
// -- Generate 16 random bytes as IV.
RandAddSeedPerfmon();
RAND_bytes(&smsg.iv[0], 16);
if (RAND_bytes(&smsg.iv[0], 16) != 1) {
printf("Could not generate a secure message IV.\n");
return 1;
}
// -- Generate a new random EC key pair with private key called r and public key called R.
@@ -3959,14 +3966,16 @@ int SecureMsgEncrypt(SecureMessage& smsg, std::string& addressFrom, std::string&
unsigned int nBytes = 32;
HMAC_CTX *ctx = HMAC_CTX_new();
if (!HMAC_Init_ex(ctx, &key_m[0], 32, EVP_sha256(), nullptr)
if (ctx == nullptr
|| !HMAC_Init_ex(ctx, &key_m[0], 32, EVP_sha256(), nullptr)
|| !HMAC_Update(ctx, (unsigned char*) &smsg.timestamp, sizeof(smsg.timestamp))
|| !HMAC_Update(ctx, &vchCiphertext[0], vchCiphertext.size())
|| !HMAC_Final(ctx, smsg.mac, &nBytes)
|| nBytes != 32)
fHmacOk = false;
HMAC_CTX_free(ctx);
if (ctx != nullptr)
HMAC_CTX_free(ctx);
if (!fHmacOk)
{
@@ -4269,14 +4278,16 @@ int SecureMsgDecrypt(bool fTestOnly, std::string& address, unsigned char *pHeade
unsigned int nBytes = 32;
HMAC_CTX *ctx = HMAC_CTX_new();
if (!HMAC_Init_ex(ctx, &key_m[0], 32, EVP_sha256(), nullptr)
if (ctx == nullptr
|| !HMAC_Init_ex(ctx, &key_m[0], 32, EVP_sha256(), nullptr)
|| !HMAC_Update(ctx, (unsigned char*) &psmsg->timestamp, sizeof(psmsg->timestamp))
|| !HMAC_Update(ctx, pPayload, nPayload)
|| !HMAC_Final(ctx, MAC, &nBytes)
|| nBytes != 32)
fHmacOk = false;
HMAC_CTX_free(ctx);
if (ctx != nullptr)
HMAC_CTX_free(ctx);
if (!fHmacOk)
{
@@ -4430,4 +4441,3 @@ int SecureMsgDecrypt(bool fTestOnly, std::string& address, SecureMessage& smsg,
{
return SecureMsgDecrypt(fTestOnly, address, &smsg.hash[0], smsg.pPayload, smsg.nPayload, msg);
};
+52 -32
View File
@@ -120,26 +120,18 @@ static bool VerifyDestFileHash(std::string& strErr)
return false;
}
fflush(g_fetch.fpDest);
fseek(g_fetch.fpDest, 0, SEEK_SET);
SHA256_CTX ctx;
SHA256_Init(&ctx);
std::vector<unsigned char> buf(64 * 1024);
int64_t total = 0;
while (true) {
size_t n = fread(buf.data(), 1, buf.size(), g_fetch.fpDest);
if (n == 0) break;
SHA256_Update(&ctx, buf.data(), n);
total += (int64_t)n;
}
if (total != g_fetch.totalSize) {
strErr = strprintf("size mismatch: have %" PRId64 " want %" PRId64, total, g_fetch.totalSize);
std::error_code ec;
const int64_t total = static_cast<int64_t>(fs::file_size(g_fetch.destPath, ec));
if (ec || total != g_fetch.totalSize) {
strErr = strprintf("size mismatch: have %" PRId64 " want %" PRId64,
ec ? -1 : total, g_fetch.totalSize);
return false;
}
uint256 actual;
SHA256_Final((unsigned char*)&actual, &ctx);
if (!ComputeSnapshotFileHash(g_fetch.destPath, actual, strErr))
return false;
if (actual != g_fetch.expectedFileHash) {
strErr = "snapshot file hash mismatch";
return false;
@@ -243,6 +235,46 @@ static void ReissueStalledChunks(int64_t timeoutMicros)
} // namespace
bool ComputeSnapshotFileHash(const fs::path& path,
uint256& fileHash,
std::string& strError)
{
FILE* file = fopen(path.string().c_str(), "rb");
if (!file) {
strError = "cannot open snapshot for hashing: " + path.string();
return false;
}
SHA256_CTX ctx;
SHA256_Init(&ctx);
std::vector<unsigned char> buffer(64 * 1024);
while (true) {
const size_t count = fread(buffer.data(), 1, buffer.size(), file);
if (count > 0)
SHA256_Update(&ctx, buffer.data(), count);
if (count < buffer.size()) {
if (ferror(file)) {
fclose(file);
strError = "failed reading snapshot while hashing";
return false;
}
break;
}
}
fclose(file);
unsigned char digest[SHA256_DIGEST_LENGTH];
SHA256_Final(digest, &ctx);
static const char hex[] = "0123456789abcdef";
std::string digestHex(SHA256_DIGEST_LENGTH * 2, '0');
for (size_t i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
digestHex[2 * i] = hex[(digest[i] >> 4) & 0x0f];
digestHex[2 * i + 1] = hex[digest[i] & 0x0f];
}
fileHash.SetHex(digestHex);
return true;
}
// ---------------------------------------------------------------------------
// Public: TryFetchSnapshot
// ---------------------------------------------------------------------------
@@ -424,24 +456,12 @@ static bool ScanLocalSnapshot()
int64_t sz = (int64_t)fs::file_size(g_localPath, ec);
if (ec) return false;
// Hash the file once on first scan to confirm it matches the compiled-in
// snapshot hash. A node won't advertise NODE_SNAPSHOT if the local file is
// corrupt or for a different height.
FILE* f = fopen(g_localPath.string().c_str(), "rb");
if (!f) return false;
SHA256_CTX ctx;
SHA256_Init(&ctx);
std::vector<unsigned char> buf(64 * 1024);
while (true) {
size_t n = fread(buf.data(), 1, buf.size(), f);
if (n == 0) break;
SHA256_Update(&ctx, buf.data(), n);
}
fclose(f);
uint256 actual;
SHA256_Final((unsigned char*)&actual, &ctx);
std::string hashError;
if (!ComputeSnapshotFileHash(g_localPath, actual, hashError)) {
printf("SnapshotNet: cannot hash local snapshot: %s\n", hashError.c_str());
return false;
}
if (actual != expectedHash) {
printf("SnapshotNet: local utxo-snapshot.bin hash mismatch — not advertising\n");
return false;
+6
View File
@@ -51,6 +51,12 @@ bool TryFetchSnapshot(const std::filesystem::path& dataDir,
int timeoutSec,
std::string& strError);
// Return SHA256 in conventional display byte order, matching sha256sum and
// the hexadecimal values compiled into checkpoints.cpp.
bool ComputeSnapshotFileHash(const std::filesystem::path& path,
uint256& fileHash,
std::string& strError);
// Server-side message dispatch. Called from main.cpp ProcessMessage.
// Returns true if strCommand was a snapshot-protocol message (handled or
// rejected for malformed input).
+1 -3
View File
@@ -65,8 +65,7 @@ public:
if (!lock.owns_lock())
{
EnterCritical(pszName, pszFile, nLine, (void*)(lock.mutex()), true);
lock.try_lock();
if (!lock.owns_lock())
if (!lock.try_lock())
LeaveCritical();
}
return lock.owns_lock();
@@ -204,4 +203,3 @@ public:
}
};
#endif
+45 -96
View File
@@ -1,125 +1,74 @@
#include <boost/test/unit_test.hpp>
#include <limits>
#include <string>
#include "bignum.h"
#include "util.h"
BOOST_AUTO_TEST_SUITE(bignum_tests)
// Unfortunately there's no standard way of preventing a function from being
// inlined, so we define a macro for it.
//
// You should use it like this:
// NOINLINE void function() {...}
#if defined(__GNUC__)
// This also works and will be defined for any compiler implementing GCC
// extensions, such as Clang and ICC.
#define NOINLINE __attribute__((noinline))
#elif defined(_MSC_VER)
#define NOINLINE __declspec(noinline)
#else
// We give out a warning because it impacts the correctness of one bignum test.
#warning You should define NOINLINE for your compiler.
#define NOINLINE
#endif
// For the following test case, it is useful to use additional tools.
//
// The simplest one to use is the compiler flag -ftrapv, which detects integer
// overflows and similar errors. However, due to optimizations and compilers
// taking advantage of undefined behavior sometimes it may not actually detect
// anything.
//
// You can also use compiler-based stack protection to possibly detect possible
// stack buffer overruns.
//
// For more accurate diagnostics, you can use an undefined arithmetic operation
// detector such as the clang-based tool:
//
// "IOC: An Integer Overflow Checker for C/C++"
//
// Available at: http://embed.cs.utah.edu/ioc/
//
// It might also be useful to use Google's AddressSanitizer to detect
// stack buffer overruns, which valgrind can't currently detect.
// Let's force this code not to be inlined, in order to actually
// test a generic version of the function. This increases the chance
// that -ftrapv will detect overflows.
NOINLINE void mysetint64(CBigNum& num, int64_t n)
{
num.setint64(n);
}
// For each number, we do 2 tests: one with inline code, then we reset the
// value to 0, then the second one with a non-inlined function.
BOOST_AUTO_TEST_CASE(bignum_setint64)
{
int64_t n;
const int64_t values[] = {
0,
1,
-1,
5,
-5,
std::numeric_limits<int64_t>::min(),
std::numeric_limits<int64_t>::max(),
};
{
n = 0;
CBigNum num(n);
BOOST_CHECK(num.ToString() == "0");
for (int64_t value : values) {
CBigNum num(value);
BOOST_CHECK_EQUAL(num.ToString(), std::to_string(value));
num.setulong(0);
BOOST_CHECK(num.ToString() == "0");
mysetint64(num, n);
BOOST_CHECK(num.ToString() == "0");
}
{
n = 1;
CBigNum num(n);
BOOST_CHECK(num.ToString() == "1");
num.setulong(0);
BOOST_CHECK(num.ToString() == "0");
mysetint64(num, n);
BOOST_CHECK(num.ToString() == "1");
}
{
n = -1;
CBigNum num(n);
BOOST_CHECK(num.ToString() == "-1");
num.setulong(0);
BOOST_CHECK(num.ToString() == "0");
mysetint64(num, n);
BOOST_CHECK(num.ToString() == "-1");
}
{
n = 5;
CBigNum num(n);
BOOST_CHECK(num.ToString() == "5");
num.setulong(0);
BOOST_CHECK(num.ToString() == "0");
mysetint64(num, n);
BOOST_CHECK(num.ToString() == "5");
}
{
n = -5;
CBigNum num(n);
BOOST_CHECK(num.ToString() == "-5");
num.setulong(0);
BOOST_CHECK(num.ToString() == "0");
mysetint64(num, n);
BOOST_CHECK(num.ToString() == "-5");
}
{
n = std::numeric_limits<int64_t>::min();
CBigNum num(n);
BOOST_CHECK(num.ToString() == "-9223372036854775808");
num.setulong(0);
BOOST_CHECK(num.ToString() == "0");
mysetint64(num, n);
BOOST_CHECK(num.ToString() == "-9223372036854775808");
}
{
n = std::numeric_limits<int64_t>::max();
CBigNum num(n);
BOOST_CHECK(num.ToString() == "9223372036854775807");
num.setulong(0);
BOOST_CHECK(num.ToString() == "0");
mysetint64(num, n);
BOOST_CHECK(num.ToString() == "9223372036854775807");
BOOST_CHECK_EQUAL(num.ToString(), "0");
mysetint64(num, value);
BOOST_CHECK_EQUAL(num.ToString(), std::to_string(value));
}
}
BOOST_AUTO_TEST_CASE(bignum_uint64_roundtrip_boundaries)
{
const uint64_t values[] = {
0,
1,
0x7f,
0x80,
uint64_t{1} << 32,
uint64_t{1} << 63,
std::numeric_limits<uint64_t>::max(),
};
for (uint64_t value : values) {
CBigNum num(value);
BOOST_CHECK_EQUAL(num.getuint64(), value);
}
CBigNum negative(-1);
BOOST_CHECK_EQUAL(negative.getuint64(), uint64_t{1});
}
BOOST_AUTO_TEST_CASE(bignum_rejects_invalid_output_base)
{
CBigNum value(42);
BOOST_CHECK_THROW(value.ToString(0), bignum_error);
BOOST_CHECK_THROW(value.ToString(1), bignum_error);
BOOST_CHECK_THROW(value.ToString(17), bignum_error);
}
BOOST_AUTO_TEST_SUITE_END()
+79
View File
@@ -0,0 +1,79 @@
// Copyright (c) 2026 Triangles developers
// Distributed under the MIT/X11 software license
#include <boost/test/unit_test.hpp>
#include "../bootstrap.h"
namespace {
std::string ManifestWithFilename(const std::string& filename)
{
return std::string(R"json({
"version": "1.6",
"chain_tip": {
"height": 2206004,
"blockhash": "b34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46"
},
"files": {
")json") + filename + R"json(": {
"sha256": "1419282DAE817315EE1B955543F6248233FE5800F5E8488734A0ECE5BD6781EA",
"type": "utxo_snapshot_v3"
}
},
"canonical": { "snapshot": ")json" + filename + R"json(" }
})json";
}
} // namespace
BOOST_AUTO_TEST_SUITE(bootstrap_security_tests)
BOOST_AUTO_TEST_CASE(remote_manifest_parses_canonical_snapshot)
{
Bootstrap::RemoteSnapshot snapshot;
std::string error;
BOOST_REQUIRE(Bootstrap::ParseRemoteSnapshotManifest(
ManifestWithFilename("utxo-snapshot.bin"), snapshot, error));
BOOST_CHECK_EQUAL(snapshot.filename, "utxo-snapshot.bin");
BOOST_CHECK_EQUAL(snapshot.height, 2206004);
BOOST_CHECK_EQUAL(
snapshot.sha256,
"1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea");
}
BOOST_AUTO_TEST_CASE(remote_manifest_rejects_path_traversal)
{
Bootstrap::RemoteSnapshot snapshot;
std::string error;
BOOST_CHECK(!Bootstrap::ParseRemoteSnapshotManifest(
ManifestWithFilename("../../wallet.dat"), snapshot, error));
BOOST_CHECK_NE(error.find("plain filename"), std::string::npos);
}
BOOST_AUTO_TEST_CASE(remote_manifest_rejects_malformed_hashes)
{
std::string manifest = ManifestWithFilename("utxo-snapshot.bin");
const std::string validHash =
"1419282DAE817315EE1B955543F6248233FE5800F5E8488734A0ECE5BD6781EA";
manifest.replace(manifest.find(validHash), validHash.size(), "not-a-sha256");
Bootstrap::RemoteSnapshot snapshot;
std::string error;
BOOST_CHECK(!Bootstrap::ParseRemoteSnapshotManifest(manifest, snapshot, error));
BOOST_CHECK_NE(error.find("invalid"), std::string::npos);
}
BOOST_AUTO_TEST_CASE(remote_manifest_rejects_non_snapshot_canonical_file)
{
std::string manifest = ManifestWithFilename("wallet.dat");
const std::string snapshotType = "utxo_snapshot_v3";
manifest.replace(manifest.find(snapshotType), snapshotType.size(), "wallet_backup");
Bootstrap::RemoteSnapshot snapshot;
std::string error;
BOOST_CHECK(!Bootstrap::ParseRemoteSnapshotManifest(manifest, snapshot, error));
BOOST_CHECK_NE(error.find("not a UTXO snapshot"), std::string::npos);
}
BOOST_AUTO_TEST_SUITE_END()
@@ -34,6 +34,7 @@ bool fUseFastIndex = false;
enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT;
void StartShutdown() { /* no-op for tests */ }
void MarkShutdownFailure() { /* no-op for tests */ }
namespace {
+1
View File
@@ -125,6 +125,7 @@ bool fUseFastIndex = false;
enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT;
void StartShutdown() { /* no-op */ }
void MarkShutdownFailure() { /* no-op for tests */ }
namespace {
+17
View File
@@ -358,6 +358,23 @@ BOOST_AUTO_TEST_CASE(pos_validation_skip_is_only_historical_fast_path)
nAssumeValidThreshold = oldAssumeValid;
}
BOOST_AUTO_TEST_CASE(pos_block_signature_is_required_above_hardened_checkpoint)
{
const int oldAssumeValid = nAssumeValidThreshold;
const int checkpointHeight = Checkpoints::GetTotalBlocksEstimate();
BOOST_CHECK(!IsBlockSignatureRequiredAtHeight(checkpointHeight));
BOOST_CHECK(IsBlockSignatureRequiredAtHeight(checkpointHeight + 1));
// A rolling performance threshold must never authorize unsigned live
// blocks, including when stale-tip state makes the node report IBD.
nAssumeValidThreshold = checkpointHeight + 100;
BOOST_CHECK(IsConsensusAssumeValidHeight(checkpointHeight + 50));
BOOST_CHECK(IsBlockSignatureRequiredAtHeight(checkpointHeight + 50));
nAssumeValidThreshold = oldAssumeValid;
}
// ─── Orphan block cap (P1 — DoS) ──────────────────────────────────────────
// The cap on stored orphan blocks prevents an attacker from filling
// memory with garbage. If too low, legitimate orphans are dropped. If
+23 -13
View File
@@ -70,6 +70,7 @@ bool fUseFastIndex = false;
enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT;
void StartShutdown() { /* no-op for tests */ }
void MarkShutdownFailure() { /* no-op for tests */ }
namespace {
@@ -100,19 +101,9 @@ struct TmpDataDir
// Compute SHA-256 of a file's bytes.
uint256 Sha256OfFile(const fs::path& p)
{
FILE* f = fopen(p.string().c_str(), "rb");
BOOST_REQUIRE_MESSAGE(f != nullptr, "open failed: " << p.string());
SHA256_CTX ctx;
SHA256_Init(&ctx);
std::vector<unsigned char> buf(64 * 1024);
while (true) {
size_t n = fread(buf.data(), 1, buf.size(), f);
if (n == 0) break;
SHA256_Update(&ctx, buf.data(), n);
}
fclose(f);
uint256 out;
SHA256_Final(reinterpret_cast<unsigned char*>(&out), &ctx);
std::string error;
BOOST_REQUIRE_MESSAGE(SnapshotNet::ComputeSnapshotFileHash(p, out, error), error);
return out;
}
@@ -121,8 +112,16 @@ uint256 Sha256OfBytes(const std::vector<unsigned char>& bytes)
SHA256_CTX ctx;
SHA256_Init(&ctx);
SHA256_Update(&ctx, bytes.data(), bytes.size());
unsigned char digest[SHA256_DIGEST_LENGTH];
SHA256_Final(digest, &ctx);
static const char hex[] = "0123456789abcdef";
std::string digestHex(SHA256_DIGEST_LENGTH * 2, '0');
for (size_t i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
digestHex[2 * i] = hex[(digest[i] >> 4) & 0x0f];
digestHex[2 * i + 1] = hex[digest[i] & 0x0f];
}
uint256 out;
SHA256_Final(reinterpret_cast<unsigned char*>(&out), &ctx);
out.SetHex(digestHex);
return out;
}
@@ -177,6 +176,17 @@ BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_SUITE(snapshotnet_hash)
BOOST_AUTO_TEST_CASE(file_hash_uses_standard_sha256_display_order)
{
TmpDataDir td;
fs::path p = td.path / "abc.bin";
WriteFile(p, {'a', 'b', 'c'});
BOOST_CHECK_EQUAL(
Sha256OfFile(p).ToString(),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
}
BOOST_AUTO_TEST_CASE(file_hash_matches_inline_sha256)
{
// Synthesize a payload, hash it via stdlib openssl directly, then hash
+1
View File
@@ -69,3 +69,4 @@ void StartShutdown()
exit(0);
}
void MarkShutdownFailure() { /* no-op for tests */ }
+9
View File
@@ -168,6 +168,15 @@ BOOST_AUTO_TEST_CASE(util_WildcardMatch)
BOOST_CHECK(WildcardMatch("abcdef", "a*f"));
BOOST_CHECK(!WildcardMatch("abcdef", "a*x"));
BOOST_CHECK(WildcardMatch("", "*"));
const std::string address = "192.0.2.44";
const std::string allow = "192.0.2.*";
BOOST_CHECK(WildcardMatch(std::string_view(address), std::string_view(allow)));
// A long non-match must not recurse once per wildcard/input combination.
const std::string longInput(4096, 'a');
const std::string longMask = "*a*a*a*a*a*a*a*a*a*a*b";
BOOST_CHECK(!WildcardMatch(std::string_view(longInput), std::string_view(longMask)));
}
BOOST_AUTO_TEST_CASE(util_FormatMoney)
+51
View File
@@ -294,6 +294,57 @@ BOOST_AUTO_TEST_CASE(coin_selection_tests)
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_SUITE(wallet_security_tests)
BOOST_AUTO_TEST_CASE(hd_key_generation_fails_when_seed_is_unavailable)
{
CWallet wallet;
wallet.fHDEnabled = true;
BOOST_CHECK_THROW(wallet.GenerateNewKey(), std::runtime_error);
BOOST_CHECK_EQUAL(wallet.nHDChainIndex, 0);
}
BOOST_AUTO_TEST_CASE(memory_wallet_encryption_roundtrip_preserves_keys)
{
CWallet wallet;
CKey original;
original.MakeNewKey(true);
BOOST_REQUIRE(wallet.AddKey(original));
SecureString passphrase;
passphrase.reserve(100);
passphrase = "correct horse battery staple";
const auto keypoolIt = mapArgs.find("-keypool");
const bool hadKeypoolArg = keypoolIt != mapArgs.end();
const std::string oldKeypoolArg = hadKeypoolArg ? keypoolIt->second : std::string();
mapArgs["-keypool"] = "0";
const bool encrypted = wallet.EncryptWallet(passphrase);
if (hadKeypoolArg)
mapArgs["-keypool"] = oldKeypoolArg;
else
mapArgs.erase("-keypool");
BOOST_REQUIRE(encrypted);
BOOST_CHECK(wallet.IsCrypted());
BOOST_CHECK(wallet.IsLocked());
SecureString wrongPassphrase;
wrongPassphrase.reserve(100);
wrongPassphrase = "wrong passphrase";
BOOST_CHECK(!wallet.Unlock(wrongPassphrase));
BOOST_CHECK(wallet.IsLocked());
BOOST_REQUIRE(wallet.Unlock(passphrase));
CKey recovered;
BOOST_REQUIRE(wallet.GetKey(original.GetPubKey().GetID(), recovered));
BOOST_CHECK(recovered.GetPubKey() == original.GetPubKey());
BOOST_CHECK(wallet.Lock());
}
BOOST_AUTO_TEST_SUITE_END()
// ----------------------------------------------------------------------------
// AbandonTransaction tests
//
+26 -8
View File
@@ -19,7 +19,7 @@
// Connection parameters (highest precedence first):
// 1. Command line flags: -rpcuser/-rpcpassword/-rpcconnect/-rpcport
// 2. triangles.conf in the data directory (or -conf=<path>)
// 3. Defaults: 127.0.0.1:19111 mainnet, 19112 testnet; no auth (must be set in conf)
// 3. Defaults: 127.0.0.1:19112 mainnet, 19111 testnet; no auth (must be set in conf)
//
// Usage:
// triangles-cli help List commands (delegates to daemon)
@@ -70,6 +70,7 @@
#define TRI_CLI_CLOSE_SOCKET(s) closesocket(s)
#else
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
@@ -108,10 +109,20 @@ static bool GetBoolArg(const string& key, bool def)
return (v != "0" && v != "false" && v != "no");
}
static void ReadConfigFile(const string& path)
static bool ReadConfigFile(const string& path, string& error)
{
#ifndef _WIN32
struct stat configStat;
if (::lstat(path.c_str(), &configStat) == 0 &&
(!S_ISREG(configStat.st_mode) || configStat.st_uid != geteuid() ||
(configStat.st_mode & (S_IRWXG | S_IRWXO)) != 0)) {
error = "refusing insecure configuration file " + path +
"; require a regular file owned by the current user with no group or other access";
return false;
}
#endif
ifstream f(path);
if (!f.good()) return;
if (!f.good()) return true;
string line;
while (getline(f, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
@@ -141,6 +152,7 @@ static void ReadConfigFile(const string& path)
mapMultiArgs[dashKey].push_back(value);
}
}
return true;
}
static fs::path GetDefaultDataDir()
@@ -231,7 +243,7 @@ static void ParseCommandLine(int argc, char* const argv[])
struct RPCConn {
string host = "127.0.0.1";
string port = "19111";
string port = "19112";
string user;
string pass;
};
@@ -244,11 +256,17 @@ static int AppInitRPCConn(RPCConn& conn)
{
std::ifstream f(confPath);
confExisted = f.good();
if (confExisted) ReadConfigFile(confPath.string());
if (confExisted) {
string configError;
if (!ReadConfigFile(confPath.string(), configError)) {
cerr << "triangles-cli: " << configError << "\n";
return 1;
}
}
}
bool fTestNet = GetBoolArg("-testnet", false);
conn.port = GetArg("-rpcport", fTestNet ? "19112" : "19111");
conn.port = GetArg("-rpcport", fTestNet ? "19111" : "19112");
conn.host = GetArg("-rpcconnect", "127.0.0.1");
conn.user = GetArg("-rpcuser", "");
conn.pass = GetArg("-rpcpassword", "");
@@ -595,9 +613,9 @@ static int CommandLineHelp(ostream& out)
<< "Options:\n"
<< " -conf=<file> Specify configuration file (default: triangles.conf)\n"
<< " -datadir=<dir> Specify data directory\n"
<< " -testnet Use testnet (RPC port 19112)\n"
<< " -testnet Use testnet (RPC port 19111)\n"
<< " -rpcconnect=<ip> Send commands to node running on <ip> (default: 127.0.0.1)\n"
<< " -rpcport=<port> Connect to JSON-RPC on <port> (default: 19111 or testnet: 19112)\n"
<< " -rpcport=<port> Connect to JSON-RPC on <port> (default: 19112 or testnet: 19111)\n"
<< " -rpcuser=<user> Username for JSON-RPC connections\n"
<< " -rpcpassword=<pw> Password for JSON-RPC connections\n"
<< " -stdin Read extra params from standard input, one per line\n"
+132 -59
View File
@@ -17,10 +17,12 @@
#undef printf
#include "rpc_httpsocket.h" // raw-socket HTTP transport (replaces Boost.Asio)
#include <filesystem>
#include <fstream>
#include <memory>
#include <list>
#include <filesystem>
#include <fstream>
#include <memory>
#include <list>
#include <cerrno>
#include <limits>
#ifndef WIN32
#include <sys/select.h>
@@ -34,13 +36,26 @@ namespace fs = std::filesystem;
void ThreadRPCServer2(void* parg);
static std::string strRPCUserColonPass;
static std::string strRPCUserColonPass;
const Object emptyobj;
CNotificationQueue* pNotificationQueue = nullptr;
void ThreadRPCServer3(void* parg);
void ThreadRPCServer3(void* parg);
static bool RPCMethodAllowed(const std::string& method)
{
const auto it = mapMultiArgs.find("-rpcallowmethod");
if (it == mapMultiArgs.end() || it->second.empty())
return true;
for (const std::string& allowed : it->second) {
if (allowed == method)
return true;
}
return false;
}
static inline unsigned short GetDefaultRPCPort()
{
@@ -316,11 +331,8 @@ static const CRPCCommand vRPCCommands[] =
{ "sendrawtransaction", &sendrawtransaction, false, false },
{ "getcheckpoint", &getcheckpoint, true, false },
{ "gencheckpoints", &gencheckpoints, true, false },
{ "publishcheckpoint", &publishcheckpoint, true, false },
{ "settrustedv2snapshotpublisher", &settrustedv2snapshotpublisher, false, false },
{ "gettrustedv2snapshotpublisher", &gettrustedv2snapshotpublisher, false, false },
{ "unsettrustedv2snapshotpublisher", &unsettrustedv2snapshotpublisher, false, false },
{ "getchaintips", &getchaintips, true, false },
{ "publishcheckpoint", &publishcheckpoint, true, false },
{ "getchaintips", &getchaintips, true, false },
{ "invalidateblock", &invalidateblock, false, false },
{ "reconsiderblock", &reconsiderblock, false, false },
{ "recalculatesupply", &recalculatesupply, false, false },
@@ -455,11 +467,27 @@ static string HTTPReply(int nStatus, const string& strMsg, bool keepalive)
strMsg.c_str());
}
int ReadHTTPStatus(std::basic_istream<char>& stream, int &proto,
string& strMethodHTTP, string& strURI)
{
string str;
getline(stream, str);
static bool ReadHTTPLine(std::basic_istream<char>& stream, std::string& line,
size_t maxLength)
{
line.clear();
char c = 0;
while (stream.get(c)) {
if (c == '\n')
return true;
if (line.size() >= maxLength)
return false;
line.push_back(c);
}
return false;
}
int ReadHTTPStatus(std::basic_istream<char>& stream, int &proto,
string& strMethodHTTP, string& strURI)
{
string str;
if (!ReadHTTPLine(stream, str, 8192))
return HTTP_BAD_REQUEST;
// Trim trailing \r
if (!str.empty() && str[str.size()-1] == '\r')
str.resize(str.size()-1);
@@ -482,27 +510,45 @@ int ReadHTTPStatus(std::basic_istream<char>& stream, int &proto,
return atoi(vWords[1].c_str());
}
int ReadHTTPHeader(std::basic_istream<char>& stream, map<string, string>& mapHeadersRet)
{
int nLen = 0;
while (true)
{
string str;
std::getline(stream, str);
if (str.empty() || str == "\r")
break;
int ReadHTTPHeader(std::basic_istream<char>& stream, map<string, string>& mapHeadersRet)
{
int nLen = 0;
size_t totalHeaderBytes = 0;
while (true)
{
string str;
if (!ReadHTTPLine(stream, str, 8192))
return -1;
totalHeaderBytes += str.size() + 1;
if (totalHeaderBytes > 64 * 1024)
return -1;
if (str.empty() || str == "\r")
break;
string::size_type nColon = str.find(":");
if (nColon != string::npos)
{
string strHeader = str.substr(0, nColon);
strHeader = TrimString(strHeader);
strHeader = ToLower(strHeader);
string strValue = str.substr(nColon+1);
strValue = TrimString(strValue);
mapHeadersRet[strHeader] = strValue;
if (strHeader == "content-length")
nLen = atoi(strValue.c_str());
}
string strHeader = str.substr(0, nColon);
strHeader = TrimString(strHeader);
strHeader = ToLower(strHeader);
string strValue = str.substr(nColon+1);
strValue = TrimString(strValue);
if (strHeader == "transfer-encoding" ||
(strHeader == "content-length" && mapHeadersRet.count(strHeader) != 0)) {
return -1;
}
mapHeadersRet[strHeader] = strValue;
if (strHeader == "content-length") {
errno = 0;
char* end = nullptr;
const unsigned long long parsed = std::strtoull(strValue.c_str(), &end, 10);
if (errno != 0 || end == strValue.c_str() || *end != '\0' ||
parsed > static_cast<unsigned long long>(MAX_SIZE) ||
parsed > static_cast<unsigned long long>(std::numeric_limits<int>::max())) {
return -1;
}
nLen = static_cast<int>(parsed);
}
}
}
return nLen;
}
@@ -702,8 +748,16 @@ void ThreadRPCServer2(void* parg)
if ((mapArgs["-rpcpassword"] == "") ||
(mapArgs["-rpcuser"] == mapArgs["-rpcpassword"]))
{
unsigned char rand_pwd[32];
RAND_bytes(rand_pwd, 32);
unsigned char rand_pwd[32];
if (RAND_bytes(rand_pwd, sizeof(rand_pwd)) != 1) {
uiInterface.ThreadSafeMessageBox(
_("Unable to generate a secure suggested RPC password. "
"The RPC server will not start."),
_("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL);
MarkShutdownFailure();
StartShutdown();
return;
}
string strWhatAmI = "To use trianglesd";
if (mapArgs.count("-server"))
strWhatAmI = strprintf(_("To use the %s option"), "\"-server\"");
@@ -720,8 +774,9 @@ void ThreadRPCServer2(void* parg)
strWhatAmI.c_str(),
GetConfigFile().string().c_str(),
EncodeBase58(&rand_pwd[0],&rand_pwd[0]+32).c_str()),
_("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL);
StartShutdown();
_("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL);
MarkShutdownFailure();
StartShutdown();
return;
}
@@ -732,23 +787,23 @@ void ThreadRPCServer2(void* parg)
"or Tor.\n");
}
// Bind the loopback interface(s) unless the operator explicitly opened the
// RPC port to other hosts with -rpcallowip.
const bool loopbackOnly = !mapArgs.count("-rpcallowip");
const int nPort = (int)GetArg("-rpcport", GetDefaultRPCPort());
std::string strBindError;
std::vector<SOCKET> vListen = BindRPCSockets(nPort, loopbackOnly, strBindError);
const int nPort = (int)GetArg("-rpcport", GetDefaultRPCPort());
const std::string rpcBind = GetArg(
std::string_view{"-rpcbind"}, std::string_view{""});
std::string strBindError;
std::vector<SOCKET> vListen = BindRPCSockets(nPort, rpcBind, strBindError);
if (vListen.empty()) {
uiInterface.ThreadSafeMessageBox(
strprintf(_("An error occurred while setting up the RPC port %d for listening: %s"),
nPort, strBindError.c_str()),
_("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL);
StartShutdown();
strprintf(_("An error occurred while setting up the RPC port %d for listening: %s"),
nPort, strBindError.c_str()),
_("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL);
MarkShutdownFailure();
StartShutdown();
return;
}
printf("RPC server listening on port %d (%s)\n", nPort,
loopbackOnly ? "loopback only" : "all interfaces");
printf("RPC server listening on port %d (%s)\n", nPort,
rpcBind.empty() ? "loopback only" : rpcBind.c_str());
// Accept loop. select() with a short timeout keeps the listener responsive
// to fShutdown. Each accepted connection is handed to its own handler thread
@@ -780,10 +835,19 @@ void ThreadRPCServer2(void* parg)
struct sockaddr_storage ss;
socklen_t len = sizeof(ss);
SOCKET hConn = accept(s, (struct sockaddr*)&ss, &len);
if (hConn == INVALID_SOCKET) {
printf("RPC accept() failed\n");
continue;
}
if (hConn == INVALID_SOCKET) {
printf("RPC accept() failed\n");
continue;
}
int64_t timeoutSeconds = GetArg("-rpcservertimeout", 30);
if (timeoutSeconds < 1)
timeoutSeconds = 1;
if (timeoutSeconds > 600)
timeoutSeconds = 600;
if (!SetRPCSocketTimeouts(hConn, static_cast<int>(timeoutSeconds))) {
printf("RPC warning: failed to set connection timeouts\n");
}
const std::string strPeer = SockaddrToString((struct sockaddr*)&ss, len);
@@ -979,7 +1043,13 @@ void ThreadRPCServer3(void* parg)
map<string, string> mapHeaders;
string strRequest;
ReadHTTP(conn->stream(), mapHeaders, strRequest);
const int requestStatus = ReadHTTP(conn->stream(), mapHeaders, strRequest);
if (requestStatus != 0) {
conn->stream() << HTTPReply(HTTP_BAD_REQUEST,
"{\"error\":\"Malformed HTTP request\"}",
false) << std::flush;
break;
}
// Handle REST API requests
string strHTTPMethod = mapHeaders.count("_method") ? mapHeaders["_method"] : "POST";
@@ -1097,9 +1167,12 @@ void ThreadRPCServer3(void* parg)
}
}
json_spirit::Value CRPCTable::execute(const std::string &strMethod, const json_spirit::Array &params) const
{
// Find method
json_spirit::Value CRPCTable::execute(const std::string &strMethod, const json_spirit::Array &params) const
{
if (!RPCMethodAllowed(strMethod))
throw JSONRPCError(RPC_METHOD_NOT_FOUND, "Method not found");
// Find method
const CRPCCommand *pcmd = tableRPC[strMethod];
if (!pcmd)
throw JSONRPCError(RPC_METHOD_NOT_FOUND, "Method not found");
-4
View File
@@ -228,9 +228,6 @@ extern json_spirit::Value getblockbynumber(const json_spirit::Array& params, boo
extern json_spirit::Value getcheckpoint(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value gencheckpoints(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value publishcheckpoint(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value settrustedv2snapshotpublisher(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value gettrustedv2snapshotpublisher(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value unsettrustedv2snapshotpublisher(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getchaintips(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value invalidateblock(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value reconsiderblock(const json_spirit::Array& params, bool fHelp);
@@ -259,4 +256,3 @@ extern json_spirit::Value smsgbroadcast(const json_spirit::Array& params, bool f
#endif
+45 -30
View File
@@ -33,6 +33,8 @@
#ifndef WIN32
#include <execinfo.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
#include "util.h"
@@ -174,9 +176,10 @@ uint64_t GetRand(uint64_t nMax)
// to give every possible output value an equal possibility
uint64_t nRange = (std::numeric_limits<uint64_t>::max() / nMax) * nMax;
uint64_t nRand = 0;
do
RAND_bytes((unsigned char*)&nRand, sizeof(nRand));
while (nRand >= nRange);
do {
if (RAND_bytes(reinterpret_cast<unsigned char*>(&nRand), sizeof(nRand)) != 1)
throw std::runtime_error("OpenSSL CSPRNG failure in GetRand");
} while (nRand >= nRange);
return (nRand % nMax);
}
@@ -188,7 +191,8 @@ int GetRandInt(int nMax)
uint256 GetRandHash()
{
uint256 hash;
RAND_bytes((unsigned char*)&hash, sizeof(hash));
if (RAND_bytes(reinterpret_cast<unsigned char*>(&hash), sizeof(hash)) != 1)
throw std::runtime_error("OpenSSL CSPRNG failure in GetRandHash");
return hash;
}
@@ -616,7 +620,9 @@ bool SoftSetBoolArg(std::string_view strArg, bool fValue)
bool WildcardMatch(std::string_view str, std::string_view mask)
{
return WildcardMatch(std::string(str), std::string(mask));
const std::string strOwned(str);
const std::string maskOwned(mask);
return WildcardMatch(strOwned.c_str(), maskOwned.c_str());
}
@@ -954,31 +960,27 @@ string DecodeBase32(const string& str)
bool WildcardMatch(const char* psz, const char* mask)
{
while (true)
{
switch (*mask)
{
case '\0':
return (*psz == '\0');
case '*':
return WildcardMatch(psz, mask+1) || (*psz && WildcardMatch(psz+1, mask));
case '?':
if (*psz == '\0')
return false;
break;
default:
if (*psz != *mask)
return false;
break;
}
psz++;
mask++;
}
}
const char* star = nullptr;
const char* retry = nullptr;
bool WildcardMatch(const string& str, const string& mask)
{
return WildcardMatch(str.c_str(), mask.c_str());
while (*psz != '\0') {
if (*mask == '?' || *mask == *psz) {
++psz;
++mask;
} else if (*mask == '*') {
star = mask++;
retry = psz;
} else if (star != nullptr) {
mask = star + 1;
psz = ++retry;
} else {
return false;
}
}
while (*mask == '*')
++mask;
return *mask == '\0';
}
@@ -1145,7 +1147,20 @@ void ReadConfigFile(map<string, string>& mapSettingsRet,
// - Section headers ([section])
// If any of those become needed, the actual conf syntax in
// contrib/triangles.conf.example should be extended first.
std::ifstream streamConfig(GetConfigFile());
const std::filesystem::path configPath = GetConfigFile();
#ifndef WIN32
struct stat configStat;
if (::lstat(configPath.string().c_str(), &configStat) == 0) {
if (!S_ISREG(configStat.st_mode) || configStat.st_uid != geteuid() ||
(configStat.st_mode & (S_IRWXG | S_IRWXO)) != 0) {
throw std::runtime_error(
"Refusing to read insecure configuration file " + configPath.string() +
"; it must be a regular file owned by the current user with no group or other access");
}
}
#endif
std::ifstream streamConfig(configPath);
if (!streamConfig.good())
return; // No triangles.conf file is OK
+425 -153
View File
@@ -13,12 +13,15 @@
#include "kernel.h"
#include "coincontrol.h"
#include "addressindex.h"
#include "init.h"
#include "util.h"
#include <cstring>
#include <memory>
#include <algorithm>
#include <limits>
#include <random>
#include <deque>
#include <openssl/crypto.h>
using namespace std;
extern unsigned int nStakeMaxAge;
@@ -26,6 +29,29 @@ extern unsigned int nStakeMaxAge;
unsigned int nStakeSplitAge = 1 * 3 * 60 * 60;
int64_t nStakeCombineThreshold = 20 * COIN;
namespace {
void CleanseWalletString(std::string& value)
{
if (!value.empty())
OPENSSL_cleanse(value.data(), value.size());
value.clear();
}
bool RewriteWalletDatabase(const std::string& walletFile, const char* skip = nullptr)
{
if (ResolveWalletDbKind() != WalletDbKind::SQLite)
return CDB::Rewrite(walletFile, skip);
try {
CWalletDB walletdb(walletFile);
return walletdb.RewriteDatabase(skip);
} catch (const std::exception& e) {
printf("RewriteWalletDatabase: %s\n", e.what());
return false;
}
}
} // namespace
//////////////////////////////////////////////////////////////////////////////
//
// mapWallet
@@ -132,32 +158,80 @@ CPubKey CWallet::GenerateNewKey()
RandAddSeedPerfmon();
CKey key;
bool fUsedHD = false;
if (fHDEnabled && !hdMnemonic.empty()) {
if (DeriveHDKey(nHDChainIndex, key)) { fUsedHD = true; fCompressed = true; }
if (fHDEnabled) {
if (hdMnemonic.empty())
throw std::runtime_error("CWallet::GenerateNewKey() : HD seed is unavailable while wallet is locked");
if (!DeriveHDKey(nHDChainIndex, key))
throw std::runtime_error("CWallet::GenerateNewKey() : HD key derivation failed");
fUsedHD = true;
fCompressed = true;
}
if (!fUsedHD)
key.MakeNewKey(fCompressed);
// Compressed public keys were introduced in version 0.6.0
if (fCompressed)
SetMinVersion(WalletFeature::ComprPubKey);
if (fCompressed && !SetMinVersion(WalletFeature::ComprPubKey))
throw std::runtime_error("CWallet::GenerateNewKey() : wallet version update failed");
CPubKey pubkey = key.GetPubKey();
// Create new metadata
int64_t nCreationTime = GetTime();
mapKeyMetadata[pubkey.GetID()] = CKeyMetadata(nCreationTime);
const int64_t nCreationTime = GetTime();
const CKeyMetadata metadata(nCreationTime);
if (fUsedHD && fFileBacked) {
const int64_t nextHDChainIndex = nHDChainIndex + 1;
std::vector<unsigned char> cryptedSecret;
if (IsCrypted()) {
if (IsLocked() || vMasterKey.size() != WALLET_CRYPTO_KEY_SIZE)
throw std::runtime_error("CWallet::GenerateNewKey() : encrypted HD wallet is locked");
bool compressed = false;
if (!EncryptSecret(vMasterKey, key.GetSecret(compressed),
pubkey.GetHash(), cryptedSecret))
throw std::runtime_error("CWallet::GenerateNewKey() : key encryption failed");
}
CWalletDB walletdb(strWalletFile);
if (!walletdb.TxnBegin())
throw std::runtime_error("CWallet::GenerateNewKey() : database transaction failed");
bool wrote = IsCrypted()
? walletdb.WriteCryptedKey(pubkey, cryptedSecret, metadata)
: walletdb.WriteKey(pubkey, key.GetPrivKey(), metadata);
wrote = wrote && walletdb.WriteHDChain(nextHDChainIndex);
if (!wrote) {
walletdb.TxnAbort();
throw std::runtime_error("CWallet::GenerateNewKey() : atomic HD key write failed");
}
if (!walletdb.TxnCommit()) {
walletdb.TxnAbort();
throw std::runtime_error("CWallet::GenerateNewKey() : atomic HD key commit failed");
}
const bool added = IsCrypted()
? CCryptoKeyStore::AddCryptedKey(pubkey, cryptedSecret)
: CBasicKeyStore::AddKey(key);
if (!added) {
printf("GenerateNewKey: HD key persisted but could not be added in memory; shutting down\n");
MarkShutdownFailure();
StartShutdown();
throw std::runtime_error("CWallet::GenerateNewKey() : in-memory HD key commit failed");
}
mapKeyMetadata[pubkey.GetID()] = metadata;
if (!nTimeFirstKey || nCreationTime < nTimeFirstKey)
nTimeFirstKey = nCreationTime;
nHDChainIndex = nextHDChainIndex;
return pubkey;
}
mapKeyMetadata[pubkey.GetID()] = metadata;
if (!AddKey(key)) {
mapKeyMetadata.erase(pubkey.GetID());
throw std::runtime_error("CWallet::GenerateNewKey() : AddKey failed");
}
if (!nTimeFirstKey || nCreationTime < nTimeFirstKey)
nTimeFirstKey = nCreationTime;
if (!AddKey(key))
throw std::runtime_error("CWallet::GenerateNewKey() : AddKey failed");
if (fUsedHD) {
nHDChainIndex++;
if (fFileBacked)
CWalletDB(strWalletFile).WriteHDChain(nHDChainIndex);
}
return key.GetPubKey();
if (fUsedHD)
++nHDChainIndex;
return pubkey;
}
bool CWallet::AddKey(const CKey& key)
@@ -215,22 +289,17 @@ bool fWalletUnlockStakingOnly = false;
bool CWallet::Lock()
{
if (IsCrypted()) {
CleanseWalletString(hdMnemonic);
CleanseWalletString(hdPassphrase);
}
if (IsLocked())
return true;
if (fDebug)
printf("Locking wallet.\n");
if (IsCrypted()) {
hdMnemonic.clear(); // keep only the encrypted copies while locked
hdPassphrase.clear();
}
{
LOCK(cs_wallet);
CWalletDB wdb(strWalletFile);
}
return LockKeyStore();
};
@@ -242,38 +311,66 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase)
CCrypter crypter;
CKeyingMaterial vMasterKey;
bool unlocked = false;
{
LOCK(cs_wallet);
for (const MasterKeyMap::value_type& pMasterKey : mapMasterKeys)
{
if(!crypter.SetKeyFromPassphrase(strWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod))
return false;
continue;
if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, vMasterKey))
return false;
continue;
if (CCryptoKeyStore::Unlock(vMasterKey)) {
if (fHDEnabled && hdMnemonic.empty() && !vchCryptedHDMnemonic.empty()) {
if (fHDEnabled) {
if (vchCryptedHDMnemonic.empty()) {
LockKeyStore();
return false;
}
CSecret sec;
if (DecryptSecret(vMasterKey, vchCryptedHDMnemonic, hdMnemonicIV, sec))
hdMnemonic.assign(sec.begin(), sec.end());
if (!DecryptSecret(vMasterKey, vchCryptedHDMnemonic, hdMnemonicIV, sec)) {
LockKeyStore();
return false;
}
std::string mnemonic(sec.begin(), sec.end());
if (!hd::CheckMnemonic(mnemonic)) {
OPENSSL_cleanse(mnemonic.data(), mnemonic.size());
LockKeyStore();
return false;
}
std::string passphrase;
if (!vchCryptedHDPassphrase.empty()) {
CSecret psec;
if (!DecryptSecret(vMasterKey, vchCryptedHDPassphrase,
hdPassphraseIV, psec)) {
OPENSSL_cleanse(mnemonic.data(), mnemonic.size());
LockKeyStore();
return false;
}
passphrase.assign(psec.begin(), psec.end());
}
CleanseWalletString(hdMnemonic);
CleanseWalletString(hdPassphrase);
hdMnemonic = std::move(mnemonic);
hdPassphrase = std::move(passphrase);
}
if (fHDEnabled && hdPassphrase.empty() && !vchCryptedHDPassphrase.empty()) {
CSecret psec;
if (DecryptSecret(vMasterKey, vchCryptedHDPassphrase, hdPassphraseIV, psec))
hdPassphrase.assign(psec.begin(), psec.end());
}
return true;
unlocked = true;
break;
}
}
SecureMsgWalletUnlocked();
return true;
}
return false;
if (!unlocked)
return false;
SecureMsgWalletUnlocked();
return true;
}
bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, const SecureString& strNewWalletPassphrase)
{
bool fWasLocked = IsLocked();
const bool fWasLocked = IsLocked();
bool changed = false;
{
LOCK(cs_wallet);
@@ -284,39 +381,73 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase,
for (MasterKeyMap::value_type& pMasterKey : mapMasterKeys)
{
if(!crypter.SetKeyFromPassphrase(strOldWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod))
return false;
continue;
if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, vMasterKey))
return false;
continue;
if (CCryptoKeyStore::Unlock(vMasterKey))
{
CMasterKey updatedMasterKey = pMasterKey.second;
int64_t nStartTime = GetTimeMillis();
crypter.SetKeyFromPassphrase(strNewWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod);
if (!crypter.SetKeyFromPassphrase(strNewWalletPassphrase,
updatedMasterKey.vchSalt,
updatedMasterKey.nDeriveIterations,
updatedMasterKey.nDerivationMethod))
break;
int64_t nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime);
pMasterKey.second.nDeriveIterations = pMasterKey.second.nDeriveIterations * (100 / ((double)nElapsed));
updatedMasterKey.nDeriveIterations =
updatedMasterKey.nDeriveIterations * (100 / ((double)nElapsed));
nStartTime = GetTimeMillis();
crypter.SetKeyFromPassphrase(strNewWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod);
if (!crypter.SetKeyFromPassphrase(strNewWalletPassphrase,
updatedMasterKey.vchSalt,
updatedMasterKey.nDeriveIterations,
updatedMasterKey.nDerivationMethod))
break;
nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime);
pMasterKey.second.nDeriveIterations = (pMasterKey.second.nDeriveIterations + pMasterKey.second.nDeriveIterations * 100 / ((double)nElapsed)) / 2;
updatedMasterKey.nDeriveIterations =
(updatedMasterKey.nDeriveIterations +
updatedMasterKey.nDeriveIterations * 100 / ((double)nElapsed)) / 2;
if (pMasterKey.second.nDeriveIterations < 25000)
pMasterKey.second.nDeriveIterations = 25000;
if (updatedMasterKey.nDeriveIterations < 25000)
updatedMasterKey.nDeriveIterations = 25000;
printf("Wallet passphrase changed to an nDeriveIterations of %i\n", pMasterKey.second.nDeriveIterations);
printf("Wallet passphrase changed to an nDeriveIterations of %i\n",
updatedMasterKey.nDeriveIterations);
if (!crypter.SetKeyFromPassphrase(strNewWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod))
return false;
if (!crypter.Encrypt(vMasterKey, pMasterKey.second.vchCryptedKey))
return false;
CWalletDB(strWalletFile).WriteMasterKey(pMasterKey.first, pMasterKey.second);
if (fWasLocked)
Lock();
return true;
if (!crypter.SetKeyFromPassphrase(strNewWalletPassphrase,
updatedMasterKey.vchSalt,
updatedMasterKey.nDeriveIterations,
updatedMasterKey.nDerivationMethod))
break;
if (!crypter.Encrypt(vMasterKey, updatedMasterKey.vchCryptedKey))
break;
bool persisted = true;
if (fFileBacked) {
try {
persisted = CWalletDB(strWalletFile).WriteMasterKey(
pMasterKey.first, updatedMasterKey);
} catch (const std::exception& e) {
printf("ChangeWalletPassphrase: %s\n", e.what());
persisted = false;
}
}
if (!persisted)
break;
pMasterKey.second = std::move(updatedMasterKey);
changed = true;
break;
}
}
LockKeyStore();
}
return false;
if (!changed)
return false;
if (!fWasLocked && !Unlock(strNewWalletPassphrase))
return false;
return true;
}
void CWallet::SetBestChain(const CBlockLocator& loc)
@@ -346,27 +477,44 @@ bool CWallet::SetMinVersion(WalletFeature nVersion, CWalletDB* pwalletdbIn, bool
if (fExplicit && static_cast<int>(nVersion) > nWalletMaxVersion)
nVersion = WalletFeature::Latest;
nWalletVersion = static_cast<int>(nVersion);
if (static_cast<int>(nVersion) > nWalletMaxVersion)
nWalletMaxVersion = static_cast<int>(nVersion);
const int newWalletVersion = static_cast<int>(nVersion);
if (fFileBacked)
{
CWalletDB* pwalletdb = pwalletdbIn ? pwalletdbIn : new CWalletDB(strWalletFile);
if (nWalletVersion >= 40000)
std::unique_ptr<CWalletDB> ownedWalletDB;
CWalletDB* pwalletdb = pwalletdbIn;
if (!pwalletdb) {
ownedWalletDB = std::make_unique<CWalletDB>(strWalletFile);
pwalletdb = ownedWalletDB.get();
if (!pwalletdb->TxnBegin())
return false;
}
bool wrote = true;
if (newWalletVersion >= 40000)
{
// Versions prior to 0.4.0 did not support the "minversion" record.
// Use a CCorruptAddress to make them crash instead.
CCorruptAddress corruptAddress;
pwalletdb->WriteSetting("addrIncoming", corruptAddress);
wrote = pwalletdb->WriteSetting("addrIncoming", corruptAddress);
}
if (wrote && newWalletVersion > 40000)
wrote = pwalletdb->WriteMinVersion(newWalletVersion);
if (!wrote) {
if (ownedWalletDB)
pwalletdb->TxnAbort();
return false;
}
if (ownedWalletDB && !pwalletdb->TxnCommit()) {
pwalletdb->TxnAbort();
return false;
}
if (nWalletVersion > 40000)
pwalletdb->WriteMinVersion(nWalletVersion);
if (!pwalletdbIn)
delete pwalletdb;
}
nWalletVersion = newWalletVersion;
if (newWalletVersion > nWalletMaxVersion)
nWalletMaxVersion = newWalletVersion;
return true;
}
@@ -385,27 +533,38 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
{
if (IsCrypted())
return false;
if (fHDEnabled && hdMnemonic.empty())
return false;
if (nMasterKeyMaxID == std::numeric_limits<unsigned int>::max())
return false;
CKeyingMaterial vMasterKey;
RandAddSeedPerfmon();
vMasterKey.resize(WALLET_CRYPTO_KEY_SIZE);
RAND_bytes(&vMasterKey[0], WALLET_CRYPTO_KEY_SIZE);
if (RAND_bytes(&vMasterKey[0], WALLET_CRYPTO_KEY_SIZE) != 1)
return false;
CMasterKey kMasterKey(nDerivationMethodIndex);
RandAddSeedPerfmon();
kMasterKey.vchSalt.resize(WALLET_CRYPTO_SALT_SIZE);
RAND_bytes(&kMasterKey.vchSalt[0], WALLET_CRYPTO_SALT_SIZE);
if (RAND_bytes(&kMasterKey.vchSalt[0], WALLET_CRYPTO_SALT_SIZE) != 1)
return false;
CCrypter crypter;
int64_t nStartTime = GetTimeMillis();
crypter.SetKeyFromPassphrase(strWalletPassphrase, kMasterKey.vchSalt, 25000, kMasterKey.nDerivationMethod);
if (!crypter.SetKeyFromPassphrase(strWalletPassphrase, kMasterKey.vchSalt,
25000, kMasterKey.nDerivationMethod))
return false;
int64_t nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime);
kMasterKey.nDeriveIterations = 2500000 / ((double)nElapsed);
nStartTime = GetTimeMillis();
crypter.SetKeyFromPassphrase(strWalletPassphrase, kMasterKey.vchSalt, kMasterKey.nDeriveIterations, kMasterKey.nDerivationMethod);
if (!crypter.SetKeyFromPassphrase(strWalletPassphrase, kMasterKey.vchSalt,
kMasterKey.nDeriveIterations,
kMasterKey.nDerivationMethod))
return false;
nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime);
kMasterKey.nDeriveIterations = (kMasterKey.nDeriveIterations + kMasterKey.nDeriveIterations * 100 / ((double)nElapsed)) / 2;
@@ -421,51 +580,109 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
{
LOCK(cs_wallet);
mapMasterKeys[++nMasterKeyMaxID] = kMasterKey;
CryptedKeyMap cryptedKeys;
if (!PrepareKeyEncryption(vMasterKey, cryptedKeys))
return false;
uint256 newMnemonicIV;
uint256 newPassphraseIV;
std::vector<unsigned char> newCryptedMnemonic;
std::vector<unsigned char> newCryptedPassphrase;
if (fHDEnabled) {
CSecret mnemonicSecret(hdMnemonic.begin(), hdMnemonic.end());
newMnemonicIV = GetRandHash();
if (!EncryptSecret(vMasterKey, mnemonicSecret, newMnemonicIV,
newCryptedMnemonic))
return false;
if (!hdPassphrase.empty()) {
CSecret passphraseSecret(hdPassphrase.begin(), hdPassphrase.end());
newPassphraseIV = GetRandHash();
if (!EncryptSecret(vMasterKey, passphraseSecret, newPassphraseIV,
newCryptedPassphrase))
return false;
}
}
const unsigned int newMasterKeyID = nMasterKeyMaxID + 1;
const int oldWalletVersion = nWalletVersion;
const int oldWalletMaxVersion = nWalletMaxVersion;
if (fFileBacked)
{
std::unique_ptr<CWalletDB> dbEnc(new CWalletDB(strWalletFile));
std::unique_ptr<CWalletDB> dbEnc = std::make_unique<CWalletDB>(strWalletFile);
if (!dbEnc->TxnBegin())
return false;
dbEnc->WriteMasterKey(nMasterKeyMaxID, kMasterKey);
bool wrote = dbEnc->WriteMasterKey(newMasterKeyID, kMasterKey);
for (const auto& item : cryptedKeys) {
if (!wrote)
break;
CKeyMetadata metadata;
auto metadataIt = mapKeyMetadata.find(item.first);
if (metadataIt != mapKeyMetadata.end())
metadata = metadataIt->second;
wrote = dbEnc->WriteCryptedKey(item.second.first,
item.second.second, metadata);
}
if (wrote && fHDEnabled) {
wrote = dbEnc->WriteHDCryptedMnemonic(newMnemonicIV,
newCryptedMnemonic);
if (wrote) {
wrote = hdPassphrase.empty()
? dbEnc->EraseHDPassphrase()
: dbEnc->WriteHDCryptedPassphrase(newPassphraseIV,
newCryptedPassphrase);
}
}
if (wrote)
wrote = SetMinVersion(WalletFeature::WalletCrypt, dbEnc.get(), true);
if (!EncryptKeys(vMasterKey))
{
if (!wrote) {
dbEnc->TxnAbort();
nWalletVersion = oldWalletVersion;
nWalletMaxVersion = oldWalletMaxVersion;
return false;
}
if (fHDEnabled && !hdMnemonic.empty()) {
CSecret sec(hdMnemonic.begin(), hdMnemonic.end());
uint256 iv = GetRandHash();
std::vector<unsigned char> cipher;
if (!EncryptSecret(vMasterKey, sec, iv, cipher)) { dbEnc->TxnAbort(); return false; }
hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher;
dbEnc->WriteHDCryptedMnemonic(iv, cipher);
}
if (fHDEnabled && !hdPassphrase.empty()) {
CSecret psec(hdPassphrase.begin(), hdPassphrase.end());
uint256 piv = GetRandHash();
std::vector<unsigned char> pcipher;
if (!EncryptSecret(vMasterKey, psec, piv, pcipher)) { dbEnc->TxnAbort(); return false; }
hdPassphraseIV = piv; vchCryptedHDPassphrase = pcipher;
dbEnc->WriteHDCryptedPassphrase(piv, pcipher);
}
SetMinVersion(WalletFeature::WalletCrypt, dbEnc.get(), true);
if (!dbEnc->TxnCommit())
if (!dbEnc->TxnCommit()) {
dbEnc->TxnAbort();
nWalletVersion = oldWalletVersion;
nWalletMaxVersion = oldWalletMaxVersion;
return false;
}
} else if (!SetMinVersion(WalletFeature::WalletCrypt, nullptr, true)) {
return false;
}
if (!CommitKeyEncryption(std::move(cryptedKeys))) {
printf("EncryptWallet: disk was updated but in-memory key encryption could not be committed; shutting down\n");
MarkShutdownFailure();
StartShutdown();
return false;
}
nMasterKeyMaxID = newMasterKeyID;
mapMasterKeys[newMasterKeyID] = kMasterKey;
if (fHDEnabled) {
hdMnemonicIV = newMnemonicIV;
hdPassphraseIV = newPassphraseIV;
vchCryptedHDMnemonic = std::move(newCryptedMnemonic);
vchCryptedHDPassphrase = std::move(newCryptedPassphrase);
}
Lock();
Unlock(strWalletPassphrase);
NewKeyPool();
if (!Unlock(strWalletPassphrase)) {
printf("EncryptWallet: encrypted wallet could not be verified; shutting down\n");
MarkShutdownFailure();
StartShutdown();
return false;
}
if (!NewKeyPool())
printf("EncryptWallet: wallet encrypted, but keypool regeneration failed\n");
Lock();
// Need to completely rewrite the wallet file; if we don't, bdb might keep
// bits of the unencrypted private key in slack space in the database file.
CDB::Rewrite(strWalletFile);
if (fFileBacked && !RewriteWalletDatabase(strWalletFile)) {
printf("EncryptWallet: secure wallet database rewrite failed; shutting down\n");
MarkShutdownFailure();
StartShutdown();
return false;
}
}
NotifyStatusChanged(this);
@@ -793,7 +1010,7 @@ bool CWallet::AbandonTransaction(const uint256& hashTx)
// the inputs (vfSpent was tracked on the wtx) and resolves the conflict.
bool fErased = EraseFromWallet(hashTx);
LogPrintf("CWallet::AbandonTransaction: %s abandoned (%u descendant(s) noted)\n",
LogPrintf("CWallet::AbandonTransaction: %s abandoned (%" PRIszu " descendant(s) noted)\n",
hashTx.ToString().c_str(), sDescendants.size());
return fErased;
}
@@ -2115,10 +2332,12 @@ bool CWallet::CreateCoinStake(const CKeyStore& keystore, unsigned int nBits, int
break; // unable to find corresponding public key
}
if (key.GetPubKey() != vchPubKey)
{
if (fDebug && GetBoolArg("-printcoinstake"))
printf("CreateCoinStake : invalid key for kernel type=%d\n", whichType);
if (key.GetPubKey() != vchPubKey)
{
if (fDebug && GetBoolArg("-printcoinstake")) {
printf("CreateCoinStake : invalid key for kernel type=%d\n",
static_cast<int>(whichType));
}
break; // keys mismatch
}
@@ -2134,7 +2353,7 @@ bool CWallet::CreateCoinStake(const CKeyStore& keystore, unsigned int nBits, int
if (GetWeight(block.GetBlockTime(), (int64_t)txNew.nTime) < nStakeSplitAge)
txNew.vout.push_back(CTxOut(0, scriptPubKeyOut)); //split stake
if (fDebug && GetBoolArg("-printcoinstake"))
printf("CreateCoinStake : added kernel type=%d\n", whichType);
printf("CreateCoinStake : added kernel type=%d\n", static_cast<int>(whichType));
fKernelFound = true;
break;
}
@@ -2351,7 +2570,7 @@ DBErrors CWallet::LoadWallet(bool& fFirstRunRet)
DBErrors nLoadWalletRet = CWalletDB(strWalletFile,"cr+").LoadWallet(this);
if (nLoadWalletRet == DB_NEED_REWRITE)
{
if (CDB::Rewrite(strWalletFile, "\x04pool"))
if (RewriteWalletDatabase(strWalletFile, "\x04pool"))
{
setKeyPool.clear();
// Note: can't top-up keypool here, because wallet is locked.
@@ -2478,9 +2697,13 @@ bool CWallet::NewKeyPool()
{
{
LOCK(cs_wallet);
CWalletDB walletdb(strWalletFile);
for (int64_t nIndex : setKeyPool)
walletdb.ErasePool(nIndex);
std::unique_ptr<CWalletDB> walletdb;
if (fFileBacked)
walletdb = std::make_unique<CWalletDB>(strWalletFile);
for (int64_t nIndex : setKeyPool) {
if (walletdb && !walletdb->ErasePool(nIndex))
return false;
}
setKeyPool.clear();
if (IsLocked())
@@ -2490,7 +2713,9 @@ bool CWallet::NewKeyPool()
for (int i = 0; i < nKeys; i++)
{
int64_t nIndex = i+1;
walletdb.WritePool(nIndex, CKeyPool(GenerateNewKey()));
CKeyPool keypool(GenerateNewKey());
if (walletdb && !walletdb->WritePool(nIndex, keypool))
return false;
setKeyPool.insert(nIndex);
}
printf("CWallet::NewKeyPool wrote %"PRId64" new keys\n", nKeys);
@@ -2506,7 +2731,9 @@ bool CWallet::TopUpKeyPool(unsigned int nSize)
if (IsLocked())
return false;
CWalletDB walletdb(strWalletFile);
std::unique_ptr<CWalletDB> walletdb;
if (fFileBacked)
walletdb = std::make_unique<CWalletDB>(strWalletFile);
// Top up key pool
unsigned int nTargetSize;
@@ -2520,7 +2747,8 @@ bool CWallet::TopUpKeyPool(unsigned int nSize)
int64_t nEnd = 1;
if (!setKeyPool.empty())
nEnd = *(--setKeyPool.end()) + 1;
if (!walletdb.WritePool(nEnd, CKeyPool(GenerateNewKey())))
CKeyPool keypool(GenerateNewKey());
if (walletdb && !walletdb->WritePool(nEnd, keypool))
throw runtime_error("TopUpKeyPool() : writing generated key failed");
setKeyPool.insert(nEnd);
printf("keypool added key %"PRId64", size=%"PRIszu"\n", nEnd, setKeyPool.size());
@@ -2946,16 +3174,18 @@ void CWallet::GetKeyBirthTimes(std::map<CKeyID, int64_t> &mapKeyBirth) const {
// ---- HD wallet (BIP39/BIP32) implementation ----
bool CWallet::DeriveHDKey(int64_t index, CKey& keyOut) const
{
if (hdMnemonic.empty())
if (hdMnemonic.empty() || index < 0 ||
static_cast<uint64_t>(index) > std::numeric_limits<uint32_t>::max())
return false;
// If a BIP39 passphrase ("25th word") was set with the seed, it MUST be
// part of every derivation — otherwise restored wallets derive different
// addresses than the originals. Empty string = no passphrase (legacy).
unsigned char priv[32];
if (!hd::DeriveTriangles(hdMnemonic, hdPassphrase, 0, 0, (uint32_t)index, priv))
if (!hd::DeriveTriangles(hdMnemonic, hdPassphrase, 0, 0,
static_cast<uint32_t>(index), priv))
return false;
CSecret secret(priv, priv + 32);
memset(priv, 0, sizeof(priv));
OPENSSL_cleanse(priv, sizeof(priv));
keyOut.SetSecret(secret, true); // HD keys are compressed
return true;
}
@@ -2972,6 +3202,7 @@ bool CWallet::SetHDSeed(const std::string& mnemonicIn, const std::string& passph
{
LOCK(cs_wallet);
if (IsLocked()) { strError = "Wallet is locked; unlock it before setting an HD seed."; return false; }
if (fHDEnabled) { strError = "Wallet already has an HD seed; refusing to replace it."; return false; }
std::string m = mnemonicIn;
if (m.empty()) {
@@ -2983,45 +3214,86 @@ bool CWallet::SetHDSeed(const std::string& mnemonicIn, const std::string& passph
unsigned char priv[32];
if (!hd::DeriveTriangles(m, passphrase, 0, 0, 0, priv)) { strError = "Key derivation failed."; return false; }
memset(priv, 0, sizeof(priv));
OPENSSL_cleanse(priv, sizeof(priv));
hdMnemonic = m;
hdPassphrase = passphrase;
fHDEnabled = true;
nHDChainIndex = 0;
uint256 newMnemonicIV;
uint256 newPassphraseIV;
std::vector<unsigned char> newCryptedMnemonic;
std::vector<unsigned char> newCryptedPassphrase;
if (IsCrypted()) {
if (vMasterKey.size() != WALLET_CRYPTO_KEY_SIZE) {
strError = "Wallet master key is unavailable.";
return false;
}
CSecret sec(m.begin(), m.end());
newMnemonicIV = GetRandHash();
if (!EncryptSecret(vMasterKey, sec, newMnemonicIV, newCryptedMnemonic)) {
strError = "Failed to encrypt seed.";
return false;
}
if (!passphrase.empty()) {
CSecret psec(passphrase.begin(), passphrase.end());
newPassphraseIV = GetRandHash();
if (!EncryptSecret(vMasterKey, psec, newPassphraseIV,
newCryptedPassphrase)) {
strError = "Failed to encrypt passphrase.";
return false;
}
}
}
if (fFileBacked) {
CWalletDB wdb(strWalletFile);
if (IsCrypted()) {
CSecret sec(m.begin(), m.end());
uint256 iv = GetRandHash();
std::vector<unsigned char> cipher;
if (!EncryptSecret(vMasterKey, sec, iv, cipher)) { strError = "Failed to encrypt seed."; return false; }
hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher;
wdb.WriteHDCryptedMnemonic(iv, cipher);
if (!passphrase.empty()) {
CSecret psec(passphrase.begin(), passphrase.end());
uint256 piv = GetRandHash();
std::vector<unsigned char> pcipher;
if (!EncryptSecret(vMasterKey, psec, piv, pcipher)) { strError = "Failed to encrypt passphrase."; return false; }
hdPassphraseIV = piv; vchCryptedHDPassphrase = pcipher;
wdb.WriteHDCryptedPassphrase(piv, pcipher);
} else {
vchCryptedHDPassphrase.clear();
wdb.EraseHDPassphrase(); // re-seed without passphrase: drop any old record
}
} else {
wdb.WriteHDMnemonic(m);
if (!passphrase.empty())
wdb.WriteHDPassphrase(passphrase);
else
wdb.EraseHDPassphrase();
if (!wdb.TxnBegin()) {
strError = "Failed to start wallet database transaction.";
return false;
}
bool wrote = false;
if (IsCrypted()) {
wrote = wdb.WriteHDCryptedMnemonic(newMnemonicIV, newCryptedMnemonic) &&
(passphrase.empty()
? wdb.EraseHDPassphrase()
: wdb.WriteHDCryptedPassphrase(newPassphraseIV,
newCryptedPassphrase));
} else {
wrote = wdb.WriteHDMnemonic(m) &&
(passphrase.empty()
? wdb.EraseHDPassphrase()
: wdb.WriteHDPassphrase(passphrase));
}
wrote = wrote && wdb.WriteHDChain(0);
if (!wrote) {
wdb.TxnAbort();
strError = "Failed to persist HD seed; wallet database transaction was rolled back.";
return false;
}
if (!wdb.TxnCommit()) {
wdb.TxnAbort();
strError = "Failed to commit HD seed to the wallet database.";
return false;
}
wdb.WriteHDChain(nHDChainIndex);
}
CleanseWalletString(hdMnemonic);
CleanseWalletString(hdPassphrase);
hdMnemonic = m;
hdPassphrase = passphrase;
hdMnemonicIV = newMnemonicIV;
hdPassphraseIV = newPassphraseIV;
vchCryptedHDMnemonic = std::move(newCryptedMnemonic);
vchCryptedHDPassphrase = std::move(newCryptedPassphrase);
fHDEnabled = true;
nHDChainIndex = 0;
// Replace any pre-existing (random) keypool with HD-derived keys so that
// getnewaddress immediately hands out deterministic m/44'/2222'/0'/0/i keys.
NewKeyPool();
if (!NewKeyPool()) {
strError = "HD seed was stored, but rebuilding the keypool failed.";
return false;
}
mnemonicOut = m;
return true;
}
+1 -3
View File
@@ -76,8 +76,6 @@ private:
bool SelectCoinsSimple(int64_t nTargetValue, unsigned int nSpendTime, int nMinConf, std::set<std::pair<const CWalletTx*,unsigned int> >& setCoinsRet, int64_t& nValueRet) const;
bool SelectCoins(int64_t nTargetValue, unsigned int nSpendTime, std::set<std::pair<const CWalletTx*,unsigned int> >& setCoinsRet, int64_t& nValueRet, const CCoinControl *coinControl=nullptr) const;
CWalletDB *pwalletdbEncryption;
// the current wallet version: clients below this version are not able to load the wallet
int nWalletVersion;
@@ -104,9 +102,9 @@ public:
nWalletMaxVersion = static_cast<int>(WalletFeature::Base);
fFileBacked = false;
nMasterKeyMaxID = 0;
pwalletdbEncryption = nullptr;
fHDEnabled = false;
nHDChainIndex = 0;
nTimeFirstKey = 0;
nOrderPosNext = 0;
nCachedStakeWeight = 0;
nCachedStakeWeightTime = 0;
+17
View File
@@ -71,6 +71,9 @@ bool SQLiteDatabase::Open(std::string& strError)
// Durability + integrity pragmas. FULL fsync on commit — a wallet must not
// lose a freshly-written key on power loss.
if (!ExecOrError("PRAGMA synchronous = FULL;", strError)) return false;
if (!ExecOrError("PRAGMA journal_mode = DELETE;", strError)) return false;
if (!ExecOrError("PRAGMA secure_delete = ON;", strError)) return false;
if (!ExecOrError("PRAGMA temp_store = MEMORY;", strError)) return false;
if (!ExecOrError("PRAGMA foreign_keys = ON;", strError)) return false;
// Fail loudly instead of silently truncating an over-long blob.
if (!ExecOrError("PRAGMA cell_size_check = ON;", strError)) return false;
@@ -156,6 +159,20 @@ bool SQLiteDatabase::Backup(const std::string& strDest) const
if (pDest) sqlite3_close(pDest);
return false;
}
#ifndef WIN32
{
std::error_code ec;
fs::permissions(strDest,
fs::perms::owner_read | fs::perms::owner_write,
fs::perm_options::replace, ec);
if (ec) {
printf("SQLiteDatabase::Backup cannot restrict destination permissions: %s\n",
ec.message().c_str());
sqlite3_close(pDest);
return false;
}
}
#endif
sqlite3_backup* bk = sqlite3_backup_init(pDest, "main", m_db, "main");
bool ok = false;
+111 -6
View File
@@ -42,6 +42,20 @@ namespace fs = std::filesystem;
static uint64_t nAccountingEntryNumber = 0;
extern bool fWalletUnlockStakingOnly;
static bool RestrictWalletFilePermissions(const fs::path& path)
{
#ifdef WIN32
(void)path;
return true;
#else
std::error_code ec;
fs::permissions(path,
fs::perms::owner_read | fs::perms::owner_write,
fs::perm_options::replace, ec);
return !ec;
#endif
}
//
// Auto-backup wallet before flush/rewrite operations.
// Copies wallet.dat to wallet.dat.auto.bak if the backup is older than the wallet.
@@ -61,10 +75,17 @@ bool AutoBackupWallet(const fs::path& walletPath)
}
if (fs::exists(backupPath)) {
uintmax_t backupSize = fs::file_size(backupPath);
if (backupSize == walletSize)
if (backupSize == walletSize) {
if (!RestrictWalletFilePermissions(backupPath))
return false;
return true;
}
}
fs::copy_file(walletPath, backupPath, fs::copy_options::overwrite_existing);
if (!RestrictWalletFilePermissions(backupPath)) {
printf("AutoBackupWallet: could not restrict backup file permissions\n");
return false;
}
printf("AutoBackupWallet: backed up wallet.dat (%llu bytes) to wallet.dat.auto.bak\n",
(unsigned long long)walletSize);
return true;
@@ -257,6 +278,10 @@ public:
unsigned int nKeyMeta;
bool fIsEncrypted;
bool fAnyUnordered;
bool fHDPlainMnemonic;
bool fHDCryptedMnemonic;
bool fHDPlainPassphrase;
bool fHDCryptedPassphrase;
int nFileVersion;
std::vector<uint256> vWalletUpgrade;
@@ -264,6 +289,10 @@ public:
nKeys = nCKeys = nKeyMeta = 0;
fIsEncrypted = false;
fAnyUnordered = false;
fHDPlainMnemonic = false;
fHDCryptedMnemonic = false;
fHDPlainPassphrase = false;
fHDCryptedPassphrase = false;
nFileVersion = 0;
}
};
@@ -380,6 +409,7 @@ static bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssVa
return false;
}
pwallet->mapMasterKeys[nID] = kMasterKey;
wss.fIsEncrypted = true;
if (pwallet->nMasterKeyMaxID < nID)
pwallet->nMasterKeyMaxID = nID;
} else if (strType == "ckey") {
@@ -417,18 +447,22 @@ static bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssVa
} else if (strType == "hdmnemonic") {
std::string m;
ssValue >> m;
wss.fHDPlainMnemonic = true;
pwallet->LoadHDMnemonic(m);
} else if (strType == "hdcmnemonic") {
std::pair<uint256, std::vector<unsigned char>> cm;
ssValue >> cm;
wss.fHDCryptedMnemonic = true;
pwallet->LoadCryptedHDMnemonic(cm.first, cm.second);
} else if (strType == "hdpassphrase") {
std::string p;
ssValue >> p;
wss.fHDPlainPassphrase = true;
pwallet->LoadHDPassphrase(p);
} else if (strType == "hdcpassphrase") {
std::pair<uint256, std::vector<unsigned char>> cp;
ssValue >> cp;
wss.fHDCryptedPassphrase = true;
pwallet->LoadCryptedHDPassphrase(cp.first, cp.second);
} else if (strType == "hdchain") {
int64_t n;
@@ -513,6 +547,35 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
if (fNoncriticalErrors && result == DB_LOAD_OK)
result = DB_NONCRITICAL_ERROR;
if (result != DB_LOAD_OK && result != DB_NONCRITICAL_ERROR)
return result;
const bool conflictingMnemonicRecords =
wss.fHDPlainMnemonic && wss.fHDCryptedMnemonic;
const bool conflictingPassphraseRecords =
wss.fHDPlainPassphrase && wss.fHDCryptedPassphrase;
const bool missingMnemonic =
(wss.fHDPlainPassphrase || wss.fHDCryptedPassphrase) &&
!(wss.fHDPlainMnemonic || wss.fHDCryptedMnemonic);
const bool mixedHDProtection =
(wss.fHDPlainMnemonic && wss.fHDCryptedPassphrase) ||
(wss.fHDCryptedMnemonic && wss.fHDPlainPassphrase);
const bool plaintextInEncryptedWallet =
wss.fIsEncrypted &&
(wss.nKeys != 0 || wss.fHDPlainMnemonic || wss.fHDPlainPassphrase);
const bool encryptedHDInPlainWallet =
!wss.fIsEncrypted &&
(wss.fHDCryptedMnemonic || wss.fHDCryptedPassphrase);
const bool encryptedKeysWithoutMasterKey =
wss.nCKeys != 0 && pwallet->mapMasterKeys.empty();
if (conflictingMnemonicRecords || conflictingPassphraseRecords ||
missingMnemonic || mixedHDProtection || plaintextInEncryptedWallet ||
encryptedHDInPlainWallet || encryptedKeysWithoutMasterKey) {
printf("Error reading wallet database: inconsistent encryption or HD seed records\n");
return DB_CORRUPT;
}
if (result != DB_LOAD_OK)
return result;
@@ -617,18 +680,55 @@ bool BackupWallet(const CWallet& wallet, const std::string& strDest)
if (!wallet.fFileBacked)
return false;
// For the SQLite backend, the database is a single file — copy directly
// (after a checkpoint flush to fold any -wal into the main file).
// SQLite's online backup API takes a consistent snapshot while the daemon
// is running; copying the live database file directly can produce a torn
// backup if a transaction commits during the copy.
if (ResolveWalletDbKind() == WalletDbKind::SQLite) {
fs::path pathSrc = GetDataDir() / wallet.strWalletFile;
fs::path pathDest(strDest);
if (fs::is_directory(pathDest))
pathDest /= wallet.strWalletFile;
std::error_code ec;
fs::copy_file(pathSrc, pathDest, fs::copy_options::overwrite_existing, ec);
if (fs::exists(pathDest, ec) && fs::equivalent(pathSrc, pathDest, ec)) {
printf("refusing to back up wallet.dat onto itself\n");
return false;
}
ec.clear();
const fs::path pathTemp = pathDest.string() +
strprintf(".tmp.%" PRId64, GetTimeMillis());
try {
CWalletDB walletdb(wallet.strWalletFile);
if (!walletdb.BackupDatabase(pathTemp.string())) {
fs::remove(pathTemp, ec);
return false;
}
} catch (const std::exception& e) {
printf("error backing up wallet.dat to %s - %s\n",
pathDest.string().c_str(), e.what());
fs::remove(pathTemp, ec);
return false;
}
if (!RestrictWalletFilePermissions(pathTemp)) {
printf("error restricting wallet backup permissions: %s\n",
pathDest.string().c_str());
fs::remove(pathTemp, ec);
return false;
}
fs::rename(pathTemp, pathDest, ec);
#ifdef WIN32
if (ec) {
printf("error copying wallet.dat to %s - %s\n",
ec.clear();
fs::remove(pathDest, ec);
ec.clear();
fs::rename(pathTemp, pathDest, ec);
}
#endif
if (ec) {
printf("error finalizing wallet backup %s - %s\n",
pathDest.string().c_str(), ec.message().c_str());
fs::remove(pathTemp, ec);
return false;
}
printf("copied wallet.dat to %s\n", pathDest.string().c_str());
@@ -653,6 +753,11 @@ bool BackupWallet(const CWallet& wallet, const std::string& strDest)
try {
fs::copy_file(pathSrc, pathDest, fs::copy_options::overwrite_existing);
if (!RestrictWalletFilePermissions(pathDest)) {
printf("error restricting wallet backup permissions: %s\n",
pathDest.string().c_str());
return false;
}
printf("copied wallet.dat to %s\n", pathDest.string().c_str());
return true;
} catch (const fs::filesystem_error& e) {
@@ -665,4 +770,4 @@ bool BackupWallet(const CWallet& wallet, const std::string& strDest)
MilliSleep(100);
}
return false;
}
}
+25 -13
View File
@@ -123,8 +123,10 @@ public:
return false;
if (fEraseUnencryptedKey)
{
Erase(std::make_pair(std::string("key"), vchPubKey.Raw()));
Erase(std::make_pair(std::string("wkey"), vchPubKey.Raw()));
if (!Erase(std::make_pair(std::string("key"), vchPubKey.Raw())))
return false;
if (!Erase(std::make_pair(std::string("wkey"), vchPubKey.Raw())))
return false;
}
return true;
}
@@ -166,13 +168,13 @@ public:
bool WriteHDMnemonic(const std::string& mnemonic) {
nWalletDBUpdated++;
Erase(std::string("hdcmnemonic"));
return Write(std::string("hdmnemonic"), mnemonic);
const bool erased = Erase(std::string("hdcmnemonic"));
return erased && Write(std::string("hdmnemonic"), mnemonic);
}
bool WriteHDCryptedMnemonic(const uint256& iv, const std::vector<unsigned char>& cipher) {
nWalletDBUpdated++;
Erase(std::string("hdmnemonic"));
return Write(std::string("hdcmnemonic"), std::make_pair(iv, cipher));
const bool erased = Erase(std::string("hdmnemonic"));
return erased && Write(std::string("hdcmnemonic"), std::make_pair(iv, cipher));
}
bool WriteHDChain(int64_t nIndex) {
nWalletDBUpdated++;
@@ -183,19 +185,19 @@ public:
// means no passphrase (legacy wallets and the common case).
bool WriteHDPassphrase(const std::string& passphrase) {
nWalletDBUpdated++;
Erase(std::string("hdcpassphrase"));
return Write(std::string("hdpassphrase"), passphrase);
const bool erased = Erase(std::string("hdcpassphrase"));
return erased && Write(std::string("hdpassphrase"), passphrase);
}
bool WriteHDCryptedPassphrase(const uint256& iv, const std::vector<unsigned char>& cipher) {
nWalletDBUpdated++;
Erase(std::string("hdpassphrase"));
return Write(std::string("hdcpassphrase"), std::make_pair(iv, cipher));
const bool erased = Erase(std::string("hdpassphrase"));
return erased && Write(std::string("hdcpassphrase"), std::make_pair(iv, cipher));
}
bool EraseHDPassphrase() {
nWalletDBUpdated++;
Erase(std::string("hdpassphrase"));
Erase(std::string("hdcpassphrase"));
return true;
const bool erasedPlain = Erase(std::string("hdpassphrase"));
const bool erasedCrypted = Erase(std::string("hdcpassphrase"));
return erasedPlain && erasedCrypted;
}
bool ReadPool(int64_t nPool, CKeyPool& keypool)
@@ -251,6 +253,16 @@ public:
return Read(std::string("version"), nVersion);
}
bool RewriteDatabase(const char* pszSkip = nullptr)
{
return m_database && m_database->Rewrite(pszSkip);
}
bool BackupDatabase(const std::string& destination) const
{
return m_database && m_database->Backup(destination);
}
bool ReadAccount(const std::string& strAccount, CAccount& account);
bool WriteAccount(const std::string& strAccount, const CAccount& account);
private: