Compare commits

...

17 Commits

Author SHA1 Message Date
sami7777 b50eecc56f Fix build: nMisbehavior is protected
Build All Platforms / build-windows-qt (push) Waiting to run
Build All Platforms / build-windows-daemon (push) Waiting to run
Build All Platforms / build-linux-qt (push) Waiting to run
Build All Platforms / build-linux-daemon (push) Waiting to run
Build All Platforms / build-macos (push) Waiting to run
Build All Platforms / release (push) Blocked by required conditions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 21:09:48 -07:00
sami7777 8953173403 Add comprehensive IBD diagnostics (IBD-DIAG prefix)
Verbose logging at every critical sync pipeline stage:
- Version handler: whether getblocks was sent and why
- Inv handler: count of new vs already-known blocks
- Block handler: every block received (throttled), ProcessBlock failures
- ProcessBlock: CheckBlock failures with details
- SendMessages: stall detection with queue sizes
- Periodic status: height, peers, askfor queue, orphan count
- Getblocks handler: what range the seed is serving

All lines prefixed with IBD-DIAG for easy grep.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 21:05:11 -07:00
sami7777 2f307c195c Disable checkpoint message relay and processing
DNS2 was sending a stale sync checkpoint (block 2,186,940) to DNS3
on connect. ProcessSyncCheckpoint then called PushGetBlocks with the
checkpoint hash as the stop point, and AskFor'd block 2,186,940
directly — overriding the normal sequential getblocks chain. DNS3
would request a block it can't process (missing 2M predecessors)
instead of syncing from genesis.

Fix: ignore incoming checkpoint messages entirely (master key was
already removed in V5 fork, no new checkpoints possible). Also stop
relaying stored checkpoint messages to new peers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 20:56:40 -07:00
sami7777 8c5024a78a Fix anti-spam check: use chain tip as fallback, add NULL safety
Instead of skipping the anti-spam difficulty check during IBD, fix it
properly:
- Fall back to pindexBest when sync checkpoint is genesis (height 0)
- Add NULL safety for GetLastBlockIndex in both PoS and PoW cases
- PoS case: if no PoS block exists yet (below 9001), skip gracefully
  since AcceptBlock already rejects PoS below MODIFIER_INTERVAL_SWITCH

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 20:24:48 -07:00
sami7777 3f823e8583 Skip anti-spam difficulty check during IBD
The anti-spam check in ProcessBlock used GetLastSyncCheckpoint() which
pointed to genesis after our reset. When processing PoS blocks,
GetLastBlockIndex(genesis, true) returned NULL (no PoS blocks at genesis),
causing a crash or Misbehaving(100) which banned the seed node.

Fix: skip the entire anti-spam check during IBD - hardcoded checkpoints
already guarantee chain integrity. Also add NULL safety for the PoS
case after IBD completes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 20:19:39 -07:00
sami7777 a4bfc6012a Fix display version: update DISPLAY_VERSION to 5.2.0
version.h had a separate DISPLAY_VERSION set (5.1.7.0) used by
version.cpp for the user-visible version string. clientversion.h
was updated but version.h was not, causing binaries to report
v5.1.7.0 despite being built from v5.2.0 source.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 19:37:24 -07:00
sami7777 136d446157 Disable sync checkpoint system that blocks IBD
The sync checkpoint (hashSyncCheckpoint) was persisted in LevelDB pointing
to block 2,186,940. On startup it was loaded from DB, overriding any code
change to the initial value. CheckSync then rejected every block below
that height during IBD since they weren't in mapBlockIndex yet.

Three-pronged fix:
- CheckSync now always returns true (master key disabled, no new sync
  checkpoints will ever be broadcast)
- AcceptBlock no longer calls sync checkpoint enforcement
- LoadBlockIndex resets sync checkpoint to genesis if stored hash is
  not in the block index (prevents assert crash)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 18:32:11 -07:00
sami7777 1966f49ce2 Fix sync checkpoint blocking IBD, bump to v5.2.0
hashSyncCheckpoint was initialized to block 2,186,940 hash, causing
CheckSync to reject ALL blocks below that height during initial block
download (they aren't in mapBlockIndex yet when checked). Changed to
genesis hash so IBD can proceed from block 0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 18:09:38 -07:00
sami7777 a4da39f23c Update CI version to 5.1.9
Build All Platforms / build-windows-qt (push) Waiting to run
Build All Platforms / build-windows-daemon (push) Waiting to run
Build All Platforms / build-linux-qt (push) Waiting to run
Build All Platforms / build-linux-daemon (push) Waiting to run
Build All Platforms / build-macos (push) Waiting to run
Build All Platforms / release (push) Blocked by required conditions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 17:54:56 -07:00
sami7777 87bfc15712 Bump version to v5.1.9
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 17:48:24 -07:00
sami7777 6353e9d5fa v5.1.9: Assumevalid fast sync + unlimited network buffers
Major sync performance overhaul:

- Assumevalid: skip FetchInputs/ConnectInputs for blocks below
  checkpoint (2,186,940). Only write txindex entries. Eliminates
  millions of LevelDB reads during initial sync.
- Skip SyncWithWallets during IBD with automatic post-IBD wallet
  rescan from genesis and SecureMsg chain scan.
- Skip wallet best-chain locator update during IBD so restarts
  trigger proper rescan.
- Remove send/receive buffer limits (were 1MB/5MB, now unlimited).
  The 1MB send buffer was the root cause of ~180 block stalls -
  ProcessMessages stops reading when nSendSize >= SendBufferSize().
- Reduce IBD pipeline batch from 500 to 100 blocks for faster
  re-requesting with near-instant block processing.
- Seed getblocks limit raised to 20000 during IBD (was 500).
- Faster message handler polling during IBD (10ms vs 100ms).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 17:41:08 -07:00
sami7777 14ce8cc2a7 Update CI version to 5.1.8
Build All Platforms / build-windows-qt (push) Waiting to run
Build All Platforms / build-windows-daemon (push) Waiting to run
Build All Platforms / build-linux-qt (push) Waiting to run
Build All Platforms / build-linux-daemon (push) Waiting to run
Build All Platforms / build-macos (push) Waiting to run
Build All Platforms / release (push) Blocked by required conditions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 03:51:12 -07:00
sami7777 d180b5870c Bump version to v5.1.8
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 03:45:55 -07:00
sami7777 596d4ab55c Enable real-time wallet sync and verbose progress during block download
Remove IBD guards on SyncWithWallets and SetBestChain so wallet
transactions appear as blocks are connected. Log every 500 blocks
during sync instead of every 10,000.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 02:58:45 -07:00
sami7777 5af26f186e v5.1.7: Add Tor process manager for .onion connectivity
Build All Platforms / build-windows-qt (push) Waiting to run
Build All Platforms / build-windows-daemon (push) Waiting to run
Build All Platforms / build-linux-qt (push) Waiting to run
Build All Platforms / build-linux-daemon (push) Waiting to run
Build All Platforms / build-macos (push) Waiting to run
Build All Platforms / release (push) Blocked by required conditions
Adds CTorProcess which finds and launches an external Tor binary as a
subprocess, providing a SOCKS5 proxy on port 19099 and a v3 hidden
service on port 24112. The wallet auto-detects Tor from common install
locations or the app directory. Falls back gracefully to clearnet-only
if Tor is not found. Also fixes Tor-only network restriction that
blocked IPv4/IPv6, and bumps version to 5.1.7.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 02:31:55 -07:00
sami7777 5530920b25 Add data directory selection dialog on first run
Shows an intro dialog on first launch letting users choose where to store
blockchain data. Saves the choice in QSettings so it only appears once.
Styled to match the existing Triangles dark theme.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 02:25:26 -07:00
sami7777 3ed9a42b3c v5.1.6: Fix block sync stall for fresh nodes, REST API refactor
Build All Platforms / build-windows-qt (push) Waiting to run
Build All Platforms / build-windows-daemon (push) Waiting to run
Build All Platforms / build-linux-qt (push) Waiting to run
Build All Platforms / build-macos (push) Waiting to run
Build All Platforms / release (push) Blocked by required conditions
Build All Platforms / build-linux-daemon (push) Failing after 25m20s
Critical fix: revert initial sync from getheaders back to getblocks.
The headers-first change (05b1fd1) broke chain continuation — after
downloading the first 2000 blocks, fresh nodes would stall because
the getheaders path has no orphan-based continuation mechanism.
The getblocks/inv/orphan cycle is required for full chain sync.

Also adds getblocks fallback to the headers handler so if headers
are used via other paths, sync still continues.

Other changes:
- Extract REST API into separate rest.cpp/rest.h
- Add REST rate limiting and CORS support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 01:57:50 -07:00
23 changed files with 2214 additions and 380 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ on:
workflow_dispatch:
env:
VERSION: "5.1.5"
VERSION: "5.2.0"
jobs:
build-windows-qt:
+4 -4
View File
@@ -3,7 +3,7 @@
# Generated by qmake (3.1) (Qt 5.15.18)
# Project: triangles-qt.pro
# Template: app
# Command: C:/msys64/mingw64/bin/qmake-qt5.exe -o Makefile triangles-qt.pro
# Command: C:/msys64/mingw64/bin/qmake-qt5.exe -o Makefile triangles-qt.pro USE_QRCODE=1 USE_UPNP=-
#############################################################################
MAKEFILE = Makefile
@@ -156,7 +156,7 @@ Makefile: triangles-qt.pro C:/msys64/mingw64/share/qt5/mkspecs/win32-g++/qmake.c
C:/msys64/mingw64/lib/qtmain.prl \
C:/msys64/mingw64/share/qt5/mkspecs/features/build_pass.prf \
src/qt/triangles.qrc
$(QMAKE) -o Makefile triangles-qt.pro
$(QMAKE) -o Makefile triangles-qt.pro USE_QRCODE=1 USE_UPNP=-
C:/msys64/mingw64/share/qt5/mkspecs/features/spec_pre.prf:
C:/msys64/mingw64/share/qt5/mkspecs/qdevice.pri:
C:/msys64/mingw64/share/qt5/mkspecs/features/device_config.prf:
@@ -244,7 +244,7 @@ C:/msys64/mingw64/lib/qtmain.prl:
C:/msys64/mingw64/share/qt5/mkspecs/features/build_pass.prf:
src/qt/triangles.qrc:
qmake: FORCE
@$(QMAKE) -o Makefile triangles-qt.pro
@$(QMAKE) -o Makefile triangles-qt.pro USE_QRCODE=1 USE_UPNP=-
qmake_all: FORCE
@@ -261,7 +261,7 @@ distclean: release-distclean debug-distclean FORCE
-$(DEL_FILE) .qmake.stash
E:/repos/triangles/src/leveldb/libleveldb.a: FORCE
cd E:/repos/triangles/src/leveldb && CC=gcc CXX=g++ TARGET_OS=OS_WINDOWS_CROSSCOMPILE $(MAKE) OPT="-fpermissive -O2" libleveldb.a libmemenv.a && ranlib E:/repos/triangles/src/leveldb/libleveldb.a && ranlib E:/repos/triangles/src/leveldb/libmemenv.a
cd E:/repos/triangles/src/leveldb && CC=gcc CXX=g++ TARGET_OS=OS_WINDOWS_CROSSCOMPILE $(MAKE) OPT="-fno-keep-inline-dllexport -march=nocona -msahf -mtune=generic -Wa,-mbig-obj -O2" libleveldb.a libmemenv.a && ranlib E:/repos/triangles/src/leveldb/libleveldb.a && ranlib E:/repos/triangles/src/leveldb/libmemenv.a
release-mocclean:
$(MAKE) -f $(MAKEFILE).Release mocclean
+6 -26
View File
@@ -87,8 +87,8 @@ namespace Checkpoints
}
// triangles: synchronized checkpoint (centrally broadcasted)
uint256 hashSyncCheckpoint = uint256("0xbd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0");
uint256 hashPendingCheckpoint = uint256("0xbd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0");
uint256 hashSyncCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
uint256 hashPendingCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
CSyncCheckpoint checkpointMessage;
CSyncCheckpoint checkpointMessagePending;
uint256 hashInvalidCheckpoint = 0;
@@ -218,30 +218,10 @@ namespace Checkpoints
}
// Check against synchronized checkpoint
// Disabled: master key removed in V5, no new sync checkpoints possible.
// Always returns true to prevent stale DB-persisted checkpoints from blocking IBD.
bool CheckSync(const uint256& hashBlock, const CBlockIndex* pindexPrev)
{
if (fTestNet) return true; // Testnet has no checkpoints
int nHeight = pindexPrev->nHeight + 1;
LOCK(cs_hashSyncCheckpoint);
// sync-checkpoint should always be accepted block
assert(mapBlockIndex.count(hashSyncCheckpoint));
const CBlockIndex* pindexSync = mapBlockIndex[hashSyncCheckpoint];
if (nHeight > pindexSync->nHeight)
{
// trace back to same height as sync-checkpoint
const CBlockIndex* pindex = pindexPrev;
while (pindex->nHeight > pindexSync->nHeight)
if (!(pindex = pindex->pprev))
return error("CheckSync: pprev null - block index structure failure");
if (pindex->nHeight < pindexSync->nHeight || pindex->GetBlockHash() != hashSyncCheckpoint)
return false; // only descendant of sync-checkpoint can pass check
}
if (nHeight == pindexSync->nHeight && hashBlock != hashSyncCheckpoint)
return false; // same height with sync-checkpoint
if (nHeight < pindexSync->nHeight && !mapBlockIndex.count(hashBlock))
return false; // lower height than sync-checkpoint
return true;
}
@@ -361,8 +341,8 @@ namespace Checkpoints
bool IsMatureSyncCheckpoint()
{
LOCK(cs_hashSyncCheckpoint);
// sync-checkpoint should always be accepted block
assert(mapBlockIndex.count(hashSyncCheckpoint));
if (!mapBlockIndex.count(hashSyncCheckpoint))
return true; // no valid sync checkpoint, treat as mature
const CBlockIndex* pindexSync = mapBlockIndex[hashSyncCheckpoint];
return (nBestHeight >= pindexSync->nHeight + nCoinbaseMaturity ||
pindexSync->GetBlockTime() + nStakeMinAge < GetAdjustedTime());
+2 -2
View File
@@ -7,8 +7,8 @@
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
#define CLIENT_VERSION_MAJOR 5
#define CLIENT_VERSION_MINOR 1
#define CLIENT_VERSION_REVISION 5
#define CLIENT_VERSION_MINOR 2
#define CLIENT_VERSION_REVISION 0
#define CLIENT_VERSION_BUILD 0
// Converts the parameter X to a string after macro replacement on X has been performed.
+61 -18
View File
@@ -12,6 +12,7 @@
#include "checkpoints.h"
#include "smessage.h"
#include "tor/onion_v3.h"
#include "tor/tor_process.h"
#ifdef ENABLE_ZMQ
#include "zmqpublishnotifier.h"
#endif
@@ -139,6 +140,7 @@ void Shutdown(void* parg)
if (fFirstThread)
{
fShutdown = true;
int64_t nDeferredWaitStart = GetTimeMillis();
while (true)
{
@@ -154,6 +156,7 @@ void Shutdown(void* parg)
SecureMsgShutdown();
ShutdownTorV3();
StopTorProcess();
#ifdef ENABLE_ZMQ
if (pzmqNotifier)
@@ -178,6 +181,7 @@ void Shutdown(void* parg)
fs::remove(GetPidFile());
UnregisterWallet(pwalletMain);
delete pwalletMain;
// DB is flushed and wallet saved - safe to force-exit if something hangs
NewThread(ExitTimeout, NULL);
MilliSleep(50);
printf("Triangles exited\n\n");
@@ -403,6 +407,12 @@ std::string HelpMessage()
" -rpcsslprivatekeyfile=<file.pem> " + _("Server private key (default: server.pem)") + "\n" +
" -rpcsslciphers=<ciphers> " + _("Acceptable ciphers (default: TLSv1+HIGH:!SSLv2:!aNULL:!eNULL:!AH:!3DES:@STRENGTH)") + "\n" +
"\n" + _("REST API options:") + "\n" +
" -rest " + _("Enable public REST API on RPC port (default: 0)") + "\n" +
" -restcorsorigin=<origin> " + _("CORS Access-Control-Allow-Origin header (default: *)") + "\n" +
" -restapikey=<key> " + _("Bearer token for authenticated wallet endpoints") + "\n" +
" -restratelimit=<n> " + _("Max requests/sec per IP for public endpoints (default: 30, 0=disabled)") + "\n" +
"\n" + _("Secure messaging options:") + "\n" +
" -nosmsg " + _("Disable secure messaging.") + "\n" +
" -debugsmsg " + _("Log extra debug messages.") + "\n" +
@@ -694,24 +704,26 @@ bool AppInit2()
//if (nSocksVersion != 4 && nSocksVersion != 5)
// return InitError(strprintf(_("Unknown -socks proxy version requested: %i"), nSocksVersion));
do {
// Network selection: enable all networks (IPv4, IPv6, Tor)
// Tor is always enabled; clearnet is also allowed for seed node discovery
// Users can restrict to Tor-only with -onlynet=tor
if (mapArgs.count("-onlynet")) {
std::set<enum Network> nets;
nets.insert(NET_TOR);
BOOST_FOREACH(std::string snet, mapMultiArgs["-onlynet"]) {
enum Network net = ParseNetwork(snet);
if (net == NET_UNROUTABLE)
return InitError(strprintf(_("Unknown network specified in -onlynet: '%s'"), snet.c_str()));
nets.insert(net);
}
for (int n = 0; n < NET_MAX; n++) {
enum Network net = (enum Network)n;
if (!nets.count(net))
SetLimited(net);
}
} while (false);
}
CService addrOnion;
// need to move onion_port to a header
// Tor proxy: always configured for .onion connectivity
CService addrOnion;
unsigned short const onion_port = 19099;
if (mapArgs.count("-tor") && mapArgs["-tor"] != "0") {
@@ -722,10 +734,8 @@ bool AppInit2()
addrOnion = CService("127.0.0.1", onion_port);
}
if (true) {
SetProxy(NET_TOR, addrOnion, 5);
SetReachable(NET_TOR);
}
SetProxy(NET_TOR, addrOnion, 5);
SetReachable(NET_TOR);
// see Step 2: parameter interactions for more information about these
fNoListen = !GetBoolArg("-listen", true);
@@ -956,18 +966,33 @@ bool AppInit2()
printf(" rescan %15"PRId64"ms\n", GetTimeMillis() - nStart);
}
// ********************************************************* Step 8.5: initialize Tor V3 identity
// ********************************************************* Step 8.5: start Tor and initialize V3 identity
{
uiInterface.InitMessage(_("Starting Tor..."));
printf("Starting Tor process...\n");
// Start the Tor process (finds/launches tor binary, provides SOCKS proxy)
std::string torDataPath = (GetDataDir() / "tor_data").string();
bool torStarted = StartTorProcess(torDataPath);
if (torStarted) {
printf("Tor process running, SOCKS proxy at %s\n",
CTorProcess::GetInstance()->GetSocksProxy().c_str());
} else {
printf("WARNING: Tor not available. .onion peers will not be reachable.\n");
printf(" Clearnet connections will still work normally.\n");
}
// Initialize Tor V3 identity (Ed25519 keys, onion address)
uiInterface.InitMessage(_("Initializing Tor V3 identity..."));
printf("Initializing Tor V3 onion identity...\n");
// Tor V3 identity is innate to Triangles — always enabled
LoadTorV3Config();
TorV3Config& torConfig = GetTorV3Config();
torConfig.enableTor = true;
torConfig.enableHiddenService = true;
torConfig.hiddenServicePort = GetListenPort();
torConfig.torDataDirectory = (GetDataDir() / "tor_data").string();
torConfig.torDataDirectory = torDataPath;
if (InitTorV3()) {
string onionAddr = CTorV3Manager::GetInstance()->GetWalletOnionAddress();
@@ -990,6 +1015,24 @@ bool AppInit2()
} else {
printf("WARNING: Failed to initialize Tor V3 identity\n");
}
// Also check if Tor gave us a hidden service hostname
if (torStarted) {
fs::path torHsHostname = fs::path(torDataPath) / "hidden_service" / "hostname";
if (fs::exists(torHsHostname)) {
ifstream f(torHsHostname.string().c_str());
string torOnion;
if (f.is_open() && getline(f, torOnion)) {
// Trim whitespace
while (!torOnion.empty() && (torOnion.back() == '\n' || torOnion.back() == '\r' || torOnion.back() == ' '))
torOnion.pop_back();
if (!torOnion.empty()) {
AddLocal(CService(torOnion, GetListenPort(), fNameLookup), LOCAL_MANUAL);
printf("Tor hidden service (from Tor process): %s\n", torOnion.c_str());
}
}
}
}
}
// ********************************************************* Step 9: import blocks
+211 -94
View File
@@ -1639,6 +1639,12 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
if (!CheckBlock(!fJustCheck, !fJustCheck, false))
return false;
// Determine if this block is covered by the hardcoded checkpoint.
// Below checkpoint: skip all input validation, FetchInputs, ConnectInputs,
// and wallet sync. The checkpoint hash guarantees chain integrity for these blocks.
bool fAssumeValid = (pindex->nHeight <= Checkpoints::GetTotalBlocksEstimate());
bool fIsInitialDownload = IsInitialBlockDownload();
//// issue here: it doesn't know the version
unsigned int nTxPos;
if (fJustCheck)
@@ -1658,6 +1664,20 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
{
uint256 hashTx = tx.GetHash();
CDiskTxPos posThisTx(pindex->nFile, pindex->nBlockPos, nTxPos);
if (!fJustCheck)
nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
// Fast path: below checkpoint, skip all input validation and spent-tracking.
// Just record where each transaction lives on disk (txindex).
if (fAssumeValid)
{
mapQueuedChanges[hashTx] = CTxIndex(posThisTx, tx.vout.size());
continue;
}
// Full validation path (above checkpoint)
// Do not allow blocks that contain transactions which 'overwrite' older transactions,
// unless those are already completely spent.
// If such overwrites are allowed, coinbases and transactions depending upon those
@@ -1681,10 +1701,6 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
if (nSigOps > MAX_BLOCK_SIGOPS)
return DoS(100, error("ConnectBlock() : too many sigops"));
CDiskTxPos posThisTx(pindex->nFile, pindex->nBlockPos, nTxPos);
if (!fJustCheck)
nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
MapPrevTx mapInputs;
if (tx.IsCoinBase())
nValueOut += tx.GetValueOut();
@@ -1717,20 +1733,18 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
mapQueuedChanges[hashTx] = CTxIndex(posThisTx, tx.vout.size());
}
if (IsProofOfWork() && pindex->nHeight > Checkpoints::GetTotalBlocksEstimate())
if (!fAssumeValid)
{
int64_t nReward = GetProofOfWorkReward(nFees);
// Check coinbase reward
if (vtx[0].GetValueOut() > nReward)
return DoS(50, error("ConnectBlock() : coinbase reward exceeded (actual=%"PRId64" vs calculated=%"PRId64")",
vtx[0].GetValueOut(),
nReward));
}
if (IsProofOfStake())
if (IsProofOfWork())
{
// Skip expensive coin age calculation and reward validation for blocks
// covered by the hardcoded checkpoint. The checkpoint guarantees chain integrity.
if (pindex->nHeight > Checkpoints::GetTotalBlocksEstimate())
int64_t nReward = GetProofOfWorkReward(nFees);
// Check coinbase reward
if (vtx[0].GetValueOut() > nReward)
return DoS(50, error("ConnectBlock() : coinbase reward exceeded (actual=%"PRId64" vs calculated=%"PRId64")",
vtx[0].GetValueOut(),
nReward));
}
if (IsProofOfStake())
{
// triangles: coin stake tx earns reward instead of paying fee
uint64_t nCoinAge;
@@ -1760,8 +1774,8 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
return error("ConnectBlock() : UpdateTxIndex failed");
}
// Update address index
if (fAddressIndex)
// Update address index (skip during IBD - will be rebuilt on next start with -reindex)
if (fAddressIndex && !fIsInitialDownload)
{
for (unsigned int i = 0; i < vtx.size(); i++)
{
@@ -1834,9 +1848,9 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
return error("ConnectBlock() : WriteBlockIndex failed");
}
// Watch for transactions paying to me
// Skip during initial block download - wallet will rescan on next normal startup
if (!IsInitialBlockDownload())
// Skip wallet sync during IBD - a full wallet rescan runs when IBD completes.
// This eliminates millions of per-transaction wallet lookups during sync.
if (!fIsInitialDownload)
{
BOOST_FOREACH(CTransaction& tx, vtx)
SyncWithWallets(tx, this, true);
@@ -2038,7 +2052,8 @@ bool CBlock::SetBestChain(CTxDB& txdb, CBlockIndex* pindexNew)
}
}
// Update best block in wallet (so we can detect restored wallets)
// Update best block in wallet (so we can detect restored wallets).
// During IBD, skip this so the wallet knows it needs rescanning on restart.
bool fIsInitialDownload = IsInitialBlockDownload();
if (!fIsInitialDownload)
{
@@ -2057,7 +2072,8 @@ bool CBlock::SetBestChain(CTxDB& txdb, CBlockIndex* pindexNew)
uint256 nBestBlockTrust = pindexBest->nHeight != 0 ? (pindexBest->nChainTrust - pindexBest->pprev->nChainTrust) : pindexBest->nChainTrust;
if (nBestHeight % 10000 == 0 || nBestHeight > 2186900)
// Log every 5000 blocks during sync, every block once caught up
if (nBestHeight % 5000 == 0 || !IsInitialBlockDownload())
printf("SetBestChain: new best=%s height=%d trust=%s blocktrust=%"PRId64" date=%s\n",
hashBestChain.ToString().substr(0,20).c_str(), nBestHeight,
CBigNum(nBestChainTrust).ToString().c_str(),
@@ -2108,6 +2124,40 @@ bool CBlock::SetBestChain(CTxDB& txdb, CBlockIndex* pindexNew)
pNotificationQueue->Push(strBlockEvent);
}
// Detect IBD-to-synced transition and trigger deferred work:
// wallet rescan (since SyncWithWallets was skipped) and smsg chain scan.
{
static bool fWasInitialDownload = true;
if (fWasInitialDownload && !fIsInitialDownload)
{
printf("*** Initial block download complete at height %d ***\n", nBestHeight);
// Update wallet best chain locator now that IBD is done
const CBlockLocator locator(pindexBest);
::SetBestChain(locator);
// Wallet rescan: SyncWithWallets was skipped during IBD, so scan
// the entire chain to pick up all wallet transactions.
if (pwalletMain)
{
printf("Starting post-IBD wallet rescan from genesis...\n");
uiInterface.InitMessage(_("Rescanning wallet..."));
int nFound = pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
printf("Post-IBD wallet rescan complete: %d transactions found\n", nFound);
}
// Secure messaging: scan chain for public keys needed to decrypt messages
if (fSecMsgEnabled)
{
printf("Starting post-IBD secure message chain scan...\n");
uiInterface.InitMessage(_("Scanning for secure messages..."));
SecureMsgScanBlockChain();
printf("Post-IBD secure message chain scan complete\n");
}
}
fWasInitialDownload = fIsInitialDownload;
}
return true;
}
@@ -2421,18 +2471,10 @@ bool CBlock::AcceptBlock()
}
}
// Before fork: enforce sync checkpoints for historical chain integrity
// After fork: no sync checkpoint enforcement (decentralized)
if (nHeight < FORK_HEIGHT_V5)
{
bool cpSatisfies = Checkpoints::CheckSync(hash, pindexPrev);
if (CheckpointsMode == Checkpoints::STRICT && !cpSatisfies)
return error("AcceptBlock() : rejected by synchronized checkpoint");
if (CheckpointsMode == Checkpoints::ADVISORY && !cpSatisfies)
strMiscWarning = _("WARNING: syncronized checkpoint violation detected, but skipped!");
}
// Sync checkpoint enforcement is disabled:
// - Master key was removed in V5 fork, no new sync checkpoints will be broadcast
// - Hardcoded checkpoints already guarantee chain integrity
// - The persisted hashSyncCheckpoint in LevelDB blocks IBD from progressing
// Enforce rule that the coinbase starts with serialized block height
CScript expect = CScript() << nHeight;
@@ -2508,37 +2550,40 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
// Skip block signature verification during initial block download (below checkpoint).
// The hardcoded checkpoint guarantees historical chain integrity.
if (!pblock->CheckBlock(true, true, !IsInitialBlockDownload()))
return error("ProcessBlock() : CheckBlock FAILED");
CBlockIndex* pcheckpoint = Checkpoints::GetLastSyncCheckpoint();
if(pcheckpoint && fDebug)
{
const CBlockIndex* pindexLastPos = GetLastBlockIndex(pcheckpoint, true);
if(pindexLastPos)
{
printf("ProcessBlock(): Last POS Block Height: %d \n", pindexLastPos->nHeight);
}
else
{
printf("ProcessBlock(): Previous POS block not found.\n");
}
printf("IBD-DIAG: CheckBlock FAILED for %s (PoS=%d, IBD=%d)\n",
hash.ToString().substr(0,20).c_str(), pblock->IsProofOfStake(), IsInitialBlockDownload());
return error("ProcessBlock() : CheckBlock FAILED");
}
// Anti-spam: reject blocks with insufficient difficulty to prevent memory flooding.
// Use sync checkpoint as reference; fall back to chain tip if checkpoint is genesis.
CBlockIndex* pcheckpoint = Checkpoints::GetLastSyncCheckpoint();
if (!pcheckpoint || pcheckpoint->nHeight == 0)
pcheckpoint = pindexBest;
if (pcheckpoint && pblock->hashPrevBlock != hashBestChain && !Checkpoints::WantedByPendingSyncCheckpoint(hash))
{
// Extra checks to prevent "fill up memory by spamming with bogus blocks"
int64_t deltaTime = pblock->GetBlockTime() - pcheckpoint->nTime;
CBigNum bnNewBlock;
bnNewBlock.SetCompact(pblock->nBits);
CBigNum bnRequired;
if (pblock->IsProofOfStake())
bnRequired.SetCompact(ComputeMinStake(GetLastBlockIndex(pcheckpoint, true)->nBits, deltaTime, pblock->nTime));
{
const CBlockIndex* pindexLastPos = GetLastBlockIndex(pcheckpoint, true);
if (pindexLastPos)
bnRequired.SetCompact(ComputeMinStake(pindexLastPos->nBits, deltaTime, pblock->nTime));
// else: no PoS history yet (below block 9001), skip — AcceptBlock rejects PoS below MODIFIER_INTERVAL_SWITCH
}
else
bnRequired.SetCompact(ComputeMinWork(GetLastBlockIndex(pcheckpoint, false)->nBits, deltaTime));
{
const CBlockIndex* pindexLastPow = GetLastBlockIndex(pcheckpoint, false);
if (pindexLastPow)
bnRequired.SetCompact(ComputeMinWork(pindexLastPow->nBits, deltaTime));
}
if (bnNewBlock > bnRequired)
if (bnRequired != 0 && bnNewBlock > bnRequired)
{
if (pfrom)
pfrom->Misbehaving(100);
@@ -2604,8 +2649,8 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
mapOrphanBlocksByPrev.erase(hashPrev);
}
if (nBestHeight % 10000 == 0 || nBestHeight > 2186900)
printf("ProcessBlock: ACCEPTED\n");
if (nBestHeight % 5000 == 0 || !IsInitialBlockDownload())
printf("ProcessBlock: ACCEPTED block %d\n", nBestHeight);
// triangles: if responsible for sync-checkpoint send it
if (pfrom && !CSyncCheckpoint::strMasterPrivKey.empty())
@@ -3217,16 +3262,22 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
}
}
// Ask the first connected node for block updates
// Ask connected nodes for block updates
// During IBD, always request blocks from any valid peer (critical for reconnection)
static int nAskedForBlocks = 0;
if (!pfrom->fClient && !pfrom->fOneShot &&
bool fShouldAsk = !pfrom->fClient && !pfrom->fOneShot &&
(pfrom->nStartingHeight > (nBestHeight - 144)) &&
(pfrom->nVersion < NOBLKS_VERSION_START ||
pfrom->nVersion >= NOBLKS_VERSION_END) &&
(nAskedForBlocks < 1 || vNodes.size() <= 1))
(IsInitialBlockDownload() || nAskedForBlocks < 1 || vNodes.size() <= 1);
printf("IBD-DIAG: version handler: peer=%s height=%d ourHeight=%d fClient=%d fOneShot=%d shouldAsk=%d nAskedForBlocks=%d IBD=%d\n",
pfrom->addr.ToString().c_str(), pfrom->nStartingHeight, nBestHeight,
pfrom->fClient, pfrom->fOneShot, fShouldAsk, nAskedForBlocks, IsInitialBlockDownload());
if (fShouldAsk)
{
nAskedForBlocks++;
pfrom->PushGetHeaders(pindexBest, uint256(0));
pfrom->PushGetBlocks(pindexBest, uint256(0));
printf("IBD-DIAG: sent getblocks from height %d to peer %s\n", nBestHeight, pfrom->addr.ToString().c_str());
}
// Relay alerts
@@ -3236,12 +3287,8 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
item.second.RelayTo(pfrom);
}
// triangles: relay sync-checkpoint
{
LOCK(Checkpoints::cs_hashSyncCheckpoint);
if (!Checkpoints::checkpointMessage.IsNull())
Checkpoints::checkpointMessage.RelayTo(pfrom);
}
// Sync checkpoint relay disabled (master key removed in V5 fork).
// Relaying stale checkpoints causes IBD nodes to request far-future blocks.
pfrom->fSuccessfullyConnected = true;
@@ -3347,13 +3394,19 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
// find last block in inv vector
unsigned int nLastBlock = (unsigned int)(-1);
int nBlockInv = 0, nTxInv = 0;
for (unsigned int nInv = 0; nInv < vInv.size(); nInv++) {
if (vInv[vInv.size() - 1 - nInv].type == MSG_BLOCK) {
if (vInv[nInv].type == MSG_BLOCK) nBlockInv++;
else nTxInv++;
if (vInv[vInv.size() - 1 - nInv].type == MSG_BLOCK && nLastBlock == (unsigned int)(-1)) {
nLastBlock = vInv.size() - 1 - nInv;
break;
}
}
printf("IBD-DIAG: inv received: %d blocks, %d tx from %s (our height=%d)\n",
nBlockInv, nTxInv, pfrom->addr.ToString().c_str(), nBestHeight);
CTxDB txdb("r");
int nNew = 0, nAlready = 0;
for (unsigned int nInv = 0; nInv < vInv.size(); nInv++)
{
const CInv &inv = vInv[nInv];
@@ -3363,25 +3416,24 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
pfrom->AddInventoryKnown(inv);
bool fAlreadyHave = AlreadyHave(txdb, inv);
if (fDebug)
printf(" got inventory: %s %s\n", inv.ToString().c_str(), fAlreadyHave ? "have" : "new");
if (inv.type == MSG_BLOCK) {
if (fAlreadyHave) nAlready++; else nNew++;
}
if (!fAlreadyHave)
pfrom->AskFor(inv);
else if (inv.type == MSG_BLOCK && mapOrphanBlocks.count(inv.hash)) {
pfrom->PushGetBlocks(pindexBest, GetOrphanRoot(mapOrphanBlocks[inv.hash]));
} else if (nInv == nLastBlock) {
// In case we are on a very long side-chain, it is possible that we already have
// the last block in an inv bundle sent in response to getblocks. Try to detect
// this situation and push another getblocks to continue.
pfrom->PushGetBlocks(mapBlockIndex[inv.hash], uint256(0));
if (fDebug)
printf("force request: %s\n", inv.ToString().c_str());
printf("IBD-DIAG: inv last block already known, pushing getblocks from %d\n",
mapBlockIndex[inv.hash]->nHeight);
}
// Track requests for our stuff
Inventory(inv.hash);
}
if (nBlockInv > 0)
printf("IBD-DIAG: inv result: %d new blocks requested, %d already have\n", nNew, nAlready);
}
@@ -3470,8 +3522,10 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
// Send the rest of the chain
if (pindex)
pindex = pindex->pnext;
int nLimit = 500;
printf("getblocks %d to %s limit %d\n", (pindex ? pindex->nHeight : -1), hashStop.ToString().substr(0,20).c_str(), nLimit);
int nLimit = IsInitialBlockDownload() ? 20000 : 500;
printf("IBD-DIAG: getblocks request from peer %s: start=%d stop=%s limit=%d\n",
pfrom->addr.ToString().c_str(), (pindex ? pindex->nHeight : -1),
hashStop.ToString().substr(0,20).c_str(), nLimit);
for (; pindex; pindex = pindex->pnext)
{
if (pindex->GetBlockHash() == hashStop)
@@ -3496,17 +3550,9 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
}
else if (strCommand == "checkpoint")
{
CSyncCheckpoint checkpoint;
vRecv >> checkpoint;
if (checkpoint.ProcessSyncCheckpoint(pfrom))
{
// Relay
pfrom->hashCheckpointKnown = checkpoint.hashCheckpoint;
LOCK(cs_vNodes);
BOOST_FOREACH(CNode* pnode, vNodes)
checkpoint.RelayTo(pnode);
}
// Sync checkpoint system disabled (master key removed in V5 fork).
// Ignore checkpoint messages — processing them during IBD causes the
// node to request a single far-future block instead of syncing sequentially.
}
else if (strCommand == "getheaders")
@@ -3599,6 +3645,11 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
if (nRequested > 0 && fDebug)
printf("requested %d blocks from headers announcement\n", nRequested);
// If we received a full batch, continue sync via getblocks
// (the getblocks/inv/orphan cycle handles chain continuation)
if (vHeaders.size() >= 2000)
pfrom->PushGetBlocks(pindexBest, uint256(0));
}
@@ -3675,19 +3726,50 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
vRecv >> block;
uint256 hashBlock = block.GetHash();
printf("received block %s\n", hashBlock.ToString().substr(0,20).c_str());
// block.print();
// Log every block during IBD (with throttling after first 100)
static int64_t nLastBlockLog = 0;
static int nBlocksReceived = 0;
nBlocksReceived++;
bool fLogThis = (nBlocksReceived <= 20) || (nBestHeight % 500 == 0) || !IsInitialBlockDownload() || (GetTime() - nLastBlockLog >= 5);
if (fLogThis) {
printf("IBD-DIAG: block received #%d hash=%s from=%s ourHeight=%d\n",
nBlocksReceived, hashBlock.ToString().substr(0,20).c_str(),
pfrom->addr.ToString().c_str(), nBestHeight);
nLastBlockLog = GetTime();
}
CInv inv(MSG_BLOCK, hashBlock);
pfrom->AddInventoryKnown(inv);
if (ProcessBlock(pfrom, &block))
{
mapAlreadyAskedFor.erase(inv);
if (block.nDoS)
if (IsInitialBlockDownload())
{
static int nBlocksSinceRequest = 0;
if (++nBlocksSinceRequest >= 100)
{
nBlocksSinceRequest = 0;
pfrom->pindexLastGetBlocksBegin = NULL;
pfrom->PushGetBlocks(pindexBest, uint256(0));
printf("IBD-DIAG: pipeline refill at height %d\n", nBestHeight);
}
}
}
else
{
printf("IBD-DIAG: ProcessBlock FAILED for block %s (height after prev=%d, DoS=%d)\n",
hashBlock.ToString().substr(0,20).c_str(), nBestHeight, block.nDoS);
}
if (block.nDoS) {
printf("IBD-DIAG: Misbehaving peer %s by %d\n",
pfrom->addr.ToString().c_str(), block.nDoS);
pfrom->Misbehaving(block.nDoS);
if (fSecMsgEnabled)
}
if (fSecMsgEnabled && !IsInitialBlockDownload())
SecureMsgScanBlock(block);
}
@@ -4121,9 +4203,44 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
pto->PushMessage("inv", vInv);
//
// Stall detection: if IBD and no new blocks for 5 seconds, re-request
//
if (IsInitialBlockDownload() && !pto->fClient)
{
static int64_t nLastBlockReceived = 0;
static int nLastHeight = 0;
static int64_t nLastStallLog = 0;
if (nBestHeight > nLastHeight) {
nLastHeight = nBestHeight;
nLastBlockReceived = GetTime();
} else if (nLastBlockReceived > 0 && GetTime() - nLastBlockReceived > 5) {
if (GetTime() - nLastStallLog >= 10) { // log every 10s max
printf("IBD-DIAG: STALL at height %d for %ds, peer=%s askfor_queue=%d send_size=%d\n",
nBestHeight, (int)(GetTime() - nLastBlockReceived),
pto->addr.ToString().c_str(),
(int)pto->mapAskFor.size(), (int)pto->nSendSize);
nLastStallLog = GetTime();
}
pto->pindexLastGetBlocksBegin = NULL;
pto->PushGetBlocks(pindexBest, uint256(0));
nLastBlockReceived = GetTime();
}
}
//
// Message: getdata
//
// Periodic IBD status
if (IsInitialBlockDownload()) {
static int64_t nLastStatus = 0;
if (GetTime() - nLastStatus >= 15) {
printf("IBD-DIAG: STATUS height=%d peers=%d askfor_queued=%d orphans=%d\n",
nBestHeight, (int)vNodes.size(), (int)pto->mapAskFor.size(), (int)mapOrphanBlocks.size());
nLastStatus = GetTime();
}
}
vector<CInv> vGetData;
int64_t nNow = GetTime() * 1000000;
CTxDB txdb("r");
+10 -1
View File
@@ -114,6 +114,7 @@ OBJS= \
obj/net_bootstrap.o \
obj/protocol.o \
obj/trianglesrpc.o \
obj/rest.o \
obj/rpcdump.o \
obj/rpcnet.o \
obj/rpcmining.o \
@@ -134,7 +135,8 @@ OBJS= \
obj/scrypt-x86.o \
obj/scrypt-x86_64.o \
obj/smessage.o \
obj/onion_v3.o
obj/onion_v3.o \
obj/tor_process.o
all: trianglesd.exe
@@ -199,6 +201,13 @@ obj/onion_v3.o: tor/onion_v3.cpp
-e '/^$$/ d' -e 's/$$/ :/' < $(@:%.o=%.d) >> $(@:%.o=%.P); \
rm -f $(@:%.o=%.d)
obj/tor_process.o: tor/tor_process.cpp
$(CXX) -c $(xCXXFLAGS) -MMD -MF $(@:%.o=%.d) -o $@ $<
@cp $(@:%.o=%.d) $(@:%.o=%.P); \
sed -e 's/#.*//' -e 's/^[^:]*: *//' -e 's/ *\\$$//' \
-e '/^$$/ d' -e 's/$$/ :/' < $(@:%.o=%.d) >> $(@:%.o=%.P); \
rm -f $(@:%.o=%.d)
obj/net_bootstrap.o: net_bootstrap.cpp
$(CXX) -c $(xCXXFLAGS) -MMD -MF $(@:%.o=%.d) -o $@ $<
@cp $(@:%.o=%.d) $(@:%.o=%.P); \
+10 -1
View File
@@ -149,6 +149,7 @@ OBJS= \
obj/net_bootstrap.o \
obj/protocol.o \
obj/trianglesrpc.o \
obj/rest.o \
obj/rpcdump.o \
obj/rpcnet.o \
obj/rpcmining.o \
@@ -169,7 +170,8 @@ OBJS= \
obj/scrypt-x86.o \
obj/scrypt-x86_64.o \
obj/smessage.o \
obj/onion_v3.o
obj/onion_v3.o \
obj/tor_process.o
# ZMQ support (optional)
# Build with: make -f makefile.unix USE_ZMQ=1
@@ -245,6 +247,13 @@ obj/onion_v3.o: tor/onion_v3.cpp
-e '/^$$/ d' -e 's/$$/ :/' < $(@:%.o=%.d) >> $(@:%.o=%.P); \
rm -f $(@:%.o=%.d)
obj/tor_process.o: tor/tor_process.cpp
$(CXX) -c $(xCXXFLAGS) -MMD -MF $(@:%.o=%.d) -o $@ $<
@cp $(@:%.o=%.d) $(@:%.o=%.P); \
sed -e 's/#.*//' -e 's/^[^:]*: *//' -e 's/ *\\$$//' \
-e '/^$$/ d' -e 's/$$/ :/' < $(@:%.o=%.d) >> $(@:%.o=%.P); \
rm -f $(@:%.o=%.d)
obj/net_bootstrap.o: net_bootstrap.cpp
$(CXX) -c $(xCXXFLAGS) -MMD -MF $(@:%.o=%.d) -o $@ $<
@cp $(@:%.o=%.d) $(@:%.o=%.P); \
+108 -26
View File
@@ -6,6 +6,7 @@
#include "irc.h"
#include "db.h"
#include "net.h"
#include "main.h"
#include "init.h"
#include "strlcpy.h"
#include "addrman.h"
@@ -40,6 +41,7 @@ void ThreadOpenAddedConnections2(void* parg);
#ifdef USE_UPNP
void ThreadMapPort2(void* parg);
#endif
void ThreadDNSAddressSeed(void* parg);
void ThreadDNSAddressSeed2(void* parg);
bool OpenNetworkConnection(const CAddress& addrConnect, CSemaphoreGrant *grantOutbound = NULL, const char *strDest = NULL, bool fOneShot = false);
@@ -888,7 +890,7 @@ void ThreadSocketHandler2(void* parg)
//
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 50000; // frequency to poll pnode->vSend
timeout.tv_usec = IsInitialBlockDownload() ? 10000 : 50000; // faster polling during IBD
fd_set fdsetRecv;
fd_set fdsetSend;
@@ -1375,6 +1377,7 @@ void ThreadOnionSeed(void* parg)
unsigned int pnSeed[] = {
0xCE58E9C2, // DNS2-OpenClaw: 194.233.88.206
0x13A7D04A, // DNS3-Sami: 74.208.167.19
};
void DumpAddresses()
@@ -1416,6 +1419,58 @@ void ThreadDumpAddress(void* parg)
printf("ThreadDumpAddress exited\n");
}
void ThreadDNSAddressSeed2(void* parg)
{
static const char* strDNSSeed[] = {
"seed1.cryptographic-triangles.org",
"seed2.cryptographic-triangles.org",
"seed3.cryptographic-triangles.org",
"backup-seed.cryptographic-triangles.org",
};
printf("Loading addresses from DNS seeds...\n");
int found = 0;
for (unsigned int seed_idx = 0; seed_idx < ARRAYLEN(strDNSSeed); seed_idx++)
{
if (fShutdown)
return;
vector<CNetAddr> vaddr;
if (LookupHost(strDNSSeed[seed_idx], vaddr))
{
BOOST_FOREACH(CNetAddr& ip, vaddr)
{
CAddress addr(CService(ip, GetDefaultPort()));
addr.nTime = GetTime() - 3*24*60*60; // 3 days ago
addrman.Add(addr, CNetAddr(strDNSSeed[seed_idx], true));
found++;
}
}
}
printf("%d addresses found from DNS seeds\n", found);
}
void ThreadDNSAddressSeed(void* parg)
{
RenameThread("Triangles-dnsseed");
try
{
vnThreadsRunning[THREAD_DNSSEED]++;
ThreadDNSAddressSeed2(parg);
vnThreadsRunning[THREAD_DNSSEED]--;
}
catch (std::exception& e) {
vnThreadsRunning[THREAD_DNSSEED]--;
PrintException(&e, "ThreadDNSAddressSeed()");
} catch (...) {
vnThreadsRunning[THREAD_DNSSEED]--;
PrintException(NULL, "ThreadDNSAddressSeed()");
}
printf("ThreadDNSAddressSeed exited\n");
}
void ThreadOpenConnections(void* parg)
{
// Make this thread recognisable as the connection opening thread
@@ -1520,24 +1575,29 @@ void ThreadOpenConnections2(void* parg)
if (fShutdown)
return;
// Add seed nodes if IRC isn't working
if (addrman.size()==0 && (GetTime() - nStart > 60) && !fTestNet)
// Add hardcoded seed nodes when we have no connections.
// Original check (addrman.size()==0) was too conservative - stale entries
// in peers.dat would prevent fallback to working hardcoded IPs forever.
{
std::vector<CAddress> vAdd;
for (unsigned int i = 0; i < ARRAYLEN(pnSeed); i++)
{
// It'll only connect to one or two seed nodes because once it connects,
// it'll get a pile of addresses with newer timestamps.
// Seed nodes are given a random 'last seen time' of between one and two
// weeks ago.
const int64_t nOneWeek = 7*24*60*60;
struct in_addr ip;
memcpy(&ip, &pnSeed[i], sizeof(ip));
CAddress addr(CService(ip, GetDefaultPort()));
addr.nTime = GetTime()-GetRand(nOneWeek)-nOneWeek;
vAdd.push_back(addr);
LOCK(cs_vNodes);
bool fNoOutbound = true;
BOOST_FOREACH(CNode* pnode, vNodes) {
if (!pnode->fInbound) { fNoOutbound = false; break; }
}
if (fNoOutbound && (GetTime() - nStart > 30) && !fTestNet)
{
std::vector<CAddress> vAdd;
for (unsigned int i = 0; i < ARRAYLEN(pnSeed); i++)
{
struct in_addr ip;
memcpy(&ip, &pnSeed[i], sizeof(ip));
CAddress addr(CService(ip, GetDefaultPort()));
addr.nTime = GetTime() - GetRand(60*60); // seen recently
vAdd.push_back(addr);
}
addrman.Add(vAdd, CNetAddr("127.0.0.1"));
printf("No outbound connections after 30s, added %d hardcoded seeds\n", (int)vAdd.size());
}
addrman.Add(vAdd, CNetAddr("127.0.0.1"));
}
//
@@ -1755,6 +1815,7 @@ void ThreadMessageHandler2(void* parg)
{
printf("ThreadMessageHandler started\n");
SetThreadPriority(THREAD_PRIORITY_BELOW_NORMAL);
bool fWasBoosted = false;
while (!fShutdown)
{
vector<CNode*> vNodesCopy;
@@ -1797,11 +1858,21 @@ void ThreadMessageHandler2(void* parg)
pnode->Release();
}
// Boost thread priority during IBD, restore when caught up
if (IsInitialBlockDownload() && !fWasBoosted) {
SetThreadPriority(THREAD_PRIORITY_NORMAL);
fWasBoosted = true;
} else if (!IsInitialBlockDownload() && fWasBoosted) {
SetThreadPriority(THREAD_PRIORITY_BELOW_NORMAL);
fWasBoosted = false;
}
// Wait and allow messages to bunch up.
// During IBD, use a shorter sleep to maximize block processing throughput.
// Reduce vnThreadsRunning so StopNode has permission to exit while
// we're sleeping, but we must always check fShutdown after doing this.
vnThreadsRunning[THREAD_MESSAGEHANDLER]--;
MilliSleep(100);
MilliSleep(IsInitialBlockDownload() ? 10 : 100);
if (fRequestShutdown)
StartShutdown();
vnThreadsRunning[THREAD_MESSAGEHANDLER]++;
@@ -1952,9 +2023,10 @@ void static Discover()
}
static void run_tor() {
// Old embedded Tor v2 client removed - incompatible with OpenSSL 3.x.
// Tor v3 onion services are handled by onion_v3.cpp via external Tor/SOCKS5.
printf("Tor v3 mode: using external Tor process via SOCKS5 proxy.\n");
// Tor process is now managed by CTorProcess (tor_process.cpp)
// which starts an external Tor binary with SOCKS5 proxy and v3 hidden service.
// The old embedded Tor v2 code was removed (incompatible with OpenSSL 3.x).
printf("Tor v3 mode: using managed Tor process via SOCKS5 proxy.\n");
set_initialized();
}
@@ -2030,9 +2102,9 @@ void StartNode(void* parg)
if (fUseUPnP)
MapPort();
// Get addresses from IRC and advertise ours
//if (!NewThread(ThreadIRCSeed, NULL))
// printf("Error: NewThread(ThreadIRCSeed) failed\n");
// DNS seed lookup
if (!NewThread(ThreadDNSAddressSeed, NULL))
printf("Error: NewThread(ThreadDNSAddressSeed) failed\n");
// Send and receive from sockets, accept connections
if (!NewThread(ThreadSocketHandler, NULL))
@@ -2094,8 +2166,18 @@ bool StopNode()
if (vnThreadsRunning[THREAD_ADDEDCONNECTIONS] > 0) printf("ThreadOpenAddedConnections still running\n");
if (vnThreadsRunning[THREAD_DUMPADDRESS] > 0) printf("ThreadDumpAddresses still running\n");
if (vnThreadsRunning[THREAD_STAKE_MINER] > 0) printf("ThreadStakeMiner still running\n");
while (vnThreadsRunning[THREAD_MESSAGEHANDLER] > 0 || vnThreadsRunning[THREAD_RPCHANDLER] > 0)
MilliSleep(20);
{
int64_t nWaitStart = GetTime();
while (vnThreadsRunning[THREAD_MESSAGEHANDLER] > 0 || vnThreadsRunning[THREAD_RPCHANDLER] > 0)
{
if (GetTime() - nWaitStart > 10)
{
printf("Timed out waiting for message/RPC threads to stop\n");
break;
}
MilliSleep(20);
}
}
MilliSleep(50);
DumpAddresses();
return true;
+9 -4
View File
@@ -22,12 +22,13 @@
class CRequestTracker;
class CNode;
class CBlockIndex;
bool IsInitialBlockDownload();
extern int nBestHeight;
inline unsigned int ReceiveFloodSize() { return 1000*GetArg("-maxreceivebuffer", 5*1000); }
inline unsigned int SendBufferSize() { return 1000*GetArg("-maxsendbuffer", 1*1000); }
inline unsigned int ReceiveFloodSize() { return (unsigned int)-1; }
inline unsigned int SendBufferSize() { return (unsigned int)-1; }
void AddOneShot(std::string strDest);
bool RecvLine(SOCKET hSocket, std::string& strLine);
@@ -429,8 +430,12 @@ public:
nNow = std::max(nNow, nLastTime);
nLastTime = nNow;
// Each retry is 2 minutes after the last
nRequestTime = std::max(nRequestTime + 2 * 60 * 1000000, nNow);
// During IBD, request immediately (no 2-minute retry delay)
// Normal operation: each retry is 2 minutes after the last
if (nRequestTime > 0 && IsInitialBlockDownload())
nRequestTime = nNow;
else
nRequestTime = std::max(nRequestTime + 2 * 60 * 1000000, nNow);
mapAskFor.insert(std::make_pair(nRequestTime, inv));
}
+3 -1
View File
@@ -63,7 +63,9 @@ void ClientModel::updateTimer()
int newNumBlocks = getNumBlocks();
int newNumBlocksOfPeers = getNumBlocksOfPeers();
if(cachedNumBlocks != newNumBlocks || cachedNumBlocksOfPeers != newNumBlocksOfPeers)
// Always emit during IBD so the speed/ETA display stays live
if(cachedNumBlocks != newNumBlocks || cachedNumBlocksOfPeers != newNumBlocksOfPeers
|| newNumBlocks < newNumBlocksOfPeers)
{
cachedNumBlocks = newNumBlocks;
cachedNumBlocksOfPeers = newNumBlocksOfPeers;
+205
View File
@@ -0,0 +1,205 @@
#include "introdialog.h"
#include "util.h"
#include <QSettings>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QFileDialog>
#include <QDir>
#include <QMessageBox>
#include <QDialogButtonBox>
#include <boost/filesystem.hpp>
IntroDialog::IntroDialog(QWidget *parent) :
QDialog(parent)
{
setWindowTitle("Triangles");
setMinimumWidth(520);
// Match existing Triangles dark theme
setStyleSheet(
"QDialog { background-color: #000; color: #f26522; }"
"QLabel { color: #f26522; }"
"QRadioButton { color: #f26522; }"
"QRadioButton::indicator { border: 1px solid #f26522; background-color: #000; width: 12px; height: 12px; border-radius: 7px; }"
"QRadioButton::indicator:checked { background-color: #f26522; }"
"QLineEdit { background-color: #1c1c1c; border: 1px solid #f26522; color: #f26522; padding: 4px; }"
"QPushButton { background-color: #000; color: #f26522; border: 1px solid #f26522; padding: 4px 16px; min-height: 20px; }"
"QPushButton:hover { background-color: #61280E; }"
);
defaultDataDir = QString::fromStdString(GetDefaultDataDir().string());
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 20, 20, 20);
mainLayout->setSpacing(12);
// Welcome header
QLabel *welcomeLabel = new QLabel(tr("Welcome to Triangles!"));
welcomeLabel->setStyleSheet("font-size: 16px; font-weight: bold; color: #f26522;");
mainLayout->addWidget(welcomeLabel);
// Description
QLabel *descLabel = new QLabel(tr(
"Triangles will store its blockchain data, wallet, and configuration in a data directory. "
"You can use the default directory or choose a custom location. "
"The data directory requires several hundred MB of free space."
));
descLabel->setWordWrap(true);
mainLayout->addWidget(descLabel);
mainLayout->addSpacing(8);
// Default directory radio
defaultRadio = new QRadioButton(tr("Use the default data directory"));
defaultRadio->setChecked(true);
mainLayout->addWidget(defaultRadio);
// Show default path
QLabel *defaultPathLabel = new QLabel(defaultDataDir);
defaultPathLabel->setStyleSheet("color: #999; margin-left: 24px; font-size: 11px;");
mainLayout->addWidget(defaultPathLabel);
mainLayout->addSpacing(4);
// Custom directory radio
customRadio = new QRadioButton(tr("Use a custom data directory:"));
mainLayout->addWidget(customRadio);
// Path input + browse button
QHBoxLayout *pathLayout = new QHBoxLayout();
pathLayout->setContentsMargins(24, 0, 0, 0);
pathEdit = new QLineEdit(defaultDataDir);
pathEdit->setEnabled(false);
pathLayout->addWidget(pathEdit);
browseButton = new QPushButton(tr("Browse..."));
browseButton->setEnabled(false);
pathLayout->addWidget(browseButton);
mainLayout->addLayout(pathLayout);
// Free space label
freeSpaceLabel = new QLabel();
freeSpaceLabel->setStyleSheet("color: #999; margin-left: 24px; font-size: 11px;");
mainLayout->addWidget(freeSpaceLabel);
mainLayout->addStretch(1);
// OK / Cancel buttons
QHBoxLayout *buttonLayout = new QHBoxLayout();
buttonLayout->addStretch(1);
QPushButton *okButton = new QPushButton(tr("OK"));
QPushButton *cancelButton = new QPushButton(tr("Cancel"));
buttonLayout->addWidget(okButton);
buttonLayout->addWidget(cancelButton);
mainLayout->addLayout(buttonLayout);
// Connections
connect(defaultRadio, SIGNAL(toggled(bool)), this, SLOT(on_defaultRadio_toggled(bool)));
connect(browseButton, SIGNAL(clicked()), this, SLOT(on_browseButton_clicked()));
connect(pathEdit, SIGNAL(textChanged(QString)), this, SLOT(updateFreeSpace()));
connect(okButton, SIGNAL(clicked()), this, SLOT(accept()));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject()));
updateFreeSpace();
}
QString IntroDialog::getDataDirectory() const
{
if (defaultRadio->isChecked())
return defaultDataDir;
return pathEdit->text();
}
void IntroDialog::setDataDirectory(const QString &dir)
{
pathEdit->setText(dir);
if (dir == defaultDataDir) {
defaultRadio->setChecked(true);
} else {
customRadio->setChecked(true);
}
}
void IntroDialog::on_browseButton_clicked()
{
QString dir = QFileDialog::getExistingDirectory(this, tr("Choose data directory"), pathEdit->text());
if (!dir.isEmpty())
pathEdit->setText(dir);
}
void IntroDialog::on_defaultRadio_toggled(bool checked)
{
pathEdit->setEnabled(!checked);
browseButton->setEnabled(!checked);
if (checked)
pathEdit->setText(defaultDataDir);
updateFreeSpace();
}
void IntroDialog::updateFreeSpace()
{
QString path = getDataDirectory();
boost::filesystem::path fsPath(path.toStdString());
// Walk up to find an existing parent
try {
while (!fsPath.empty() && !boost::filesystem::exists(fsPath))
fsPath = fsPath.parent_path();
if (!fsPath.empty()) {
boost::filesystem::space_info si = boost::filesystem::space(fsPath);
double freeGB = (double)si.available / (1024.0 * 1024.0 * 1024.0);
freeSpaceLabel->setText(tr("Free space: %1 GB").arg(QString::number(freeGB, 'f', 2)));
} else {
freeSpaceLabel->setText(tr("Cannot determine free space"));
}
} catch (const boost::filesystem::filesystem_error &) {
freeSpaceLabel->setText(tr("Cannot determine free space"));
}
}
bool IntroDialog::pickDataDirectory()
{
namespace fs = boost::filesystem;
QSettings settings;
// If -datadir was passed on the command line, skip the dialog entirely
if (mapArgs.count("-datadir"))
return true;
QString dataDir = settings.value("strDataDir", "").toString();
if (dataDir.isEmpty()) {
// First run - show the dialog
IntroDialog dlg;
if (dlg.exec() != QDialog::Accepted)
return false;
dataDir = dlg.getDataDirectory();
settings.setValue("strDataDir", dataDir);
}
// If the saved path is the default, don't set -datadir (let normal defaults work)
QString defaultDir = QString::fromStdString(GetDefaultDataDir().string());
if (dataDir != defaultDir) {
mapArgs["-datadir"] = dataDir.toStdString();
}
// Ensure the directory exists
try {
fs::create_directories(fs::path(dataDir.toStdString()));
} catch (const fs::filesystem_error &) {
QMessageBox::critical(0, "Triangles",
QString("Error: Could not create data directory \"%1\".").arg(dataDir));
return false;
}
return true;
}
+42
View File
@@ -0,0 +1,42 @@
#ifndef INTRODIALOG_H
#define INTRODIALOG_H
#include <QDialog>
#include <QLabel>
#include <QLineEdit>
#include <QRadioButton>
#include <QPushButton>
/** Data directory selection dialog shown on first run. */
class IntroDialog : public QDialog
{
Q_OBJECT
public:
explicit IntroDialog(QWidget *parent = 0);
QString getDataDirectory() const;
void setDataDirectory(const QString &dir);
/**
* Check settings or show the dialog to choose data directory.
* Returns true if a directory was selected, false if the user cancelled.
* Sets mapArgs["-datadir"] if a non-default directory was chosen.
*/
static bool pickDataDirectory();
private slots:
void on_browseButton_clicked();
void on_defaultRadio_toggled(bool checked);
void updateFreeSpace();
private:
QRadioButton *defaultRadio;
QRadioButton *customRadio;
QLineEdit *pathEdit;
QPushButton *browseButton;
QLabel *freeSpaceLabel;
QString defaultDataDir;
};
#endif // INTRODIALOG_H
+5
View File
@@ -8,6 +8,7 @@
#include "guiutil.h"
#include "guiconstants.h"
#include "introdialog.h"
#include "init.h"
#include "ui_interface.h"
#include "qtipcserver.h"
@@ -131,6 +132,10 @@ int main(int argc, char *argv[])
// Command-line options take precedence:
ParseParameters(argc, argv);
// Show data directory selection dialog on first run (unless -datadir was passed)
if (!IntroDialog::pickDataDirectory())
return 0;
// ... then triangles.conf:
if (!boost::filesystem::is_directory(GetDataDir(false)))
{
+57 -13
View File
@@ -751,24 +751,57 @@ void TrianglesGUI::setNumBlocks(int count, int nTotalBlocks)
QString tooltip;
QString importText;
importText = tr("Synchronizing with network...");
if(count < nTotalBlocks)
{
// Calculate blocks/sec - only update rate when new blocks arrive
static int lastCount = 0;
static qint64 lastRateTime = 0;
static float blocksPerSec = 0.0f;
qint64 now = QDateTime::currentMSecsSinceEpoch();
if (count > lastCount) {
// New blocks arrived - recalculate speed
if (lastRateTime > 0) {
float elapsed = (now - lastRateTime) / 1000.0f;
if (elapsed > 0.1f) {
float instantRate = (count - lastCount) / elapsed;
blocksPerSec = (blocksPerSec < 0.1f) ? instantRate : (blocksPerSec * 0.7f + instantRate * 0.3f);
}
}
lastCount = count;
lastRateTime = now;
} else if (lastRateTime > 0 && (now - lastRateTime) > 10000) {
// No blocks for 10+ seconds - show 0
blocksPerSec = 0.0f;
}
int nRemainingBlocks = nTotalBlocks - count;
float nPercentageDone = count / (nTotalBlocks * 0.01f);
progressBarLabel->setText(importText);
// Build informative status text
QString speedText;
if (blocksPerSec >= 1.0f) {
int etaSeconds = (int)(nRemainingBlocks / blocksPerSec);
QString etaStr;
if (etaSeconds < 60)
etaStr = tr("%n sec", "", etaSeconds);
else if (etaSeconds < 3600)
etaStr = tr("%n min", "", etaSeconds / 60);
else
etaStr = tr("%1h %2m").arg(etaSeconds / 3600).arg((etaSeconds % 3600) / 60);
speedText = tr("Syncing: %1 blk/s ~%2 remaining").arg(blocksPerSec, 0, 'f', 1).arg(etaStr);
} else {
speedText = tr("Synchronizing with network...");
}
progressBarLabel->setText(speedText);
progressBarLabel->setVisible(true);
progressBar->setFormat(tr("~%n block(s) remaining", "", nRemainingBlocks));
progressBar->setFormat(tr("Block %1 / %2 (%3%)").arg(count).arg(nTotalBlocks).arg(nPercentageDone, 0, 'f', 2));
progressBar->setMaximum(nTotalBlocks);
progressBar->setValue(count);
progressBar->setVisible(true);
ui->label_blocks->setText(tr("%n blocks", "", count));
ui->label_blocks->setVisible(true);
ui->label_blocks->setVisible(false);
tooltip = tr("Downloaded %1 of %2 blocks of transaction history (%3% done).").arg(count).arg(nTotalBlocks).arg(nPercentageDone, 0, 'f', 2);
tooltip = tr("Downloaded %1 of %2 blocks of transaction history (%3% done).").arg(count).arg(nTotalBlocks).arg(nPercentageDone, 0, 'f', 2);
}
else
{
@@ -871,16 +904,27 @@ void TrianglesGUI::changeEvent(QEvent *e)
void TrianglesGUI::closeEvent(QCloseEvent *event)
{
#ifndef Q_OS_MAC // Ignored on Mac
if(clientModel)
{
#ifndef Q_OS_MAC // Ignored on Mac
if(!clientModel->getOptionsModel()->getMinimizeToTray() &&
!clientModel->getOptionsModel()->getMinimizeOnClose())
if(clientModel->getOptionsModel()->getMinimizeOnClose())
{
QApplication::quit();
// Minimize to taskbar instead of closing
QMainWindow::showMinimized();
event->ignore();
return;
}
if(clientModel->getOptionsModel()->getMinimizeToTray() && trayIcon)
{
// Hide to system tray instead of closing
hide();
event->ignore();
return;
}
#endif
}
#endif
// Actually closing - quit the application
QApplication::quit();
QMainWindow::closeEvent(event);
}
+925
View File
@@ -0,0 +1,925 @@
// Copyright (c) 2024 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "rest.h"
#include "trianglesrpc.h"
#include "main.h"
#include "sync.h"
#include "util.h"
#include "base58.h"
#include "addressindex.h"
#include "wallet.h"
#include "init.h"
#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>
using namespace std;
using namespace json_spirit;
// Forward declarations from rpcblockchain.cpp
extern Object blockToJSON(const CBlock& block, const CBlockIndex* blockindex, bool fPrintTransactionDetail);
extern void TxToJSON(const CTransaction& tx, const uint256 hashBlock, json_spirit::Object& entry);
// Forward declaration from trianglesrpc.cpp
extern bool HTTPAuthorized(map<string, string>& mapHeaders);
extern string rfc1123Time();
// ============================================================================
// Rate limiter
// ============================================================================
struct IPRateLimiter
{
int64_t nTokens;
int64_t nLastRefill;
IPRateLimiter() : nTokens(0), nLastRefill(0) {}
bool Allow(int64_t nMaxTokens, int64_t nRefillRate)
{
int64_t nNow = GetTime();
if (nLastRefill == 0) {
nLastRefill = nNow;
nTokens = nMaxTokens;
}
// Refill tokens
int64_t nElapsed = nNow - nLastRefill;
if (nElapsed > 0) {
nTokens += nElapsed * nRefillRate;
if (nTokens > nMaxTokens)
nTokens = nMaxTokens;
nLastRefill = nNow;
}
if (nTokens > 0) {
nTokens--;
return true;
}
return false;
}
};
static CCriticalSection cs_rateLimiter;
static map<string, IPRateLimiter> mapRateLimiters;
static int64_t nLastCleanup = 0;
bool CheckRESTRateLimit(const string& strIP)
{
int64_t nLimit = GetArg("-restratelimit", 30);
int64_t nBurst = nLimit * 2; // burst = 2x sustained rate
if (nLimit <= 0)
return true; // rate limiting disabled
LOCK(cs_rateLimiter);
// Periodic cleanup of stale entries (every 60s)
int64_t nNow = GetTime();
if (nNow - nLastCleanup > 60) {
map<string, IPRateLimiter>::iterator it = mapRateLimiters.begin();
while (it != mapRateLimiters.end()) {
if (nNow - it->second.nLastRefill > 300) // 5 min stale
mapRateLimiters.erase(it++);
else
++it;
}
nLastCleanup = nNow;
}
return mapRateLimiters[strIP].Allow(nBurst, nLimit);
}
// ============================================================================
// HTTP response helpers
// ============================================================================
string HTTPReplyREST(int nStatus, const string& strMsg, const string& contentType)
{
string strCorsOrigin = GetArg("-restcorsorigin", "*");
const char *cStatus;
if (nStatus == 200) cStatus = "OK";
else if (nStatus == 204) cStatus = "No Content";
else if (nStatus == 400) cStatus = "Bad Request";
else if (nStatus == 401) cStatus = "Unauthorized";
else if (nStatus == 403) cStatus = "Forbidden";
else if (nStatus == 404) cStatus = "Not Found";
else if (nStatus == 429) cStatus = "Too Many Requests";
else if (nStatus == 500) cStatus = "Internal Server Error";
else if (nStatus == 503) cStatus = "Service Unavailable";
else cStatus = "";
return strprintf(
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"Content-Length: %" PRIszu "\r\n"
"Content-Type: %s\r\n"
"Access-Control-Allow-Origin: %s\r\n"
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"
"Access-Control-Allow-Headers: Content-Type, Authorization\r\n"
"Access-Control-Max-Age: 86400\r\n"
"Server: Triangles/%s\r\n"
"\r\n"
"%s",
nStatus,
cStatus,
rfc1123Time().c_str(),
strMsg.size(),
contentType.c_str(),
strCorsOrigin.c_str(),
FormatFullVersion().c_str(),
strMsg.c_str());
}
// ============================================================================
// URL parsing
// ============================================================================
static void ParseRESTPath(const string& strURI, vector<string>& parts, map<string, string>& queryParams)
{
string path = strURI;
// Split query string
size_t qpos = path.find('?');
string queryString;
if (qpos != string::npos) {
queryString = path.substr(qpos + 1);
path = path.substr(0, qpos);
}
// Split path into parts
boost::split(parts, path, boost::is_any_of("/"));
// Parse query parameters
if (!queryString.empty()) {
vector<string> pairs;
boost::split(pairs, queryString, boost::is_any_of("&"));
for (size_t i = 0; i < pairs.size(); i++) {
size_t eq = pairs[i].find('=');
if (eq != string::npos)
queryParams[pairs[i].substr(0, eq)] = pairs[i].substr(eq + 1);
}
}
}
bool IsRESTPath(const string& strURI)
{
return strURI.size() >= 6 && strURI.substr(0, 6) == "/rest/";
}
// ============================================================================
// Auth helpers
// ============================================================================
static bool RESTAuthorized(map<string, string>& mapHeaders)
{
// Check Bearer token first (if -restapikey is set)
string strApiKey = GetArg("-restapikey", "");
if (!strApiKey.empty()) {
string strAuth = mapHeaders.count("authorization") ? mapHeaders["authorization"] : "";
if (strAuth.substr(0, 7) == "Bearer ") {
string strToken = strAuth.substr(7);
boost::trim(strToken);
if (TimingResistantEqual(strToken, strApiKey))
return true;
}
}
// Fall back to Basic Auth (same as RPC)
if (mapHeaders.count("authorization"))
return HTTPAuthorized(mapHeaders);
return false;
}
// ============================================================================
// JSON error helper
// ============================================================================
static string RESTError(const string& message, int code = -1)
{
Object obj;
obj.push_back(Pair("error", message));
if (code != -1)
obj.push_back(Pair("code", code));
return write_string(Value(obj), false) + "\n";
}
// ============================================================================
// Helper: call an RPC method and return JSON string
// ============================================================================
static bool CallRPCMethod(const string& method, const Array& params,
string& strReply, int& nStatus)
{
try {
Value result = tableRPC.execute(method, params);
strReply = write_string(result, false) + "\n";
nStatus = HTTP_OK;
return true;
}
catch (Object& objError) {
int code = find_value(objError, "code").get_int();
string msg = find_value(objError, "message").get_str();
if (code == RPC_METHOD_NOT_FOUND) nStatus = HTTP_NOT_FOUND;
else if (code == RPC_WALLET_UNLOCK_NEEDED) nStatus = HTTP_FORBIDDEN;
else if (code == RPC_INVALID_PARAMETER || code == RPC_INVALID_ADDRESS_OR_KEY) nStatus = HTTP_BAD_REQUEST;
else nStatus = HTTP_INTERNAL_SERVER_ERROR;
strReply = RESTError(msg, code);
return true;
}
catch (std::exception& e) {
nStatus = HTTP_INTERNAL_SERVER_ERROR;
strReply = RESTError(e.what());
return true;
}
}
// ============================================================================
// Public endpoint handlers
// ============================================================================
// GET /rest/chaininfo
static bool HandleChainInfo(string& strReply, int& nStatus)
{
LOCK(cs_main);
Object obj, diff;
obj.push_back(Pair("chain", fTestNet ? string("test") : string("main")));
obj.push_back(Pair("blocks", (int)nBestHeight));
obj.push_back(Pair("bestblockhash", hashBestChain.GetHex()));
diff.push_back(Pair("proof-of-work", GetDifficulty()));
diff.push_back(Pair("proof-of-stake", GetDifficulty(GetLastBlockIndex(pindexBest, true))));
obj.push_back(Pair("difficulty", diff));
obj.push_back(Pair("moneysupply", ValueFromAmount(pindexBest->nMoneySupply)));
strReply = write_string(Value(obj), false) + "\n";
nStatus = HTTP_OK;
return true;
}
// GET /rest/block/{hash_or_param}[.hex]
static bool HandleBlock(const string& param, const string& format, string& strReply, int& nStatus)
{
LOCK(cs_main);
uint256 hash(param);
if (mapBlockIndex.count(hash) == 0) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Block not found");
return true;
}
CBlock block;
CBlockIndex* pblockindex = mapBlockIndex[hash];
block.ReadFromDisk(pblockindex, true);
if (format == "hex") {
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
ssBlock << block;
strReply = HexStr(ssBlock.begin(), ssBlock.end()) + "\n";
} else {
Object obj = blockToJSON(block, pblockindex, false);
strReply = write_string(Value(obj), false) + "\n";
}
nStatus = HTTP_OK;
return true;
}
// GET /rest/blockheader/{hash}
static bool HandleBlockHeader(const string& param, string& strReply, int& nStatus)
{
LOCK(cs_main);
uint256 hash(param);
if (mapBlockIndex.count(hash) == 0) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Block not found");
return true;
}
CBlockIndex* pblockindex = mapBlockIndex[hash];
Object result;
result.push_back(Pair("hash", pblockindex->GetBlockHash().GetHex()));
result.push_back(Pair("confirmations", pindexBest->nHeight - pblockindex->nHeight + 1));
result.push_back(Pair("height", pblockindex->nHeight));
result.push_back(Pair("version", pblockindex->nVersion));
result.push_back(Pair("merkleroot", pblockindex->hashMerkleRoot.GetHex()));
result.push_back(Pair("time", (boost::int64_t)pblockindex->GetBlockTime()));
result.push_back(Pair("nonce", (boost::uint64_t)pblockindex->nNonce));
result.push_back(Pair("bits", HexBits(pblockindex->nBits)));
result.push_back(Pair("difficulty", GetDifficulty(pblockindex)));
result.push_back(Pair("flags", strprintf("%s%s",
pblockindex->IsProofOfStake() ? "proof-of-stake" : "proof-of-work",
pblockindex->GeneratedStakeModifier() ? " stake-modifier" : "")));
if (pblockindex->pprev)
result.push_back(Pair("previousblockhash", pblockindex->pprev->GetBlockHash().GetHex()));
if (pblockindex->pnext)
result.push_back(Pair("nextblockhash", pblockindex->pnext->GetBlockHash().GetHex()));
strReply = write_string(Value(result), false) + "\n";
nStatus = HTTP_OK;
return true;
}
// GET /rest/tx/{txid}[.hex]
static bool HandleTx(const string& param, const string& format, string& strReply, int& nStatus)
{
LOCK(cs_main);
uint256 hash(param);
CTransaction tx;
uint256 hashBlock = 0;
if (!GetTransaction(hash, tx, hashBlock)) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Transaction not found");
return true;
}
if (format == "hex") {
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << tx;
strReply = HexStr(ssTx.begin(), ssTx.end()) + "\n";
} else {
Object obj;
obj.push_back(Pair("txid", tx.GetHash().GetHex()));
TxToJSON(tx, hashBlock, obj);
strReply = write_string(Value(obj), false) + "\n";
}
nStatus = HTTP_OK;
return true;
}
// GET /rest/blockhashbyheight/{n}
static bool HandleBlockHashByHeight(const string& param, string& strReply, int& nStatus)
{
LOCK(cs_main);
int nHeight = atoi(param.c_str());
if (nHeight < 0 || nHeight > nBestHeight) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Block height out of range");
return true;
}
CBlockIndex* pblockindex = FindBlockByHeight(nHeight);
Object obj;
obj.push_back(Pair("blockhash", pblockindex->phashBlock->GetHex()));
strReply = write_string(Value(obj), false) + "\n";
nStatus = HTTP_OK;
return true;
}
// GET /rest/blockbyheight/{n}
static bool HandleBlockByHeight(const string& param, const string& format, string& strReply, int& nStatus)
{
LOCK(cs_main);
int nHeight = atoi(param.c_str());
if (nHeight < 0 || nHeight > nBestHeight) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Block height out of range");
return true;
}
CBlockIndex* pblockindex = FindBlockByHeight(nHeight);
CBlock block;
block.ReadFromDisk(pblockindex, true);
if (format == "hex") {
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
ssBlock << block;
strReply = HexStr(ssBlock.begin(), ssBlock.end()) + "\n";
} else {
Object obj = blockToJSON(block, pblockindex, false);
strReply = write_string(Value(obj), false) + "\n";
}
nStatus = HTTP_OK;
return true;
}
// GET /rest/mempool
static bool HandleMempool(string& strReply, int& nStatus)
{
vector<uint256> vtxid;
mempool.queryHashes(vtxid);
Array a;
BOOST_FOREACH(const uint256& hash, vtxid)
a.push_back(hash.ToString());
strReply = write_string(Value(a), false) + "\n";
nStatus = HTTP_OK;
return true;
}
// GET /rest/difficulty
static bool HandleDifficulty(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getdifficulty", params, strReply, nStatus);
}
// GET /rest/supply
static bool HandleSupply(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("gettxoutsetinfo", params, strReply, nStatus);
}
// GET /rest/staking
static bool HandleStaking(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getstakinginfo", params, strReply, nStatus);
}
// GET /rest/mining
static bool HandleMining(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getmininginfo", params, strReply, nStatus);
}
// GET /rest/subsidy
static bool HandleSubsidy(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getsubsidy", params, strReply, nStatus);
}
// GET /rest/estimatefee
static bool HandleEstimateFee(string& strReply, int& nStatus)
{
Array params;
params.push_back(6); // default 6 blocks
return CallRPCMethod("estimatefee", params, strReply, nStatus);
}
// GET /rest/checkpoint
static bool HandleCheckpoint(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getcheckpoint", params, strReply, nStatus);
}
// GET /rest/network
static bool HandleNetwork(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getnetworkinfo", params, strReply, nStatus);
}
// GET /rest/peers
static bool HandlePeers(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getpeerinfo", params, strReply, nStatus);
}
// GET /rest/validate/{address}
static bool HandleValidate(const string& addr, string& strReply, int& nStatus)
{
Array params;
params.push_back(addr);
return CallRPCMethod("validateaddress", params, strReply, nStatus);
}
// GET /rest/address/{addr}/balance
static bool HandleAddressBalance(const string& addr, string& strReply, int& nStatus)
{
if (!fAddressIndex) {
nStatus = 503;
strReply = RESTError("Address index not enabled. Start daemon with -addressindex=1");
return true;
}
Object addrObj;
Array addrArray;
addrArray.push_back(addr);
addrObj.push_back(Pair("addresses", addrArray));
Array params;
params.push_back(addrObj);
return CallRPCMethod("getaddressbalance", params, strReply, nStatus);
}
// GET /rest/address/{addr}/utxos
static bool HandleAddressUtxos(const string& addr, string& strReply, int& nStatus)
{
if (!fAddressIndex) {
nStatus = 503;
strReply = RESTError("Address index not enabled. Start daemon with -addressindex=1");
return true;
}
Object addrObj;
Array addrArray;
addrArray.push_back(addr);
addrObj.push_back(Pair("addresses", addrArray));
Array params;
params.push_back(addrObj);
return CallRPCMethod("getaddressutxos", params, strReply, nStatus);
}
// GET /rest/address/{addr}/txids[?start=N&end=N]
static bool HandleAddressTxids(const string& addr, const map<string, string>& queryParams,
string& strReply, int& nStatus)
{
if (!fAddressIndex) {
nStatus = 503;
strReply = RESTError("Address index not enabled. Start daemon with -addressindex=1");
return true;
}
Object addrObj;
Array addrArray;
addrArray.push_back(addr);
addrObj.push_back(Pair("addresses", addrArray));
map<string, string>::const_iterator itStart = queryParams.find("start");
map<string, string>::const_iterator itEnd = queryParams.find("end");
if (itStart != queryParams.end())
addrObj.push_back(Pair("start", atoi(itStart->second.c_str())));
if (itEnd != queryParams.end())
addrObj.push_back(Pair("end", atoi(itEnd->second.c_str())));
Array params;
params.push_back(addrObj);
return CallRPCMethod("getaddresstxids", params, strReply, nStatus);
}
// POST /rest/tx/decode body: {"hex":"..."}
static bool HandleTxDecode(const string& strBody, string& strReply, int& nStatus)
{
Value valBody;
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Invalid JSON body. Expected: {\"hex\":\"...\"}");
return true;
}
Object bodyObj = valBody.get_obj();
Value hexVal = find_value(bodyObj, "hex");
if (hexVal.type() != str_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Missing 'hex' field in request body");
return true;
}
Array params;
params.push_back(hexVal.get_str());
return CallRPCMethod("decoderawtransaction", params, strReply, nStatus);
}
// POST /rest/tx/send body: {"hex":"..."}
static bool HandleTxSend(const string& strBody, string& strReply, int& nStatus)
{
Value valBody;
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Invalid JSON body. Expected: {\"hex\":\"...\"}");
return true;
}
Object bodyObj = valBody.get_obj();
Value hexVal = find_value(bodyObj, "hex");
if (hexVal.type() != str_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Missing 'hex' field in request body");
return true;
}
Array params;
params.push_back(hexVal.get_str());
return CallRPCMethod("sendrawtransaction", params, strReply, nStatus);
}
// ============================================================================
// Wallet endpoint handlers (authenticated)
// ============================================================================
// GET /rest/wallet/info
static bool HandleWalletInfo(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getwalletinfo", params, strReply, nStatus);
}
// GET /rest/wallet/balance
static bool HandleWalletBalance(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getbalance", params, strReply, nStatus);
}
// GET /rest/wallet/transactions[?count=N&skip=N]
static bool HandleWalletTransactions(const map<string, string>& queryParams,
string& strReply, int& nStatus)
{
Array params;
params.push_back("*"); // all accounts
map<string, string>::const_iterator itCount = queryParams.find("count");
map<string, string>::const_iterator itSkip = queryParams.find("skip");
int nCount = 10;
int nSkip = 0;
if (itCount != queryParams.end())
nCount = atoi(itCount->second.c_str());
if (itSkip != queryParams.end())
nSkip = atoi(itSkip->second.c_str());
params.push_back(nCount);
params.push_back(nSkip);
return CallRPCMethod("listtransactions", params, strReply, nStatus);
}
// GET /rest/wallet/transaction/{txid}
static bool HandleWalletTransaction(const string& txid, string& strReply, int& nStatus)
{
Array params;
params.push_back(txid);
return CallRPCMethod("gettransaction", params, strReply, nStatus);
}
// GET /rest/wallet/unspent[?minconf=N&maxconf=N]
static bool HandleWalletUnspent(const map<string, string>& queryParams,
string& strReply, int& nStatus)
{
Array params;
map<string, string>::const_iterator itMin = queryParams.find("minconf");
map<string, string>::const_iterator itMax = queryParams.find("maxconf");
params.push_back(itMin != queryParams.end() ? atoi(itMin->second.c_str()) : 1);
params.push_back(itMax != queryParams.end() ? atoi(itMax->second.c_str()) : 9999999);
return CallRPCMethod("listunspent", params, strReply, nStatus);
}
// GET /rest/wallet/addresses
static bool HandleWalletAddresses(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("listaddressgroupings", params, strReply, nStatus);
}
// GET /rest/wallet/staking
static bool HandleWalletStaking(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("getstakinginfo", params, strReply, nStatus);
}
// POST /rest/wallet/address/new body: {} or {"account":"..."}
static bool HandleWalletNewAddress(const string& strBody, string& strReply, int& nStatus)
{
Array params;
if (!strBody.empty()) {
Value valBody;
if (read_string(strBody, valBody) && valBody.type() == obj_type) {
Value acctVal = find_value(valBody.get_obj(), "account");
if (acctVal.type() == str_type)
params.push_back(acctVal.get_str());
}
}
return CallRPCMethod("getnewaddress", params, strReply, nStatus);
}
// POST /rest/wallet/send body: {"address":"...", "amount":N}
static bool HandleWalletSend(const string& strBody, string& strReply, int& nStatus)
{
Value valBody;
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Invalid JSON body. Expected: {\"address\":\"...\", \"amount\":N}");
return true;
}
Object bodyObj = valBody.get_obj();
Value addrVal = find_value(bodyObj, "address");
Value amtVal = find_value(bodyObj, "amount");
if (addrVal.type() != str_type || (amtVal.type() != real_type && amtVal.type() != int_type)) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Missing 'address' (string) or 'amount' (number) in request body");
return true;
}
Array params;
params.push_back(addrVal.get_str());
params.push_back(amtVal);
// Optional comment fields
Value commentVal = find_value(bodyObj, "comment");
Value commentToVal = find_value(bodyObj, "comment_to");
if (commentVal.type() == str_type)
params.push_back(commentVal.get_str());
else
params.push_back("");
if (commentToVal.type() == str_type)
params.push_back(commentToVal.get_str());
return CallRPCMethod("sendtoaddress", params, strReply, nStatus);
}
// POST /rest/wallet/sendmany body: {"recipients":{"addr":amount,...}}
static bool HandleWalletSendMany(const string& strBody, string& strReply, int& nStatus)
{
Value valBody;
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Invalid JSON body. Expected: {\"recipients\":{\"addr\":amount,...}}");
return true;
}
Object bodyObj = valBody.get_obj();
Value recipVal = find_value(bodyObj, "recipients");
if (recipVal.type() != obj_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Missing 'recipients' object in request body");
return true;
}
Array params;
params.push_back(""); // fromaccount (default)
params.push_back(recipVal); // {addr: amount, ...}
return CallRPCMethod("sendmany", params, strReply, nStatus);
}
// POST /rest/wallet/unlock body: {"passphrase":"...", "timeout":N, "staking_only":bool}
static bool HandleWalletUnlock(const string& strBody, string& strReply, int& nStatus)
{
Value valBody;
if (!read_string(strBody, valBody) || valBody.type() != obj_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Invalid JSON body. Expected: {\"passphrase\":\"...\", \"timeout\":N}");
return true;
}
Object bodyObj = valBody.get_obj();
Value passVal = find_value(bodyObj, "passphrase");
Value timeVal = find_value(bodyObj, "timeout");
if (passVal.type() != str_type || timeVal.type() != int_type) {
nStatus = HTTP_BAD_REQUEST;
strReply = RESTError("Missing 'passphrase' (string) or 'timeout' (integer) in request body");
return true;
}
Array params;
params.push_back(passVal.get_str());
params.push_back(timeVal.get_int());
Value stakingVal = find_value(bodyObj, "staking_only");
if (stakingVal.type() == bool_type)
params.push_back(stakingVal.get_bool());
return CallRPCMethod("walletpassphrase", params, strReply, nStatus);
}
// POST /rest/wallet/lock
static bool HandleWalletLock(string& strReply, int& nStatus)
{
Array params;
return CallRPCMethod("walletlock", params, strReply, nStatus);
}
// ============================================================================
// Main router
// ============================================================================
bool HandleRESTRequest(const string& strMethod,
const string& strURI,
const string& strBody,
map<string, string>& mapHeaders,
string& strReply,
string& strContentType,
int& nStatus)
{
strContentType = "application/json";
// OPTIONS: CORS preflight
if (strMethod == "OPTIONS") {
nStatus = 204;
strReply = "";
return true;
}
// Parse path
vector<string> parts;
map<string, string> queryParams;
ParseRESTPath(strURI, parts, queryParams);
// parts: ["", "rest", "resource", "param", ...]
if (parts.size() < 3) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Not found");
return true;
}
string resource = parts[2];
// Detect format suffix (.hex, .json)
string format = "json";
string lastPart = parts.size() > 3 ? parts[parts.size() - 1] : "";
size_t dotPos = lastPart.rfind('.');
string param;
if (dotPos != string::npos) {
param = lastPart.substr(0, dotPos);
format = lastPart.substr(dotPos + 1);
} else {
param = lastPart;
}
if (format == "hex")
strContentType = "text/plain";
// ---- Wallet endpoints (authenticated) ----
if (resource == "wallet") {
if (!RESTAuthorized(mapHeaders)) {
nStatus = HTTP_UNAUTHORIZED;
strReply = RESTError("Authentication required for wallet endpoints");
return true;
}
if (parts.size() < 4) {
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Unknown wallet endpoint");
return true;
}
string walletResource = parts[3];
if (strMethod == "GET") {
if (walletResource == "info")
return HandleWalletInfo(strReply, nStatus);
if (walletResource == "balance")
return HandleWalletBalance(strReply, nStatus);
if (walletResource == "transactions")
return HandleWalletTransactions(queryParams, strReply, nStatus);
if (walletResource == "transaction" && parts.size() > 4)
return HandleWalletTransaction(parts[4], strReply, nStatus);
if (walletResource == "unspent")
return HandleWalletUnspent(queryParams, strReply, nStatus);
if (walletResource == "addresses")
return HandleWalletAddresses(strReply, nStatus);
if (walletResource == "staking")
return HandleWalletStaking(strReply, nStatus);
}
else if (strMethod == "POST") {
if (walletResource == "address" && parts.size() > 4 && parts[4] == "new")
return HandleWalletNewAddress(strBody, strReply, nStatus);
if (walletResource == "send")
return HandleWalletSend(strBody, strReply, nStatus);
if (walletResource == "sendmany")
return HandleWalletSendMany(strBody, strReply, nStatus);
if (walletResource == "unlock")
return HandleWalletUnlock(strBody, strReply, nStatus);
if (walletResource == "lock")
return HandleWalletLock(strReply, nStatus);
}
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Unknown wallet endpoint");
return true;
}
// ---- Public GET endpoints ----
if (strMethod == "GET") {
if (resource == "chaininfo")
return HandleChainInfo(strReply, nStatus);
if (resource == "block" && !param.empty())
return HandleBlock(param, format, strReply, nStatus);
if (resource == "blockheader" && !param.empty())
return HandleBlockHeader(param, strReply, nStatus);
if (resource == "tx") {
// /rest/tx/decode and /rest/tx/send are POST-only
if (!param.empty())
return HandleTx(param, format, strReply, nStatus);
}
if (resource == "blockhashbyheight" && !param.empty())
return HandleBlockHashByHeight(param, strReply, nStatus);
if (resource == "blockbyheight" && !param.empty())
return HandleBlockByHeight(param, format, strReply, nStatus);
if (resource == "mempool")
return HandleMempool(strReply, nStatus);
if (resource == "difficulty")
return HandleDifficulty(strReply, nStatus);
if (resource == "supply")
return HandleSupply(strReply, nStatus);
if (resource == "staking")
return HandleStaking(strReply, nStatus);
if (resource == "mining")
return HandleMining(strReply, nStatus);
if (resource == "subsidy")
return HandleSubsidy(strReply, nStatus);
if (resource == "estimatefee")
return HandleEstimateFee(strReply, nStatus);
if (resource == "checkpoint")
return HandleCheckpoint(strReply, nStatus);
if (resource == "network")
return HandleNetwork(strReply, nStatus);
if (resource == "peers")
return HandlePeers(strReply, nStatus);
if (resource == "validate" && !param.empty())
return HandleValidate(param, strReply, nStatus);
// Address endpoints: /rest/address/{addr}/balance etc.
if (resource == "address" && parts.size() >= 5) {
string addr = parts[3];
string addrAction = parts[4];
if (addrAction == "balance")
return HandleAddressBalance(addr, strReply, nStatus);
if (addrAction == "utxos")
return HandleAddressUtxos(addr, strReply, nStatus);
if (addrAction == "txids")
return HandleAddressTxids(addr, queryParams, strReply, nStatus);
}
}
// ---- Public POST endpoints ----
if (strMethod == "POST") {
if (resource == "tx" && parts.size() >= 4) {
string txAction = parts[3];
if (txAction == "decode")
return HandleTxDecode(strBody, strReply, nStatus);
if (txAction == "send")
return HandleTxSend(strBody, strReply, nStatus);
}
}
nStatus = HTTP_NOT_FOUND;
strReply = RESTError("Unknown REST endpoint");
return true;
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright (c) 2024 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef TRIANGLES_REST_H
#define TRIANGLES_REST_H
#include <string>
#include <map>
class CBlock;
class CBlockIndex;
class CTransaction;
// REST API response builder with CORS headers
std::string HTTPReplyREST(int nStatus, const std::string& strMsg,
const std::string& contentType = "application/json");
// Main REST request router
// Returns true if the URI was handled as a REST request, false if it should fall through to RPC
bool HandleRESTRequest(const std::string& strMethod,
const std::string& strURI,
const std::string& strBody,
std::map<std::string, std::string>& mapHeaders,
std::string& strReply,
std::string& strContentType,
int& nStatus);
// Check if a URI is a REST API path
bool IsRESTPath(const std::string& strURI);
// Check rate limit for an IP address. Returns true if allowed.
bool CheckRESTRateLimit(const std::string& strIP);
#endif // TRIANGLES_REST_H
+413
View File
@@ -0,0 +1,413 @@
// Copyright (c) 2024-2025 Triangles developers
// Tor Process Manager - launches and manages an external Tor binary
// Distributed under the MIT/X11 software license
#ifdef WIN32
#define NOMINMAX
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif
#endif
#include "tor_process.h"
#include "../util.h"
#include "../net.h"
#include <boost/filesystem.hpp>
#include <fstream>
#include <cstdio>
#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <tlhelp32.h>
#else
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <unistd.h>
#endif
namespace fs = boost::filesystem;
static CTorProcess* torProcessInstance = nullptr;
CTorProcess* CTorProcess::GetInstance()
{
if (!torProcessInstance) {
torProcessInstance = new CTorProcess();
}
return torProcessInstance;
}
CTorProcess::CTorProcess()
: socksPort(19099)
, hiddenServicePort(24112)
, running(false)
#ifdef WIN32
, hProcess(NULL)
, processId(0)
#else
, processId(0)
#endif
{
}
CTorProcess::~CTorProcess()
{
Stop();
}
std::string CTorProcess::FindTorBinary()
{
// List of candidate paths to search for the Tor binary
std::vector<std::string> candidates;
#ifdef WIN32
// Same directory as the wallet executable
char exePath[MAX_PATH];
if (GetModuleFileNameA(NULL, exePath, MAX_PATH)) {
fs::path exeDir = fs::path(exePath).parent_path();
candidates.push_back((exeDir / "tor.exe").string());
candidates.push_back((exeDir / "tor" / "tor.exe").string());
candidates.push_back((exeDir / "Tor" / "tor.exe").string());
}
// Data directory
candidates.push_back((GetDataDir() / "tor.exe").string());
candidates.push_back((GetDataDir() / "tor" / "tor.exe").string());
// Common Windows install locations
const char* programFiles = getenv("ProgramFiles");
if (programFiles) {
candidates.push_back(std::string(programFiles) + "\\Tor\\tor.exe");
candidates.push_back(std::string(programFiles) + "\\Tor Browser\\Browser\\TorBrowser\\Tor\\tor.exe");
}
const char* programFilesX86 = getenv("ProgramFiles(x86)");
if (programFilesX86) {
candidates.push_back(std::string(programFilesX86) + "\\Tor\\tor.exe");
candidates.push_back(std::string(programFilesX86) + "\\Tor Browser\\Browser\\TorBrowser\\Tor\\tor.exe");
}
const char* localAppData = getenv("LOCALAPPDATA");
if (localAppData) {
candidates.push_back(std::string(localAppData) + "\\Tor Browser\\Browser\\TorBrowser\\Tor\\tor.exe");
}
// Tor Expert Bundle (common install)
candidates.push_back("C:\\Tor\\tor.exe");
#else
// Same directory as the wallet executable
char exePath[4096];
ssize_t len = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
if (len > 0) {
exePath[len] = '\0';
fs::path exeDir = fs::path(exePath).parent_path();
candidates.push_back((exeDir / "tor").string());
candidates.push_back((exeDir / "tor" / "tor").string());
}
// Data directory
candidates.push_back((GetDataDir() / "tor").string());
candidates.push_back((GetDataDir() / "tor" / "tor").string());
// Standard Linux/macOS locations
candidates.push_back("/usr/bin/tor");
candidates.push_back("/usr/local/bin/tor");
candidates.push_back("/usr/sbin/tor");
candidates.push_back("/opt/tor/bin/tor");
candidates.push_back("/snap/bin/tor");
// Homebrew (macOS)
candidates.push_back("/opt/homebrew/bin/tor");
candidates.push_back("/usr/local/opt/tor/bin/tor");
#endif
// Check each candidate
for (const std::string& path : candidates) {
if (fs::exists(path)) {
printf("Found Tor binary at: %s\n", path.c_str());
return path;
}
}
return "";
}
bool CTorProcess::IsPortInUse(int port)
{
#ifdef WIN32
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) return false;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(port);
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
closesocket(sock);
return (result == 0);
#else
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) return false;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(port);
int result = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
close(sock);
return (result == 0);
#endif
}
bool CTorProcess::WriteTorrc()
{
fs::path dataPath(torDataDir);
fs::create_directories(dataPath);
torrcPath = (dataPath / "torrc").string();
// Hidden service directory
fs::path hsDir = dataPath / "hidden_service";
fs::create_directories(hsDir);
std::ofstream torrc(torrcPath.c_str());
if (!torrc.is_open()) {
printf("ERROR: Cannot write torrc to %s\n", torrcPath.c_str());
return false;
}
torrc << "# Triangles Wallet Tor Configuration (auto-generated)\n";
torrc << "# Do not edit - this file is overwritten on startup\n\n";
// SOCKS proxy for wallet connections
torrc << "SocksPort " << socksPort << "\n";
// Data directory for Tor state
fs::path torStateDir = dataPath / "state";
fs::create_directories(torStateDir);
torrc << "DataDirectory " << torStateDir.string() << "\n";
// V3 hidden service so this node is reachable via .onion
torrc << "HiddenServiceDir " << hsDir.string() << "\n";
torrc << "HiddenServiceVersion 3\n";
torrc << "HiddenServicePort " << hiddenServicePort
<< " 127.0.0.1:" << hiddenServicePort << "\n";
// Reduce bandwidth/resource usage for wallet use
torrc << "ClientOnly 1\n";
// Disable unused features
torrc << "AvoidDiskWrites 1\n";
torrc << "Log notice stderr\n";
torrc.close();
printf("Wrote torrc to %s (SOCKS %d, HS port %d)\n",
torrcPath.c_str(), socksPort, hiddenServicePort);
return true;
}
bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort)
{
socksPort = socks;
hiddenServicePort = hsPort;
torDataDir = dataDir;
// Check if something is already listening on our SOCKS port
if (IsPortInUse(socksPort)) {
printf("Tor SOCKS port %d already in use - assuming Tor is running\n", socksPort);
running = true;
return true;
}
// Find Tor binary
torBinaryPath = FindTorBinary();
if (torBinaryPath.empty()) {
printf("WARNING: Tor binary not found. Install Tor for .onion connectivity.\n");
printf(" Windows: Download from https://www.torproject.org/download/tor/\n");
printf(" Linux: apt install tor or yum install tor\n");
printf(" Place tor executable next to the wallet binary for auto-detection.\n");
return false;
}
// Write configuration
if (!WriteTorrc()) {
printf("ERROR: Failed to write Tor configuration\n");
return false;
}
printf("Starting Tor process: %s -f %s\n", torBinaryPath.c_str(), torrcPath.c_str());
#ifdef WIN32
STARTUPINFOA si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE; // Run hidden
ZeroMemory(&pi, sizeof(pi));
std::string cmdLine = "\"" + torBinaryPath + "\" -f \"" + torrcPath + "\"";
if (!CreateProcessA(
NULL,
(LPSTR)cmdLine.c_str(),
NULL, NULL,
FALSE,
CREATE_NO_WINDOW,
NULL, NULL,
&si, &pi))
{
printf("ERROR: Failed to start Tor process (error %lu)\n", GetLastError());
return false;
}
hProcess = pi.hProcess;
processId = pi.dwProcessId;
CloseHandle(pi.hThread);
printf("Tor process started (PID %lu)\n", processId);
#else
pid_t pid = fork();
if (pid < 0) {
printf("ERROR: Failed to fork for Tor process\n");
return false;
}
if (pid == 0) {
// Child process - exec Tor
// Redirect stdout/stderr to /dev/null to avoid cluttering wallet output
freopen("/dev/null", "w", stdout);
freopen("/dev/null", "w", stderr);
execl(torBinaryPath.c_str(), torBinaryPath.c_str(),
"-f", torrcPath.c_str(), (char*)NULL);
// If exec fails, exit child
_exit(1);
}
processId = pid;
printf("Tor process started (PID %d)\n", processId);
#endif
running = true;
// Wait a few seconds for Tor to bootstrap, then check if SOCKS is up
printf("Waiting for Tor to bootstrap...\n");
for (int i = 0; i < 30; i++) {
MilliSleep(1000);
if (fShutdown) {
Stop();
return false;
}
if (IsPortInUse(socksPort)) {
printf("Tor SOCKS proxy ready on port %d (took %ds)\n", socksPort, i + 1);
// Read and display the hidden service hostname if available
fs::path hsHostname = fs::path(torDataDir) / "hidden_service" / "hostname";
if (fs::exists(hsHostname)) {
std::ifstream f(hsHostname.string().c_str());
std::string hostname;
if (f.is_open() && std::getline(f, hostname)) {
printf("Tor hidden service: %s\n", hostname.c_str());
}
}
return true;
}
// Check if Tor process is still alive
if (!IsRunning()) {
printf("ERROR: Tor process exited prematurely\n");
running = false;
return false;
}
}
printf("WARNING: Tor started but SOCKS proxy not yet ready after 30s\n");
printf(" Tor may still be bootstrapping. .onion connections will work once ready.\n");
return true;
}
void CTorProcess::Stop()
{
if (!running) return;
#ifdef WIN32
if (hProcess != NULL) {
printf("Stopping Tor process (PID %lu)...\n", processId);
TerminateProcess(hProcess, 0);
WaitForSingleObject(hProcess, 5000);
CloseHandle(hProcess);
hProcess = NULL;
}
#else
if (processId > 0) {
printf("Stopping Tor process (PID %d)...\n", processId);
kill(processId, SIGTERM);
// Wait up to 5 seconds for graceful shutdown
for (int i = 0; i < 50; i++) {
int status;
pid_t result = waitpid(processId, &status, WNOHANG);
if (result != 0) break;
MilliSleep(100);
}
// Force kill if still running
kill(processId, SIGKILL);
waitpid(processId, NULL, 0);
}
#endif
processId = 0;
running = false;
printf("Tor process stopped\n");
}
bool CTorProcess::IsRunning()
{
if (!running) return false;
#ifdef WIN32
if (hProcess == NULL) return false;
DWORD exitCode;
if (GetExitCodeProcess(hProcess, &exitCode)) {
return (exitCode == STILL_ACTIVE);
}
return false;
#else
if (processId <= 0) return false;
int status;
pid_t result = waitpid(processId, &status, WNOHANG);
if (result == 0) return true; // Still running
if (result == processId) {
running = false;
return false; // Exited
}
return false;
#endif
}
std::string CTorProcess::GetSocksProxy() const
{
return "127.0.0.1:" + std::to_string(socksPort);
}
// Global convenience functions
bool StartTorProcess(const std::string& dataDir)
{
return CTorProcess::GetInstance()->Start(dataDir);
}
void StopTorProcess()
{
CTorProcess::GetInstance()->Stop();
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright (c) 2024-2025 Triangles developers
// Tor Process Manager - launches and manages an external Tor binary
// Distributed under the MIT/X11 software license
#ifndef TRIANGLES_TOR_PROCESS_H
#define TRIANGLES_TOR_PROCESS_H
#include <string>
#ifdef WIN32
#include <windows.h>
#endif
// Manages starting/stopping an external Tor process
// Provides SOCKS5 proxy for .onion connectivity and v3 hidden service
class CTorProcess
{
private:
std::string torBinaryPath;
std::string torDataDir;
std::string torrcPath;
int socksPort;
int hiddenServicePort;
bool running;
#ifdef WIN32
HANDLE hProcess;
DWORD processId;
#else
pid_t processId;
#endif
// Find the Tor binary in standard locations
std::string FindTorBinary();
// Generate torrc configuration file
bool WriteTorrc();
// Check if SOCKS port is already in use (another Tor running)
bool IsPortInUse(int port);
public:
CTorProcess();
~CTorProcess();
// Start the Tor process
// Returns true if Tor was started or is already running
bool Start(const std::string& dataDir, int socksPort = 19099, int hsPort = 24112);
// Stop the Tor process
void Stop();
// Check if Tor is running
bool IsRunning();
// Get the SOCKS proxy address
std::string GetSocksProxy() const;
// Get the Tor binary path (for diagnostics)
std::string GetBinaryPath() const { return torBinaryPath; }
// Singleton access
static CTorProcess* GetInstance();
};
// Global convenience functions
bool StartTorProcess(const std::string& dataDir);
void StopTorProcess();
#endif // TRIANGLES_TOR_PROCESS_H
+18 -183
View File
@@ -1020,187 +1020,10 @@ static string JSONRPCExecBatch(const Array& vReq)
return write_string(Value(ret), false) + "\n";
}
// REST API support
extern Object blockToJSON(const CBlock& block, const CBlockIndex* blockindex, bool fPrintTransactionDetail);
extern void TxToJSON(const CTransaction& tx, const uint256 hashBlock, json_spirit::Object& entry);
// REST API support (implementation in rest.cpp)
#include "rest.h"
static string HTTPReplyREST(int nStatus, const string& strMsg, const string& contentType = "application/json")
{
const char *cStatus;
if (nStatus == HTTP_OK) cStatus = "OK";
else if (nStatus == HTTP_BAD_REQUEST) cStatus = "Bad Request";
else if (nStatus == HTTP_NOT_FOUND) cStatus = "Not Found";
else if (nStatus == HTTP_INTERNAL_SERVER_ERROR) cStatus = "Internal Server Error";
else cStatus = "";
return strprintf(
"HTTP/1.1 %d %s\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"Content-Length: %"PRIszu"\r\n"
"Content-Type: %s\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Server: Triangles-json-rpc/%s\r\n"
"\r\n"
"%s",
nStatus,
cStatus,
rfc1123Time().c_str(),
strMsg.size(),
contentType.c_str(),
FormatFullVersion().c_str(),
strMsg.c_str());
}
static bool HandleRESTRequest(const string& strURI, string& strReply, string& strContentType, int& nStatus)
{
// Parse: /rest/<resource>[/<param>][.format]
vector<string> parts;
string uri = strURI;
// Remove query string if present
size_t qpos = uri.find('?');
if (qpos != string::npos) uri = uri.substr(0, qpos);
boost::split(parts, uri, boost::is_any_of("/"));
// parts[0]="" parts[1]="rest" parts[2]="resource" parts[3]="param.format"
if (parts.size() < 3) {
nStatus = HTTP_NOT_FOUND;
strReply = "{\"error\":\"Not found\"}";
return true;
}
string resource = parts[2];
// Get param and format from last path component
string lastPart = parts.size() > 3 ? parts[parts.size()-1] : "";
string param, format = "json";
size_t dotPos = lastPart.rfind('.');
if (dotPos != string::npos) {
param = lastPart.substr(0, dotPos);
format = lastPart.substr(dotPos+1);
} else {
param = lastPart;
}
strContentType = (format == "hex") ? "text/plain" : "application/json";
nStatus = HTTP_OK;
try {
LOCK(cs_main);
if (resource == "chaininfo") {
Object obj, diff;
obj.push_back(Pair("chain", fTestNet ? string("test") : string("main")));
obj.push_back(Pair("blocks", (int)nBestHeight));
obj.push_back(Pair("bestblockhash", hashBestChain.GetHex()));
diff.push_back(Pair("proof-of-work", GetDifficulty()));
diff.push_back(Pair("proof-of-stake", GetDifficulty(GetLastBlockIndex(pindexBest, true))));
obj.push_back(Pair("difficulty", diff));
obj.push_back(Pair("moneysupply", ValueFromAmount(pindexBest->nMoneySupply)));
strReply = write_string(Value(obj), false) + "\n";
}
else if (resource == "block" && !param.empty()) {
uint256 hash(param);
if (mapBlockIndex.count(hash) == 0) {
nStatus = HTTP_NOT_FOUND;
strReply = "{\"error\":\"Block not found\"}";
return true;
}
CBlock block;
CBlockIndex* pblockindex = mapBlockIndex[hash];
block.ReadFromDisk(pblockindex, true);
if (format == "hex") {
CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
ssBlock << block;
strReply = HexStr(ssBlock.begin(), ssBlock.end()) + "\n";
} else {
Object obj = blockToJSON(block, pblockindex, false);
strReply = write_string(Value(obj), false) + "\n";
}
}
else if (resource == "blockheader" && !param.empty()) {
uint256 hash(param);
if (mapBlockIndex.count(hash) == 0) {
nStatus = HTTP_NOT_FOUND;
strReply = "{\"error\":\"Block not found\"}";
return true;
}
CBlockIndex* pblockindex = mapBlockIndex[hash];
Object result;
result.push_back(Pair("hash", pblockindex->GetBlockHash().GetHex()));
result.push_back(Pair("confirmations", pindexBest->nHeight - pblockindex->nHeight + 1));
result.push_back(Pair("height", pblockindex->nHeight));
result.push_back(Pair("version", pblockindex->nVersion));
result.push_back(Pair("merkleroot", pblockindex->hashMerkleRoot.GetHex()));
result.push_back(Pair("time", (boost::int64_t)pblockindex->GetBlockTime()));
result.push_back(Pair("nonce", (boost::uint64_t)pblockindex->nNonce));
result.push_back(Pair("bits", HexBits(pblockindex->nBits)));
result.push_back(Pair("difficulty", GetDifficulty(pblockindex)));
result.push_back(Pair("flags", strprintf("%s%s",
pblockindex->IsProofOfStake() ? "proof-of-stake" : "proof-of-work",
pblockindex->GeneratedStakeModifier() ? " stake-modifier" : "")));
if (pblockindex->pprev)
result.push_back(Pair("previousblockhash", pblockindex->pprev->GetBlockHash().GetHex()));
if (pblockindex->pnext)
result.push_back(Pair("nextblockhash", pblockindex->pnext->GetBlockHash().GetHex()));
strReply = write_string(Value(result), false) + "\n";
}
else if (resource == "tx" && !param.empty()) {
uint256 hash(param);
CTransaction tx;
uint256 hashBlock = 0;
if (!GetTransaction(hash, tx, hashBlock))
{
nStatus = HTTP_NOT_FOUND;
strReply = "{\"error\":\"Transaction not found\"}";
return true;
}
if (format == "hex") {
CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION);
ssTx << tx;
strReply = HexStr(ssTx.begin(), ssTx.end()) + "\n";
} else {
Object obj;
obj.push_back(Pair("txid", tx.GetHash().GetHex()));
TxToJSON(tx, hashBlock, obj);
strReply = write_string(Value(obj), false) + "\n";
}
}
else if (resource == "blockhashbyheight" && !param.empty()) {
int nHeight = atoi(param.c_str());
if (nHeight < 0 || nHeight > nBestHeight) {
nStatus = HTTP_NOT_FOUND;
strReply = "{\"error\":\"Block height out of range\"}";
return true;
}
CBlockIndex* pblockindex = FindBlockByHeight(nHeight);
Object obj;
obj.push_back(Pair("blockhash", pblockindex->phashBlock->GetHex()));
strReply = write_string(Value(obj), false) + "\n";
}
else if (resource == "mempool") {
vector<uint256> vtxid;
mempool.queryHashes(vtxid);
Array a;
BOOST_FOREACH(const uint256& hash, vtxid)
a.push_back(hash.ToString());
strReply = write_string(Value(a), false) + "\n";
}
else {
nStatus = HTTP_NOT_FOUND;
strReply = "{\"error\":\"Unknown REST endpoint\"}";
}
}
catch (std::exception& e) {
nStatus = HTTP_INTERNAL_SERVER_ERROR;
strReply = strprintf("{\"error\":\"%s\"}", e.what());
}
catch (...) {
nStatus = HTTP_INTERNAL_SERVER_ERROR;
strReply = "{\"error\":\"Internal server error\"}";
}
return true;
}
// Old HandleRESTRequest removed - now in rest.cpp
/**
* Handle SSE (Server-Sent Events) stream connection.
@@ -1294,20 +1117,32 @@ void ThreadRPCServer3(void* parg)
ReadHTTP(conn->stream(), mapHeaders, strRequest);
// Handle REST API requests (unauthenticated, read-only)
// Handle REST API requests
string strHTTPMethod = mapHeaders.count("_method") ? mapHeaders["_method"] : "POST";
string strURI = mapHeaders.count("_uri") ? mapHeaders["_uri"] : "/";
if (strHTTPMethod == "GET" && strURI.substr(0, 6) == "/rest/")
if (IsRESTPath(strURI) || (strHTTPMethod == "OPTIONS" && IsRESTPath(strURI)))
{
if (!GetBoolArg("-rest", false))
{
conn->stream() << HTTPReplyREST(HTTP_FORBIDDEN, "{\"error\":\"REST API not enabled. Start with -rest=1\"}") << std::flush;
break;
}
// Rate limit public (non-wallet) endpoints
if (strURI.find("/rest/wallet/") == string::npos)
{
string strPeerIP = conn->peer_address_to_string();
if (!CheckRESTRateLimit(strPeerIP))
{
conn->stream() << HTTPReplyREST(429, "{\"error\":\"Rate limit exceeded. Try again later.\"}") << std::flush;
break;
}
}
string strReply, strContentType;
int nRESTStatus;
HandleRESTRequest(strURI, strReply, strContentType, nRESTStatus);
HandleRESTRequest(strHTTPMethod, strURI, strRequest, mapHeaders, strReply, strContentType, nRESTStatus);
conn->stream() << HTTPReplyREST(nRESTStatus, strReply, strContentType) << std::flush;
break;
}
+10 -3
View File
@@ -484,10 +484,17 @@ bool CTxDB::LoadBlockIndex()
hashBestChain.ToString().substr(0,20).c_str(), nBestHeight, CBigNum(nBestChainTrust).ToString().c_str(),
DateTimeStrFormat("%x %H:%M:%S", pindexBest->GetBlockTime()).c_str());
// triangles: load hashSyncCheckpoint
// triangles: load hashSyncCheckpoint (best-effort, non-fatal)
if (!ReadSyncCheckpoint(Checkpoints::hashSyncCheckpoint))
return error("CTxDB::LoadBlockIndex() : hashSyncCheckpoint not loaded");
printf("LoadBlockIndex(): synchronized checkpoint %s\n", Checkpoints::hashSyncCheckpoint.ToString().c_str());
printf("LoadBlockIndex(): no sync checkpoint in DB, using default\n");
else
printf("LoadBlockIndex(): synchronized checkpoint %s\n", Checkpoints::hashSyncCheckpoint.ToString().c_str());
// If the stored checkpoint isn't in our index, reset to genesis so we don't assert-crash
if (!mapBlockIndex.count(Checkpoints::hashSyncCheckpoint))
{
printf("LoadBlockIndex(): sync checkpoint not in index, resetting to genesis\n");
Checkpoints::hashSyncCheckpoint = (!fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet);
}
// Load bnBestInvalidTrust, OK if it doesn't exist
CBigNum bnBestInvalidTrust;
+3 -3
View File
@@ -30,7 +30,7 @@ static const int DATABASE_VERSION = 70509;
// network protocol versioning
//
static const int PROTOCOL_VERSION = 70205;
static const int PROTOCOL_VERSION = 70206;
// v5 hard fork: require new protocol version (disconnects old nodes)
static const int MIN_PROTO_VERSION = 70205;
@@ -52,8 +52,8 @@ static const int BIP0031_VERSION = 60000;
static const int MEMPOOL_GD_VERSION = 60002;
#define DISPLAY_VERSION_MAJOR 5
#define DISPLAY_VERSION_MINOR 1
#define DISPLAY_VERSION_REVISION 5
#define DISPLAY_VERSION_MINOR 2
#define DISPLAY_VERSION_REVISION 0
#define DISPLAY_VERSION_BUILD 0
#endif
+6
View File
@@ -202,6 +202,7 @@ HEADERS += src/qt/trianglesgui.h \
src/qt/sendcoinsdialog.h \
src/qt/addressbookpage.h \
src/qt/aboutdialog.h \
src/qt/introdialog.h \
src/qt/editaddressdialog.h \
src/qt/trianglesaddressvalidator.h \
src/alert.h \
@@ -217,6 +218,7 @@ HEADERS += src/qt/trianglesgui.h \
src/kernel.h \
src/net_bootstrap.h \
src/tor/onion_v3.h \
src/tor/tor_process.h \
src/tor/tor_crypto_compat.h \
src/scrypt.h \
src/pbkdf2.h \
@@ -261,6 +263,7 @@ HEADERS += src/qt/trianglesgui.h \
src/qt/transactionview.h \
src/qt/walletmodel.h \
src/trianglesrpc.h \
src/rest.h \
src/qt/overviewpage.h \
src/qt/csvmodelwriter.h \
src/crypter.h \
@@ -314,6 +317,7 @@ SOURCES += src/qt/triangles.cpp src/qt/trianglesgui.cpp \
src/qt/coincontroltreewidget.cpp \
src/qt/addressbookpage.cpp \
src/qt/aboutdialog.cpp \
src/qt/introdialog.cpp \
src/qt/editaddressdialog.cpp \
src/qt/trianglesaddressvalidator.cpp \
# Old embedded Tor v2 client removed - incompatible with OpenSSL 3.x
@@ -351,6 +355,7 @@ SOURCES += src/qt/triangles.cpp src/qt/trianglesgui.cpp \
src/qt/transactionview.cpp \
src/qt/walletmodel.cpp \
src/trianglesrpc.cpp \
src/rest.cpp \
src/rpcdump.cpp \
src/rpcnet.cpp \
src/rpcmining.cpp \
@@ -384,6 +389,7 @@ SOURCES += src/qt/triangles.cpp src/qt/trianglesgui.cpp \
src/kernel.cpp \
src/net_bootstrap.cpp \
src/tor/onion_v3.cpp \
src/tor/tor_process.cpp \
src/scrypt-arm.S \
src/scrypt-x86.S \
src/scrypt-x86_64.S \