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
+4 -4
View File
@@ -6,7 +6,7 @@ if(POLICY CMP0167)
endif() endif()
project(Triangles project(Triangles
VERSION 6.0.0 VERSION 6.1.7
DESCRIPTION "Cryptographic Triangles Wallet" DESCRIPTION "Cryptographic Triangles Wallet"
LANGUAGES C CXX LANGUAGES C CXX
) )
@@ -84,10 +84,10 @@ option(BUILD_QT "Build triangles-qt (Qt5 GUI wallet)" ON)
option(BUILD_DAEMON "Build trianglesd (headless daemon)" ON) option(BUILD_DAEMON "Build trianglesd (headless daemon)" ON)
option(BUILD_CLI "Build triangles-cli (JSON-RPC client)" ON) option(BUILD_CLI "Build triangles-cli (JSON-RPC client)" ON)
option(BUILD_TESTS "Build test_triangles (Boost.Test unit tests)" ON) option(BUILD_TESTS "Build test_triangles (Boost.Test unit tests)" ON)
option(USE_UPNP "Enable UPnP support via miniupnpc" ON) option(USE_UPNP "Enable UPnP support via miniupnpc" OFF)
option(USE_IPV6 "Enable IPv6 support" ON) option(USE_IPV6 "Enable IPv6 support" ON)
option(USE_QRCODE "Enable QR code generation via libqrencode" OFF) option(USE_QRCODE "Enable QR code generation via libqrencode" OFF)
option(USE_DBUS "Enable D-Bus notifications (Linux only)" ON) option(USE_DBUS "Enable D-Bus notifications (Linux only)" OFF)
option(USE_ZMQ "Enable ZMQ publisher support" OFF) option(USE_ZMQ "Enable ZMQ publisher support" OFF)
# Triangles is Tor-native. Tor is REQUIRED — disabling it at build time is # Triangles is Tor-native. Tor is REQUIRED — disabling it at build time is
# not a supported configuration. The 2026-06-23 DNS2 clearnet-fork incident # not a supported configuration. The 2026-06-23 DNS2 clearnet-fork incident
@@ -104,7 +104,7 @@ if(DEFINED USE_TOR_EMBEDDED AND NOT USE_TOR_EMBEDDED)
"instead.") "instead.")
endif() endif()
option(USE_O3 "Use -O3 optimization instead of -O2" OFF) option(USE_O3 "Use -O3 optimization instead of -O2" OFF)
option(ENABLE_PIE "Build position-independent executables" OFF) option(ENABLE_PIE "Build position-independent executables" ON)
option(ENABLE_STATIC "Prefer static linking (Linux release builds)" OFF) option(ENABLE_STATIC "Prefer static linking (Linux release builds)" OFF)
# Embedded I2P (i2pd) — runs an I2P router in-process alongside Tor. # Embedded I2P (i2pd) — runs an I2P router in-process alongside Tor.
+18 -16
View File
@@ -907,46 +907,48 @@ void StartShutdown() {}
# "$@" preservation. We use bash explicitly (not sh) for "$@" array # "$@" preservation. We use bash explicitly (not sh) for "$@" array
# semantics — paths may contain spaces, so word-splitting on IFS # semantics — paths may contain spaces, so word-splitting on IFS
# would corrupt them. # would corrupt them.
file(WRITE "${FUZZ_LINK_WRAPPER}" set(FUZZ_LINK_WRAPPER_CONTENT [=[#!/bin/bash
"#!/bin/bash
# Auto-generated by CMake (BUILD_FUZZ block). Discovers triangles_common + # Auto-generated by CMake (BUILD_FUZZ block). Discovers triangles_common +
# trianglesd .o files at link time and exec's the clang++ link line. # trianglesd .o files at link time and exec's the clang++ link line.
# #
# Usage: link.sh clang++ [link-args...] # Usage: link.sh clang++ [link-args...]
# Final exec: clang++ <each .o> <each original link-arg> # Final exec: clang++ <each .o> <each original link-arg>
set -euo pipefail set -euo pipefail
PROG=\"\$1\" PROG="$1"
shift shift
TRIANGLES_COMMON_DIR=\"${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/triangles_common.dir\" TRIANGLES_COMMON_DIR="@CMAKE_CURRENT_BINARY_DIR@/CMakeFiles/triangles_common.dir"
TRIANGLESD_DIR=\"${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/trianglesd_objects.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 # 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 # own clang-instrumented copy in fuzz_objs/ that we want to keep separate
# from the main build's copy). # from the main build's copy).
declare -a OBJS=() declare -a OBJS=()
for f in \"\$TRIANGLES_COMMON_DIR\"/*.o \"\$TRIANGLES_COMMON_DIR\"/*/*.o; do for f in "$TRIANGLES_COMMON_DIR"/*.o "$TRIANGLES_COMMON_DIR"/*/*.o; do
[ -f \"\$f\" ] || continue [ -f "$f" ] || continue
case \"\$f\" in case "$f" in
*/script.cpp.o) continue ;; */script.cpp.o) continue ;;
esac esac
OBJS+=(\"\$f\") OBJS+=("$f")
done done
if [ -d "\$TRIANGLESD_DIR" ]; then if [ -d "$TRIANGLESD_DIR" ]; then
for f in "\$TRIANGLESD_DIR"/*.o; do for f in "$TRIANGLESD_DIR"/*.o; do
[ -f "\$f" ] || continue [ -f "$f" ] || continue
# init.cpp defines the daemon's main(); the fuzz harness has its own # 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 # (libFuzzer's). wallet.cpp, noui.cpp etc. are safe — they don't
# define main and their external references (pwalletMain, # define main and their external references (pwalletMain,
# uiInterface, nDerivationMethodIndex) are satisfied by the stub # uiInterface, nDerivationMethodIndex) are satisfied by the stub
# object file we add at the end of the link line. # object file we add at the end of the link line.
case "\$f" in case "$f" in
*/init.cpp.o) continue ;; */init.cpp.o) continue ;;
esac esac
OBJS+=("\$f") OBJS+=("$f")
done done
fi fi
# Final arg list: PROG, then all .o files, then all original link args. # 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 file(CHMOD "${FUZZ_LINK_WRAPPER}" PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE GROUP_READ GROUP_EXECUTE
+70 -57
View File
@@ -14,6 +14,8 @@
#include <openssl/opensslv.h> #include <openssl/opensslv.h>
#include <algorithm> #include <algorithm>
#include <cctype>
#include <limits>
#include <stdexcept> #include <stdexcept>
#include <vector> #include <vector>
@@ -69,16 +71,10 @@ public:
throw bignum_error("CBigNum::CBigNum() : BN_new() returned NULL"); 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)) if (!BN_copy(pbn, b.pbn))
{
BN_clear_free(pbn);
throw bignum_error("CBigNum::CBigNum(const CBigNum&) : BN_copy failed"); throw bignum_error("CBigNum::CBigNum(const CBigNum&) : BN_copy failed");
}
} }
CBigNum& operator=(const CBigNum& b) CBigNum& operator=(const CBigNum& b)
@@ -99,21 +95,20 @@ public:
const BIGNUM* get() const { return pbn; } const BIGNUM* get() const { return pbn; }
//CBigNum(char n) is not portable. Use 'signed char' or 'unsigned char'. //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(signed char n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); }
CBigNum(short n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); } CBigNum(short n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); }
CBigNum(int n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); } CBigNum(int n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); }
CBigNum(long n) { pbn = BN_new(); if (n >= 0) setulong(n); else setint64(n); } CBigNum(long n) : CBigNum() { if (n >= 0) setulong(n); else setint64(n); }
CBigNum(long long n) { pbn = BN_new(); setint64(n); } CBigNum(long long n) : CBigNum() { setint64(n); }
CBigNum(unsigned char n) { pbn = BN_new(); setulong(n); } CBigNum(unsigned char n) : CBigNum() { setulong(n); }
CBigNum(unsigned short n) { pbn = BN_new(); setulong(n); } CBigNum(unsigned short n) : CBigNum() { setulong(n); }
CBigNum(unsigned int n) { pbn = BN_new(); setulong(n); } CBigNum(unsigned int n) : CBigNum() { setulong(n); }
CBigNum(unsigned long n) { pbn = BN_new(); setulong(n); } CBigNum(unsigned long n) : CBigNum() { setulong(n); }
CBigNum(unsigned long long n) { pbn = BN_new(); setuint64(n); } CBigNum(unsigned long long n) : CBigNum() { setuint64(n); }
explicit CBigNum(uint256 n) { pbn = BN_new(); setuint256(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); setvch(vch);
} }
@@ -216,21 +211,23 @@ public:
pch[1] = (nSize >> 16) & 0xff; pch[1] = (nSize >> 16) & 0xff;
pch[2] = (nSize >> 8) & 0xff; pch[2] = (nSize >> 8) & 0xff;
pch[3] = (nSize) & 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); const int nSize = BN_bn2mpi(pbn, nullptr);
if (nSize < 4) if (nSize <= 4)
return 0; return 0;
std::vector<unsigned char> vch(nSize); std::vector<unsigned char> vch(static_cast<size_t>(nSize));
BN_bn2mpi(pbn, &vch[0]); if (BN_bn2mpi(pbn, vch.data()) != nSize)
throw bignum_error("CBigNum::getuint64() : BN_bn2mpi failed");
if (vch.size() > 4) if (vch.size() > 4)
vch[4] &= 0x7f; vch[4] &= 0x7f;
uint64_t n = 0; uint64_t n = 0;
for (unsigned int i = 0, j = vch.size()-1; i < sizeof(n) && j >= 4; i++, j--) for (size_t i = 0; i < sizeof(n) && i + 4 < vch.size(); ++i)
((unsigned char*)&n)[i] = vch[j]; n |= static_cast<uint64_t>(vch[vch.size() - 1 - i]) << (8 * i);
return n; return n;
} }
@@ -258,7 +255,8 @@ public:
pch[1] = (nSize >> 16) & 0xff; pch[1] = (nSize >> 16) & 0xff;
pch[2] = (nSize >> 8) & 0xff; pch[2] = (nSize >> 8) & 0xff;
pch[3] = (nSize) & 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) void setuint256(uint256 n)
@@ -286,29 +284,33 @@ public:
pch[1] = (nSize >> 16) & 0xff; pch[1] = (nSize >> 16) & 0xff;
pch[2] = (nSize >> 8) & 0xff; pch[2] = (nSize >> 8) & 0xff;
pch[3] = (nSize >> 0) & 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 uint256 getuint256() const
{ {
unsigned int nSize = BN_bn2mpi(pbn, nullptr); const int mpiSize = BN_bn2mpi(pbn, nullptr);
if (nSize < 4) if (mpiSize <= 4)
return 0; return 0;
std::vector<unsigned char> vch(nSize); std::vector<unsigned char> vch(static_cast<size_t>(mpiSize));
BN_bn2mpi(pbn, &vch[0]); if (BN_bn2mpi(pbn, vch.data()) != mpiSize)
if (vch.size() > 4) throw bignum_error("CBigNum::getuint256() : BN_bn2mpi failed");
vch[4] &= 0x7f; vch[4] &= 0x7f;
uint256 n = 0; uint256 n = 0;
for (unsigned int i = 0, j = vch.size()-1; i < sizeof(n) && j >= 4; i++, j--) for (size_t i = 0; i < sizeof(n) && i + 4 < vch.size(); ++i)
((unsigned char*)&n)[i] = vch[j]; reinterpret_cast<unsigned char*>(&n)[i] = vch[vch.size() - 1 - i];
return n; return n;
} }
void setvch(const std::vector<unsigned char>& vch) 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); 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 // BIGNUM's byte stream format expects 4 bytes of
// big endian size data info at the front // big endian size data info at the front
vch2[0] = (nSize >> 24) & 0xff; vch2[0] = (nSize >> 24) & 0xff;
@@ -316,20 +318,25 @@ public:
vch2[2] = (nSize >> 8) & 0xff; vch2[2] = (nSize >> 8) & 0xff;
vch2[3] = (nSize >> 0) & 0xff; vch2[3] = (nSize >> 0) & 0xff;
// swap data to big endian // swap data to big endian
reverse_copy(vch.begin(), vch.end(), vch2.begin() + 4); for (size_t i = 0; i < vch.size(); ++i)
BN_mpi2bn(&vch2[0], vch2.size(), pbn); 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 std::vector<unsigned char> getvch() const
{ {
unsigned int nSize = BN_bn2mpi(pbn, nullptr); const int mpiSize = BN_bn2mpi(pbn, nullptr);
if (nSize <= 4) if (mpiSize <= 4)
return std::vector<unsigned char>(); return std::vector<unsigned char>();
std::vector<unsigned char> vch(nSize); std::vector<unsigned char> mpi(static_cast<size_t>(mpiSize));
BN_bn2mpi(pbn, &vch[0]); if (BN_bn2mpi(pbn, mpi.data()) != mpiSize)
vch.erase(vch.begin(), vch.begin() + 4); throw bignum_error("CBigNum::getvch() : BN_bn2mpi failed");
reverse(vch.begin(), vch.end());
return vch; 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) CBigNum& SetCompact(unsigned int nCompact)
@@ -340,16 +347,20 @@ public:
if (nSize >= 1) vch[4] = (nCompact >> 16) & 0xff; if (nSize >= 1) vch[4] = (nCompact >> 16) & 0xff;
if (nSize >= 2) vch[5] = (nCompact >> 8) & 0xff; if (nSize >= 2) vch[5] = (nCompact >> 8) & 0xff;
if (nSize >= 3) vch[6] = (nCompact >> 0) & 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; return *this;
} }
unsigned int GetCompact() const unsigned int GetCompact() const
{ {
unsigned int nSize = BN_bn2mpi(pbn, nullptr); const int mpiSize = BN_bn2mpi(pbn, nullptr);
std::vector<unsigned char> vch(nSize); if (mpiSize <= 4)
nSize -= 4; return 0;
BN_bn2mpi(pbn, &vch[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; unsigned int nCompact = nSize << 24;
if (nSize >= 1) nCompact |= (vch[4] << 16); if (nSize >= 1) nCompact |= (vch[4] << 16);
if (nSize >= 2) nCompact |= (vch[5] << 8); if (nSize >= 2) nCompact |= (vch[5] << 8);
@@ -361,7 +372,7 @@ public:
{ {
// skip 0x // skip 0x
const char* psz = str.c_str(); const char* psz = str.c_str();
while (isspace(*psz)) while (isspace(static_cast<unsigned char>(*psz)))
psz++; psz++;
bool fNegative = false; bool fNegative = false;
if (*psz == '-') if (*psz == '-')
@@ -369,15 +380,15 @@ public:
fNegative = true; fNegative = true;
psz++; psz++;
} }
if (psz[0] == '0' && tolower(psz[1]) == 'x') if (psz[0] == '0' && tolower(static_cast<unsigned char>(psz[1])) == 'x')
psz += 2; psz += 2;
while (isspace(*psz)) while (isspace(static_cast<unsigned char>(*psz)))
psz++; psz++;
// hex string to bignum // 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 }; 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; *this = 0;
while (isxdigit(*psz)) while (isxdigit(static_cast<unsigned char>(*psz)))
{ {
*this <<= 4; *this <<= 4;
int n = phexdigit[(unsigned char)*psz++]; int n = phexdigit[(unsigned char)*psz++];
@@ -389,6 +400,8 @@ public:
std::string ToString(int nBase=10) const 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; CAutoBN_CTX pctx;
CBigNum bnBase = nBase; CBigNum bnBase = nBase;
CBigNum bn0 = 0; CBigNum bn0 = 0;
+217 -122
View File
@@ -4,6 +4,7 @@
#include "bootstrap.h" #include "bootstrap.h"
#include "utxosnapshot.h" #include "utxosnapshot.h"
#include "txdb.h" #include "txdb.h"
#include "checkpoints.h"
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
@@ -22,6 +23,7 @@
#include "key.h" #include "key.h"
#include "base58.h" #include "base58.h"
#include "util.h" #include "util.h"
#include "json/nlohmann_json.hpp"
extern const std::string strMessageMagic; extern const std::string strMessageMagic;
@@ -30,12 +32,14 @@ extern const std::string strMessageMagic;
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <cstdlib> #include <cstdlib>
#include <cctype>
#ifdef WIN32 #ifdef WIN32
#include <winsock2.h> #include <winsock2.h>
#include <ws2tcpip.h> #include <ws2tcpip.h>
#else #else
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h> #include <netdb.h>
#include <unistd.h> #include <unistd.h>
#endif #endif
@@ -88,6 +92,20 @@ static SOCKET ConnectDirectTCP(const std::string& host, int port, std::string& s
if (hSocket == INVALID_SOCKET) if (hSocket == INVALID_SOCKET)
continue; 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) if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0)
break; // success break; // success
@@ -154,8 +172,11 @@ struct HttpConn {
strError = "Failed to create SSL context"; strError = "Failed to create SSL context";
return false; return false;
} }
// Skip cert verification — we verify data integrity via checkpoint hashes if (SSL_CTX_set_default_verify_paths(ctx) != 1) {
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr); strError = "Failed to load the system TLS trust store";
return false;
}
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr);
ssl = SSL_new(ctx); ssl = SSL_new(ctx);
if (!ssl) { if (!ssl) {
@@ -163,7 +184,11 @@ struct HttpConn {
return false; return false;
} }
SSL_set_fd(ssl, (int)sock); 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) { if (SSL_connect(ssl) != 1) {
unsigned long err = ERR_get_error(); unsigned long err = ERR_get_error();
@@ -172,6 +197,10 @@ struct HttpConn {
strError = "TLS handshake failed with " + hostname + ": " + errBuf; strError = "TLS handshake failed with " + hostname + ": " + errBuf;
return false; return false;
} }
if (SSL_get_verify_result(ssl) != X509_V_OK) {
strError = "TLS certificate verification failed for " + hostname;
return false;
}
return true; return true;
} }
}; };
@@ -229,13 +258,18 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
ProgressCallback progressFn, ProgressCallback progressFn,
std::string& strError, std::string& strError,
bool noProxy, bool noProxy,
int portOverride) int portOverride,
int64_t maxDownloadBytes)
{ {
try { try {
if (maxDownloadBytes <= 0) {
strError = "Download size limit must be positive";
return false;
}
std::string currentHost = host; std::string currentHost = host;
std::string currentPath = urlPath; std::string currentPath = urlPath;
int currentPort = (portOverride > 0) ? portOverride : PORT; int currentPort = (portOverride > 0) ? portOverride : PORT;
bool useSSL = false; bool useSSL = (currentPort == 443);
std::string headerData; std::string headerData;
int redirectCount = 0; int redirectCount = 0;
const int MAX_REDIRECTS = 5; 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 // Parse redirect URL — supports http://, https://, and relative paths
if (location.compare(0, 7, "http://") == 0 || if (location.compare(0, 7, "http://") == 0 ||
location.compare(0, 8, "https://") == 0) { location.compare(0, 8, "https://") == 0) {
if (!ParseAbsoluteUrl(location, useSSL, currentHost, bool redirectUsesSSL = false;
currentPort, currentPath)) { std::string redirectHost;
std::string redirectPath;
int redirectPort = 0;
if (!ParseAbsoluteUrl(location, redirectUsesSSL, redirectHost,
redirectPort, redirectPath)) {
strError = "Unsupported redirect location: " + location; strError = "Unsupported redirect location: " + location;
return false; 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] == '/') { } else if (!location.empty() && location[0] == '/') {
currentPath = location; currentPath = location;
} else { } else {
@@ -362,6 +408,10 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
if (lineEnd != std::string::npos) if (lineEnd != std::string::npos)
content_length = std::stoll(headerData.substr(valStart, lineEnd - valStart)); 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 // Open output file
FILE* file = fopen(destPath.string().c_str(), "wb"); 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 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; bytes_written += n;
if (progressFn && (bytes_written - last_progress >= 262144)) { 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"; fs::path tmpPath = fs::temp_directory_path() / "triangles_bootstrap_filelist.txt";
std::string urlPath = std::string(BASE_PATH) + "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; return false;
// Read lines // Read lines
@@ -452,23 +514,6 @@ bool FetchFileList(const std::string& host,
// --- tar.gz bootstrap support --- // --- 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, bool ParseManifest(const fs::path& manifestPath,
SnapshotManifest& manifest, SnapshotManifest& manifest,
std::string& strError) std::string& strError)
@@ -575,12 +620,9 @@ bool VerifyManifest(const SnapshotManifest& manifest,
} }
// ─── Signature verification (#11) ───────────────────────────────────── // ─── Signature verification (#11) ─────────────────────────────────────
// If the manifest includes a signature, verify it against the // Legacy pre-built indexes are never accepted without authentication.
// compiled-in snapshot signing key. This prevents MITM attacks // This format is disabled below, but keep its verifier fail-closed so a
// where an attacker replaces the snapshot file on the bootstrap server. // future caller cannot silently revive the old trust behavior.
//
// If no signature is present, print a warning but continue (backward
// compatibility with older snapshots that pre-date signing).
if (!manifest.signature.empty()) { if (!manifest.signature.empty()) {
// Build the message that was signed: "height||hash" (ASCII) // Build the message that was signed: "height||hash" (ASCII)
std::string message = std::to_string(manifest.height) + "||" + manifest.hash; 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"; strError = "Snapshot manifest signature INVALID — possible tampering detected";
return false; return false;
} else { } else {
// rc < 0 means error (e.g., placeholder zero pubkey not yet deployed) strError = "Snapshot manifest signature verification error";
printf("WARNING: Snapshot manifest signature verification error (rc=%d). " return false;
"Signing key may not be deployed yet. Proceeding without verification.\n", rc);
} }
} else { } else {
printf("WARNING: Snapshot manifest has no signature — loading WITHOUT signature verification\n"); strError = "Snapshot manifest has no signature";
return false;
} }
return true; return true;
@@ -672,6 +714,13 @@ bool DownloadBootstrap(const std::string& host,
ProgressCallback progressFn, ProgressCallback progressFn,
std::string& strError) 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; bool gotBlockFile = false;
// FastImport removed (commit bdb7253). v2 UTXO snapshot is the ONLY // FastImport removed (commit bdb7253). v2 UTXO snapshot is the ONLY
@@ -758,8 +807,10 @@ bool DownloadBootstrap(const std::string& host,
fs::remove(manifestPath); fs::remove(manifestPath);
return true; return true;
#endif
} }
#if 0
namespace { namespace {
// Try to find the canonical UTXO snapshot entry in the bootstrap server's // 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); fs::remove(filePath);
return true; return true;
} }
#endif
// Re-enter anonymous namespace for the remaining file-private helpers. // Re-enter anonymous namespace for the remaining file-private helpers.
// (IsTrustedSnapshotSigner / VerifySignedMessage / ExtractJsonString are // (IsTrustedSnapshotSigner / VerifySignedMessage / ExtractJsonString are
@@ -899,6 +951,7 @@ bool UnsetTrustedSnapshotPublisher(std::string& strError)
namespace { namespace {
#if 0
bool IsTrustedSnapshotSigner(const std::string& addr) bool IsTrustedSnapshotSigner(const std::string& addr)
{ {
// 1. Runtime override (set via RPC). // 1. Runtime override (set via RPC).
@@ -1072,6 +1125,7 @@ bool FindCanonicalSnapshotInManifest(const std::string& manifestText,
return true; return true;
} }
#endif
// Read an entire file into a string. Empty string on error. // Read an entire file into a string. Empty string on error.
std::string ReadFileToString(const fs::path& path) std::string ReadFileToString(const fs::path& path)
@@ -1114,6 +1168,80 @@ std::string Sha256OfFile(const fs::path& path)
} // anonymous namespace } // 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, bool DownloadUtxoSnapshot(const std::string& host,
const fs::path& dataDir, const fs::path& dataDir,
ProgressCallback progressFn, ProgressCallback progressFn,
@@ -1121,79 +1249,51 @@ bool DownloadUtxoSnapshot(const std::string& host,
{ {
const bool noProxy = true; const bool noProxy = true;
// Step 1: discover the canonical snapshot filename + expected SHA256 + // manifest.json is discovery metadata, not a trust root. The only accepted
// per-snapshot manifest filename from the big manifest.json. Falls back // snapshot hash is the one compiled into this release for the same height.
// to legacy URL if manifest unavailable.
std::string snapshotFilename = "utxo-snapshot.bin";
std::string expectedSha256;
std::string snapshotManifestFilename;
bool haveManifest = false;
fs::path tmpManifest = dataDir / "manifest.json.tmp"; fs::path tmpManifest = dataDir / "manifest.json.tmp";
if (DownloadFile(host, "manifest.json", tmpManifest, nullptr, strError, noProxy)) { if (!DownloadFile(host, std::string(BASE_PATH) + "manifest.json",
std::string text = ReadFileToString(tmpManifest); tmpManifest, nullptr, strError, noProxy,
-1, 4 * 1024 * 1024)) {
fs::remove(tmpManifest); fs::remove(tmpManifest);
return false;
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();
} }
// Step 2: verify the per-snapshot manifest's signature. This is the const std::string manifestText = ReadFileToString(tmpManifest);
// AUTHENTICATION gate — the signature attests that the listed snapshot fs::remove(tmpManifest);
// file came from a trusted operator. No checkpoint required; signature if (manifestText.empty()) {
// alone proves authenticity. strError = "Cannot read downloaded manifest.json";
if (!snapshotManifestFilename.empty()) { return false;
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");
} }
// 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"; 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()); 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; return false;
} }
// Step 4: verify the downloaded file's SHA256 against the manifest. const std::string actualSha256 = Sha256OfFile(tmpPath);
if (!expectedSha256.empty()) { if (actualSha256.empty()) {
std::string actualSha = Sha256OfFile(tmpPath); strError = "Cannot read downloaded snapshot for SHA256 verification";
if (actualSha.empty()) { fs::remove(tmpPath);
strError = "Cannot read downloaded snapshot for SHA256 verification"; return false;
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());
} }
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"); printf("Bootstrap: UTXO snapshot downloaded, loading into database...\n");
// Step 5: load the snapshot. requireCheckpoint is FALSE — signature is // File hash and tip checkpoint are independent gates. The hash commits to
// the authentication gate; checkpoints would force snapshots only at // the complete serialized UTXO set; the checkpoint commits to chain identity.
// specific heights. Signature alone is sufficient. if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError, /*requireCheckpoint=*/true)) {
if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError, /*requireCheckpoint=*/false)) {
fs::remove(tmpPath); fs::remove(tmpPath);
return false; return false;
} }
+23 -25
View File
@@ -8,13 +8,14 @@
#include <vector> #include <vector>
#include <functional> #include <functional>
#include <filesystem> #include <filesystem>
#include <cstdint>
namespace Bootstrap { namespace Bootstrap {
// Bootstrap server configuration // Bootstrap server configuration
static const char* DEFAULT_HOST = "bootstrap.cryptographic-triangles.org"; inline constexpr const char* DEFAULT_HOST = "bootstrap.cryptographic-triangles.org";
static const char* BASE_PATH = "/"; inline constexpr const char* BASE_PATH = "/";
static const int PORT = 80; inline constexpr int PORT = 443;
// Progress callback: (bytesDownloaded, totalBytes) // Progress callback: (bytesDownloaded, totalBytes)
typedef std::function<void(int64_t, int64_t)> ProgressCallback; typedef std::function<void(int64_t, int64_t)> ProgressCallback;
@@ -31,7 +32,8 @@ namespace Bootstrap {
ProgressCallback progressFn, ProgressCallback progressFn,
std::string& strError, std::string& strError,
bool noProxy = false, 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) // Fetch the file manifest (list of relative paths to download)
bool FetchFileList(const std::string& host, bool FetchFileList(const std::string& host,
@@ -46,6 +48,23 @@ namespace Bootstrap {
ProgressCallback progressFn, ProgressCallback progressFn,
std::string& strError); 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) // Snapshot manifest (parsed from snapshot.manifest in bootstrap archive)
struct SnapshotManifest { struct SnapshotManifest {
int format; // format version, must be 1 int format; // format version, must be 1
@@ -73,27 +92,6 @@ namespace Bootstrap {
ProgressCallback progressFn, ProgressCallback progressFn,
std::string& strError); 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 } // namespace Bootstrap
#endif // TRIANGLES_BOOTSTRAP_H #endif // TRIANGLES_BOOTSTRAP_H
+2 -1
View File
@@ -425,7 +425,8 @@ bool LoadSignedCheckpoints(
if (Bootstrap::DownloadFile(host, "signed-checkpoints.json", if (Bootstrap::DownloadFile(host, "signed-checkpoints.json",
std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp", std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp",
nullptr, strError, 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"; std::filesystem::path tmp = std::filesystem::temp_directory_path() / "signed-checkpoints.json.tmp";
FILE* f = fopen(tmp.string().c_str(), "rb"); FILE* f = fopen(tmp.string().c_str(), "rb");
if (f) { if (f) {
+7 -48
View File
@@ -353,53 +353,14 @@ namespace Checkpoints
bool SetCheckpointPrivKey(std::string strPrivKey) bool SetCheckpointPrivKey(std::string strPrivKey)
{ {
// Test signing a sync-checkpoint with genesis block (void)strPrivKey;
CSyncCheckpoint checkpoint; return error("SetCheckpointPrivKey: synchronized checkpoints are disabled");
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;
} }
bool SendSyncCheckpoint(uint256 hashCheckpoint) bool SendSyncCheckpoint(uint256 hashCheckpoint)
{ {
CSyncCheckpoint checkpoint; (void)hashCheckpoint;
checkpoint.hashCheckpoint = hashCheckpoint; return error("SendSyncCheckpoint: synchronized checkpoints are disabled");
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;
} }
// Is the sync-checkpoint outside maturity window? // Is the sync-checkpoint outside maturity window?
@@ -420,13 +381,11 @@ const std::string CSyncCheckpoint::strMasterPubKey = "";
std::string CSyncCheckpoint::strMasterPrivKey = ""; std::string CSyncCheckpoint::strMasterPrivKey = "";
// triangles: verify signature of sync-checkpoint message // 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() bool CSyncCheckpoint::CheckSignature()
{ {
// Deserialize the checkpoint data without signature verification return error("CSyncCheckpoint::CheckSignature: synchronized checkpoints are disabled");
CDataStream sMsg(vchMsg, SER_NETWORK, PROTOCOL_VERSION);
sMsg >> *(CUnsignedSyncCheckpoint*)this;
return true;
} }
// triangles: process synchronized checkpoint // triangles: process synchronized checkpoint
+92 -61
View File
@@ -30,8 +30,11 @@
#include "addressindex.h" #include "addressindex.h"
#include "chaindb_migrate.h" #include "chaindb_migrate.h"
#include <memory> #include <memory>
#include <atomic>
#include <cstdlib>
#include <thread> #include <thread>
#include <vector> #include <vector>
#include <cerrno>
// Forward declaration: InitError / InitWarning are defined further down // Forward declaration: InitError / InitWarning are defined further down
// in this file but referenced by AppInit (line ~423) before the definition. // in this file but referenced by AppInit (line ~423) before the definition.
@@ -66,6 +69,8 @@ using namespace std;
namespace fs = std::filesystem; namespace fs = std::filesystem;
namespace { namespace {
std::atomic<int> g_shutdownExitCode{EXIT_SUCCESS};
// Acquire an exclusive, non-blocking advisory lock on the datadir .lock file // Acquire an exclusive, non-blocking advisory lock on the datadir .lock file
// and hold it for the lifetime of the process. Replaces // and hold it for the lifetime of the process. Replaces
// boost::interprocess::file_lock. The descriptor/handle is intentionally never // 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 return true; // fd held until process exit
#endif #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 } // namespace
void MarkShutdownFailure()
{
g_shutdownExitCode.store(EXIT_FAILURE, std::memory_order_relaxed);
}
std::unique_ptr<CWallet> pwalletMain; std::unique_ptr<CWallet> pwalletMain;
CClientUIInterface uiInterface; CClientUIInterface uiInterface;
std::string strWalletFileName; std::string strWalletFileName;
@@ -144,7 +173,8 @@ void ExitTimeout(void* parg)
{ {
#ifdef WIN32 #ifdef WIN32
MilliSleep(5000); MilliSleep(5000);
ExitProcess(0); ExitProcess(static_cast<UINT>(
g_shutdownExitCode.load(std::memory_order_relaxed)));
#endif #endif
} }
@@ -421,7 +451,11 @@ void Shutdown(void* parg)
// MakeChainDB()->Close(); // MakeChainDB()->Close();
bitdb.Flush(false); bitdb.Flush(false);
bitdb.Flush(true); 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()); UnregisterWallet(pwalletMain.get());
pwalletMain.reset(); pwalletMain.reset();
// DB is flushed and wallet saved - safe to force-exit if something hangs // DB is flushed and wallet saved - safe to force-exit if something hangs
@@ -431,7 +465,7 @@ void Shutdown(void* parg)
fExit = true; fExit = true;
#ifndef QT_GUI #ifndef QT_GUI
// ensure non-UI client gets exited here, but let Triangles-Qt reach 'return 0;' in triangles.cpp // 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 #endif
} }
else else
@@ -528,8 +562,10 @@ bool AppInit(int argc, char* argv[])
} catch (...) { } catch (...) {
PrintException(nullptr, "AppInit()"); PrintException(nullptr, "AppInit()");
} }
if (!fRet) if (!fRet) {
MarkShutdownFailure();
Shutdown(nullptr); Shutdown(nullptr);
}
return fRet; return fRet;
} }
@@ -610,8 +646,8 @@ std::string HelpMessage()
//" -onlynet=<net> " + _("Only connect to nodes in network <net> (IPv4, IPv6 or Tor)") + "\n" + //" -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" + //" -discover " + _("Discover own IP address (default: 1 when listening and no -externalip)") + "\n" +
//" -irc " + _("Find peers using internet relay chat (default: 0)") + "\n" + //" -irc " + _("Find peers using internet relay chat (default: 0)") + "\n" +
//" -listen " + _("Accept connections from outside (default: 1 if no -proxy or -connect)") + "\n" + " -listen " + _("Accept inbound peer connections (default: 1 unless -proxy or -connect is set)") + "\n" +
//" -bind=<addr> " + _("Bind to given address. Use [host]:port notation for IPv6") + "\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" + // -dnsseed " + _("Find peers using DNS lookup (default: 1)") + "\n" +
" -staking " + _("Stake your coins to support network and gain reward (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" + " -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 #endif
" -rpcuser=<user> " + _("Username for JSON-RPC connections") + "\n" + " -rpcuser=<user> " + _("Username for JSON-RPC connections") + "\n" +
" -rpcpassword=<pw> " + _("Password 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" + " -rpcport=<port> " + _("Listen for JSON-RPC connections on <port> (default: 19112 or testnet: 19111)") + "\n" +
" -rpcallowip=<ip> " + _("Allow JSON-RPC connections from specified IP address") + "\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" + " -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" + " -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" + " -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); fUseUPnP = GetBoolArg("-upnp", USE_UPNP);
#endif #endif
bool fBound = false; bool fBound = false;
if (true) { if (!fNoListen) {
if (true) { if (mapArgs.count("-bind")) {
do { for (const std::string& bindAddress : mapMultiArgs["-bind"]) {
// 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.
CService addrBind; CService addrBind;
struct in_addr any; if (!Lookup(bindAddress.c_str(), addrBind, GetListenPort(), false))
any.s_addr = htonl(INADDR_ANY); return InitError(strprintf(_("Cannot resolve -bind address: '%s'"),
addrBind = CService(any, GetListenPort()); bindAddress.c_str()));
fBound |= Bind(addrBind); fBound |= Bind(addrBind);
} while (false); }
} else {
struct in_addr any;
any.s_addr = htonl(INADDR_ANY);
fBound = Bind(CService(any, GetListenPort()));
} }
if (!fBound) if (!fBound)
return InitError(_("Failed to listen on any port.")); 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{""}))) return InitError(_("Synchronized checkpoint signing is disabled."));
InitError(_("Unable to sign checkpoint, wrong checkpointkey?\n"));
} }
for (string strDest : mapMultiArgs["-seednode"]) 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())); StartupPerfLog("network_init", GetTimeMillis() - nStart, strprintf("listen=%d seednodes=%" PRIszu, !fNoListen, mapMultiArgs["-seednode"].size()));
// ********************************************************* Step 6b: bootstrap download (daemon) // ********************************************************* Step 6b: bootstrap download (daemon)
// Automatic: if data dir has no blockchain, bootstrap without asking. // Remote HTTP bootstrap is opt-in via -bootstrap. Fresh nodes otherwise
// Can also be forced with -bootstrap flag, or disabled with -nobootstrap. // 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). // 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 // 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. // Bootstrap auto-download works for both GUI and daemon.
// GUI users get the same automatic bootstrap on fresh installs. // GUI users get the same automatic bootstrap on fresh installs.
{ {
bool wantsBootstrap = GetBoolArg("-bootstrap", false);
bool noBootstrap = GetBoolArg("-nobootstrap", false); bool noBootstrap = GetBoolArg("-nobootstrap", false);
bool snapshotMode = GetBoolArg("-snapshot", true); bool wantsBootstrap = GetBoolArg("-bootstrap", false) && !noBootstrap;
fs::path dataPath = GetDataDir(); 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); bool needsBootstrap = Bootstrap::NeedsBootstrap(dataPath);
if (needsBootstrap && !noBootstrap) { if (needsBootstrap && !wantsBootstrap) {
printf("Bootstrap: no blockchain data found — downloading UTXO snapshot automatically.\n"); printf("Bootstrap: no blockchain data found; remote bootstrap is disabled unless -bootstrap is set.\n");
printf("Bootstrap: (use -nobootstrap to skip)\n");
uiInterface.InitMessage(_("Downloading UTXO snapshot..."));
wantsBootstrap = true;
} }
if (wantsBootstrap) if (wantsBootstrap)
@@ -1208,7 +1234,6 @@ bool AppInit2()
int64_t nBootstrapStart = GetTimeMillis(); int64_t nBootstrapStart = GetTimeMillis();
fs::path dataPath = GetDataDir(); fs::path dataPath = GetDataDir();
std::string host = Bootstrap::DEFAULT_HOST; std::string host = Bootstrap::DEFAULT_HOST;
std::string strError;
int64_t lastGuiUpdate = 0; int64_t lastGuiUpdate = 0;
auto progressFn = [&lastGuiUpdate](int64_t bytesDownloaded, int64_t totalBytes) { auto progressFn = [&lastGuiUpdate](int64_t bytesDownloaded, int64_t totalBytes) {
@@ -1250,19 +1275,10 @@ bool AppInit2()
triedUtxoSnapshot = true; 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) { if (!success) {
uiInterface.InitMessage(_("Downloading blockchain snapshot...")); printf("Bootstrap: no trusted compiled-hash snapshot available; syncing from peers.\n");
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");
}
} }
StartupPerfLog("bootstrap_download", GetTimeMillis() - nBootstrapStart, StartupPerfLog("bootstrap_download", GetTimeMillis() - nBootstrapStart,
@@ -1282,13 +1298,23 @@ bool AppInit2()
printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n"); printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n");
uiInterface.InitMessage(_("Loading UTXO snapshot...")); 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; 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"); printf("UTXO snapshot loaded successfully.\n");
} else { } else {
printf("UTXO snapshot load failed: %s\n", strError.c_str()); printf("UTXO snapshot load failed: %s\n", strError.c_str());
@@ -1501,6 +1527,11 @@ bool AppInit2()
{ {
fs::path walletPath = GetDataDir() / strWalletFileName; fs::path walletPath = GetDataDir() / strWalletFileName;
if (fs::exists(walletPath)) { if (fs::exists(walletPath)) {
#ifndef WIN32
std::string permissionError;
if (!EnsureOwnerOnlyFile(walletPath, permissionError))
return InitError(permissionError);
#endif
uintmax_t wsize = fs::file_size(walletPath); uintmax_t wsize = fs::file_size(walletPath);
printf("Wallet file size: %llu bytes\n", (unsigned long long)wsize); printf("Wallet file size: %llu bytes\n", (unsigned long long)wsize);
if (wsize < 1024) { if (wsize < 1024) {
@@ -1933,10 +1964,10 @@ bool AppInit2()
printf("mapAddressBook.size() = %" PRIszu "\n", pwalletMain->mapAddressBook.size()); printf("mapAddressBook.size() = %" PRIszu "\n", pwalletMain->mapAddressBook.size());
if (!NewThread(StartNode, nullptr)) if (!NewThread(StartNode, nullptr))
InitError(_("Error: could not start node")); return InitError(_("Error: could not start node"));
if (fServer) if (fServer && !NewThread(ThreadRPCServer, nullptr))
NewThread(ThreadRPCServer, nullptr); return InitError(_("Error: could not start the RPC server"));
// ********************************************************* Step 11.6: P2P UTXO snapshot fetch // ********************************************************* Step 11.6: P2P UTXO snapshot fetch
// If the chain is empty and snapshot mode is enabled (default), spawn a // 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::unique_ptr<CWallet> pwalletMain;
extern std::string strWalletFileName; extern std::string strWalletFileName;
void StartShutdown(); void StartShutdown();
void MarkShutdownFailure();
bool ShutdownRequested(); bool ShutdownRequested();
void Shutdown(void* parg); void Shutdown(void* parg);
bool AppInit2(); bool AppInit2();
@@ -19,4 +20,3 @@ std::string HelpMessage();
#endif #endif
+41 -20
View File
@@ -190,28 +190,49 @@ bool CCryptoKeyStore::GetPubKey(const CKeyID &address, CPubKey& vchPubKeyOut) co
return false; return false;
} }
bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn) bool CCryptoKeyStore::PrepareKeyEncryption(CKeyingMaterial& vMasterKeyIn,
CryptedKeyMap& cryptedKeysOut) const
{ {
{ LOCK(cs_KeyStore);
LOCK(cs_KeyStore); if (!mapCryptedKeys.empty() || IsCrypted())
if (!mapCryptedKeys.empty() || IsCrypted()) return false;
return false;
fUseCrypto = true; cryptedKeysOut.clear();
for (KeyMap::value_type& mKey : mapKeys) for (const KeyMap::value_type& mKey : mapKeys)
{ {
CKey key; CKey key;
if (!key.SetSecret(mKey.second.first, mKey.second.second)) if (!key.SetSecret(mKey.second.first, mKey.second.second))
return false; return false;
const CPubKey vchPubKey = key.GetPubKey(); const CPubKey vchPubKey = key.GetPubKey();
std::vector<unsigned char> vchCryptedSecret; std::vector<unsigned char> vchCryptedSecret;
bool fCompressed; bool fCompressed;
if (!EncryptSecret(vMasterKeyIn, key.GetSecret(fCompressed), vchPubKey.GetHash(), vchCryptedSecret)) if (!EncryptSecret(vMasterKeyIn, key.GetSecret(fCompressed),
return false; vchPubKey.GetHash(), vchCryptedSecret))
if (!AddCryptedKey(vchPubKey, vchCryptedSecret)) return false;
return false; if (!cryptedKeysOut.emplace(vchPubKey.GetID(),
} std::make_pair(vchPubKey,
mapKeys.clear(); 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; 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 "util_signal.h"
#include "sync.h" #include "sync.h"
#include <utility>
class CScript; class CScript;
/** A virtual base class for key stores */ /** A virtual base class for key stores */
@@ -112,7 +114,13 @@ protected:
bool SetCrypted(); 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 EncryptKeys(CKeyingMaterial& vMasterKeyIn);
bool Unlock(const CKeyingMaterial& vMasterKeyIn); bool Unlock(const CKeyingMaterial& vMasterKeyIn);
+22 -6
View File
@@ -1747,6 +1747,11 @@ bool IsConsensusAssumeValidHeight(int nHeight)
|| (nHeight <= nAssumeValidThreshold); || (nHeight <= nAssumeValidThreshold);
} }
bool IsBlockSignatureRequiredAtHeight(int nHeight)
{
return nHeight > Checkpoints::GetTotalBlocksEstimate();
}
void static InvalidChainFound(CBlockIndex* pindexNew) void static InvalidChainFound(CBlockIndex* pindexNew)
{ {
if (pindexNew->nChainTrust > nBestInvalidTrust) if (pindexNew->nChainTrust > nBestInvalidTrust)
@@ -3401,6 +3406,12 @@ bool CBlock::AcceptBlock()
uint256 hashProofOfStake = 0, targetProofOfStake = 0; uint256 hashProofOfStake = 0, targetProofOfStake = 0;
if (IsProofOfStake()) 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)) if (IsConsensusAssumeValidHeight(nHeight))
{ {
// Historical fast path: blocks at/below hardcoded checkpoint or // 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)) 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()); 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 // Operational IBD state is never permission to skip a live proof-of-stake
// Skip block signature verification during initial block download (below checkpoint). // block signature. Only a candidate height committed by the latest
// The hardcoded checkpoint guarantees historical chain integrity. // hardened checkpoint uses the historical fast path.
if (!pblock->CheckBlock(true, true, !IsInitialBlockDownload())) 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", printf("IBD-DIAG: CheckBlock FAILED for %s (PoS=%d, IBD=%d)\n",
hash.ToString().substr(0,20).c_str(), pblock->IsProofOfStake(), IsInitialBlockDownload()); hash.ToString().substr(0,20).c_str(), pblock->IsProofOfStake(), IsInitialBlockDownload());
@@ -3786,6 +3803,7 @@ bool CheckDiskSpace(uint64_t nAdditionalBytes)
strMiscWarning = strMessage; strMiscWarning = strMessage;
printf("*** %s\n", strMessage.c_str()); printf("*** %s\n", strMessage.c_str());
uiInterface.ThreadSafeMessageBox(strMessage, "Triangles", CClientUIInterface::OK | CClientUIInterface::ICON_EXCLAMATION | CClientUIInterface::MODAL); uiInterface.ThreadSafeMessageBox(strMessage, "Triangles", CClientUIInterface::OK | CClientUIInterface::ICON_EXCLAMATION | CClientUIInterface::MODAL);
MarkShutdownFailure();
StartShutdown(); StartShutdown();
return false; return false;
} }
@@ -6039,5 +6057,3 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
} }
return true; 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 // assume-valid validation. This intentionally excludes operational IBD states
// such as a stale tip; stale-tip IBD must not disable live PoS checks. // such as a stale tip; stale-tip IBD must not disable live PoS checks.
[[nodiscard]] bool IsConsensusAssumeValidHeight(int nHeight); [[nodiscard]] bool IsConsensusAssumeValidHeight(int nHeight);
[[nodiscard]] bool IsBlockSignatureRequiredAtHeight(int nHeight);
std::string GetWarnings(std::string strFor); std::string GetWarnings(std::string strFor);
bool GetTransaction(const uint256 &hash, CTransaction &tx, uint256 &hashBlock); bool GetTransaction(const uint256 &hash, CTransaction &tx, uint256 &hashBlock);
uint256 WantedByOrphan(const CBlock* pblockOrphan); uint256 WantedByOrphan(const CBlock* pblockOrphan);
+39 -5
View File
@@ -605,7 +605,8 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest)
} }
if (fDebug) { if (fDebug) {
printf("ConnectNode(): pszDest: %s\n", pszDest); printf("ConnectNode(): destination: %s\n",
pszDest ? pszDest : addrConnect.ToString().c_str());
} }
/// debug print /// debug print
@@ -1738,7 +1739,7 @@ void ThreadOnionSeed(void* parg)
// Fetch dynamic seeds with retry — up to 4 attempts with increasing backoff. // Fetch dynamic seeds with retry — up to 4 attempts with increasing backoff.
// This is the primary discovery mechanism — seeds.cryptographic-triangles.org // This is the primary discovery mechanism — seeds.cryptographic-triangles.org
{ if (!GetBoolArg("-noseedurl", false)) {
bool ok = false; bool ok = false;
int delays[] = {0, 30, 60, 120}; int delays[] = {0, 30, 60, 120};
for (int attempt = 0; attempt < 4 && !ok && !fShutdown; attempt++) { for (int attempt = 0; attempt < 4 && !ok && !fShutdown; attempt++) {
@@ -1806,7 +1807,8 @@ void ThreadOnionSeed(void* parg)
else else
printf("ThreadOnionSeed: low outbound peers (%d), re-seeding...\n", nOutbound); 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 // Re-queue hardcoded seeds for direct connection
for (unsigned int seed_idx = 0; strOnionSeed[seed_idx][0] != nullptr; seed_idx++) { 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); seedPath = seedHost.substr(slashPos);
seedHost = seedHost.substr(0, 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()); 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) // 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); SSL_set_fd(ssl, (int)hSocket);
int ret = SSL_connect(ssl); int ret = SSL_connect(ssl);
@@ -1944,6 +1959,15 @@ bool ThreadHTTPSeedFetch2(void* parg)
closesocket(hSocket); closesocket(hSocket);
return false; 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()); 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 // Read response over TLS
std::string response; std::string response;
char buf[4096]; char buf[4096];
static constexpr size_t MAX_SEED_RESPONSE_SIZE = 1024 * 1024;
while (true) { while (true) {
int nBytes = SSL_read(ssl, buf, sizeof(buf)); int nBytes = SSL_read(ssl, buf, sizeof(buf));
if (nBytes <= 0) if (nBytes <= 0)
break; 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); response.append(buf, nBytes);
} }
@@ -2002,7 +2035,8 @@ bool ThreadHTTPSeedFetch2(void* parg)
// Check status code // Check status code
std::string statusLine = response.substr(0, response.find("\r\n")); 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()); printf("HTTPS seed fetch: %s from %s\n", statusLine.c_str(), seedHost.c_str());
return false; return false;
} }
+27 -10
View File
@@ -141,12 +141,27 @@ inline SOCKET ConnectRPCSocket(const std::string& host, int port)
return hSocket; return hSocket;
} }
// Create listening sockets for the RPC server. When loopbackOnly is true the inline bool SetRPCSocketTimeouts(SOCKET socket, int timeoutSeconds)
// 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 #ifdef WIN32
// two never conflict. Returns the bound, listening sockets; empty + strError on DWORD timeout = static_cast<DWORD>(timeoutSeconds * 1000);
// total failure (partial success — e.g. only IPv4 — is returned as success). #else
inline std::vector<SOCKET> BindRPCSockets(int port, bool loopbackOnly, std::string& strError) 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; 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)); std::memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM; 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; struct addrinfo* res = nullptr;
const std::string portStr = std::to_string(port); const std::string portStr = std::to_string(port);
// "localhost" resolves to the loopback addresses (127.0.0.1 and ::1); const char* node = wildcard ? nullptr :
// nullptr + AI_PASSIVE yields the wildcard addresses. (bindAddress.empty() ? "localhost" : bindAddress.c_str());
const char* node = loopbackOnly ? "localhost" : nullptr;
int gai = ::getaddrinfo(node, portStr.c_str(), &hints, &res); int gai = ::getaddrinfo(node, portStr.c_str(), &hints, &res);
if (gai != 0) { if (gai != 0) {
strError = std::string("RPC bind: getaddrinfo failed: ") + gai_strerror(gai); 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; 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/onion_v3.h"
#include "tor/tor_embedded.h" #include "tor/tor_embedded.h"
#include <memory>
using namespace json_spirit; using namespace json_spirit;
using namespace std; using namespace std;
@@ -1519,7 +1521,7 @@ Value walletpassphrase(const Array& params, bool fHelp)
if (pwalletMain->IsCrypted() && (fHelp || params.size() < 2 || params.size() > 3)) if (pwalletMain->IsCrypted() && (fHelp || params.size() < 2 || params.size() > 3))
throw runtime_error( throw runtime_error(
"walletpassphrase <passphrase> <timeout> [stakingonly]\n" "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 [stakingonly] is true sending functions are disabled.");
if (fHelp) if (fHelp)
return true; return true;
@@ -1530,6 +1532,14 @@ Value walletpassphrase(const Array& params, bool fHelp)
if (!pwalletMain->IsLocked()) if (!pwalletMain->IsLocked())
throw JSONRPCError(RPC_WALLET_ALREADY_UNLOCKED, "Error: Wallet is already unlocked, use walletlock first if need to change unlock settings."); 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 // Note that the walletpassphrase is stored in params[0] which is not mlock()ed
SecureString strWalletPass; SecureString strWalletPass;
strWalletPass.reserve(100); strWalletPass.reserve(100);
@@ -1545,15 +1555,18 @@ Value walletpassphrase(const Array& params, bool fHelp)
"walletpassphrase <passphrase> <timeout>\n" "walletpassphrase <passphrase> <timeout>\n"
"Stores the wallet decryption key in memory for <timeout> seconds."); "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 // triangles: if user OS account compromised prevent trivial sendmoney commands
if (params.size() > 2) fWalletUnlockStakingOnly = stakingOnly;
fWalletUnlockStakingOnly = params[2].get_bool(); if (!NewThread(ThreadCleanWalletPassphrase, sleepTime.get())) {
else pwalletMain->Lock();
fWalletUnlockStakingOnly = false; 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; return Value::null;
} }
+5
View File
@@ -553,3 +553,8 @@ scrypt_core_loop2:
#endif #endif
#endif #endif
#if defined(__ELF__)
.section .note.GNU-stack,"",%progbits
#endif
+4
View File
@@ -910,3 +910,7 @@ xmm_scrypt_core_loop2:
ret ret
#endif #endif
#if defined(__ELF__)
.section .note.GNU-stack,"",@progbits
#endif
+5 -1
View File
@@ -856,4 +856,8 @@ xmm_scrypt_core_loop2:
popq %rbx popq %rbx
ret 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); memcpy(civ+i, &nonse, 4);
HMAC_CTX *ctx = HMAC_CTX_new(); HMAC_CTX *ctx = HMAC_CTX_new();
if (ctx == nullptr)
return 1;
unsigned int nBytes; unsigned int nBytes;
if (!HMAC_Init_ex(ctx, &civ[0], 32, EVP_sha256(), nullptr) 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 if (sha256Hash[31] == 0
&& sha256Hash[30] == 0 && sha256Hash[30] == 0
&& (~(sha256Hash[29]) & ((1<<0) || (1<<1) || (1<<2)) )) && (sha256Hash[29] & 1U) == 0)
{ {
if (fDebugSmsg) if (fDebugSmsg)
printf("Hash Valid.\n"); printf("Hash Valid.\n");
@@ -3614,6 +3616,8 @@ int SecureMsgSetHash(unsigned char *pHeader, unsigned char *pPayload, uint32_t n
bool found = false; bool found = false;
HMAC_CTX *ctx = HMAC_CTX_new(); HMAC_CTX *ctx = HMAC_CTX_new();
if (ctx == nullptr)
return 1;
uint32_t nonse = 0; uint32_t nonse = 0;
@@ -3655,7 +3659,7 @@ int SecureMsgSetHash(unsigned char *pHeader, unsigned char *pPayload, uint32_t n
if (sha256Hash[31] == 0 if (sha256Hash[31] == 0
&& sha256Hash[30] == 0 && sha256Hash[30] == 0
&& (~(sha256Hash[29]) & ((1<<0) || (1<<1) || (1<<2)) )) && (sha256Hash[29] & 1U) == 0)
// && sha256Hash[29] == 0) // && sha256Hash[29] == 0)
{ {
found = true; found = true;
@@ -3794,7 +3798,10 @@ int SecureMsgEncrypt(SecureMessage& smsg, std::string& addressFrom, std::string&
// -- Generate 16 random bytes as IV. // -- Generate 16 random bytes as IV.
RandAddSeedPerfmon(); 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. // -- 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; unsigned int nBytes = 32;
HMAC_CTX *ctx = HMAC_CTX_new(); 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, (unsigned char*) &smsg.timestamp, sizeof(smsg.timestamp))
|| !HMAC_Update(ctx, &vchCiphertext[0], vchCiphertext.size()) || !HMAC_Update(ctx, &vchCiphertext[0], vchCiphertext.size())
|| !HMAC_Final(ctx, smsg.mac, &nBytes) || !HMAC_Final(ctx, smsg.mac, &nBytes)
|| nBytes != 32) || nBytes != 32)
fHmacOk = false; fHmacOk = false;
HMAC_CTX_free(ctx); if (ctx != nullptr)
HMAC_CTX_free(ctx);
if (!fHmacOk) if (!fHmacOk)
{ {
@@ -4269,14 +4278,16 @@ int SecureMsgDecrypt(bool fTestOnly, std::string& address, unsigned char *pHeade
unsigned int nBytes = 32; unsigned int nBytes = 32;
HMAC_CTX *ctx = HMAC_CTX_new(); 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, (unsigned char*) &psmsg->timestamp, sizeof(psmsg->timestamp))
|| !HMAC_Update(ctx, pPayload, nPayload) || !HMAC_Update(ctx, pPayload, nPayload)
|| !HMAC_Final(ctx, MAC, &nBytes) || !HMAC_Final(ctx, MAC, &nBytes)
|| nBytes != 32) || nBytes != 32)
fHmacOk = false; fHmacOk = false;
HMAC_CTX_free(ctx); if (ctx != nullptr)
HMAC_CTX_free(ctx);
if (!fHmacOk) 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); 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; return false;
} }
fflush(g_fetch.fpDest); fflush(g_fetch.fpDest);
fseek(g_fetch.fpDest, 0, SEEK_SET);
SHA256_CTX ctx; std::error_code ec;
SHA256_Init(&ctx); const int64_t total = static_cast<int64_t>(fs::file_size(g_fetch.destPath, ec));
if (ec || total != g_fetch.totalSize) {
std::vector<unsigned char> buf(64 * 1024); strErr = strprintf("size mismatch: have %" PRId64 " want %" PRId64,
int64_t total = 0; ec ? -1 : total, g_fetch.totalSize);
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);
return false; return false;
} }
uint256 actual; uint256 actual;
SHA256_Final((unsigned char*)&actual, &ctx); if (!ComputeSnapshotFileHash(g_fetch.destPath, actual, strErr))
return false;
if (actual != g_fetch.expectedFileHash) { if (actual != g_fetch.expectedFileHash) {
strErr = "snapshot file hash mismatch"; strErr = "snapshot file hash mismatch";
return false; return false;
@@ -243,6 +235,46 @@ static void ReissueStalledChunks(int64_t timeoutMicros)
} // namespace } // 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 // Public: TryFetchSnapshot
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -424,24 +456,12 @@ static bool ScanLocalSnapshot()
int64_t sz = (int64_t)fs::file_size(g_localPath, ec); int64_t sz = (int64_t)fs::file_size(g_localPath, ec);
if (ec) return false; 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; 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) { if (actual != expectedHash) {
printf("SnapshotNet: local utxo-snapshot.bin hash mismatch — not advertising\n"); printf("SnapshotNet: local utxo-snapshot.bin hash mismatch — not advertising\n");
return false; return false;
+6
View File
@@ -51,6 +51,12 @@ bool TryFetchSnapshot(const std::filesystem::path& dataDir,
int timeoutSec, int timeoutSec,
std::string& strError); 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. // Server-side message dispatch. Called from main.cpp ProcessMessage.
// Returns true if strCommand was a snapshot-protocol message (handled or // Returns true if strCommand was a snapshot-protocol message (handled or
// rejected for malformed input). // rejected for malformed input).
+1 -3
View File
@@ -65,8 +65,7 @@ public:
if (!lock.owns_lock()) if (!lock.owns_lock())
{ {
EnterCritical(pszName, pszFile, nLine, (void*)(lock.mutex()), true); EnterCritical(pszName, pszFile, nLine, (void*)(lock.mutex()), true);
lock.try_lock(); if (!lock.try_lock())
if (!lock.owns_lock())
LeaveCritical(); LeaveCritical();
} }
return lock.owns_lock(); return lock.owns_lock();
@@ -204,4 +203,3 @@ public:
} }
}; };
#endif #endif
+45 -96
View File
@@ -1,125 +1,74 @@
#include <boost/test/unit_test.hpp> #include <boost/test/unit_test.hpp>
#include <limits> #include <limits>
#include <string>
#include "bignum.h" #include "bignum.h"
#include "util.h" #include "util.h"
BOOST_AUTO_TEST_SUITE(bignum_tests) 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__) #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)) #define NOINLINE __attribute__((noinline))
#elif defined(_MSC_VER) #elif defined(_MSC_VER)
#define NOINLINE __declspec(noinline) #define NOINLINE __declspec(noinline)
#else #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 #define NOINLINE
#endif #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) NOINLINE void mysetint64(CBigNum& num, int64_t n)
{ {
num.setint64(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) 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(),
};
{ for (int64_t value : values) {
n = 0; CBigNum num(value);
CBigNum num(n); BOOST_CHECK_EQUAL(num.ToString(), std::to_string(value));
BOOST_CHECK(num.ToString() == "0");
num.setulong(0); num.setulong(0);
BOOST_CHECK(num.ToString() == "0"); BOOST_CHECK_EQUAL(num.ToString(), "0");
mysetint64(num, n); mysetint64(num, value);
BOOST_CHECK(num.ToString() == "0"); BOOST_CHECK_EQUAL(num.ToString(), std::to_string(value));
}
{
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_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() 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; enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT;
void StartShutdown() { /* no-op for tests */ } void StartShutdown() { /* no-op for tests */ }
void MarkShutdownFailure() { /* no-op for tests */ }
namespace { namespace {
+1
View File
@@ -125,6 +125,7 @@ bool fUseFastIndex = false;
enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT; enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT;
void StartShutdown() { /* no-op */ } void StartShutdown() { /* no-op */ }
void MarkShutdownFailure() { /* no-op for tests */ }
namespace { namespace {
+17
View File
@@ -358,6 +358,23 @@ BOOST_AUTO_TEST_CASE(pos_validation_skip_is_only_historical_fast_path)
nAssumeValidThreshold = oldAssumeValid; 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) ────────────────────────────────────────── // ─── Orphan block cap (P1 — DoS) ──────────────────────────────────────────
// The cap on stored orphan blocks prevents an attacker from filling // The cap on stored orphan blocks prevents an attacker from filling
// memory with garbage. If too low, legitimate orphans are dropped. If // 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; enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT;
void StartShutdown() { /* no-op for tests */ } void StartShutdown() { /* no-op for tests */ }
void MarkShutdownFailure() { /* no-op for tests */ }
namespace { namespace {
@@ -100,19 +101,9 @@ struct TmpDataDir
// Compute SHA-256 of a file's bytes. // Compute SHA-256 of a file's bytes.
uint256 Sha256OfFile(const fs::path& p) 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; uint256 out;
SHA256_Final(reinterpret_cast<unsigned char*>(&out), &ctx); std::string error;
BOOST_REQUIRE_MESSAGE(SnapshotNet::ComputeSnapshotFileHash(p, out, error), error);
return out; return out;
} }
@@ -121,8 +112,16 @@ uint256 Sha256OfBytes(const std::vector<unsigned char>& bytes)
SHA256_CTX ctx; SHA256_CTX ctx;
SHA256_Init(&ctx); SHA256_Init(&ctx);
SHA256_Update(&ctx, bytes.data(), bytes.size()); 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; uint256 out;
SHA256_Final(reinterpret_cast<unsigned char*>(&out), &ctx); out.SetHex(digestHex);
return out; return out;
} }
@@ -177,6 +176,17 @@ BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_SUITE(snapshotnet_hash) 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) BOOST_AUTO_TEST_CASE(file_hash_matches_inline_sha256)
{ {
// Synthesize a payload, hash it via stdlib openssl directly, then hash // Synthesize a payload, hash it via stdlib openssl directly, then hash
+1
View File
@@ -69,3 +69,4 @@ void StartShutdown()
exit(0); 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*f"));
BOOST_CHECK(!WildcardMatch("abcdef", "a*x")); BOOST_CHECK(!WildcardMatch("abcdef", "a*x"));
BOOST_CHECK(WildcardMatch("", "*")); 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) 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_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 // AbandonTransaction tests
// //
+26 -8
View File
@@ -19,7 +19,7 @@
// Connection parameters (highest precedence first): // Connection parameters (highest precedence first):
// 1. Command line flags: -rpcuser/-rpcpassword/-rpcconnect/-rpcport // 1. Command line flags: -rpcuser/-rpcpassword/-rpcconnect/-rpcport
// 2. triangles.conf in the data directory (or -conf=<path>) // 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: // Usage:
// triangles-cli help List commands (delegates to daemon) // triangles-cli help List commands (delegates to daemon)
@@ -70,6 +70,7 @@
#define TRI_CLI_CLOSE_SOCKET(s) closesocket(s) #define TRI_CLI_CLOSE_SOCKET(s) closesocket(s)
#else #else
#include <sys/types.h> #include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <netinet/in.h> #include <netinet/in.h>
#include <arpa/inet.h> #include <arpa/inet.h>
@@ -108,10 +109,20 @@ static bool GetBoolArg(const string& key, bool def)
return (v != "0" && v != "false" && v != "no"); 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); ifstream f(path);
if (!f.good()) return; if (!f.good()) return true;
string line; string line;
while (getline(f, line)) { while (getline(f, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back(); if (!line.empty() && line.back() == '\r') line.pop_back();
@@ -141,6 +152,7 @@ static void ReadConfigFile(const string& path)
mapMultiArgs[dashKey].push_back(value); mapMultiArgs[dashKey].push_back(value);
} }
} }
return true;
} }
static fs::path GetDefaultDataDir() static fs::path GetDefaultDataDir()
@@ -231,7 +243,7 @@ static void ParseCommandLine(int argc, char* const argv[])
struct RPCConn { struct RPCConn {
string host = "127.0.0.1"; string host = "127.0.0.1";
string port = "19111"; string port = "19112";
string user; string user;
string pass; string pass;
}; };
@@ -244,11 +256,17 @@ static int AppInitRPCConn(RPCConn& conn)
{ {
std::ifstream f(confPath); std::ifstream f(confPath);
confExisted = f.good(); 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); 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.host = GetArg("-rpcconnect", "127.0.0.1");
conn.user = GetArg("-rpcuser", ""); conn.user = GetArg("-rpcuser", "");
conn.pass = GetArg("-rpcpassword", ""); conn.pass = GetArg("-rpcpassword", "");
@@ -595,9 +613,9 @@ static int CommandLineHelp(ostream& out)
<< "Options:\n" << "Options:\n"
<< " -conf=<file> Specify configuration file (default: triangles.conf)\n" << " -conf=<file> Specify configuration file (default: triangles.conf)\n"
<< " -datadir=<dir> Specify data directory\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" << " -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" << " -rpcuser=<user> Username for JSON-RPC connections\n"
<< " -rpcpassword=<pw> Password for JSON-RPC connections\n" << " -rpcpassword=<pw> Password for JSON-RPC connections\n"
<< " -stdin Read extra params from standard input, one per line\n" << " -stdin Read extra params from standard input, one per line\n"
+132 -59
View File
@@ -17,10 +17,12 @@
#undef printf #undef printf
#include "rpc_httpsocket.h" // raw-socket HTTP transport (replaces Boost.Asio) #include "rpc_httpsocket.h" // raw-socket HTTP transport (replaces Boost.Asio)
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <memory> #include <memory>
#include <list> #include <list>
#include <cerrno>
#include <limits>
#ifndef WIN32 #ifndef WIN32
#include <sys/select.h> #include <sys/select.h>
@@ -34,13 +36,26 @@ namespace fs = std::filesystem;
void ThreadRPCServer2(void* parg); void ThreadRPCServer2(void* parg);
static std::string strRPCUserColonPass; static std::string strRPCUserColonPass;
const Object emptyobj; const Object emptyobj;
CNotificationQueue* pNotificationQueue = nullptr; 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() static inline unsigned short GetDefaultRPCPort()
{ {
@@ -316,11 +331,8 @@ static const CRPCCommand vRPCCommands[] =
{ "sendrawtransaction", &sendrawtransaction, false, false }, { "sendrawtransaction", &sendrawtransaction, false, false },
{ "getcheckpoint", &getcheckpoint, true, false }, { "getcheckpoint", &getcheckpoint, true, false },
{ "gencheckpoints", &gencheckpoints, true, false }, { "gencheckpoints", &gencheckpoints, true, false },
{ "publishcheckpoint", &publishcheckpoint, true, false }, { "publishcheckpoint", &publishcheckpoint, true, false },
{ "settrustedv2snapshotpublisher", &settrustedv2snapshotpublisher, false, false }, { "getchaintips", &getchaintips, true, false },
{ "gettrustedv2snapshotpublisher", &gettrustedv2snapshotpublisher, false, false },
{ "unsettrustedv2snapshotpublisher", &unsettrustedv2snapshotpublisher, false, false },
{ "getchaintips", &getchaintips, true, false },
{ "invalidateblock", &invalidateblock, false, false }, { "invalidateblock", &invalidateblock, false, false },
{ "reconsiderblock", &reconsiderblock, false, false }, { "reconsiderblock", &reconsiderblock, false, false },
{ "recalculatesupply", &recalculatesupply, false, false }, { "recalculatesupply", &recalculatesupply, false, false },
@@ -455,11 +467,27 @@ static string HTTPReply(int nStatus, const string& strMsg, bool keepalive)
strMsg.c_str()); strMsg.c_str());
} }
int ReadHTTPStatus(std::basic_istream<char>& stream, int &proto, static bool ReadHTTPLine(std::basic_istream<char>& stream, std::string& line,
string& strMethodHTTP, string& strURI) size_t maxLength)
{ {
string str; line.clear();
getline(stream, str); 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 // Trim trailing \r
if (!str.empty() && str[str.size()-1] == '\r') if (!str.empty() && str[str.size()-1] == '\r')
str.resize(str.size()-1); str.resize(str.size()-1);
@@ -482,27 +510,45 @@ int ReadHTTPStatus(std::basic_istream<char>& stream, int &proto,
return atoi(vWords[1].c_str()); return atoi(vWords[1].c_str());
} }
int ReadHTTPHeader(std::basic_istream<char>& stream, map<string, string>& mapHeadersRet) int ReadHTTPHeader(std::basic_istream<char>& stream, map<string, string>& mapHeadersRet)
{ {
int nLen = 0; int nLen = 0;
while (true) size_t totalHeaderBytes = 0;
{ while (true)
string str; {
std::getline(stream, str); string str;
if (str.empty() || str == "\r") if (!ReadHTTPLine(stream, str, 8192))
break; return -1;
totalHeaderBytes += str.size() + 1;
if (totalHeaderBytes > 64 * 1024)
return -1;
if (str.empty() || str == "\r")
break;
string::size_type nColon = str.find(":"); string::size_type nColon = str.find(":");
if (nColon != string::npos) if (nColon != string::npos)
{ {
string strHeader = str.substr(0, nColon); string strHeader = str.substr(0, nColon);
strHeader = TrimString(strHeader); strHeader = TrimString(strHeader);
strHeader = ToLower(strHeader); strHeader = ToLower(strHeader);
string strValue = str.substr(nColon+1); string strValue = str.substr(nColon+1);
strValue = TrimString(strValue); strValue = TrimString(strValue);
mapHeadersRet[strHeader] = strValue; if (strHeader == "transfer-encoding" ||
if (strHeader == "content-length") (strHeader == "content-length" && mapHeadersRet.count(strHeader) != 0)) {
nLen = atoi(strValue.c_str()); 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; return nLen;
} }
@@ -702,8 +748,16 @@ void ThreadRPCServer2(void* parg)
if ((mapArgs["-rpcpassword"] == "") || if ((mapArgs["-rpcpassword"] == "") ||
(mapArgs["-rpcuser"] == mapArgs["-rpcpassword"])) (mapArgs["-rpcuser"] == mapArgs["-rpcpassword"]))
{ {
unsigned char rand_pwd[32]; unsigned char rand_pwd[32];
RAND_bytes(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"; string strWhatAmI = "To use trianglesd";
if (mapArgs.count("-server")) if (mapArgs.count("-server"))
strWhatAmI = strprintf(_("To use the %s option"), "\"-server\""); strWhatAmI = strprintf(_("To use the %s option"), "\"-server\"");
@@ -720,8 +774,9 @@ void ThreadRPCServer2(void* parg)
strWhatAmI.c_str(), strWhatAmI.c_str(),
GetConfigFile().string().c_str(), GetConfigFile().string().c_str(),
EncodeBase58(&rand_pwd[0],&rand_pwd[0]+32).c_str()), EncodeBase58(&rand_pwd[0],&rand_pwd[0]+32).c_str()),
_("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL); _("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL);
StartShutdown(); MarkShutdownFailure();
StartShutdown();
return; return;
} }
@@ -732,23 +787,23 @@ void ThreadRPCServer2(void* parg)
"or Tor.\n"); "or Tor.\n");
} }
// Bind the loopback interface(s) unless the operator explicitly opened the const int nPort = (int)GetArg("-rpcport", GetDefaultRPCPort());
// RPC port to other hosts with -rpcallowip. const std::string rpcBind = GetArg(
const bool loopbackOnly = !mapArgs.count("-rpcallowip"); std::string_view{"-rpcbind"}, std::string_view{""});
const int nPort = (int)GetArg("-rpcport", GetDefaultRPCPort());
std::string strBindError;
std::string strBindError; std::vector<SOCKET> vListen = BindRPCSockets(nPort, rpcBind, strBindError);
std::vector<SOCKET> vListen = BindRPCSockets(nPort, loopbackOnly, strBindError);
if (vListen.empty()) { if (vListen.empty()) {
uiInterface.ThreadSafeMessageBox( uiInterface.ThreadSafeMessageBox(
strprintf(_("An error occurred while setting up the RPC port %d for listening: %s"), strprintf(_("An error occurred while setting up the RPC port %d for listening: %s"),
nPort, strBindError.c_str()), nPort, strBindError.c_str()),
_("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL); _("Error"), CClientUIInterface::OK | CClientUIInterface::MODAL);
StartShutdown(); MarkShutdownFailure();
StartShutdown();
return; return;
} }
printf("RPC server listening on port %d (%s)\n", nPort, printf("RPC server listening on port %d (%s)\n", nPort,
loopbackOnly ? "loopback only" : "all interfaces"); rpcBind.empty() ? "loopback only" : rpcBind.c_str());
// Accept loop. select() with a short timeout keeps the listener responsive // Accept loop. select() with a short timeout keeps the listener responsive
// to fShutdown. Each accepted connection is handed to its own handler thread // to fShutdown. Each accepted connection is handed to its own handler thread
@@ -780,10 +835,19 @@ void ThreadRPCServer2(void* parg)
struct sockaddr_storage ss; struct sockaddr_storage ss;
socklen_t len = sizeof(ss); socklen_t len = sizeof(ss);
SOCKET hConn = accept(s, (struct sockaddr*)&ss, &len); SOCKET hConn = accept(s, (struct sockaddr*)&ss, &len);
if (hConn == INVALID_SOCKET) { if (hConn == INVALID_SOCKET) {
printf("RPC accept() failed\n"); printf("RPC accept() failed\n");
continue; 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); const std::string strPeer = SockaddrToString((struct sockaddr*)&ss, len);
@@ -979,7 +1043,13 @@ void ThreadRPCServer3(void* parg)
map<string, string> mapHeaders; map<string, string> mapHeaders;
string strRequest; 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 // Handle REST API requests
string strHTTPMethod = mapHeaders.count("_method") ? mapHeaders["_method"] : "POST"; 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 json_spirit::Value CRPCTable::execute(const std::string &strMethod, const json_spirit::Array &params) const
{ {
// Find method if (!RPCMethodAllowed(strMethod))
throw JSONRPCError(RPC_METHOD_NOT_FOUND, "Method not found");
// Find method
const CRPCCommand *pcmd = tableRPC[strMethod]; const CRPCCommand *pcmd = tableRPC[strMethod];
if (!pcmd) if (!pcmd)
throw JSONRPCError(RPC_METHOD_NOT_FOUND, "Method not found"); 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 getcheckpoint(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value gencheckpoints(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 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 getchaintips(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value invalidateblock(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); 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 #endif
+45 -30
View File
@@ -33,6 +33,8 @@
#ifndef WIN32 #ifndef WIN32
#include <execinfo.h> #include <execinfo.h>
#include <sys/stat.h>
#include <unistd.h>
#endif #endif
#include "util.h" #include "util.h"
@@ -174,9 +176,10 @@ uint64_t GetRand(uint64_t nMax)
// to give every possible output value an equal possibility // to give every possible output value an equal possibility
uint64_t nRange = (std::numeric_limits<uint64_t>::max() / nMax) * nMax; uint64_t nRange = (std::numeric_limits<uint64_t>::max() / nMax) * nMax;
uint64_t nRand = 0; uint64_t nRand = 0;
do do {
RAND_bytes((unsigned char*)&nRand, sizeof(nRand)); if (RAND_bytes(reinterpret_cast<unsigned char*>(&nRand), sizeof(nRand)) != 1)
while (nRand >= nRange); throw std::runtime_error("OpenSSL CSPRNG failure in GetRand");
} while (nRand >= nRange);
return (nRand % nMax); return (nRand % nMax);
} }
@@ -188,7 +191,8 @@ int GetRandInt(int nMax)
uint256 GetRandHash() uint256 GetRandHash()
{ {
uint256 hash; 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; return hash;
} }
@@ -616,7 +620,9 @@ bool SoftSetBoolArg(std::string_view strArg, bool fValue)
bool WildcardMatch(std::string_view str, std::string_view mask) 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) bool WildcardMatch(const char* psz, const char* mask)
{ {
while (true) const char* star = nullptr;
{ const char* retry = nullptr;
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++;
}
}
bool WildcardMatch(const string& str, const string& mask) while (*psz != '\0') {
{ if (*mask == '?' || *mask == *psz) {
return WildcardMatch(str.c_str(), mask.c_str()); ++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]) // - Section headers ([section])
// If any of those become needed, the actual conf syntax in // If any of those become needed, the actual conf syntax in
// contrib/triangles.conf.example should be extended first. // 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()) if (!streamConfig.good())
return; // No triangles.conf file is OK return; // No triangles.conf file is OK
+425 -153
View File
@@ -13,12 +13,15 @@
#include "kernel.h" #include "kernel.h"
#include "coincontrol.h" #include "coincontrol.h"
#include "addressindex.h" #include "addressindex.h"
#include "init.h"
#include "util.h" #include "util.h"
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <algorithm> #include <algorithm>
#include <limits>
#include <random> #include <random>
#include <deque> #include <deque>
#include <openssl/crypto.h>
using namespace std; using namespace std;
extern unsigned int nStakeMaxAge; extern unsigned int nStakeMaxAge;
@@ -26,6 +29,29 @@ extern unsigned int nStakeMaxAge;
unsigned int nStakeSplitAge = 1 * 3 * 60 * 60; unsigned int nStakeSplitAge = 1 * 3 * 60 * 60;
int64_t nStakeCombineThreshold = 20 * COIN; 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 // mapWallet
@@ -132,32 +158,80 @@ CPubKey CWallet::GenerateNewKey()
RandAddSeedPerfmon(); RandAddSeedPerfmon();
CKey key; CKey key;
bool fUsedHD = false; bool fUsedHD = false;
if (fHDEnabled && !hdMnemonic.empty()) { if (fHDEnabled) {
if (DeriveHDKey(nHDChainIndex, key)) { fUsedHD = true; fCompressed = true; } 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) if (!fUsedHD)
key.MakeNewKey(fCompressed); key.MakeNewKey(fCompressed);
// Compressed public keys were introduced in version 0.6.0 // Compressed public keys were introduced in version 0.6.0
if (fCompressed) if (fCompressed && !SetMinVersion(WalletFeature::ComprPubKey))
SetMinVersion(WalletFeature::ComprPubKey); throw std::runtime_error("CWallet::GenerateNewKey() : wallet version update failed");
CPubKey pubkey = key.GetPubKey(); CPubKey pubkey = key.GetPubKey();
// Create new metadata const int64_t nCreationTime = GetTime();
int64_t nCreationTime = GetTime(); const CKeyMetadata metadata(nCreationTime);
mapKeyMetadata[pubkey.GetID()] = CKeyMetadata(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) if (!nTimeFirstKey || nCreationTime < nTimeFirstKey)
nTimeFirstKey = nCreationTime; nTimeFirstKey = nCreationTime;
if (fUsedHD)
if (!AddKey(key)) ++nHDChainIndex;
throw std::runtime_error("CWallet::GenerateNewKey() : AddKey failed"); return pubkey;
if (fUsedHD) {
nHDChainIndex++;
if (fFileBacked)
CWalletDB(strWalletFile).WriteHDChain(nHDChainIndex);
}
return key.GetPubKey();
} }
bool CWallet::AddKey(const CKey& key) bool CWallet::AddKey(const CKey& key)
@@ -215,22 +289,17 @@ bool fWalletUnlockStakingOnly = false;
bool CWallet::Lock() bool CWallet::Lock()
{ {
if (IsCrypted()) {
CleanseWalletString(hdMnemonic);
CleanseWalletString(hdPassphrase);
}
if (IsLocked()) if (IsLocked())
return true; return true;
if (fDebug) if (fDebug)
printf("Locking wallet.\n"); 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(); return LockKeyStore();
}; };
@@ -242,38 +311,66 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase)
CCrypter crypter; CCrypter crypter;
CKeyingMaterial vMasterKey; CKeyingMaterial vMasterKey;
bool unlocked = false;
{ {
LOCK(cs_wallet); LOCK(cs_wallet);
for (const MasterKeyMap::value_type& pMasterKey : mapMasterKeys) for (const MasterKeyMap::value_type& pMasterKey : mapMasterKeys)
{ {
if(!crypter.SetKeyFromPassphrase(strWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod)) if(!crypter.SetKeyFromPassphrase(strWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod))
return false; continue;
if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, vMasterKey)) if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, vMasterKey))
return false; continue;
if (CCryptoKeyStore::Unlock(vMasterKey)) { if (CCryptoKeyStore::Unlock(vMasterKey)) {
if (fHDEnabled && hdMnemonic.empty() && !vchCryptedHDMnemonic.empty()) { if (fHDEnabled) {
if (vchCryptedHDMnemonic.empty()) {
LockKeyStore();
return false;
}
CSecret sec; CSecret sec;
if (DecryptSecret(vMasterKey, vchCryptedHDMnemonic, hdMnemonicIV, sec)) if (!DecryptSecret(vMasterKey, vchCryptedHDMnemonic, hdMnemonicIV, sec)) {
hdMnemonic.assign(sec.begin(), sec.end()); 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()) { unlocked = true;
CSecret psec; break;
if (DecryptSecret(vMasterKey, vchCryptedHDPassphrase, hdPassphraseIV, psec))
hdPassphrase.assign(psec.begin(), psec.end());
}
return true;
} }
} }
SecureMsgWalletUnlocked();
return true;
} }
return false; if (!unlocked)
return false;
SecureMsgWalletUnlocked();
return true;
} }
bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, const SecureString& strNewWalletPassphrase) bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, const SecureString& strNewWalletPassphrase)
{ {
bool fWasLocked = IsLocked(); const bool fWasLocked = IsLocked();
bool changed = false;
{ {
LOCK(cs_wallet); LOCK(cs_wallet);
@@ -284,39 +381,73 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase,
for (MasterKeyMap::value_type& pMasterKey : mapMasterKeys) for (MasterKeyMap::value_type& pMasterKey : mapMasterKeys)
{ {
if(!crypter.SetKeyFromPassphrase(strOldWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod)) if(!crypter.SetKeyFromPassphrase(strOldWalletPassphrase, pMasterKey.second.vchSalt, pMasterKey.second.nDeriveIterations, pMasterKey.second.nDerivationMethod))
return false; continue;
if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, vMasterKey)) if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, vMasterKey))
return false; continue;
if (CCryptoKeyStore::Unlock(vMasterKey)) if (CCryptoKeyStore::Unlock(vMasterKey))
{ {
CMasterKey updatedMasterKey = pMasterKey.second;
int64_t nStartTime = GetTimeMillis(); 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); 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(); 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); 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) if (updatedMasterKey.nDeriveIterations < 25000)
pMasterKey.second.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)) if (!crypter.SetKeyFromPassphrase(strNewWalletPassphrase,
return false; updatedMasterKey.vchSalt,
if (!crypter.Encrypt(vMasterKey, pMasterKey.second.vchCryptedKey)) updatedMasterKey.nDeriveIterations,
return false; updatedMasterKey.nDerivationMethod))
CWalletDB(strWalletFile).WriteMasterKey(pMasterKey.first, pMasterKey.second); break;
if (fWasLocked) if (!crypter.Encrypt(vMasterKey, updatedMasterKey.vchCryptedKey))
Lock(); break;
return true;
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) 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) if (fExplicit && static_cast<int>(nVersion) > nWalletMaxVersion)
nVersion = WalletFeature::Latest; nVersion = WalletFeature::Latest;
nWalletVersion = static_cast<int>(nVersion); const int newWalletVersion = static_cast<int>(nVersion);
if (static_cast<int>(nVersion) > nWalletMaxVersion)
nWalletMaxVersion = static_cast<int>(nVersion);
if (fFileBacked) if (fFileBacked)
{ {
CWalletDB* pwalletdb = pwalletdbIn ? pwalletdbIn : new CWalletDB(strWalletFile); std::unique_ptr<CWalletDB> ownedWalletDB;
if (nWalletVersion >= 40000) 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. // Versions prior to 0.4.0 did not support the "minversion" record.
// Use a CCorruptAddress to make them crash instead. // Use a CCorruptAddress to make them crash instead.
CCorruptAddress corruptAddress; 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; return true;
} }
@@ -385,27 +533,38 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
{ {
if (IsCrypted()) if (IsCrypted())
return false; return false;
if (fHDEnabled && hdMnemonic.empty())
return false;
if (nMasterKeyMaxID == std::numeric_limits<unsigned int>::max())
return false;
CKeyingMaterial vMasterKey; CKeyingMaterial vMasterKey;
RandAddSeedPerfmon(); RandAddSeedPerfmon();
vMasterKey.resize(WALLET_CRYPTO_KEY_SIZE); 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); CMasterKey kMasterKey(nDerivationMethodIndex);
RandAddSeedPerfmon(); RandAddSeedPerfmon();
kMasterKey.vchSalt.resize(WALLET_CRYPTO_SALT_SIZE); 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; CCrypter crypter;
int64_t nStartTime = GetTimeMillis(); 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); int64_t nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime);
kMasterKey.nDeriveIterations = 2500000 / ((double)nElapsed); kMasterKey.nDeriveIterations = 2500000 / ((double)nElapsed);
nStartTime = GetTimeMillis(); 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); nElapsed = std::max((int64_t)1, GetTimeMillis() - nStartTime);
kMasterKey.nDeriveIterations = (kMasterKey.nDeriveIterations + kMasterKey.nDeriveIterations * 100 / ((double)nElapsed)) / 2; kMasterKey.nDeriveIterations = (kMasterKey.nDeriveIterations + kMasterKey.nDeriveIterations * 100 / ((double)nElapsed)) / 2;
@@ -421,51 +580,109 @@ bool CWallet::EncryptWallet(const SecureString& strWalletPassphrase)
{ {
LOCK(cs_wallet); 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) if (fFileBacked)
{ {
std::unique_ptr<CWalletDB> dbEnc(new CWalletDB(strWalletFile)); std::unique_ptr<CWalletDB> dbEnc = std::make_unique<CWalletDB>(strWalletFile);
if (!dbEnc->TxnBegin()) if (!dbEnc->TxnBegin())
return false; 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(); dbEnc->TxnAbort();
nWalletVersion = oldWalletVersion;
nWalletMaxVersion = oldWalletMaxVersion;
return false; return false;
} }
if (!dbEnc->TxnCommit()) {
if (fHDEnabled && !hdMnemonic.empty()) { dbEnc->TxnAbort();
CSecret sec(hdMnemonic.begin(), hdMnemonic.end()); nWalletVersion = oldWalletVersion;
uint256 iv = GetRandHash(); nWalletMaxVersion = oldWalletMaxVersion;
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())
return false; 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(); Lock();
Unlock(strWalletPassphrase); if (!Unlock(strWalletPassphrase)) {
NewKeyPool(); 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(); Lock();
// Need to completely rewrite the wallet file; if we don't, bdb might keep if (fFileBacked && !RewriteWalletDatabase(strWalletFile)) {
// bits of the unencrypted private key in slack space in the database file. printf("EncryptWallet: secure wallet database rewrite failed; shutting down\n");
CDB::Rewrite(strWalletFile); MarkShutdownFailure();
StartShutdown();
return false;
}
} }
NotifyStatusChanged(this); NotifyStatusChanged(this);
@@ -793,7 +1010,7 @@ bool CWallet::AbandonTransaction(const uint256& hashTx)
// the inputs (vfSpent was tracked on the wtx) and resolves the conflict. // the inputs (vfSpent was tracked on the wtx) and resolves the conflict.
bool fErased = EraseFromWallet(hashTx); 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()); hashTx.ToString().c_str(), sDescendants.size());
return fErased; return fErased;
} }
@@ -2115,10 +2332,12 @@ bool CWallet::CreateCoinStake(const CKeyStore& keystore, unsigned int nBits, int
break; // unable to find corresponding public key break; // unable to find corresponding public key
} }
if (key.GetPubKey() != vchPubKey) if (key.GetPubKey() != vchPubKey)
{ {
if (fDebug && GetBoolArg("-printcoinstake")) if (fDebug && GetBoolArg("-printcoinstake")) {
printf("CreateCoinStake : invalid key for kernel type=%d\n", whichType); printf("CreateCoinStake : invalid key for kernel type=%d\n",
static_cast<int>(whichType));
}
break; // keys mismatch 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) if (GetWeight(block.GetBlockTime(), (int64_t)txNew.nTime) < nStakeSplitAge)
txNew.vout.push_back(CTxOut(0, scriptPubKeyOut)); //split stake txNew.vout.push_back(CTxOut(0, scriptPubKeyOut)); //split stake
if (fDebug && GetBoolArg("-printcoinstake")) 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; fKernelFound = true;
break; break;
} }
@@ -2351,7 +2570,7 @@ DBErrors CWallet::LoadWallet(bool& fFirstRunRet)
DBErrors nLoadWalletRet = CWalletDB(strWalletFile,"cr+").LoadWallet(this); DBErrors nLoadWalletRet = CWalletDB(strWalletFile,"cr+").LoadWallet(this);
if (nLoadWalletRet == DB_NEED_REWRITE) if (nLoadWalletRet == DB_NEED_REWRITE)
{ {
if (CDB::Rewrite(strWalletFile, "\x04pool")) if (RewriteWalletDatabase(strWalletFile, "\x04pool"))
{ {
setKeyPool.clear(); setKeyPool.clear();
// Note: can't top-up keypool here, because wallet is locked. // Note: can't top-up keypool here, because wallet is locked.
@@ -2478,9 +2697,13 @@ bool CWallet::NewKeyPool()
{ {
{ {
LOCK(cs_wallet); LOCK(cs_wallet);
CWalletDB walletdb(strWalletFile); std::unique_ptr<CWalletDB> walletdb;
for (int64_t nIndex : setKeyPool) if (fFileBacked)
walletdb.ErasePool(nIndex); walletdb = std::make_unique<CWalletDB>(strWalletFile);
for (int64_t nIndex : setKeyPool) {
if (walletdb && !walletdb->ErasePool(nIndex))
return false;
}
setKeyPool.clear(); setKeyPool.clear();
if (IsLocked()) if (IsLocked())
@@ -2490,7 +2713,9 @@ bool CWallet::NewKeyPool()
for (int i = 0; i < nKeys; i++) for (int i = 0; i < nKeys; i++)
{ {
int64_t nIndex = i+1; int64_t nIndex = i+1;
walletdb.WritePool(nIndex, CKeyPool(GenerateNewKey())); CKeyPool keypool(GenerateNewKey());
if (walletdb && !walletdb->WritePool(nIndex, keypool))
return false;
setKeyPool.insert(nIndex); setKeyPool.insert(nIndex);
} }
printf("CWallet::NewKeyPool wrote %"PRId64" new keys\n", nKeys); printf("CWallet::NewKeyPool wrote %"PRId64" new keys\n", nKeys);
@@ -2506,7 +2731,9 @@ bool CWallet::TopUpKeyPool(unsigned int nSize)
if (IsLocked()) if (IsLocked())
return false; return false;
CWalletDB walletdb(strWalletFile); std::unique_ptr<CWalletDB> walletdb;
if (fFileBacked)
walletdb = std::make_unique<CWalletDB>(strWalletFile);
// Top up key pool // Top up key pool
unsigned int nTargetSize; unsigned int nTargetSize;
@@ -2520,7 +2747,8 @@ bool CWallet::TopUpKeyPool(unsigned int nSize)
int64_t nEnd = 1; int64_t nEnd = 1;
if (!setKeyPool.empty()) if (!setKeyPool.empty())
nEnd = *(--setKeyPool.end()) + 1; 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"); throw runtime_error("TopUpKeyPool() : writing generated key failed");
setKeyPool.insert(nEnd); setKeyPool.insert(nEnd);
printf("keypool added key %"PRId64", size=%"PRIszu"\n", nEnd, setKeyPool.size()); 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 ---- // ---- HD wallet (BIP39/BIP32) implementation ----
bool CWallet::DeriveHDKey(int64_t index, CKey& keyOut) const 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; return false;
// If a BIP39 passphrase ("25th word") was set with the seed, it MUST be // If a BIP39 passphrase ("25th word") was set with the seed, it MUST be
// part of every derivation — otherwise restored wallets derive different // part of every derivation — otherwise restored wallets derive different
// addresses than the originals. Empty string = no passphrase (legacy). // addresses than the originals. Empty string = no passphrase (legacy).
unsigned char priv[32]; 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; return false;
CSecret secret(priv, priv + 32); CSecret secret(priv, priv + 32);
memset(priv, 0, sizeof(priv)); OPENSSL_cleanse(priv, sizeof(priv));
keyOut.SetSecret(secret, true); // HD keys are compressed keyOut.SetSecret(secret, true); // HD keys are compressed
return true; return true;
} }
@@ -2972,6 +3202,7 @@ bool CWallet::SetHDSeed(const std::string& mnemonicIn, const std::string& passph
{ {
LOCK(cs_wallet); LOCK(cs_wallet);
if (IsLocked()) { strError = "Wallet is locked; unlock it before setting an HD seed."; return false; } 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; std::string m = mnemonicIn;
if (m.empty()) { if (m.empty()) {
@@ -2983,45 +3214,86 @@ bool CWallet::SetHDSeed(const std::string& mnemonicIn, const std::string& passph
unsigned char priv[32]; unsigned char priv[32];
if (!hd::DeriveTriangles(m, passphrase, 0, 0, 0, priv)) { strError = "Key derivation failed."; return false; } 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; uint256 newMnemonicIV;
hdPassphrase = passphrase; uint256 newPassphraseIV;
fHDEnabled = true; std::vector<unsigned char> newCryptedMnemonic;
nHDChainIndex = 0; 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) { if (fFileBacked) {
CWalletDB wdb(strWalletFile); CWalletDB wdb(strWalletFile);
if (IsCrypted()) { if (!wdb.TxnBegin()) {
CSecret sec(m.begin(), m.end()); strError = "Failed to start wallet database transaction.";
uint256 iv = GetRandHash(); return false;
std::vector<unsigned char> cipher; }
if (!EncryptSecret(vMasterKey, sec, iv, cipher)) { strError = "Failed to encrypt seed."; return false; }
hdMnemonicIV = iv; vchCryptedHDMnemonic = cipher; bool wrote = false;
wdb.WriteHDCryptedMnemonic(iv, cipher); if (IsCrypted()) {
if (!passphrase.empty()) { wrote = wdb.WriteHDCryptedMnemonic(newMnemonicIV, newCryptedMnemonic) &&
CSecret psec(passphrase.begin(), passphrase.end()); (passphrase.empty()
uint256 piv = GetRandHash(); ? wdb.EraseHDPassphrase()
std::vector<unsigned char> pcipher; : wdb.WriteHDCryptedPassphrase(newPassphraseIV,
if (!EncryptSecret(vMasterKey, psec, piv, pcipher)) { strError = "Failed to encrypt passphrase."; return false; } newCryptedPassphrase));
hdPassphraseIV = piv; vchCryptedHDPassphrase = pcipher; } else {
wdb.WriteHDCryptedPassphrase(piv, pcipher); wrote = wdb.WriteHDMnemonic(m) &&
} else { (passphrase.empty()
vchCryptedHDPassphrase.clear(); ? wdb.EraseHDPassphrase()
wdb.EraseHDPassphrase(); // re-seed without passphrase: drop any old record : wdb.WriteHDPassphrase(passphrase));
} }
} else { wrote = wrote && wdb.WriteHDChain(0);
wdb.WriteHDMnemonic(m);
if (!passphrase.empty()) if (!wrote) {
wdb.WriteHDPassphrase(passphrase); wdb.TxnAbort();
else strError = "Failed to persist HD seed; wallet database transaction was rolled back.";
wdb.EraseHDPassphrase(); 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 // Replace any pre-existing (random) keypool with HD-derived keys so that
// getnewaddress immediately hands out deterministic m/44'/2222'/0'/0/i keys. // 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; mnemonicOut = m;
return true; 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 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; 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 // the current wallet version: clients below this version are not able to load the wallet
int nWalletVersion; int nWalletVersion;
@@ -104,9 +102,9 @@ public:
nWalletMaxVersion = static_cast<int>(WalletFeature::Base); nWalletMaxVersion = static_cast<int>(WalletFeature::Base);
fFileBacked = false; fFileBacked = false;
nMasterKeyMaxID = 0; nMasterKeyMaxID = 0;
pwalletdbEncryption = nullptr;
fHDEnabled = false; fHDEnabled = false;
nHDChainIndex = 0; nHDChainIndex = 0;
nTimeFirstKey = 0;
nOrderPosNext = 0; nOrderPosNext = 0;
nCachedStakeWeight = 0; nCachedStakeWeight = 0;
nCachedStakeWeightTime = 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 // Durability + integrity pragmas. FULL fsync on commit — a wallet must not
// lose a freshly-written key on power loss. // lose a freshly-written key on power loss.
if (!ExecOrError("PRAGMA synchronous = FULL;", strError)) return false; 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; if (!ExecOrError("PRAGMA foreign_keys = ON;", strError)) return false;
// Fail loudly instead of silently truncating an over-long blob. // Fail loudly instead of silently truncating an over-long blob.
if (!ExecOrError("PRAGMA cell_size_check = ON;", strError)) return false; 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); if (pDest) sqlite3_close(pDest);
return false; 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"); sqlite3_backup* bk = sqlite3_backup_init(pDest, "main", m_db, "main");
bool ok = false; bool ok = false;
+111 -6
View File
@@ -42,6 +42,20 @@ namespace fs = std::filesystem;
static uint64_t nAccountingEntryNumber = 0; static uint64_t nAccountingEntryNumber = 0;
extern bool fWalletUnlockStakingOnly; 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. // Auto-backup wallet before flush/rewrite operations.
// Copies wallet.dat to wallet.dat.auto.bak if the backup is older than the wallet. // 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)) { if (fs::exists(backupPath)) {
uintmax_t backupSize = fs::file_size(backupPath); uintmax_t backupSize = fs::file_size(backupPath);
if (backupSize == walletSize) if (backupSize == walletSize) {
if (!RestrictWalletFilePermissions(backupPath))
return false;
return true; return true;
}
} }
fs::copy_file(walletPath, backupPath, fs::copy_options::overwrite_existing); 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", printf("AutoBackupWallet: backed up wallet.dat (%llu bytes) to wallet.dat.auto.bak\n",
(unsigned long long)walletSize); (unsigned long long)walletSize);
return true; return true;
@@ -257,6 +278,10 @@ public:
unsigned int nKeyMeta; unsigned int nKeyMeta;
bool fIsEncrypted; bool fIsEncrypted;
bool fAnyUnordered; bool fAnyUnordered;
bool fHDPlainMnemonic;
bool fHDCryptedMnemonic;
bool fHDPlainPassphrase;
bool fHDCryptedPassphrase;
int nFileVersion; int nFileVersion;
std::vector<uint256> vWalletUpgrade; std::vector<uint256> vWalletUpgrade;
@@ -264,6 +289,10 @@ public:
nKeys = nCKeys = nKeyMeta = 0; nKeys = nCKeys = nKeyMeta = 0;
fIsEncrypted = false; fIsEncrypted = false;
fAnyUnordered = false; fAnyUnordered = false;
fHDPlainMnemonic = false;
fHDCryptedMnemonic = false;
fHDPlainPassphrase = false;
fHDCryptedPassphrase = false;
nFileVersion = 0; nFileVersion = 0;
} }
}; };
@@ -380,6 +409,7 @@ static bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssVa
return false; return false;
} }
pwallet->mapMasterKeys[nID] = kMasterKey; pwallet->mapMasterKeys[nID] = kMasterKey;
wss.fIsEncrypted = true;
if (pwallet->nMasterKeyMaxID < nID) if (pwallet->nMasterKeyMaxID < nID)
pwallet->nMasterKeyMaxID = nID; pwallet->nMasterKeyMaxID = nID;
} else if (strType == "ckey") { } else if (strType == "ckey") {
@@ -417,18 +447,22 @@ static bool ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssVa
} else if (strType == "hdmnemonic") { } else if (strType == "hdmnemonic") {
std::string m; std::string m;
ssValue >> m; ssValue >> m;
wss.fHDPlainMnemonic = true;
pwallet->LoadHDMnemonic(m); pwallet->LoadHDMnemonic(m);
} else if (strType == "hdcmnemonic") { } else if (strType == "hdcmnemonic") {
std::pair<uint256, std::vector<unsigned char>> cm; std::pair<uint256, std::vector<unsigned char>> cm;
ssValue >> cm; ssValue >> cm;
wss.fHDCryptedMnemonic = true;
pwallet->LoadCryptedHDMnemonic(cm.first, cm.second); pwallet->LoadCryptedHDMnemonic(cm.first, cm.second);
} else if (strType == "hdpassphrase") { } else if (strType == "hdpassphrase") {
std::string p; std::string p;
ssValue >> p; ssValue >> p;
wss.fHDPlainPassphrase = true;
pwallet->LoadHDPassphrase(p); pwallet->LoadHDPassphrase(p);
} else if (strType == "hdcpassphrase") { } else if (strType == "hdcpassphrase") {
std::pair<uint256, std::vector<unsigned char>> cp; std::pair<uint256, std::vector<unsigned char>> cp;
ssValue >> cp; ssValue >> cp;
wss.fHDCryptedPassphrase = true;
pwallet->LoadCryptedHDPassphrase(cp.first, cp.second); pwallet->LoadCryptedHDPassphrase(cp.first, cp.second);
} else if (strType == "hdchain") { } else if (strType == "hdchain") {
int64_t n; int64_t n;
@@ -513,6 +547,35 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
if (fNoncriticalErrors && result == DB_LOAD_OK) if (fNoncriticalErrors && result == DB_LOAD_OK)
result = DB_NONCRITICAL_ERROR; 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) if (result != DB_LOAD_OK)
return result; return result;
@@ -617,18 +680,55 @@ bool BackupWallet(const CWallet& wallet, const std::string& strDest)
if (!wallet.fFileBacked) if (!wallet.fFileBacked)
return false; return false;
// For the SQLite backend, the database is a single file — copy directly // SQLite's online backup API takes a consistent snapshot while the daemon
// (after a checkpoint flush to fold any -wal into the main file). // is running; copying the live database file directly can produce a torn
// backup if a transaction commits during the copy.
if (ResolveWalletDbKind() == WalletDbKind::SQLite) { if (ResolveWalletDbKind() == WalletDbKind::SQLite) {
fs::path pathSrc = GetDataDir() / wallet.strWalletFile; fs::path pathSrc = GetDataDir() / wallet.strWalletFile;
fs::path pathDest(strDest); fs::path pathDest(strDest);
if (fs::is_directory(pathDest)) if (fs::is_directory(pathDest))
pathDest /= wallet.strWalletFile; pathDest /= wallet.strWalletFile;
std::error_code ec; 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) { 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()); pathDest.string().c_str(), ec.message().c_str());
fs::remove(pathTemp, ec);
return false; return false;
} }
printf("copied wallet.dat to %s\n", pathDest.string().c_str()); 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 { try {
fs::copy_file(pathSrc, pathDest, fs::copy_options::overwrite_existing); 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()); printf("copied wallet.dat to %s\n", pathDest.string().c_str());
return true; return true;
} catch (const fs::filesystem_error& e) { } catch (const fs::filesystem_error& e) {
@@ -665,4 +770,4 @@ bool BackupWallet(const CWallet& wallet, const std::string& strDest)
MilliSleep(100); MilliSleep(100);
} }
return false; return false;
} }
+25 -13
View File
@@ -123,8 +123,10 @@ public:
return false; return false;
if (fEraseUnencryptedKey) if (fEraseUnencryptedKey)
{ {
Erase(std::make_pair(std::string("key"), vchPubKey.Raw())); if (!Erase(std::make_pair(std::string("key"), vchPubKey.Raw())))
Erase(std::make_pair(std::string("wkey"), vchPubKey.Raw())); return false;
if (!Erase(std::make_pair(std::string("wkey"), vchPubKey.Raw())))
return false;
} }
return true; return true;
} }
@@ -166,13 +168,13 @@ public:
bool WriteHDMnemonic(const std::string& mnemonic) { bool WriteHDMnemonic(const std::string& mnemonic) {
nWalletDBUpdated++; nWalletDBUpdated++;
Erase(std::string("hdcmnemonic")); const bool erased = Erase(std::string("hdcmnemonic"));
return Write(std::string("hdmnemonic"), mnemonic); return erased && Write(std::string("hdmnemonic"), mnemonic);
} }
bool WriteHDCryptedMnemonic(const uint256& iv, const std::vector<unsigned char>& cipher) { bool WriteHDCryptedMnemonic(const uint256& iv, const std::vector<unsigned char>& cipher) {
nWalletDBUpdated++; nWalletDBUpdated++;
Erase(std::string("hdmnemonic")); const bool erased = Erase(std::string("hdmnemonic"));
return Write(std::string("hdcmnemonic"), std::make_pair(iv, cipher)); return erased && Write(std::string("hdcmnemonic"), std::make_pair(iv, cipher));
} }
bool WriteHDChain(int64_t nIndex) { bool WriteHDChain(int64_t nIndex) {
nWalletDBUpdated++; nWalletDBUpdated++;
@@ -183,19 +185,19 @@ public:
// means no passphrase (legacy wallets and the common case). // means no passphrase (legacy wallets and the common case).
bool WriteHDPassphrase(const std::string& passphrase) { bool WriteHDPassphrase(const std::string& passphrase) {
nWalletDBUpdated++; nWalletDBUpdated++;
Erase(std::string("hdcpassphrase")); const bool erased = Erase(std::string("hdcpassphrase"));
return Write(std::string("hdpassphrase"), passphrase); return erased && Write(std::string("hdpassphrase"), passphrase);
} }
bool WriteHDCryptedPassphrase(const uint256& iv, const std::vector<unsigned char>& cipher) { bool WriteHDCryptedPassphrase(const uint256& iv, const std::vector<unsigned char>& cipher) {
nWalletDBUpdated++; nWalletDBUpdated++;
Erase(std::string("hdpassphrase")); const bool erased = Erase(std::string("hdpassphrase"));
return Write(std::string("hdcpassphrase"), std::make_pair(iv, cipher)); return erased && Write(std::string("hdcpassphrase"), std::make_pair(iv, cipher));
} }
bool EraseHDPassphrase() { bool EraseHDPassphrase() {
nWalletDBUpdated++; nWalletDBUpdated++;
Erase(std::string("hdpassphrase")); const bool erasedPlain = Erase(std::string("hdpassphrase"));
Erase(std::string("hdcpassphrase")); const bool erasedCrypted = Erase(std::string("hdcpassphrase"));
return true; return erasedPlain && erasedCrypted;
} }
bool ReadPool(int64_t nPool, CKeyPool& keypool) bool ReadPool(int64_t nPool, CKeyPool& keypool)
@@ -251,6 +253,16 @@ public:
return Read(std::string("version"), nVersion); 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 ReadAccount(const std::string& strAccount, CAccount& account);
bool WriteAccount(const std::string& strAccount, const CAccount& account); bool WriteAccount(const std::string& strAccount, const CAccount& account);
private: private: