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