Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7c5c6596a | |||
| 16b35f6b2b | |||
| 7faf13dc31 | |||
| db65324b7a | |||
| c98bdbe335 | |||
| 6f1227b022 | |||
| 12205cdc37 | |||
| 2fc0e8155a | |||
| eeda728564 | |||
| 0df054bbcb | |||
| fbd931a392 | |||
| a792f90489 | |||
| 64939a9793 | |||
| b506a48192 | |||
| dee0d9ef62 | |||
| 22e220acaa | |||
| 1c068f4782 | |||
| a671708f0b | |||
| be90d39cd4 | |||
| 4d0478add5 | |||
| 734979c93b | |||
| 00af636aca | |||
| 9377b3a52f | |||
| 1881ff867e | |||
| caddfb1789 |
@@ -0,0 +1,157 @@
|
||||
# IBD Stall Bug Analysis — `triangles_v5`
|
||||
|
||||
**Date:** 2026-04-24
|
||||
**Symptom:** Node syncs from genesis, accepts blocks normally up to a point (observed: ~6284), then permanently stalls. `askfor_queue=0`, `orphans=0`, no new blocks ever arrive.
|
||||
|
||||
---
|
||||
|
||||
## Root Cause: Header Sync Cache Exhaustion Without Refill
|
||||
|
||||
The bug is a **broken feedback loop** between the header planner and block downloader. The node drains its header cache faster than it refills it, and once the cache is empty, the pipeline freezes with no recovery path.
|
||||
|
||||
### The Pipeline (how it should work)
|
||||
|
||||
```
|
||||
getheaders → 2000 headers → mapHeaderSync → AskFor(MSG_BLOCK) → mapAskFor → getdata → block received → ProcessBlock → QueueHeaderSyncBlocksParallel (refill)
|
||||
↑
|
||||
└── every 500 blocks: getheaders+getblocks to ALL peers
|
||||
```
|
||||
|
||||
### The Bug Path (how it actually dies)
|
||||
|
||||
**Step 1: Initial header fetch**
|
||||
- `version` handler sends `getblocks` + `getheaders` to peer
|
||||
- Peer responds with up to 2000 headers → stored in `mapHeaderSync`
|
||||
- `QueueHeaderSyncBlocksParallel(512)` queues up to 512 blocks via `AskFor()`
|
||||
|
||||
**Step 2: Blocks download and consume headers**
|
||||
- Blocks arrive, `ProcessBlock()` accepts them
|
||||
- Each accepted block calls `MarkHeaderSyncBlockAccepted()` which **erases** it from `mapHeaderSync`
|
||||
- After accepting, `QueueHeaderSyncBlocksParallel(512)` tries to queue more from remaining `mapHeaderSync` entries
|
||||
|
||||
**Step 3: The critical gap — header cache runs dry**
|
||||
- The header cache holds at most `MAX_HEADER_SYNC_CACHE = 15000` entries
|
||||
- But `getheaders` only returns **2000 headers per batch**
|
||||
- The `HEADER_DOWNLOAD_WINDOW = 512` means only 512 blocks are in-flight at once
|
||||
- So blocks are consumed from the cache faster than headers are fetched
|
||||
- Each accepted block erases its entry; if all 2000 headers are downloaded before the next `getheaders` fires...
|
||||
|
||||
**Step 4: Cache empties → pipeline dies**
|
||||
- `mapHeaderSync` becomes empty
|
||||
- `hashBestHeaderSync` is recomputed to `0` (by `RecomputeBestHeaderSync()`)
|
||||
- The refill condition `if (hashBestHeaderSync != 0)` at line ~3599 evaluates **false**
|
||||
- No more blocks are queued, ever
|
||||
|
||||
**Step 5: No recovery mechanism kicks in**
|
||||
- The `ContinueHeaderSync()` call only fires when `vHeaders.size() >= 2000` (full batch)
|
||||
- If the last batch was smaller (partial response, or exactly 2000 consumed), **no new `getheaders` is sent**
|
||||
- The pipeline refill every 500 blocks only fires **when a block is received** — but no blocks are coming
|
||||
- The stall detection sends `getblocks` (not `getheaders`), which produces `inv` messages → `AskFor()` for individual blocks
|
||||
- But `getblocks` uses `CBlockLocator` with exponential spacing, which maps to an old block → peer sends `inv` for blocks we already have → walk-forward logic tries to progress but may loop or stall
|
||||
|
||||
### Why It's Worse on Fast Connections / Sync-from-Zero
|
||||
|
||||
- Blocks download fast (PoS blocks are tiny)
|
||||
- All 2000 headers are consumed quickly
|
||||
- The window between "all headers consumed" and "need more headers" is tiny
|
||||
- On slow Tor connections, the 15-minute TTL eviction (`PruneHeaderSync`) adds a second failure mode: headers that took too long to download get evicted, creating gaps in `GetHeaderSyncDownloadPath()`
|
||||
|
||||
---
|
||||
|
||||
## Affected Code Locations
|
||||
|
||||
| File | Line(s) | Issue |
|
||||
|------|---------|-------|
|
||||
| `main.cpp` | 137 | `MAX_HEADER_SYNC_CACHE = 15000` — cache is large but `getheaders` only returns 2000 |
|
||||
| `main.cpp` | 138 | `HEADER_DOWNLOAD_WINDOW = 512` — window is smaller than header batch |
|
||||
| `main.cpp` | 298-345 | `AddHeaderSyncNode()` / `PruneHeaderSync()` — TTL eviction can create gaps in the download path |
|
||||
| `main.cpp` | 380-396 | `GetHeaderSyncDownloadPath()` — walks back from tip; **breaks on first gap** in `mapHeaderSync` chain |
|
||||
| `main.cpp` | 455-461 | `MarkHeaderSyncBlockAccepted()` — erases from `mapHeaderSync`, may set `hashBestHeaderSync = 0` |
|
||||
| `main.cpp` | 3599-3606 | Block-accepted refill — **guarded by `hashBestHeaderSync != 0`**, skips when cache is empty |
|
||||
| `main.cpp` | 4972-4976 | `getheaders` continuation — **only fires on full batch** (`vHeaders.size() >= 2000`) |
|
||||
| `main.cpp` | 5110-5121 | Pipeline refill every 500 blocks — **only fires when blocks arrive**, useless during stall |
|
||||
| `main.cpp` | 5952-5980 | Stall detection — sends `getblocks` (not `getheaders`), can't restart header planner |
|
||||
|
||||
---
|
||||
|
||||
## Fix Options
|
||||
|
||||
### Fix A: Refill headers when cache runs dry (minimal, targeted)
|
||||
|
||||
In the block-accepted handler, when `hashBestHeaderSync == 0`, send `getheaders` to all peers:
|
||||
|
||||
```cpp
|
||||
// After the existing refill (line ~3599)
|
||||
if (hashBestHeaderSync == 0)
|
||||
{
|
||||
// Header cache exhausted — request more headers from all peers
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
{
|
||||
if (!pnode->fClient && pnode->nVersion != 0)
|
||||
{
|
||||
pnode->pindexLastGetHeadersBegin = NULL;
|
||||
pnode->PushGetHeaders(pindexBest, uint256(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fix B: Also send getheaders in stall detection (defense in depth)
|
||||
|
||||
In the stall handler (line ~5968), alongside the `getblocks`, also send `getheaders`:
|
||||
|
||||
```cpp
|
||||
pto->PushGetBlocks(...); // existing
|
||||
pto->pindexLastGetHeadersBegin = NULL;
|
||||
pto->PushGetHeaders(pindexBest, uint256(0)); // ADD THIS
|
||||
```
|
||||
|
||||
### Fix C: Don't let the header cache fully drain (robustness)
|
||||
|
||||
In `QueueHeaderSyncBlocksParallel()`, stop consuming the last N entries from the cache to keep the chain intact. When only `HEADER_DOWNLOAD_WINDOW` entries remain, trigger a `getheaders` continuation before consuming more.
|
||||
|
||||
### Fix D: Periodic getheaders in SendMessages loop (most robust)
|
||||
|
||||
Add a periodic `getheaders` request in the `SendMessages` loop, similar to how stall detection already sends periodic `getblocks`. This ensures headers are always being fetched regardless of block progress:
|
||||
|
||||
```cpp
|
||||
// In SendMessages, alongside stall detection:
|
||||
if (!pto->fClient && IsInitialBlockDownload() && hashBestHeaderSync == 0)
|
||||
{
|
||||
static int64_t nLastHeaderRequest = 0;
|
||||
if (GetTime() - nLastHeaderRequest >= 30)
|
||||
{
|
||||
pto->pindexLastGetHeadersBegin = NULL;
|
||||
pto->PushGetHeaders(pindexBest, uint256(0));
|
||||
nLastHeaderRequest = GetTime();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommended Fix
|
||||
|
||||
**Fix A + Fix B together** — minimal code change, covers both the block-accepted path and the stall recovery path. Fix D adds belt-and-suspenders protection in the main loop.
|
||||
|
||||
---
|
||||
|
||||
## Secondary Issue: TTL Eviction Creating Path Gaps
|
||||
|
||||
`PruneHeaderSync()` evicts entries older than 15 minutes. If block download is slow (Tor, slow peers), entries at the beginning of the download path can be evicted while entries at the end still exist. `GetHeaderSyncDownloadPath()` walks back from the tip and **breaks on the first missing entry**, making the entire tail of the cache unreachable.
|
||||
|
||||
**Fix:** In `GetHeaderSyncDownloadPath()`, skip gaps instead of breaking:
|
||||
|
||||
```cpp
|
||||
while (hashTip != 0 && !mapBlockIndex.count(hashTip))
|
||||
{
|
||||
auto mi = mapHeaderSync.find(hashTip);
|
||||
if (mi == mapHeaderSync.end())
|
||||
break; // Currently breaks — could skip to next known ancestor instead
|
||||
vPath.push_back(hashTip);
|
||||
hashTip = mi->second.header.hashPrevBlock;
|
||||
}
|
||||
```
|
||||
|
||||
This is a secondary concern but contributes to cache exhaustion on high-latency connections.
|
||||
+22
-1
@@ -6,7 +6,7 @@ if(POLICY CMP0167)
|
||||
endif()
|
||||
|
||||
project(Triangles
|
||||
VERSION 5.8.1.0
|
||||
VERSION 5.9.2
|
||||
DESCRIPTION "Cryptographic Triangles Wallet"
|
||||
LANGUAGES C CXX
|
||||
)
|
||||
@@ -17,6 +17,24 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
|
||||
# ── Build acceleration ──
|
||||
# ccache: auto-detect and use if available
|
||||
find_program(CCACHE_PROGRAM ccache)
|
||||
if(CCACHE_PROGRAM)
|
||||
set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
|
||||
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
|
||||
message(STATUS "ccache found: ${CCACHE_PROGRAM}")
|
||||
else()
|
||||
message(STATUS "ccache not found — install it for faster rebuilds")
|
||||
endif()
|
||||
|
||||
# Unity (jumbo) build: batch source files to reduce header parsing overhead
|
||||
option(ENABLE_UNITY_BUILD "Enable CMake unity (jumbo) builds" OFF)
|
||||
if(ENABLE_UNITY_BUILD)
|
||||
set(CMAKE_UNITY_BUILD ON)
|
||||
set(CMAKE_UNITY_BUILD_BATCH_SIZE 8)
|
||||
endif()
|
||||
|
||||
# ── Output directories ──
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||
@@ -113,4 +131,7 @@ message(STATUS " D-Bus: ${USE_DBUS}")
|
||||
message(STATUS " ZMQ: ${USE_ZMQ}")
|
||||
message(STATUS " Embedded Tor: ${USE_TOR_EMBEDDED}")
|
||||
message(STATUS " Static linking: ${ENABLE_STATIC}")
|
||||
message(STATUS " ccache: ${CCACHE_PROGRAM}")
|
||||
message(STATUS " Unity build: ${ENABLE_UNITY_BUILD}")
|
||||
message(STATUS " Precompiled header: ON")
|
||||
message(STATUS "")
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
# Triangles Modernization Roadmap
|
||||
|
||||
**Goal:** Make TRI faster to sync, safer for wallets, and more useful as a currency — without breaking consensus.
|
||||
|
||||
**Invariant:** Any change that modifies block validation, stake modifier computation, transaction format, or signature verification MUST preserve exact consensus with existing v5.x nodes. When in doubt, test against a synced v5.8.1 node.
|
||||
|
||||
---
|
||||
|
||||
## Priority 1: Faster Syncing (High Impact, Low Risk)
|
||||
|
||||
### 1.1 Update Checkpoints (Easy, Immediate)
|
||||
**Problem:** Last hardcoded checkpoint is at block 2,186,940. `IsInitialBlockDownload()` returns false past this point, causing orphan limit to drop from 4000 to 750 — exactly what caused the fork deadlock.
|
||||
|
||||
**Fix:** Add checkpoints every ~50,000 blocks up to current height (~2,207,000+).
|
||||
```cpp
|
||||
// src/checkpoints.cpp - add recent checkpoints
|
||||
{2190000, uint256("...")},
|
||||
{2195000, uint256("...")},
|
||||
{2200000, uint256("...")},
|
||||
{2205000, uint256("...")},
|
||||
{2210000, uint256("...")},
|
||||
```
|
||||
**Risk:** None — checkpoints are only used for IBD detection and quick rejection of clearly wrong chains.
|
||||
|
||||
### 1.2 Increase Post-Checkpoint Orphan Limit (Easy)
|
||||
**Problem:** 750 orphans after IBD is too low for a low-peer network. During the fork incident, 750 orphans filled up and the node deadlocked.
|
||||
|
||||
**Fix:**
|
||||
```cpp
|
||||
// src/main.h
|
||||
static const unsigned int MAX_ORPHAN_BLOCKS = 2000; // was 750
|
||||
```
|
||||
**Risk:** Slightly more memory usage during forks. Worth it for resilience.
|
||||
|
||||
### 1.3 Parallel Block Download (Medium Effort)
|
||||
**Problem:** Current implementation downloads blocks sequentially from one peer at a time during IBD.
|
||||
|
||||
**Fix:** Increase batch sizes and allow concurrent block downloads from multiple peers:
|
||||
```cpp
|
||||
// src/main.cpp
|
||||
// During IBD, request blocks from multiple peers simultaneously
|
||||
unsigned int nGetDataBatchSize = IsInitialBlockDownload() ? 8000 : 1000; // was 4000
|
||||
```
|
||||
**Risk:** Low — larger batch sizes are already proven in Bitcoin forks.
|
||||
|
||||
### 1.4 Header-First Sync (Medium Effort)
|
||||
**Problem:** Node downloads full blocks before validating headers. A bad peer can waste bandwidth.
|
||||
|
||||
**Fix:** Download and validate all headers first (compact ~80 bytes each), then download full blocks only for the best chain.
|
||||
- Separate `getheaders`/`headers` message handling
|
||||
- Download blocks only for the best header chain
|
||||
- Reduces wasted bandwidth during forks by 95%+
|
||||
|
||||
### 1.5 Bootstrap Over HTTPS with Resume (Easy)
|
||||
**Problem:** Built-in bootstrap (`-bootstrap`) uses raw TCP and can't resume interrupted downloads.
|
||||
|
||||
**Fix:** The existing `bootstrap.cpp` already supports downloading. Add:
|
||||
- Resume support (Range headers)
|
||||
- SHA256 verification of downloaded archive
|
||||
- Better progress reporting
|
||||
- Fallback mirrors
|
||||
|
||||
---
|
||||
|
||||
## Priority 2: Wallet Safety (Critical)
|
||||
|
||||
### 2.1 Automatic Wallet Backup Before Dangerous Operations (Easy)
|
||||
**Problem:** Corrupt wallet = lost funds. No automatic backup before risky operations.
|
||||
|
||||
**Fix:** In `walletdb.cpp`, before any rewrite:
|
||||
```cpp
|
||||
// Before wallet.dat rewrite, copy to wallet.dat.bak
|
||||
if (boost::filesystem::exists(pathWallet)) {
|
||||
boost::filesystem::copy_file(pathWallet, pathWallet + ".bak",
|
||||
boost::filesystem::copy_option::overwrite_if_exists);
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Detect and Report BDB Corruption (Easy)
|
||||
**Problem:** BDB corruption silently corrupts wallet. User doesn't know until it's too late.
|
||||
|
||||
**Fix:** Add wallet integrity check on load:
|
||||
```cpp
|
||||
// In CWallet::LoadWallet()
|
||||
// After opening, verify BDB environment is healthy
|
||||
// If DB_RUNRECOVERY, auto-salvage and warn user
|
||||
```
|
||||
|
||||
### 2.3 Wallet.dat Versioning (Medium Effort)
|
||||
**Problem:** Single wallet.dat file. If it corrupts during write, funds are lost.
|
||||
|
||||
**Fix:** Implement copy-on-write wallet saves:
|
||||
- Write new wallet data to `wallet.dat.new`
|
||||
- Atomically rename `wallet.dat` → `wallet.dat.old`, `wallet.dat.new` → `wallet.dat`
|
||||
- Keep last 3 wallet revisions
|
||||
- On load, try wallet.dat first, fall back to wallet.dat.old if corrupt
|
||||
|
||||
### 2.4 Seed Phrase / HD Wallet (High Effort, High Impact)
|
||||
**Problem:** Losing wallet.dat = losing everything. No recovery mechanism.
|
||||
|
||||
**Fix:** Implement BIP39/BIP44 HD wallet as optional upgrade:
|
||||
- Generate 12/24-word seed phrase on new wallet creation
|
||||
- Derive all keys from seed deterministically
|
||||
- Import seed on any device to recover wallet
|
||||
- Keep backward compatibility with existing non-HD wallets
|
||||
|
||||
---
|
||||
|
||||
## Priority 3: Network Resilience (Medium Impact)
|
||||
|
||||
### 3.1 Better Peer Management (Medium Effort)
|
||||
**Problem:** Low peer counts (2-6) lead to fork divergence. No prioritization of reliable peers.
|
||||
|
||||
**Fix:**
|
||||
- Peer reliability scoring (track which peers provide valid blocks)
|
||||
- Prefer peers that are ahead and on the same chain
|
||||
- Automatic disconnection of stale/forked peers
|
||||
- Increase default `maxconnections` from 64 to 128
|
||||
|
||||
### 3.2 Compact Block Relay (High Effort)
|
||||
**Problem:** Full blocks are sent even when the receiver likely already has most transactions.
|
||||
|
||||
**Fix:** Implement BIP 152 compact blocks:
|
||||
- Send block header + short transaction IDs
|
||||
- Receiver fills in from mempool, only requests missing transactions
|
||||
- Reduces bandwidth by ~90% during normal operation
|
||||
|
||||
### 3.3 DNS Seed Infrastructure (Easy)
|
||||
**Problem:** `dnsseed=0` when Tor-only means no automatic peer discovery.
|
||||
|
||||
**Fix:** Run a DNS seed server that resolves to known reliable onion addresses:
|
||||
```
|
||||
seed.cryptographic-triangles.org → returns onion addresses of healthy nodes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Priority 4: User Experience (Medium Impact)
|
||||
|
||||
### 4.1 Progress Reporting for IBD (Easy)
|
||||
**Problem:** Users see "downloading blocks..." with no useful progress indicator.
|
||||
|
||||
**Fix:**
|
||||
- Report `headers` vs `blocks` progress separately
|
||||
- Show estimated time remaining based on download speed
|
||||
- Log progress every 1000 blocks (currently every 5000)
|
||||
- Qt wallet: update progress bar more frequently
|
||||
|
||||
### 4.2 Staking Dashboard Improvements (Easy)
|
||||
**Problem:** Qt wallet shows staking info but not clearly.
|
||||
|
||||
**Fix:**
|
||||
- Show expected time to stake more prominently
|
||||
- Display staking weight as percentage of network
|
||||
- Notify when stake is found (system notification)
|
||||
- Show "staking" indicator in system tray
|
||||
|
||||
### 4.3 Transaction Fee Estimation (Medium Effort)
|
||||
**Problem:** No fee estimation. Users guess.
|
||||
|
||||
**Fix:** Track recent block inclusion rates by fee level, provide fee recommendations.
|
||||
|
||||
---
|
||||
|
||||
## Priority 5: Code Modernization (Low Urgency, Good Hygiene)
|
||||
|
||||
### 5.1 C++17/20 Features
|
||||
- Replace raw pointers with smart pointers where safe
|
||||
- Use `std::optional`, `std::string_view`, `std::filesystem`
|
||||
- Replace boost::filesystem with std::filesystem (C++17)
|
||||
|
||||
### 5.2 Build System
|
||||
- CMake is already in place (good)
|
||||
- Add sanitizers (ASAN, UBSAN) to CI
|
||||
- Static analysis with clang-tidy
|
||||
|
||||
### 5.3 Testing
|
||||
- Current test coverage is thin
|
||||
- Add unit tests for:
|
||||
- Checkpoint validation
|
||||
- Stake modifier computation
|
||||
- Bootstrap download/resume
|
||||
- Wallet BDB recovery
|
||||
- Orphan block handling
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Change
|
||||
|
||||
These are consensus-critical and must remain identical:
|
||||
- Block validation rules
|
||||
- Stake modifier computation (`ComputeNextStakeModifier`)
|
||||
- Transaction signature verification
|
||||
- Block reward schedule
|
||||
- PoW/PoS target computation
|
||||
- Chain trust / difficulty adjustment
|
||||
- Message serialization format
|
||||
- Protocol version handshaking
|
||||
|
||||
Any change to these requires a coordinated network upgrade (hard fork).
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **This week:** Update checkpoints (1.1), increase orphan limit (1.2), wallet backup before save (2.1)
|
||||
2. **Next week:** Better progress reporting (4.1), increase batch size (1.3)
|
||||
3. **Month 1:** Wallet versioning (2.3), bootstrap resume (1.5)
|
||||
4. **Month 2:** Header-first sync (1.4), peer reliability (3.1)
|
||||
5. **Month 3+:** HD wallet (2.4), compact blocks (3.2)
|
||||
@@ -27,12 +27,14 @@ Triangles is a privacy-focused cryptocurrency featuring Proof-of-Stake consensus
|
||||
The Triangles network operates exclusively over Tor for privacy:
|
||||
|
||||
**Tor v3 Seeds:**
|
||||
- `gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion:24112`
|
||||
- `futmtrvh6j34t7s6yjdxfia6iwuyfzwh4k5eqfof5kfhoqk3xmi3qoqd.onion:24112`
|
||||
- `jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112`
|
||||
- `uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112`
|
||||
- `el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112`
|
||||
- `sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112`
|
||||
- `i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112`
|
||||
|
||||
**DNS Seeds:**
|
||||
- `seed1.cryptographic-triangles.org`
|
||||
- `seed2.cryptographic-triangles.org`
|
||||
**HTTP Seed List:**
|
||||
- `seeds.cryptographic-triangles.org/seeds.txt` - Dynamically updated list of active onion peers
|
||||
|
||||
## Building from Source
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ add_library(hash9_crypto STATIC
|
||||
)
|
||||
target_include_directories(hash9_crypto PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set_target_properties(hash9_crypto PROPERTIES LINKER_LANGUAGE C)
|
||||
# Hash9 C files have colliding static symbols (IV512, DECL_STATE, etc.) — skip unity
|
||||
set_target_properties(hash9_crypto PROPERTIES UNITY_BUILD OFF)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 2. JSON library (header-only nlohmann/json via json_compat.h shim)
|
||||
@@ -72,6 +74,7 @@ set(CORE_SOURCES
|
||||
rpcsmessage.cpp
|
||||
zmqpublishnotifier.cpp
|
||||
txdb-leveldb.cpp
|
||||
utxosnapshot.cpp
|
||||
lz4/lz4.c
|
||||
tor/onion_v3.cpp
|
||||
tor/tor_process.cpp
|
||||
@@ -184,6 +187,31 @@ endif()
|
||||
|
||||
add_dependencies(triangles_common generate_build_info build_leveldb)
|
||||
|
||||
# ── Precompiled header (heavy STL + Boost + OpenSSL includes, C++ only) ──
|
||||
target_precompile_headers(triangles_common PRIVATE
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<string$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<vector$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<map$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<deque$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<algorithm$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<sstream$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<stdexcept$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<cstdint$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<cstring$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<memory$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<functional$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<boost/filesystem.hpp$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<boost/filesystem/fstream.hpp$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<boost/thread.hpp$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<boost/thread/mutex.hpp$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<boost/thread/condition_variable.hpp$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<boost/algorithm/string.hpp$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<openssl/sha.h$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<openssl/crypto.h$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<openssl/rand.h$<ANGLE-R>>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:<openssl/evp.h$<ANGLE-R>>"
|
||||
)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 4. Headless daemon (trianglesd)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -195,6 +223,7 @@ if(BUILD_DAEMON)
|
||||
)
|
||||
# No QT_GUI define — daemon gets the #if !defined(QT_GUI) code paths
|
||||
target_link_libraries(trianglesd PRIVATE triangles_common)
|
||||
target_precompile_headers(trianglesd REUSE_FROM triangles_common)
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(trianglesd PROPERTIES SUFFIX ".exe")
|
||||
|
||||
+316
-68
@@ -2,6 +2,7 @@
|
||||
// Distributed under the MIT/X11 software license
|
||||
|
||||
#include "bootstrap.h"
|
||||
#include "utxosnapshot.h"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/fstream.hpp>
|
||||
@@ -14,6 +15,9 @@
|
||||
#include "netbase.h"
|
||||
#include "net.h"
|
||||
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/err.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstdio>
|
||||
@@ -22,8 +26,10 @@
|
||||
|
||||
#ifdef WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
@@ -40,80 +46,287 @@ bool NeedsBootstrap(const fs::path& dataDir)
|
||||
return !fs::exists(dataDir / "blk0001.dat");
|
||||
}
|
||||
|
||||
// Send all bytes on a raw socket
|
||||
static bool SendAll(SOCKET sock, const char* data, size_t len)
|
||||
// Direct TCP connection bypassing Tor SOCKS proxy.
|
||||
// Used for bootstrap downloads where the server is on clearnet.
|
||||
static SOCKET ConnectDirectTCP(const std::string& host, int port, std::string& strError)
|
||||
{
|
||||
while (len > 0) {
|
||||
int n = send(sock, data, (int)std::min(len, (size_t)65536), MSG_NOSIGNAL);
|
||||
if (n <= 0) return false;
|
||||
data += n;
|
||||
len -= n;
|
||||
struct addrinfo hints, *result, *rp;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
std::string portStr = std::to_string(port);
|
||||
int rc = getaddrinfo(host.c_str(), portStr.c_str(), &hints, &result);
|
||||
if (rc != 0) {
|
||||
strError = "DNS resolution failed for " + host;
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
return true;
|
||||
|
||||
SOCKET hSocket = INVALID_SOCKET;
|
||||
for (rp = result; rp != NULL; rp = rp->ai_next) {
|
||||
hSocket = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
if (hSocket == INVALID_SOCKET)
|
||||
continue;
|
||||
|
||||
if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0)
|
||||
break; // success
|
||||
|
||||
closesocket(hSocket);
|
||||
hSocket = INVALID_SOCKET;
|
||||
}
|
||||
freeaddrinfo(result);
|
||||
|
||||
if (hSocket == INVALID_SOCKET)
|
||||
strError = "Cannot connect to " + host + ":" + portStr;
|
||||
|
||||
return hSocket;
|
||||
}
|
||||
|
||||
// Read until `delim` found in received data. Returns data including delimiter.
|
||||
static bool RecvUntil(SOCKET sock, std::string& out, const std::string& delim)
|
||||
{
|
||||
out.clear();
|
||||
char c;
|
||||
while (true) {
|
||||
int n = recv(sock, &c, 1, 0);
|
||||
if (n <= 0) return false;
|
||||
out += c;
|
||||
if (out.size() >= delim.size() &&
|
||||
out.compare(out.size() - delim.size(), delim.size(), delim) == 0)
|
||||
return true;
|
||||
if (out.size() > 64 * 1024) return false; // header too large
|
||||
// RAII wrapper for an HTTP(S) connection (socket + optional TLS)
|
||||
struct HttpConn {
|
||||
SOCKET sock;
|
||||
SSL_CTX* ctx;
|
||||
SSL* ssl;
|
||||
|
||||
HttpConn() : sock(INVALID_SOCKET), ctx(nullptr), ssl(nullptr) {}
|
||||
~HttpConn() { Close(); }
|
||||
|
||||
void Close() {
|
||||
if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); ssl = nullptr; }
|
||||
if (ctx) { SSL_CTX_free(ctx); ctx = nullptr; }
|
||||
if (sock != INVALID_SOCKET) { closesocket(sock); sock = INVALID_SOCKET; }
|
||||
}
|
||||
|
||||
bool Send(const char* data, size_t len) {
|
||||
while (len > 0) {
|
||||
int n = ssl ? SSL_write(ssl, data, (int)std::min(len, (size_t)65536))
|
||||
: send(sock, data, (int)std::min(len, (size_t)65536), MSG_NOSIGNAL);
|
||||
if (n <= 0) return false;
|
||||
data += n;
|
||||
len -= n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int Recv(char* buf, int len) {
|
||||
return ssl ? SSL_read(ssl, buf, len) : recv(sock, buf, len, 0);
|
||||
}
|
||||
|
||||
// Read until delimiter found. Returns data including delimiter.
|
||||
bool RecvUntil(std::string& out, const std::string& delim) {
|
||||
out.clear();
|
||||
char c;
|
||||
while (true) {
|
||||
int n = Recv(&c, 1);
|
||||
if (n <= 0) return false;
|
||||
out += c;
|
||||
if (out.size() >= delim.size() &&
|
||||
out.compare(out.size() - delim.size(), delim.size(), delim) == 0)
|
||||
return true;
|
||||
if (out.size() > 64 * 1024) return false; // header too large
|
||||
}
|
||||
}
|
||||
|
||||
// Establish TLS on an already-connected socket
|
||||
bool StartTLS(const std::string& hostname, std::string& strError) {
|
||||
ctx = SSL_CTX_new(TLS_client_method());
|
||||
if (!ctx) {
|
||||
strError = "Failed to create SSL context";
|
||||
return false;
|
||||
}
|
||||
// Skip cert verification — we verify data integrity via checkpoint hashes
|
||||
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr);
|
||||
|
||||
ssl = SSL_new(ctx);
|
||||
if (!ssl) {
|
||||
strError = "Failed to create SSL object";
|
||||
return false;
|
||||
}
|
||||
SSL_set_fd(ssl, (int)sock);
|
||||
SSL_set_tlsext_host_name(ssl, hostname.c_str()); // SNI
|
||||
|
||||
if (SSL_connect(ssl) != 1) {
|
||||
unsigned long err = ERR_get_error();
|
||||
char errBuf[256];
|
||||
ERR_error_string_n(err, errBuf, sizeof(errBuf));
|
||||
strError = "TLS handshake failed with " + hostname + ": " + errBuf;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse host, port, and path from an absolute URL.
|
||||
// Sets useSSL, host, port, path. Returns false for unsupported schemes.
|
||||
static bool ParseAbsoluteUrl(const std::string& url,
|
||||
bool& useSSL, std::string& host,
|
||||
int& port, std::string& path)
|
||||
{
|
||||
if (url.compare(0, 8, "https://") == 0) {
|
||||
useSSL = true;
|
||||
std::string rest = url.substr(8);
|
||||
size_t pathStart = rest.find('/');
|
||||
if (pathStart != std::string::npos) {
|
||||
host = rest.substr(0, pathStart);
|
||||
path = rest.substr(pathStart);
|
||||
} else {
|
||||
host = rest;
|
||||
path = "/";
|
||||
}
|
||||
size_t colonPos = host.find(':');
|
||||
if (colonPos != std::string::npos) {
|
||||
port = std::atoi(host.c_str() + colonPos + 1);
|
||||
host = host.substr(0, colonPos);
|
||||
} else {
|
||||
port = 443;
|
||||
}
|
||||
return true;
|
||||
} else if (url.compare(0, 7, "http://") == 0) {
|
||||
useSSL = false;
|
||||
std::string rest = url.substr(7);
|
||||
size_t pathStart = rest.find('/');
|
||||
if (pathStart != std::string::npos) {
|
||||
host = rest.substr(0, pathStart);
|
||||
path = rest.substr(pathStart);
|
||||
} else {
|
||||
host = rest;
|
||||
path = "/";
|
||||
}
|
||||
size_t colonPos = host.find(':');
|
||||
if (colonPos != std::string::npos) {
|
||||
port = std::atoi(host.c_str() + colonPos + 1);
|
||||
host = host.substr(0, colonPos);
|
||||
} else {
|
||||
port = 80;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
const fs::path& destPath,
|
||||
ProgressCallback progressFn,
|
||||
std::string& strError)
|
||||
std::string& strError,
|
||||
bool noProxy)
|
||||
{
|
||||
try {
|
||||
// Connect through Tor SOCKS proxy (ConnectSocketByName respects SetProxy)
|
||||
SOCKET hSocket = INVALID_SOCKET;
|
||||
CService addr;
|
||||
if (!ConnectSocketByName(addr, hSocket, host.c_str(), PORT, 30)) {
|
||||
strError = "Cannot connect to " + host + " (check Tor proxy)";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send HTTP GET request
|
||||
std::string request =
|
||||
"GET " + urlPath + " HTTP/1.1\r\n"
|
||||
"Host: " + host + "\r\n"
|
||||
"Connection: close\r\n"
|
||||
"User-Agent: Triangles\r\n"
|
||||
"\r\n";
|
||||
|
||||
if (!SendAll(hSocket, request.data(), request.size())) {
|
||||
closesocket(hSocket);
|
||||
strError = "Failed to send request to " + host;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read response headers
|
||||
std::string currentHost = host;
|
||||
std::string currentPath = urlPath;
|
||||
int currentPort = PORT;
|
||||
bool useSSL = false;
|
||||
std::string headerData;
|
||||
if (!RecvUntil(hSocket, headerData, "\r\n\r\n")) {
|
||||
closesocket(hSocket);
|
||||
strError = "Failed to read HTTP headers from " + host;
|
||||
return false;
|
||||
}
|
||||
int redirectCount = 0;
|
||||
const int MAX_REDIRECTS = 5;
|
||||
|
||||
// Parse status code from "HTTP/1.x NNN ..."
|
||||
unsigned int status_code = 0;
|
||||
size_t sp = headerData.find(' ');
|
||||
if (sp != std::string::npos)
|
||||
status_code = atoi(headerData.c_str() + sp + 1);
|
||||
HttpConn conn;
|
||||
|
||||
if (status_code != 200) {
|
||||
closesocket(hSocket);
|
||||
strError = "HTTP error " + std::to_string(status_code) + " for " + urlPath;
|
||||
return false;
|
||||
// Connection + redirect loop
|
||||
while (true) {
|
||||
conn.Close(); // clean slate for each attempt
|
||||
|
||||
if (noProxy) {
|
||||
conn.sock = ConnectDirectTCP(currentHost, currentPort, strError);
|
||||
if (conn.sock == INVALID_SOCKET)
|
||||
return false;
|
||||
} else {
|
||||
CService addr;
|
||||
if (!ConnectSocketByName(addr, conn.sock, currentHost.c_str(), currentPort, 30)) {
|
||||
strError = "Cannot connect to " + currentHost + " (check Tor proxy)";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Establish TLS when needed
|
||||
if (useSSL) {
|
||||
if (!conn.StartTLS(currentHost, strError))
|
||||
return false;
|
||||
printf("Bootstrap: TLS established with %s:%d\n",
|
||||
currentHost.c_str(), currentPort);
|
||||
}
|
||||
|
||||
// Send HTTP GET request
|
||||
std::string request =
|
||||
"GET " + currentPath + " HTTP/1.1\r\n"
|
||||
"Host: " + currentHost + "\r\n"
|
||||
"Connection: close\r\n"
|
||||
"User-Agent: Triangles\r\n"
|
||||
"\r\n";
|
||||
|
||||
if (!conn.Send(request.data(), request.size())) {
|
||||
strError = "Failed to send request to " + currentHost;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read response headers
|
||||
if (!conn.RecvUntil(headerData, "\r\n\r\n")) {
|
||||
strError = "Failed to read HTTP headers from " + currentHost;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse status code from "HTTP/1.x NNN ..."
|
||||
unsigned int status_code = 0;
|
||||
size_t sp = headerData.find(' ');
|
||||
if (sp != std::string::npos)
|
||||
status_code = atoi(headerData.c_str() + sp + 1);
|
||||
|
||||
// Handle HTTP redirects
|
||||
if (status_code == 301 || status_code == 302 ||
|
||||
status_code == 307 || status_code == 308) {
|
||||
|
||||
if (++redirectCount > MAX_REDIRECTS) {
|
||||
strError = "Too many redirects for " + urlPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find Location header (case-insensitive)
|
||||
std::string lowerHdr = headerData;
|
||||
std::transform(lowerHdr.begin(), lowerHdr.end(),
|
||||
lowerHdr.begin(), ::tolower);
|
||||
size_t locPos = lowerHdr.find("\nlocation:");
|
||||
if (locPos == std::string::npos) {
|
||||
strError = "Redirect " + std::to_string(status_code) + " without Location header";
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t valStart = locPos + 10; // skip "\nlocation:"
|
||||
while (valStart < headerData.size() && headerData[valStart] == ' ')
|
||||
valStart++;
|
||||
size_t lineEnd = headerData.find("\r\n", valStart);
|
||||
std::string location;
|
||||
if (lineEnd != std::string::npos)
|
||||
location = headerData.substr(valStart, lineEnd - valStart);
|
||||
else
|
||||
location = headerData.substr(valStart);
|
||||
boost::trim(location);
|
||||
|
||||
// Parse redirect URL — supports http://, https://, and relative paths
|
||||
if (location.compare(0, 7, "http://") == 0 ||
|
||||
location.compare(0, 8, "https://") == 0) {
|
||||
if (!ParseAbsoluteUrl(location, useSSL, currentHost,
|
||||
currentPort, currentPath)) {
|
||||
strError = "Unsupported redirect location: " + location;
|
||||
return false;
|
||||
}
|
||||
} else if (!location.empty() && location[0] == '/') {
|
||||
currentPath = location;
|
||||
} else {
|
||||
strError = "Unsupported redirect location: " + location;
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("Bootstrap: redirect %d -> %s%s%s (port %d)\n",
|
||||
status_code, useSSL ? "https://" : "http://",
|
||||
currentHost.c_str(), currentPath.c_str(), currentPort);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status_code != 200) {
|
||||
strError = "HTTP error " + std::to_string(status_code) + " for " + currentPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
break; // Got 200, proceed to download
|
||||
}
|
||||
|
||||
// Parse Content-Length
|
||||
@@ -132,7 +345,6 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
// Open output file
|
||||
FILE* file = fopen(destPath.string().c_str(), "wb");
|
||||
if (!file) {
|
||||
closesocket(hSocket);
|
||||
strError = "Cannot create file: " + destPath.string();
|
||||
return false;
|
||||
}
|
||||
@@ -143,10 +355,9 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
char chunk[65536];
|
||||
|
||||
while (true) {
|
||||
int n = recv(hSocket, chunk, sizeof(chunk), 0);
|
||||
int n = conn.Recv(chunk, sizeof(chunk));
|
||||
if (n < 0) {
|
||||
fclose(file);
|
||||
closesocket(hSocket);
|
||||
fs::remove(destPath);
|
||||
strError = "Network error during download";
|
||||
return false;
|
||||
@@ -163,7 +374,7 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
closesocket(hSocket);
|
||||
// conn destructor handles socket + SSL cleanup
|
||||
|
||||
// Verify download size if Content-Length was provided
|
||||
if (content_length > 0 && bytes_written != content_length) {
|
||||
@@ -183,13 +394,14 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
|
||||
bool FetchFileList(const std::string& host,
|
||||
std::vector<std::string>& files,
|
||||
std::string& strError)
|
||||
std::string& strError,
|
||||
bool noProxy)
|
||||
{
|
||||
// Download filelist.txt to a temp file
|
||||
fs::path tmpPath = fs::temp_directory_path() / "triangles_bootstrap_filelist.txt";
|
||||
|
||||
std::string urlPath = std::string(BASE_PATH) + "filelist.txt";
|
||||
if (!DownloadFile(host, urlPath, tmpPath, nullptr, strError))
|
||||
if (!DownloadFile(host, urlPath, tmpPath, nullptr, strError, noProxy))
|
||||
return false;
|
||||
|
||||
// Read lines
|
||||
@@ -458,10 +670,12 @@ bool DownloadBootstrap(const std::string& host,
|
||||
bool gotBlockFile = false;
|
||||
|
||||
// Try downloading bootstrap.tar.gz first
|
||||
// Bootstrap server is on clearnet — bypass Tor proxy for DNS + HTTP
|
||||
const bool noProxy = true;
|
||||
fs::path tmpTarGz = dataDir / "bootstrap.tar.gz.tmp";
|
||||
std::string tarUrl = std::string(BASE_PATH) + "triangles-bootstrap.tar.gz";
|
||||
|
||||
bool tarDownloaded = DownloadFile(host, tarUrl, tmpTarGz, progressFn, strError);
|
||||
bool tarDownloaded = DownloadFile(host, tarUrl, tmpTarGz, progressFn, strError, noProxy);
|
||||
|
||||
if (tarDownloaded) {
|
||||
bool extractOk = ExtractTarGz(tmpTarGz, dataDir, strError);
|
||||
@@ -476,7 +690,7 @@ bool DownloadBootstrap(const std::string& host,
|
||||
// Fallback: try filelist.txt + individual file downloads
|
||||
std::string fallbackError;
|
||||
std::vector<std::string> files;
|
||||
if (!FetchFileList(host, files, fallbackError)) {
|
||||
if (!FetchFileList(host, files, fallbackError, noProxy)) {
|
||||
if (!tarDownloaded)
|
||||
strError = strError + " (fallback also failed: " + fallbackError + ")";
|
||||
else
|
||||
@@ -489,7 +703,7 @@ bool DownloadBootstrap(const std::string& host,
|
||||
fs::create_directories(destPath.parent_path());
|
||||
|
||||
std::string urlPath = std::string(BASE_PATH) + files[i];
|
||||
if (!DownloadFile(host, urlPath, destPath, progressFn, strError))
|
||||
if (!DownloadFile(host, urlPath, destPath, progressFn, strError, noProxy))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -553,4 +767,38 @@ bool DownloadBootstrap(const std::string& host,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DownloadUtxoSnapshot(const std::string& host,
|
||||
const fs::path& dataDir,
|
||||
ProgressCallback progressFn,
|
||||
std::string& strError)
|
||||
{
|
||||
const bool noProxy = true;
|
||||
const char* snapshotFilename = "utxo-snapshot.bin";
|
||||
|
||||
// Download utxo-snapshot.bin to a temp file
|
||||
fs::path tmpPath = dataDir / "utxo-snapshot.bin.tmp";
|
||||
std::string urlPath = std::string(BASE_PATH) + snapshotFilename;
|
||||
|
||||
printf("Bootstrap: downloading UTXO snapshot from %s%s...\n", host.c_str(), urlPath.c_str());
|
||||
|
||||
if (!DownloadFile(host, urlPath, tmpPath, progressFn, strError, noProxy)) {
|
||||
fs::remove(tmpPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("Bootstrap: UTXO snapshot downloaded, loading into database...\n");
|
||||
|
||||
// Load the snapshot into a fresh txleveldb
|
||||
if (!UtxoSnapshot::LoadSnapshot(tmpPath, dataDir, strError)) {
|
||||
fs::remove(tmpPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clean up the temp file
|
||||
fs::remove(tmpPath);
|
||||
|
||||
printf("Bootstrap: UTXO snapshot loaded successfully.\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Bootstrap
|
||||
|
||||
+15
-3
@@ -22,16 +22,20 @@ namespace Bootstrap {
|
||||
// Check if data dir already has blockchain data
|
||||
bool NeedsBootstrap(const boost::filesystem::path& dataDir);
|
||||
|
||||
// Download a single file via HTTP GET, write to destPath
|
||||
// Download a single file via HTTP GET, write to destPath.
|
||||
// If noProxy is true, bypass Tor SOCKS proxy and connect directly
|
||||
// (used for clearnet bootstrap downloads).
|
||||
bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
const boost::filesystem::path& destPath,
|
||||
ProgressCallback progressFn,
|
||||
std::string& strError);
|
||||
std::string& strError,
|
||||
bool noProxy = false);
|
||||
|
||||
// Fetch the file manifest (list of relative paths to download)
|
||||
bool FetchFileList(const std::string& host,
|
||||
std::vector<std::string>& files,
|
||||
std::string& strError);
|
||||
std::string& strError,
|
||||
bool noProxy = false);
|
||||
|
||||
// Download bootstrap.tar.gz and extract to dataDir.
|
||||
// Falls back to filelist.txt + individual file download if tar.gz unavailable.
|
||||
@@ -58,6 +62,14 @@ namespace Bootstrap {
|
||||
bool VerifyManifest(const SnapshotManifest& manifest,
|
||||
std::string& strError);
|
||||
|
||||
// Download a UTXO snapshot and load it into a fresh txleveldb.
|
||||
// This is much faster than downloading the full bootstrap archive.
|
||||
// Returns true if snapshot was downloaded and loaded successfully.
|
||||
bool DownloadUtxoSnapshot(const std::string& host,
|
||||
const boost::filesystem::path& dataDir,
|
||||
ProgressCallback progressFn,
|
||||
std::string& strError);
|
||||
|
||||
} // namespace Bootstrap
|
||||
|
||||
#endif // TRIANGLES_BOOTSTRAP_H
|
||||
|
||||
@@ -33,6 +33,13 @@ namespace Checkpoints
|
||||
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
|
||||
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
|
||||
{2186940, uint256("0xbd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0")},
|
||||
{2190000, uint256("0x682baf783581468ba18f9967254a7f3944e8b8c4cc7101e7d99b68f4f9dd5271")},
|
||||
{2200000, uint256("0x0a8d0442f031f1258120f713f34e45f4f9a625fb753558e27b89b32ad5a9a740")},
|
||||
{2205000, uint256("0xf7aa893ec012181e321783d6a5487addf6997377908faa3e760c9054a4217d29")},
|
||||
{2206000, uint256("0x780ae878f8b10b6cbd51ceb2c0799c90d3551fa916be62974375071b9e36581c")},
|
||||
{2207000, uint256("0x8836d67b0f08036c4a7c26ff0a29d4461a52b6d8f552165ad9c1abec2f3cadfd")},
|
||||
{2208000, uint256("0xe4a19e8a29fa7aae47f7563377af3e896fee18ed069a64e325b8c2c6c820a1be")},
|
||||
{2209000, uint256("0x04c78a6fc863bed918a9364c58c64489943b2e85d84ddb1ac2fba584f390d5dc")},
|
||||
};
|
||||
|
||||
static MapCheckpoints mapCheckpointsTestnet = {
|
||||
@@ -49,6 +56,13 @@ namespace Checkpoints
|
||||
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
|
||||
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
|
||||
{2186940, uint256("0xbd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0")},
|
||||
{2190000, uint256("0x682baf783581468ba18f9967254a7f3944e8b8c4cc7101e7d99b68f4f9dd5271")},
|
||||
{2200000, uint256("0x0a8d0442f031f1258120f713f34e45f4f9a625fb753558e27b89b32ad5a9a740")},
|
||||
{2205000, uint256("0xf7aa893ec012181e321783d6a5487addf6997377908faa3e760c9054a4217d29")},
|
||||
{2206000, uint256("0x780ae878f8b10b6cbd51ceb2c0799c90d3551fa916be62974375071b9e36581c")},
|
||||
{2207000, uint256("0x8836d67b0f08036c4a7c26ff0a29d4461a52b6d8f552165ad9c1abec2f3cadfd")},
|
||||
{2208000, uint256("0xe4a19e8a29fa7aae47f7563377af3e896fee18ed069a64e325b8c2c6c820a1be")},
|
||||
{2209000, uint256("0x04c78a6fc863bed918a9364c58c64489943b2e85d84ddb1ac2fba584f390d5dc")},
|
||||
};
|
||||
|
||||
bool CheckHardened(int nHeight, const uint256& hash)
|
||||
|
||||
+2
-2
@@ -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 8
|
||||
#define CLIENT_VERSION_REVISION 1
|
||||
#define CLIENT_VERSION_MINOR 9
|
||||
#define CLIENT_VERSION_REVISION 2
|
||||
#define CLIENT_VERSION_BUILD 0
|
||||
|
||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||
|
||||
@@ -28,6 +28,7 @@ extern unsigned int nWalletDBUpdated;
|
||||
|
||||
void ThreadFlushWalletDB(void* parg);
|
||||
bool BackupWallet(const CWallet& wallet, const std::string& strDest);
|
||||
bool AutoBackupWallet(const boost::filesystem::path& walletPath);
|
||||
|
||||
|
||||
class CDBEnv
|
||||
|
||||
+82
-11
@@ -14,6 +14,7 @@
|
||||
#include "smessage.h"
|
||||
#include "openssl_compat.h"
|
||||
#include "bootstrap.h"
|
||||
#include "utxosnapshot.h"
|
||||
#include "tor/tor_embedded.h"
|
||||
#include "tor/onion_v3.h"
|
||||
#include "tor/tor_process.h"
|
||||
@@ -669,7 +670,7 @@ bool AppInit2()
|
||||
nScriptCheckThreads = 16;
|
||||
if (nScriptCheckThreads > 1)
|
||||
{
|
||||
pScriptCheckQueue = new CCheckQueue<CScriptCheck>(128);
|
||||
pScriptCheckQueue = new CCheckQueue<CScriptCheck>(32);
|
||||
pScriptCheckThreads = new boost::thread_group();
|
||||
for (int i = 0; i < nScriptCheckThreads - 1; ++i)
|
||||
pScriptCheckThreads->create_thread(&ThreadScriptCheck);
|
||||
@@ -907,9 +908,6 @@ bool AppInit2()
|
||||
std::string host = Bootstrap::DEFAULT_HOST;
|
||||
std::string strError;
|
||||
|
||||
uiInterface.InitMessage(_("Downloading blockchain snapshot..."));
|
||||
printf("Bootstrap: contacting %s...\n", host.c_str());
|
||||
|
||||
auto progressFn = [](int64_t bytesDownloaded, int64_t totalBytes) {
|
||||
if (totalBytes > 0) {
|
||||
printf("\rBootstrap: %lld / %lld MB (%lld%%)",
|
||||
@@ -920,20 +918,66 @@ bool AppInit2()
|
||||
}
|
||||
};
|
||||
|
||||
bool success = Bootstrap::DownloadBootstrap(host, dataPath, progressFn, strError);
|
||||
// Try UTXO snapshot first (fast: ~2-10 MB download)
|
||||
bool success = false;
|
||||
bool triedUtxoSnapshot = false;
|
||||
if (needsBootstrap && !fs::exists(dataPath / "txleveldb")) {
|
||||
uiInterface.InitMessage(_("Downloading UTXO snapshot..."));
|
||||
printf("Bootstrap: trying UTXO snapshot from %s (fast path)...\n", host.c_str());
|
||||
|
||||
if (!success) {
|
||||
printf("\nBootstrap: failed: %s\n", strError.c_str());
|
||||
printf("Bootstrap: skipping, will sync from network.\n");
|
||||
} else {
|
||||
printf("\nBootstrap: done.\n");
|
||||
std::string utxoError;
|
||||
if (Bootstrap::DownloadUtxoSnapshot(host, dataPath, progressFn, utxoError)) {
|
||||
printf("\nBootstrap: UTXO snapshot loaded — will sync remaining blocks from network.\n");
|
||||
success = true;
|
||||
} else {
|
||||
printf("\nBootstrap: UTXO snapshot unavailable: %s\n", utxoError.c_str());
|
||||
printf("Bootstrap: falling back to full bootstrap download...\n");
|
||||
}
|
||||
triedUtxoSnapshot = true;
|
||||
}
|
||||
|
||||
// Fall back to full bootstrap.tar.gz if UTXO snapshot failed
|
||||
if (!success) {
|
||||
uiInterface.InitMessage(_("Downloading blockchain snapshot..."));
|
||||
printf("Bootstrap: contacting %s...\n", host.c_str());
|
||||
|
||||
success = Bootstrap::DownloadBootstrap(host, dataPath, progressFn, strError);
|
||||
|
||||
if (!success) {
|
||||
printf("\nBootstrap: failed: %s\n", strError.c_str());
|
||||
printf("Bootstrap: skipping, will sync from network.\n");
|
||||
} else {
|
||||
printf("\nBootstrap: done.\n");
|
||||
}
|
||||
}
|
||||
|
||||
StartupPerfLog("bootstrap_download", GetTimeMillis() - nBootstrapStart,
|
||||
strprintf("host=%s success=%d", host.c_str(), success));
|
||||
strprintf("host=%s success=%d utxo_snapshot=%d", host.c_str(), success, triedUtxoSnapshot));
|
||||
}
|
||||
} // end bootstrap scope
|
||||
#endif
|
||||
|
||||
// ********************************************************* Step 6c: manual UTXO snapshot loading
|
||||
// If utxo-snapshot.bin exists in data dir and no txleveldb, load it.
|
||||
{
|
||||
fs::path dataPath = GetDataDir();
|
||||
fs::path snapshotFile = dataPath / "utxo-snapshot.bin";
|
||||
fs::path txleveldbDir = dataPath / "txleveldb";
|
||||
|
||||
if (fs::exists(snapshotFile) && !fs::exists(txleveldbDir)) {
|
||||
printf("Found utxo-snapshot.bin — loading UTXO snapshot...\n");
|
||||
uiInterface.InitMessage(_("Loading UTXO snapshot..."));
|
||||
|
||||
std::string strError;
|
||||
if (UtxoSnapshot::LoadSnapshot(snapshotFile, dataPath, strError)) {
|
||||
printf("UTXO snapshot loaded successfully.\n");
|
||||
} else {
|
||||
printf("UTXO snapshot load failed: %s\n", strError.c_str());
|
||||
printf("Will proceed with normal sync.\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ********************************************************* Step 7: load blockchain
|
||||
|
||||
if (!bitdb.Open(GetDataDir()))
|
||||
@@ -952,6 +996,18 @@ bool AppInit2()
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle -reindex: delete the LevelDB block index so it gets rebuilt
|
||||
// from the raw blk*.dat files via FastImportBlockFile().
|
||||
// This recalculates money supply, tx index, and UTXO set from scratch.
|
||||
if (GetBoolArg("-reindex", false))
|
||||
{
|
||||
printf("Reindex requested: removing block index database...\n");
|
||||
uiInterface.InitMessage(_("Removing block index for reindex..."));
|
||||
fs::path txleveldbPath = GetDataDir() / "txleveldb";
|
||||
if (fs::exists(txleveldbPath))
|
||||
fs::remove_all(txleveldbPath);
|
||||
}
|
||||
|
||||
uiInterface.InitMessage(_("Loading block index..."));
|
||||
printf("Loading block index...\n");
|
||||
nStart = GetTimeMillis();
|
||||
@@ -1041,6 +1097,21 @@ bool AppInit2()
|
||||
nStart = GetTimeMillis();
|
||||
bool fFirstRun = true;
|
||||
pwalletMain = new CWallet(strWalletFileName);
|
||||
|
||||
// Auto-backup wallet.dat before loading (protects against corruption during load/flush)
|
||||
{
|
||||
fs::path walletPath = GetDataDir() / strWalletFileName;
|
||||
if (fs::exists(walletPath)) {
|
||||
uintmax_t wsize = fs::file_size(walletPath);
|
||||
printf("Wallet file size: %llu bytes\n", (unsigned long long)wsize);
|
||||
if (wsize < 1024) {
|
||||
strErrors << _("WARNING: wallet.dat is suspiciously small (") << wsize << _(" bytes). It may be corrupt.\n");
|
||||
printf("WARNING: wallet.dat is only %llu bytes - possibly corrupt!\n", (unsigned long long)wsize);
|
||||
}
|
||||
AutoBackupWallet(walletPath);
|
||||
}
|
||||
}
|
||||
|
||||
DBErrors nLoadWalletRet = pwalletMain->LoadWallet(fFirstRun);
|
||||
if (nLoadWalletRet != DB_LOAD_OK)
|
||||
{
|
||||
|
||||
+6
-2
@@ -31,9 +31,13 @@ int64_t GetWeight(int64_t nIntervalBeginning, int64_t nIntervalEnd)
|
||||
if (nAge < 0)
|
||||
return 0;
|
||||
|
||||
// After v5 fork: remove max age cap so coins aged during the freeze can stake
|
||||
// After v5 fork: use soft cap of 7 days instead of hard nStakeMaxAge.
|
||||
// This prevents "stake surprise" where a whale who was offline for weeks
|
||||
// comes back with massively amplified staking power and dominates blocks.
|
||||
// The 7-day cap still allows generous accumulation while limiting abuse.
|
||||
static const int64_t STAKE_AGE_SOFT_CAP = 7 * 24 * 60 * 60; // 7 days
|
||||
if (pindexBest && pindexBest->nHeight >= FORK_HEIGHT_V5)
|
||||
return nAge;
|
||||
return min(nAge, STAKE_AGE_SOFT_CAP);
|
||||
|
||||
return min(nAge, (int64_t)nStakeMaxAge);
|
||||
}
|
||||
|
||||
+876
-87
File diff suppressed because it is too large
Load Diff
+134
-4
@@ -12,6 +12,7 @@
|
||||
#include "scrypt.h"
|
||||
#include "hashblock.h"
|
||||
#include "checkqueue.h"
|
||||
#include "sigcache.h"
|
||||
|
||||
#include <list>
|
||||
|
||||
@@ -38,7 +39,8 @@ static const unsigned int MAX_BLOCK_SIZE_GEN = MAX_BLOCK_SIZE/2;
|
||||
static const unsigned int MAX_BLOCK_SIGOPS = MAX_BLOCK_SIZE/50;
|
||||
static const unsigned int MAX_ORPHAN_TRANSACTIONS = MAX_BLOCK_SIZE/100;
|
||||
static const unsigned int MAX_ORPHAN_BLOCKS = 750;
|
||||
static const unsigned int MAX_ORPHAN_BLOCKS_IBD = 4000;
|
||||
static const unsigned int MAX_ORPHAN_BLOCKS_IBD = 1500;
|
||||
static const unsigned int MAX_REORG_DEPTH = 100; // reject reorgs deeper than this (finality)
|
||||
static const unsigned int MAX_INV_SZ = 50000;
|
||||
static const int64_t MIN_TX_FEE = (1 * CENT) / 100;
|
||||
static const int64_t MIN_RELAY_TX_FEE = (1 * CENT) / 100;
|
||||
@@ -59,10 +61,10 @@ static const int fHaveUPnP = false;
|
||||
|
||||
static const uint256 hashGenesisBlockOfficial("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
|
||||
static const uint256 hashGenesisBlockTestNet ("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
|
||||
inline int64_t GetMaxTimeDrift(int nHeight) { return (nHeight >= FORK_HEIGHT_V5_4) ? 3 * 60 : 10 * 60; }
|
||||
inline int64_t GetMaxTimeDrift(int nHeight) { return (nHeight >= FORK_HEIGHT_V5_4) ? 90 : 10 * 60; }
|
||||
inline int64_t PastDrift(int64_t nTime, int nHeight) { return nTime - GetMaxTimeDrift(nHeight); }
|
||||
inline int64_t FutureDrift(int64_t nTime, int nHeight) { return nTime + GetMaxTimeDrift(nHeight); }
|
||||
// Height-less overloads always use post-V5.4 rules (3-min drift).
|
||||
// Height-less overloads always use post-V5.4 rules (90-second drift).
|
||||
// All nodes are well past FORK_HEIGHT_V5_4; using the global nBestHeight
|
||||
// here previously caused nodes at different heights to disagree on block
|
||||
// validity during the fork transition — a consensus-splitting bug.
|
||||
@@ -83,6 +85,7 @@ extern uint256 nBestChainTrust;
|
||||
extern uint256 nBestInvalidTrust;
|
||||
extern uint256 hashBestChain;
|
||||
extern CBlockIndex* pindexBest;
|
||||
extern CBlockIndex* pindexFinalized; // auto-checkpoint: deepest finalized block
|
||||
extern unsigned int nTransactionsUpdated;
|
||||
extern uint64_t nLastBlockTx;
|
||||
extern uint64_t nLastBlockSize;
|
||||
@@ -136,6 +139,7 @@ bool IsInitialBlockDownload();
|
||||
std::string GetWarnings(std::string strFor);
|
||||
bool GetTransaction(const uint256 &hash, CTransaction &tx, uint256 &hashBlock);
|
||||
uint256 WantedByOrphan(const CBlock* pblockOrphan);
|
||||
unsigned int LimitOrphanBlocks(unsigned int nMaxOrphans);
|
||||
const CBlockIndex* GetLastBlockIndex(const CBlockIndex* pindex, bool fProofOfStake);
|
||||
void StakeMiner(CWallet *pwallet);
|
||||
void ResendWalletTransactions(bool fForce = false);
|
||||
@@ -1559,6 +1563,12 @@ public:
|
||||
return vHave.empty();
|
||||
}
|
||||
|
||||
// Return the first hash in the locator (peer's tip), or 0 if empty
|
||||
uint256 GetTipHash() const
|
||||
{
|
||||
return vHave.empty() ? uint256(0) : vHave[0];
|
||||
}
|
||||
|
||||
void Set(const CBlockIndex* pindex)
|
||||
{
|
||||
vHave.clear();
|
||||
@@ -1678,6 +1688,121 @@ public:
|
||||
};
|
||||
|
||||
extern CTxMemPool mempool;
|
||||
extern CScriptVerifyCache scriptVerifyCache;
|
||||
|
||||
/**
|
||||
* Compact block relay for Tor-only networks.
|
||||
*
|
||||
* Instead of sending a full block, send the header + short transaction IDs.
|
||||
* The receiver reconstructs the block from its mempool. For PoS blocks with
|
||||
* 0-2 transactions (the common case), the coinstake is always prefilled, so
|
||||
* the compact block IS the complete block — no extra round-trip needed.
|
||||
*/
|
||||
|
||||
/** Short transaction ID: first 6 bytes of SipHash(txid) */
|
||||
static inline uint64_t GetShortTxId(const uint256& txhash, uint64_t nonce)
|
||||
{
|
||||
// Simple short ID: XOR txhash prefix with nonce
|
||||
uint64_t id = 0;
|
||||
memcpy(&id, txhash.begin(), 6); // first 6 bytes
|
||||
id ^= nonce;
|
||||
return id & 0xFFFFFFFFFFFFULL; // mask to 48 bits
|
||||
}
|
||||
|
||||
class CCompactBlock
|
||||
{
|
||||
public:
|
||||
// Block header fields
|
||||
int nVersion;
|
||||
uint256 hashPrevBlock;
|
||||
uint256 hashMerkleRoot;
|
||||
unsigned int nTime;
|
||||
unsigned int nBits;
|
||||
unsigned int nNonce;
|
||||
std::vector<unsigned char> vchBlockSig;
|
||||
|
||||
// Compact block data
|
||||
uint64_t nShortIdNonce; // nonce for short ID calculation
|
||||
std::vector<uint64_t> vShortTxIds; // short IDs for non-prefilled txs
|
||||
std::vector<std::pair<uint16_t, CTransaction>> vPrefilledTxn; // index + full tx
|
||||
|
||||
CCompactBlock() : nVersion(0), nTime(0), nBits(0), nNonce(0), nShortIdNonce(0) {}
|
||||
|
||||
// Construct from a full block: prefill coinbase + coinstake, short-ID the rest
|
||||
CCompactBlock(const CBlock& block)
|
||||
{
|
||||
nVersion = block.nVersion;
|
||||
hashPrevBlock = block.hashPrevBlock;
|
||||
hashMerkleRoot = block.hashMerkleRoot;
|
||||
nTime = block.nTime;
|
||||
nBits = block.nBits;
|
||||
nNonce = block.nNonce;
|
||||
vchBlockSig = block.vchBlockSig;
|
||||
nShortIdNonce = GetRand(std::numeric_limits<uint64_t>::max());
|
||||
|
||||
for (uint16_t i = 0; i < block.vtx.size(); i++)
|
||||
{
|
||||
if (i <= 1) {
|
||||
// Always prefill coinbase (idx 0) and coinstake (idx 1)
|
||||
vPrefilledTxn.push_back(std::make_pair(i, block.vtx[i]));
|
||||
} else {
|
||||
vShortTxIds.push_back(GetShortTxId(block.vtx[i].GetHash(), nShortIdNonce));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SERIALIZE
|
||||
(
|
||||
READWRITE(nVersion);
|
||||
READWRITE(hashPrevBlock);
|
||||
READWRITE(hashMerkleRoot);
|
||||
READWRITE(nTime);
|
||||
READWRITE(nBits);
|
||||
READWRITE(nNonce);
|
||||
READWRITE(vchBlockSig);
|
||||
READWRITE(nShortIdNonce);
|
||||
READWRITE(vShortTxIds);
|
||||
READWRITE(vPrefilledTxn);
|
||||
)
|
||||
|
||||
uint256 GetBlockHash() const
|
||||
{
|
||||
CBlock hdr;
|
||||
hdr.nVersion = nVersion;
|
||||
hdr.hashPrevBlock = hashPrevBlock;
|
||||
hdr.hashMerkleRoot = hashMerkleRoot;
|
||||
hdr.nTime = nTime;
|
||||
hdr.nBits = nBits;
|
||||
hdr.nNonce = nNonce;
|
||||
return hdr.GetHash();
|
||||
}
|
||||
};
|
||||
|
||||
class CBlockTxnRequest
|
||||
{
|
||||
public:
|
||||
uint256 blockhash;
|
||||
std::vector<uint16_t> vIndex; // indices of missing transactions
|
||||
|
||||
IMPLEMENT_SERIALIZE
|
||||
(
|
||||
READWRITE(blockhash);
|
||||
READWRITE(vIndex);
|
||||
)
|
||||
};
|
||||
|
||||
class CBlockTxnResponse
|
||||
{
|
||||
public:
|
||||
uint256 blockhash;
|
||||
std::vector<CTransaction> vTxn;
|
||||
|
||||
IMPLEMENT_SERIALIZE
|
||||
(
|
||||
READWRITE(blockhash);
|
||||
READWRITE(vTxn);
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* Closure representing one script check for parallel verification.
|
||||
@@ -1702,7 +1827,12 @@ public:
|
||||
|
||||
bool operator()()
|
||||
{
|
||||
return ptxTo && VerifyScript(scriptSig, scriptPubKey, *ptxTo, nIn, nHashType);
|
||||
if (!ptxTo)
|
||||
return false;
|
||||
if (!VerifyScript(scriptSig, scriptPubKey, *ptxTo, nIn, nHashType))
|
||||
return false;
|
||||
scriptVerifyCache.Set(ptxTo->GetHash(), nIn);
|
||||
return true;
|
||||
}
|
||||
|
||||
void swap(CScriptCheck& other)
|
||||
|
||||
+14
-3
@@ -413,7 +413,7 @@ void StakeMiner(CWallet *pwallet)
|
||||
if (fTryToSync)
|
||||
{
|
||||
fTryToSync = false;
|
||||
if (vNodes.size() < 1 || nBestHeight < GetNumBlocksOfPeers())
|
||||
if (vNodes.size() < 2 || nBestHeight < GetNumBlocksOfPeers())
|
||||
{
|
||||
MilliSleep(60000);
|
||||
continue;
|
||||
@@ -446,9 +446,20 @@ void StakeMiner(CWallet *pwallet)
|
||||
{
|
||||
printf("StakeMiner(): A proof-of-stake block has been found! %s\n", pblock->GetHash().ToString().c_str());
|
||||
SetThreadPriority(THREAD_PRIORITY_NORMAL);
|
||||
CheckStake(pblock.get(), *pwallet);
|
||||
bool fAccepted = CheckStake(pblock.get(), *pwallet);
|
||||
SetThreadPriority(THREAD_PRIORITY_LOWEST);
|
||||
MilliSleep(500);
|
||||
if (fAccepted)
|
||||
{
|
||||
MilliSleep(500);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Block was orphaned or rejected — apply a cooldown to reduce
|
||||
// fork oscillation. Without this, the staker immediately retries
|
||||
// with a different timestamp, potentially creating competing forks.
|
||||
printf("StakeMiner(): block not accepted, cooldown 30s\n");
|
||||
MilliSleep(30000);
|
||||
}
|
||||
}
|
||||
else
|
||||
MilliSleep(500);
|
||||
|
||||
+146
-44
@@ -37,7 +37,7 @@ extern "C" {
|
||||
// int tor_main(int argc, char *argv[]);
|
||||
}
|
||||
|
||||
static const int MAX_OUTBOUND_CONNECTIONS = 16;
|
||||
static const int MAX_OUTBOUND_CONNECTIONS = 8; // reduced from 16 for Tor-only small networks
|
||||
|
||||
void ThreadMessageHandler2(void* parg);
|
||||
void ThreadSocketHandler2(void* parg);
|
||||
@@ -47,7 +47,7 @@ void ThreadOpenAddedConnections2(void* parg);
|
||||
void ThreadMapPort2(void* parg);
|
||||
#endif
|
||||
void ThreadHTTPSeedFetch(void* parg);
|
||||
void ThreadHTTPSeedFetch2(void* parg);
|
||||
bool ThreadHTTPSeedFetch2(void* parg);
|
||||
bool OpenNetworkConnection(const CAddress& addrConnect, CSemaphoreGrant *grantOutbound = NULL, const char *strDest = NULL, bool fOneShot = false);
|
||||
|
||||
|
||||
@@ -690,6 +690,9 @@ void CNode::copyStats(CNodeStats &stats)
|
||||
X(fInbound);
|
||||
X(nStartingHeight);
|
||||
X(nMisbehavior);
|
||||
X(nPingUsecTime);
|
||||
X(nBlocksDelivered);
|
||||
X(nAvgBlockLatencyUs);
|
||||
}
|
||||
#undef X
|
||||
|
||||
@@ -1029,10 +1032,6 @@ void ThreadSocketHandler2(void* parg)
|
||||
if (nErr != WSAEWOULDBLOCK)
|
||||
printf("socket error accept failed: %d\n", nErr);
|
||||
}
|
||||
else if (nInbound >= GetArg("-maxconnections", 125) - MAX_OUTBOUND_CONNECTIONS)
|
||||
{
|
||||
closesocket(hSocket);
|
||||
}
|
||||
else if (CNode::IsBanned(addr))
|
||||
{
|
||||
printf("connection from %s dropped (banned)\n", addr.ToString().c_str());
|
||||
@@ -1040,12 +1039,36 @@ void ThreadSocketHandler2(void* parg)
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("accepted connection %s\n", addr.ToString().c_str());
|
||||
CNode* pnode = new CNode(hSocket, addr, "", true);
|
||||
pnode->AddRef();
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
vNodes.push_back(pnode);
|
||||
int nMaxInbound = GetArg("-maxconnections", 125) - MAX_OUTBOUND_CONNECTIONS;
|
||||
bool fAccept = (nInbound < nMaxInbound);
|
||||
|
||||
// Reserve 2 extra inbound slots for known seed nodes
|
||||
if (!fAccept) {
|
||||
bool fIsSeed = false;
|
||||
static const char *(*strOnionSeedCheck)[1] = fTestNet ? strTestNetOnionSeed : strMainNetOnionSeed;
|
||||
std::string incomingAddr = addr.ToStringIP();
|
||||
for (unsigned int si = 0; strOnionSeedCheck[si][0] != NULL; si++) {
|
||||
if (incomingAddr.find(strOnionSeedCheck[si][0]) != std::string::npos) {
|
||||
fIsSeed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fIsSeed && nInbound < nMaxInbound + 2) {
|
||||
fAccept = true;
|
||||
printf("accepted seed node %s (reserved slot)\n", addr.ToString().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (fAccept) {
|
||||
printf("accepted connection %s\n", addr.ToString().c_str());
|
||||
CNode* pnode = new CNode(hSocket, addr, "", true);
|
||||
pnode->AddRef();
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
vNodes.push_back(pnode);
|
||||
}
|
||||
} else {
|
||||
closesocket(hSocket);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1137,14 +1160,14 @@ void ThreadSocketHandler2(void* parg)
|
||||
printf("socket no message in first 60 seconds, %d %d\n", pnode->nLastRecv != 0, pnode->nLastSend != 0);
|
||||
pnode->fDisconnect = true;
|
||||
}
|
||||
else if (GetTime() - pnode->nLastSend > 90*60 && GetTime() - pnode->nLastSendEmpty > 90*60)
|
||||
else if (GetTime() - pnode->nLastSend > 10*60 && GetTime() - pnode->nLastSendEmpty > 10*60)
|
||||
{
|
||||
printf("socket not sending\n");
|
||||
printf("socket not sending (10min timeout)\n");
|
||||
pnode->fDisconnect = true;
|
||||
}
|
||||
else if (GetTime() - pnode->nLastRecv > 90*60)
|
||||
else if (GetTime() - pnode->nLastRecv > 10*60)
|
||||
{
|
||||
printf("socket inactivity timeout\n");
|
||||
printf("socket inactivity timeout (10min)\n");
|
||||
pnode->fDisconnect = true;
|
||||
}
|
||||
}
|
||||
@@ -1381,7 +1404,7 @@ void ThreadOnionSeed(void* parg)
|
||||
|
||||
|
||||
|
||||
// Load hardcoded .onion seeds (if any)
|
||||
// Load hardcoded .onion seeds and queue them for immediate direct connection
|
||||
static const char *(*strOnionSeed)[1] = fTestNet ? strTestNetOnionSeed : strMainNetOnionSeed;
|
||||
int found = 0;
|
||||
|
||||
@@ -1394,27 +1417,50 @@ void ThreadOnionSeed(void* parg)
|
||||
CAddress addr = CAddress(CService(parsed, GetDefaultPort()));
|
||||
addr.nTime = GetTime() - 3*nOneDay - GetRand(4*nOneDay);
|
||||
addrman.Add(addr, parsed);
|
||||
|
||||
// Queue for immediate direct connection (OneShot) — don't wait for
|
||||
// addrman selection which deprioritizes stale timestamps
|
||||
std::string oneShotAddr = std::string(strOnionSeed[seed_idx][0])
|
||||
+ ":" + std::to_string(GetDefaultPort());
|
||||
AddOneShot(oneShotAddr);
|
||||
found++;
|
||||
}
|
||||
|
||||
printf("%d addresses from hardcoded .onion seeds\n", found);
|
||||
printf("%d addresses from hardcoded .onion seeds (queued as OneShot)\n", found);
|
||||
|
||||
// Also fetch dynamic seeds from HTTP seed list
|
||||
// Wait for Tor to establish circuits before attempting HTTPS seed fetch.
|
||||
// The hardcoded OneShot connections can race ahead meanwhile.
|
||||
printf("ThreadOnionSeed: waiting 20s for Tor circuits before HTTPS seed fetch...\n");
|
||||
for (int i = 0; i < 20 && !fShutdown; i++)
|
||||
MilliSleep(1000);
|
||||
|
||||
// Fetch dynamic seeds with retry — up to 4 attempts with increasing backoff.
|
||||
// This is the primary discovery mechanism — seeds.cryptographic-triangles.org
|
||||
ThreadHTTPSeedFetch2(NULL);
|
||||
{
|
||||
bool ok = false;
|
||||
int delays[] = {0, 30, 60, 120};
|
||||
for (int attempt = 0; attempt < 4 && !ok && !fShutdown; attempt++) {
|
||||
if (attempt > 0) {
|
||||
printf("ThreadOnionSeed: HTTPS seed fetch retry %d in %ds...\n", attempt, delays[attempt]);
|
||||
for (int i = 0; i < delays[attempt] && !fShutdown; i++)
|
||||
MilliSleep(1000);
|
||||
}
|
||||
if (!fShutdown)
|
||||
ok = ThreadHTTPSeedFetch2(NULL);
|
||||
}
|
||||
if (!ok && !fShutdown)
|
||||
printf("ThreadOnionSeed: all HTTPS seed fetch attempts failed\n");
|
||||
}
|
||||
|
||||
printf("ThreadOnionSeed: initial seeding complete\n");
|
||||
|
||||
// Periodic re-seeding: if the node becomes isolated (0 outbound peers),
|
||||
// re-fetch the seed list. Check every 10 minutes, re-seed at most once
|
||||
// per 30 minutes to avoid hammering the seed server.
|
||||
// Periodic re-seeding for isolated or under-connected nodes.
|
||||
// EMERGENCY MODE: When 0 outbound peers, check every 15 seconds
|
||||
// NORMAL MODE: Check every 2 minutes, re-seed when < 2 outbound peers
|
||||
int64_t nLastReseed = GetTime();
|
||||
bool bFirstReseed = true;
|
||||
while (!fShutdown) {
|
||||
for (int i = 0; i < 600 && !fShutdown; i++) // sleep 10 minutes
|
||||
MilliSleep(1000);
|
||||
|
||||
if (fShutdown) break;
|
||||
|
||||
// Count outbound peers to determine check interval
|
||||
int nOutbound = 0;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
@@ -1423,10 +1469,52 @@ void ThreadOnionSeed(void* parg)
|
||||
nOutbound++;
|
||||
}
|
||||
|
||||
if (nOutbound == 0 && GetTime() - nLastReseed > 30 * 60) {
|
||||
printf("ThreadOnionSeed: no outbound peers, re-seeding...\n");
|
||||
// Emergency mode: 0 peers = check every 15 seconds
|
||||
// Low mode: 1 peer = check every 30 seconds
|
||||
// Normal: 2+ peers = check every 2 minutes
|
||||
int nSleepSeconds = (nOutbound == 0) ? 15 : (nOutbound < 2) ? 30 : 120;
|
||||
for (int i = 0; i < nSleepSeconds && !fShutdown; i++)
|
||||
MilliSleep(1000);
|
||||
|
||||
if (fShutdown) break;
|
||||
|
||||
// Recount after sleep
|
||||
nOutbound = 0;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
if (!pnode->fInbound)
|
||||
nOutbound++;
|
||||
}
|
||||
|
||||
// Emergency (0 peers): no cooldown, reseed immediately
|
||||
// Low (1 peer): 60 second cooldown
|
||||
// Normal (<2): 5 min first, 15 min subsequent
|
||||
int64_t nCooldown;
|
||||
if (nOutbound == 0)
|
||||
nCooldown = 0; // immediate
|
||||
else if (nOutbound < 2)
|
||||
nCooldown = bFirstReseed ? 60 : 5 * 60;
|
||||
else
|
||||
nCooldown = bFirstReseed ? 5 * 60 : 15 * 60;
|
||||
|
||||
if (nOutbound < 2 && GetTime() - nLastReseed > nCooldown) {
|
||||
if (nOutbound == 0)
|
||||
printf("ThreadOnionSeed: EMERGENCY - 0 outbound peers, re-seeding immediately!\n");
|
||||
else
|
||||
printf("ThreadOnionSeed: low outbound peers (%d), re-seeding...\n", nOutbound);
|
||||
|
||||
ThreadHTTPSeedFetch2(NULL);
|
||||
|
||||
// Re-queue hardcoded seeds for direct connection
|
||||
for (unsigned int seed_idx = 0; strOnionSeed[seed_idx][0] != NULL; seed_idx++) {
|
||||
std::string oneShotAddr = std::string(strOnionSeed[seed_idx][0])
|
||||
+ ":" + std::to_string(GetDefaultPort());
|
||||
AddOneShot(oneShotAddr);
|
||||
}
|
||||
|
||||
nLastReseed = GetTime();
|
||||
bFirstReseed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1486,7 +1574,7 @@ void ThreadDumpAddress(void* parg)
|
||||
printf("ThreadDumpAddress exited\n");
|
||||
}
|
||||
|
||||
void ThreadHTTPSeedFetch2(void* parg)
|
||||
bool ThreadHTTPSeedFetch2(void* parg)
|
||||
{
|
||||
static const char* DEFAULT_SEED_URL_HOST = "seeds.cryptographic-triangles.org";
|
||||
static const char* DEFAULT_SEED_URL_PATH = "/seeds.txt";
|
||||
@@ -1515,7 +1603,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
|
||||
if (!ConnectSocketByName(addrResolved, hSocket, connectDest.c_str(), HTTPS_PORT, nConnectTimeout)) {
|
||||
printf("HTTPS seed fetch: cannot connect to %s through Tor proxy\n", seedHost.c_str());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set up TLS over the connected socket
|
||||
@@ -1523,7 +1611,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
if (!ctx) {
|
||||
printf("HTTPS seed fetch: SSL_CTX_new failed\n");
|
||||
closesocket(hSocket);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use system default CA certificates for verification
|
||||
@@ -1535,7 +1623,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
printf("HTTPS seed fetch: SSL_new failed\n");
|
||||
SSL_CTX_free(ctx);
|
||||
closesocket(hSocket);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set SNI hostname (required for Caddy/Let's Encrypt)
|
||||
@@ -1552,7 +1640,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
SSL_free(ssl);
|
||||
SSL_CTX_free(ctx);
|
||||
closesocket(hSocket);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("HTTPS seed fetch: TLS connection established to %s\n", seedHost.c_str());
|
||||
@@ -1575,7 +1663,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
SSL_free(ssl);
|
||||
SSL_CTX_free(ctx);
|
||||
closesocket(hSocket);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
nSent += nBytes;
|
||||
}
|
||||
@@ -1600,21 +1688,21 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
|
||||
if (response.empty()) {
|
||||
printf("HTTPS seed fetch: empty response from %s\n", seedHost.c_str());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse HTTP response - find end of headers
|
||||
size_t headerEnd = response.find("\r\n\r\n");
|
||||
if (headerEnd == std::string::npos) {
|
||||
printf("HTTPS seed fetch: malformed response (no header terminator)\n");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check status code
|
||||
std::string statusLine = response.substr(0, response.find("\r\n"));
|
||||
if (statusLine.find("200") == std::string::npos) {
|
||||
printf("HTTPS seed fetch: %s from %s\n", statusLine.c_str(), seedHost.c_str());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string body = response.substr(headerEnd + 4);
|
||||
@@ -1626,7 +1714,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
while (std::getline(lines, line))
|
||||
{
|
||||
if (fShutdown)
|
||||
return;
|
||||
return false;
|
||||
|
||||
// Trim whitespace and carriage returns
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
|
||||
@@ -1667,17 +1755,24 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
CAddress addr(CService(parsed, port));
|
||||
addr.nTime = GetTime() - 3*24*60*60; // 3 days ago
|
||||
addrman.Add(addr, CNetAddr("https-seed", true));
|
||||
// Queue the first 8 seeds for immediate direct connection
|
||||
if (found < 8) {
|
||||
std::string oneShotAddr = addrStr + ":" + std::to_string(port);
|
||||
AddOneShot(oneShotAddr);
|
||||
}
|
||||
found++;
|
||||
}
|
||||
}
|
||||
|
||||
printf("%d addresses found from HTTPS seed list (%s)\n", found, seedHost.c_str());
|
||||
return found > 0;
|
||||
|
||||
} catch (std::exception& e) {
|
||||
printf("HTTPS seed fetch failed: %s\n", e.what());
|
||||
if (ssl) { SSL_shutdown(ssl); SSL_free(ssl); }
|
||||
if (ctx) SSL_CTX_free(ctx);
|
||||
if (hSocket != INVALID_SOCKET) closesocket(hSocket);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2297,8 +2392,11 @@ void StartNode(void* parg)
|
||||
RenameThread("Triangles-start");
|
||||
|
||||
if (semOutbound == NULL) {
|
||||
// initialize semaphore
|
||||
int nMaxOutbound = min(MAX_OUTBOUND_CONNECTIONS, (int)GetArg("-maxconnections", 125));
|
||||
// initialize semaphore — use -maxoutbound if specified, else default
|
||||
int nMaxOutbound = (int)GetArg("-maxoutbound", MAX_OUTBOUND_CONNECTIONS);
|
||||
nMaxOutbound = min(nMaxOutbound, (int)GetArg("-maxconnections", 125));
|
||||
nMaxOutbound = max(nMaxOutbound, 1); // at least 1 outbound
|
||||
printf("Max outbound connections: %d\n", nMaxOutbound);
|
||||
semOutbound = new CSemaphore(nMaxOutbound);
|
||||
}
|
||||
|
||||
@@ -2368,9 +2466,13 @@ bool StopNode()
|
||||
fShutdown = true;
|
||||
nTransactionsUpdated++;
|
||||
int64_t nStart = GetTime();
|
||||
if (semOutbound)
|
||||
for (int i=0; i<MAX_OUTBOUND_CONNECTIONS; i++)
|
||||
if (semOutbound) {
|
||||
int nMaxOutbound = (int)GetArg("-maxoutbound", MAX_OUTBOUND_CONNECTIONS);
|
||||
nMaxOutbound = min(nMaxOutbound, (int)GetArg("-maxconnections", 125));
|
||||
nMaxOutbound = max(nMaxOutbound, 1);
|
||||
for (int i=0; i<nMaxOutbound; i++)
|
||||
semOutbound->post();
|
||||
}
|
||||
do
|
||||
{
|
||||
int nThreadsRunning = 0;
|
||||
|
||||
@@ -26,7 +26,7 @@ extern int nBestHeight;
|
||||
|
||||
|
||||
|
||||
inline unsigned int ReceiveFloodSize() { return 100 * 1024 * 1024; } // 100 MB
|
||||
inline unsigned int ReceiveFloodSize() { return 50 * 1024 * 1024; } // 50 MB (reduced for Tor-only network)
|
||||
inline unsigned int SendBufferSize() { return 32 * 1024 * 1024; } // 32 MB
|
||||
|
||||
void AddOneShot(std::string strDest);
|
||||
@@ -146,6 +146,9 @@ public:
|
||||
bool fInbound;
|
||||
int nStartingHeight;
|
||||
int nMisbehavior;
|
||||
int64_t nPingUsecTime;
|
||||
int nBlocksDelivered;
|
||||
int64_t nAvgBlockLatencyUs;
|
||||
};
|
||||
|
||||
|
||||
@@ -253,6 +256,7 @@ public:
|
||||
bool fSuccessfullyConnected;
|
||||
bool fDisconnect;
|
||||
bool fPreferHeaders; // peer requested block announcements via headers (sendheaders)
|
||||
bool fSendCmpct; // peer supports compact block relay (sendcmpct)
|
||||
CSemaphoreGrant grantOutbound;
|
||||
int nRefCount;
|
||||
protected:
|
||||
@@ -272,6 +276,17 @@ public:
|
||||
CBlockIndex* pindexLastGetHeadersBegin;
|
||||
uint256 hashLastGetHeadersEnd;
|
||||
int nStartingHeight;
|
||||
int64_t nLastTipCheck; // last time we asked this peer for chain tip
|
||||
int64_t nAvgBlockLatencyUs; // rolling average block delivery latency (microseconds)
|
||||
int nBlocksDelivered; // count of blocks delivered by this peer
|
||||
int nBestKnownHeight; // highest block height known to this peer (updated from inv/block msgs)
|
||||
int nIncompatibleGetblocks; // count of getblocks with no common blocks (fork detection)
|
||||
|
||||
// BIP 31 ping/pong latency tracking
|
||||
uint64_t nPingNonceSent; // nonce of last ping sent (0 = no outstanding ping)
|
||||
int64_t nPingUsecStart; // microsecond timestamp when last ping was sent
|
||||
int64_t nPingUsecTime; // last measured round-trip time (microseconds), 0 = unknown
|
||||
int nPingRetryCount; // consecutive pings without pong response
|
||||
|
||||
// flood relay
|
||||
std::vector<CAddress> vAddrToSend;
|
||||
@@ -310,6 +325,7 @@ public:
|
||||
fSuccessfullyConnected = false;
|
||||
fDisconnect = false;
|
||||
fPreferHeaders = false;
|
||||
fSendCmpct = false;
|
||||
nRefCount = 0;
|
||||
nSendSize = 0;
|
||||
nSendOffset = 0;
|
||||
@@ -319,6 +335,15 @@ public:
|
||||
pindexLastGetHeadersBegin = 0;
|
||||
hashLastGetHeadersEnd = 0;
|
||||
nStartingHeight = -1;
|
||||
nLastTipCheck = 0;
|
||||
nAvgBlockLatencyUs = 0;
|
||||
nBlocksDelivered = 0;
|
||||
nBestKnownHeight = -1;
|
||||
nIncompatibleGetblocks = 0;
|
||||
nPingNonceSent = 0;
|
||||
nPingUsecStart = 0;
|
||||
nPingUsecTime = 0;
|
||||
nPingRetryCount = 0;
|
||||
fGetAddr = false;
|
||||
nMisbehavior = 0;
|
||||
hashCheckpointKnown = 0;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Hardcoded onion seed nodes for initial peer discovery.
|
||||
// Also fetched dynamically via https://seeds.cryptographic-triangles.org/seeds.txt
|
||||
static const char *strMainNetOnionSeed[][1] = {
|
||||
{"6ygpphp2qsucwvhwefv6h6ehvk6zjf7b7zdp4ggkzjjwe76cg6jwm7id.onion"},
|
||||
{"jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion"},
|
||||
{"uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion"},
|
||||
{"el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion"},
|
||||
|
||||
@@ -868,7 +868,7 @@ QWidget#line {
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="http://explorer.triangles.technology"> &#187; TRI block explorer</a></body></string>
|
||||
<a href="https://blocks.cryptographic-triangles.org"> &#187; TRI block explorer</a></body></string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "addressindex.h"
|
||||
#include "txdb.h"
|
||||
#include "base58.h"
|
||||
#include "utxosnapshot.h"
|
||||
|
||||
using namespace json_spirit;
|
||||
using namespace std;
|
||||
@@ -28,6 +29,9 @@ double GetDifficulty(const CBlockIndex* blockindex)
|
||||
blockindex = GetLastBlockIndex(pindexBest, false);
|
||||
}
|
||||
|
||||
if (blockindex == NULL)
|
||||
return 1.0;
|
||||
|
||||
int nShift = (blockindex->nBits >> 24) & 0xff;
|
||||
|
||||
double dDiff =
|
||||
@@ -360,6 +364,188 @@ Value gettxoutsetinfo(const Array& params, bool fHelp)
|
||||
return obj;
|
||||
}
|
||||
|
||||
static void GetActiveChainVector(std::vector<CBlockIndex*>& chain)
|
||||
{
|
||||
chain.clear();
|
||||
|
||||
if (!pindexBest)
|
||||
throw runtime_error("recalculatesupply: no best block");
|
||||
|
||||
for (CBlockIndex* pindex = pindexBest; pindex; pindex = pindex->pprev)
|
||||
chain.push_back(pindex);
|
||||
|
||||
std::reverse(chain.begin(), chain.end());
|
||||
}
|
||||
|
||||
static int64_t ComputeActiveChainSupplyFromBlocks(const std::vector<CBlockIndex*>& chain, int& nBlocksScanned, int& nTransactionsScanned)
|
||||
{
|
||||
nBlocksScanned = 0;
|
||||
nTransactionsScanned = 0;
|
||||
|
||||
CTxDB txdb("r");
|
||||
int64_t nSupply = 0;
|
||||
|
||||
for (std::vector<CBlockIndex*>::const_iterator pindexIt = chain.begin(); pindexIt != chain.end(); ++pindexIt)
|
||||
{
|
||||
CBlockIndex* pindex = *pindexIt;
|
||||
if (!pindex)
|
||||
throw runtime_error("recalculatesupply: null active-chain block index");
|
||||
|
||||
if (pindex->nHeight == 0)
|
||||
{
|
||||
nBlocksScanned++;
|
||||
continue;
|
||||
}
|
||||
|
||||
CBlock block;
|
||||
int64_t nBlockValueIn = 0;
|
||||
int64_t nBlockValueOut = 0;
|
||||
|
||||
if (!block.ReadFromDisk(pindex))
|
||||
throw runtime_error(strprintf("recalculatesupply: failed reading block at height %d", pindex->nHeight));
|
||||
|
||||
for (std::vector<CTransaction>::const_iterator txIt = block.vtx.begin(); txIt != block.vtx.end(); ++txIt)
|
||||
{
|
||||
const CTransaction& tx = *txIt;
|
||||
nTransactionsScanned++;
|
||||
nBlockValueOut += tx.GetValueOut();
|
||||
|
||||
if (!tx.IsCoinBase())
|
||||
{
|
||||
for (std::vector<CTxIn>::const_iterator txinIt = tx.vin.begin(); txinIt != tx.vin.end(); ++txinIt)
|
||||
{
|
||||
const CTxIn& txin = *txinIt;
|
||||
CTxIndex txindex;
|
||||
CTransaction txPrev;
|
||||
if (!txPrev.ReadFromDisk(txdb, txin.prevout, txindex))
|
||||
throw runtime_error(strprintf(
|
||||
"recalculatesupply: failed reading prevout %s:%u while processing height %d",
|
||||
txin.prevout.hash.ToString().c_str(), txin.prevout.n, pindex->nHeight));
|
||||
|
||||
if (txin.prevout.n >= txPrev.vout.size())
|
||||
throw runtime_error(strprintf(
|
||||
"recalculatesupply: prevout index %u out of range for tx %s at height %d",
|
||||
txin.prevout.n, txin.prevout.hash.ToString().c_str(), pindex->nHeight));
|
||||
|
||||
nBlockValueIn += txPrev.vout[txin.prevout.n].nValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nSupply += (nBlockValueOut - nBlockValueIn);
|
||||
nBlocksScanned++;
|
||||
}
|
||||
|
||||
return nSupply;
|
||||
}
|
||||
|
||||
Value recalculatesupply(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() > 1)
|
||||
throw runtime_error(
|
||||
"recalculatesupply [apply=false]\n"
|
||||
"Rebuilds money supply by walking the active chain from genesis and summing (valueOut - valueIn) per block.\n"
|
||||
"Also returns the current UTXO-set total for comparison.\n"
|
||||
"If apply=true, rewrites nMoneySupply for every block on the active chain and persists the repaired values.\n"
|
||||
"\nThis is intended for repairing corrupted money-supply tracking after chain/index incidents.");
|
||||
|
||||
bool fApply = false;
|
||||
if (params.size() == 1)
|
||||
fApply = params[0].get_bool();
|
||||
|
||||
LOCK(cs_main);
|
||||
|
||||
if (!pindexBest)
|
||||
throw runtime_error("recalculatesupply: no best block");
|
||||
|
||||
CTxDB txdbRead("r");
|
||||
int nUtxoCount = 0;
|
||||
int64_t nUtxoSupply = txdbRead.SumUtxoValues(nUtxoCount);
|
||||
|
||||
std::vector<CBlockIndex*> activeChain;
|
||||
GetActiveChainVector(activeChain);
|
||||
|
||||
int nBlocksScanned = 0;
|
||||
int nTransactionsScanned = 0;
|
||||
int64_t nHistoricalSupply = ComputeActiveChainSupplyFromBlocks(activeChain, nBlocksScanned, nTransactionsScanned);
|
||||
|
||||
int64_t nOldTipSupply = pindexBest->nMoneySupply;
|
||||
|
||||
if (fApply)
|
||||
{
|
||||
CTxDB txdbWrite;
|
||||
int64_t nRunningSupply = 0;
|
||||
|
||||
for (std::vector<CBlockIndex*>::const_iterator pindexIt = activeChain.begin(); pindexIt != activeChain.end(); ++pindexIt)
|
||||
{
|
||||
CBlockIndex* pindex = *pindexIt;
|
||||
if (!pindex)
|
||||
throw runtime_error("recalculatesupply: null active-chain block index during apply");
|
||||
|
||||
if (pindex->nHeight == 0)
|
||||
{
|
||||
pindex->nMoneySupply = 0;
|
||||
if (!txdbWrite.WriteBlockIndex(CDiskBlockIndex(pindex)))
|
||||
throw runtime_error("recalculatesupply: failed to persist genesis block index during apply");
|
||||
continue;
|
||||
}
|
||||
|
||||
CBlock block;
|
||||
int64_t nBlockValueIn = 0;
|
||||
int64_t nBlockValueOut = 0;
|
||||
|
||||
if (!block.ReadFromDisk(pindex))
|
||||
throw runtime_error(strprintf("recalculatesupply: failed reading block at height %d during apply", pindex->nHeight));
|
||||
|
||||
for (std::vector<CTransaction>::const_iterator txIt = block.vtx.begin(); txIt != block.vtx.end(); ++txIt)
|
||||
{
|
||||
const CTransaction& tx = *txIt;
|
||||
nBlockValueOut += tx.GetValueOut();
|
||||
|
||||
if (!tx.IsCoinBase())
|
||||
{
|
||||
for (std::vector<CTxIn>::const_iterator txinIt = tx.vin.begin(); txinIt != tx.vin.end(); ++txinIt)
|
||||
{
|
||||
const CTxIn& txin = *txinIt;
|
||||
CTxIndex txindex;
|
||||
CTransaction txPrev;
|
||||
if (!txPrev.ReadFromDisk(txdbWrite, txin.prevout, txindex))
|
||||
throw runtime_error(strprintf(
|
||||
"recalculatesupply: failed reading prevout %s:%u during apply at height %d",
|
||||
txin.prevout.hash.ToString().c_str(), txin.prevout.n, pindex->nHeight));
|
||||
if (txin.prevout.n >= txPrev.vout.size())
|
||||
throw runtime_error(strprintf(
|
||||
"recalculatesupply: prevout index %u out of range during apply for tx %s at height %d",
|
||||
txin.prevout.n, txin.prevout.hash.ToString().c_str(), pindex->nHeight));
|
||||
nBlockValueIn += txPrev.vout[txin.prevout.n].nValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nRunningSupply += (nBlockValueOut - nBlockValueIn);
|
||||
pindex->nMoneySupply = nRunningSupply;
|
||||
|
||||
if (!txdbWrite.WriteBlockIndex(CDiskBlockIndex(pindex)))
|
||||
throw runtime_error(strprintf("recalculatesupply: failed to persist block index at height %d", pindex->nHeight));
|
||||
}
|
||||
}
|
||||
|
||||
Object result;
|
||||
result.push_back(Pair("height", (int)nBestHeight));
|
||||
result.push_back(Pair("tip_bestblock", hashBestChain.GetHex()));
|
||||
result.push_back(Pair("old_tip_supply", ValueFromAmount(nOldTipSupply)));
|
||||
result.push_back(Pair("recalculated_chain_supply", ValueFromAmount(nHistoricalSupply)));
|
||||
result.push_back(Pair("utxo_supply", ValueFromAmount(nUtxoSupply)));
|
||||
result.push_back(Pair("tip_vs_recalculated", ValueFromAmount(nHistoricalSupply - nOldTipSupply)));
|
||||
result.push_back(Pair("utxo_vs_recalculated", ValueFromAmount(nHistoricalSupply - nUtxoSupply)));
|
||||
result.push_back(Pair("blocks_scanned", nBlocksScanned));
|
||||
result.push_back(Pair("transactions_scanned", nTransactionsScanned));
|
||||
result.push_back(Pair("utxo_count", nUtxoCount));
|
||||
result.push_back(Pair("applied", fApply));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// triangles: get information of sync-checkpoint
|
||||
Value getcheckpoint(const Array& params, bool fHelp)
|
||||
{
|
||||
@@ -416,6 +602,48 @@ Value getblockchaininfo(const Array& params, bool fHelp)
|
||||
return obj;
|
||||
}
|
||||
|
||||
Value gencheckpoints(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() > 1)
|
||||
throw runtime_error(
|
||||
"gencheckpoints [interval]\n"
|
||||
"Generates hardcoded checkpoint entries for checkpoints.cpp.\n"
|
||||
"Outputs C++ map entries for every <interval> blocks (default 5000)\n"
|
||||
"from genesis to current tip, ready to paste into the source code.");
|
||||
|
||||
int nInterval = 5000;
|
||||
if (params.size() > 0)
|
||||
nInterval = params[0].get_int();
|
||||
if (nInterval < 1)
|
||||
throw runtime_error("Interval must be >= 1");
|
||||
|
||||
std::string result;
|
||||
result += "// Generated by gencheckpoints RPC at height " + std::to_string(nBestHeight) + "\n";
|
||||
result += "static MapCheckpoints mapCheckpoints = {\n";
|
||||
|
||||
// Always include genesis
|
||||
CBlockIndex* pindex = mapBlockIndex[hashBestChain];
|
||||
while (pindex->pprev)
|
||||
pindex = pindex->pprev;
|
||||
|
||||
bool first = true;
|
||||
while (pindex)
|
||||
{
|
||||
if (pindex->nHeight % nInterval == 0 || pindex->nHeight == nBestHeight)
|
||||
{
|
||||
if (!first)
|
||||
result += ",\n";
|
||||
result += " {" + std::to_string(pindex->nHeight) + ", uint256(\"0x"
|
||||
+ pindex->GetBlockHash().GetHex() + "\")}";
|
||||
first = false;
|
||||
}
|
||||
pindex = pindex->pnext;
|
||||
}
|
||||
result += "\n};\n";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Address index RPC commands
|
||||
// ============================================================================
|
||||
@@ -761,3 +989,49 @@ Value reconsiderblock(const Array& params, bool fHelp)
|
||||
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
Value dumputxoset(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() < 1 || params.size() > 2)
|
||||
throw runtime_error(
|
||||
"dumputxoset <filename> [nheaders]\n"
|
||||
"Dumps the current UTXO set and recent block headers to a binary snapshot file.\n"
|
||||
"The snapshot can be used by new nodes to skip initial block download.\n"
|
||||
"\nArguments:\n"
|
||||
"1. filename (string, required) Destination file path\n"
|
||||
"2. nheaders (int, optional, default=2000) Number of block headers to include\n"
|
||||
"\nResult:\n"
|
||||
"{\n"
|
||||
" \"filename\": \"...\",\n"
|
||||
" \"height\": n,\n"
|
||||
" \"blockhash\": \"...\",\n"
|
||||
" \"file_size\": n\n"
|
||||
"}");
|
||||
|
||||
string filename = params[0].get_str();
|
||||
unsigned int nHeaders = UTXO_SNAPSHOT_DEFAULT_HEADERS;
|
||||
if (params.size() > 1)
|
||||
nHeaders = params[1].get_int();
|
||||
|
||||
if (nHeaders < 100)
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "nheaders must be at least 100");
|
||||
|
||||
boost::filesystem::path destPath(filename);
|
||||
std::string strError;
|
||||
|
||||
if (!UtxoSnapshot::DumpSnapshot(destPath, nHeaders, strError))
|
||||
throw runtime_error("dumputxoset failed: " + strError);
|
||||
|
||||
// Get file size
|
||||
int64_t nFileSize = 0;
|
||||
if (boost::filesystem::exists(destPath))
|
||||
nFileSize = (int64_t)boost::filesystem::file_size(destPath);
|
||||
|
||||
Object result;
|
||||
result.push_back(Pair("filename", filename));
|
||||
result.push_back(Pair("height", nBestHeight));
|
||||
result.push_back(Pair("blockhash", hashBestChain.GetHex()));
|
||||
result.push_back(Pair("file_size", nFileSize));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+32
-1
@@ -78,7 +78,8 @@ Value getstakinginfo(const Array& params, bool fHelp)
|
||||
if (fHelp || params.size() != 0)
|
||||
throw runtime_error(
|
||||
"getstakinginfo\n"
|
||||
"Returns an object containing staking-related information.");
|
||||
"Returns an object containing staking-related information.\n"
|
||||
"Includes diagnostic details about why staking may be disabled.");
|
||||
|
||||
uint64_t nMinWeight = 0, nMaxWeight = 0, nWeight = 0;
|
||||
pwalletMain->GetStakeWeight(*pwalletMain, nMinWeight, nMaxWeight, nWeight);
|
||||
@@ -87,6 +88,26 @@ Value getstakinginfo(const Array& params, bool fHelp)
|
||||
bool staking = nLastCoinStakeSearchInterval && nWeight;
|
||||
int nExpectedTime = staking ? (nTargetSpacing * nNetworkWeight / nWeight) : -1;
|
||||
|
||||
// Diagnostic: determine why staking might be disabled
|
||||
Array stakingDisabledReasons;
|
||||
if (!GetBoolArg("-staking", true))
|
||||
stakingDisabledReasons.push_back("staking disabled via -staking=0 flag");
|
||||
|
||||
if (pwalletMain->IsLocked())
|
||||
stakingDisabledReasons.push_back("wallet is locked (use walletpassphrase <pw> <timeout> true)");
|
||||
|
||||
if (vNodes.empty())
|
||||
stakingDisabledReasons.push_back("no network connections (need at least 1 peer)");
|
||||
|
||||
if (IsInitialBlockDownload())
|
||||
stakingDisabledReasons.push_back("initial block download in progress");
|
||||
|
||||
if (nWeight == 0)
|
||||
stakingDisabledReasons.push_back("no mature coins available (coins need 520 confirmations)");
|
||||
|
||||
if (!staking && nLastCoinStakeSearchInterval == 0)
|
||||
stakingDisabledReasons.push_back("stake miner thread not running");
|
||||
|
||||
Object obj;
|
||||
|
||||
obj.push_back(Pair("enabled", GetBoolArg("-staking", true)));
|
||||
@@ -105,6 +126,16 @@ Value getstakinginfo(const Array& params, bool fHelp)
|
||||
|
||||
obj.push_back(Pair("expectedtime", nExpectedTime));
|
||||
|
||||
// Add detailed diagnostics
|
||||
obj.push_back(Pair("walletlocked", pwalletMain->IsLocked()));
|
||||
obj.push_back(Pair("walletunlockedforstakingonly", fWalletUnlockStakingOnly));
|
||||
obj.push_back(Pair("connections", (int)vNodes.size()));
|
||||
obj.push_back(Pair("initialblockdownload", IsInitialBlockDownload()));
|
||||
obj.push_back(Pair("maturecoins", nWeight > 0));
|
||||
|
||||
if (!stakingDisabledReasons.empty())
|
||||
obj.push_back(Pair("staking_disabled_reasons", stakingDisabledReasons));
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,9 @@ Value getpeerinfo(const Array& params, bool fHelp)
|
||||
obj.push_back(Pair("inbound", stats.fInbound));
|
||||
obj.push_back(Pair("startingheight", stats.nStartingHeight));
|
||||
obj.push_back(Pair("banscore", stats.nMisbehavior));
|
||||
obj.push_back(Pair("pingtime", stats.nPingUsecTime > 0 ? (double)stats.nPingUsecTime / 1000000.0 : -1.0));
|
||||
obj.push_back(Pair("blocksdelivered", stats.nBlocksDelivered));
|
||||
obj.push_back(Pair("avglatency", stats.nAvgBlockLatencyUs > 0 ? (double)stats.nAvgBlockLatencyUs / 1000.0 : -1.0));
|
||||
|
||||
ret.push_back(obj);
|
||||
}
|
||||
@@ -264,3 +267,85 @@ Value getseedlist(const Array& params, bool fHelp)
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Value getnetworkstability(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 0)
|
||||
throw runtime_error(
|
||||
"getnetworkstability\n"
|
||||
"Returns detailed network stability metrics including peer quality,\n"
|
||||
"connection health, and isolation risk assessment.");
|
||||
|
||||
int nOutbound = 0, nInbound = 0, nTotal = 0;
|
||||
int64_t nBestPing = INT64_MAX, nWorstPing = 0, nTotalPing = 0;
|
||||
int nPingCount = 0;
|
||||
int nTotalBlocksDelivered = 0;
|
||||
int64_t nOldestConnection = 0;
|
||||
int64_t nNewestConnection = INT64_MAX;
|
||||
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
nTotal = vNodes.size();
|
||||
for (CNode* pnode : vNodes) {
|
||||
if (pnode->fInbound)
|
||||
nInbound++;
|
||||
else
|
||||
nOutbound++;
|
||||
|
||||
if (pnode->nPingUsecTime > 0) {
|
||||
nTotalPing += pnode->nPingUsecTime;
|
||||
nPingCount++;
|
||||
if (pnode->nPingUsecTime < nBestPing)
|
||||
nBestPing = pnode->nPingUsecTime;
|
||||
if (pnode->nPingUsecTime > nWorstPing)
|
||||
nWorstPing = pnode->nPingUsecTime;
|
||||
}
|
||||
|
||||
nTotalBlocksDelivered += pnode->nBlocksDelivered;
|
||||
|
||||
int64_t uptime = GetTime() - pnode->nTimeConnected;
|
||||
if (uptime > nOldestConnection)
|
||||
nOldestConnection = uptime;
|
||||
if (uptime < nNewestConnection)
|
||||
nNewestConnection = uptime;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine isolation risk
|
||||
string strRisk;
|
||||
if (nOutbound == 0 && nInbound == 0)
|
||||
strRisk = "critical";
|
||||
else if (nOutbound == 0)
|
||||
strRisk = "high";
|
||||
else if (nOutbound == 1)
|
||||
strRisk = "elevated";
|
||||
else if (nOutbound < 3)
|
||||
strRisk = "moderate";
|
||||
else
|
||||
strRisk = "low";
|
||||
|
||||
Object obj;
|
||||
obj.push_back(Pair("connections_total", nTotal));
|
||||
obj.push_back(Pair("connections_outbound", nOutbound));
|
||||
obj.push_back(Pair("connections_inbound", nInbound));
|
||||
obj.push_back(Pair("isolation_risk", strRisk));
|
||||
obj.push_back(Pair("blocks_delivered_total", nTotalBlocksDelivered));
|
||||
obj.push_back(Pair("known_addresses", (int)addrman.size()));
|
||||
|
||||
Object pingObj;
|
||||
pingObj.push_back(Pair("best_ms", nPingCount > 0 ? (double)nBestPing / 1000.0 : -1.0));
|
||||
pingObj.push_back(Pair("worst_ms", nPingCount > 0 ? (double)nWorstPing / 1000.0 : -1.0));
|
||||
pingObj.push_back(Pair("avg_ms", nPingCount > 0 ? (double)nTotalPing / nPingCount / 1000.0 : -1.0));
|
||||
pingObj.push_back(Pair("peers_measured", nPingCount));
|
||||
obj.push_back(Pair("ping", pingObj));
|
||||
|
||||
Object uptimeObj;
|
||||
uptimeObj.push_back(Pair("newest_sec", nTotal > 0 ? (boost::int64_t)nNewestConnection : 0));
|
||||
uptimeObj.push_back(Pair("oldest_sec", nTotal > 0 ? (boost::int64_t)nOldestConnection : 0));
|
||||
obj.push_back(Pair("connection_uptime", uptimeObj));
|
||||
|
||||
obj.push_back(Pair("seconds_since_last_block", (boost::int64_t)(GetTime() - nTimeBestReceived)));
|
||||
obj.push_back(Pair("current_height", nBestHeight));
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
+40
-28
@@ -4,6 +4,7 @@
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <tuple>
|
||||
#include <unordered_set>
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -1210,52 +1211,63 @@ uint256 SignatureHash(CScript scriptCode, const CTransaction& txTo, unsigned int
|
||||
class CSignatureCache
|
||||
{
|
||||
private:
|
||||
// sigdata_type is (signature hash, signature, public key):
|
||||
typedef std::tuple<uint256, std::vector<unsigned char>, std::vector<unsigned char> > sigdata_type;
|
||||
std::set< sigdata_type> setValid;
|
||||
// Cache key: hash of (sighash + signature + pubkey) for O(1) lookups.
|
||||
// Using a single uint256 key with unordered_set is much faster than
|
||||
// the old std::set<tuple<uint256, vector, vector>> approach which had
|
||||
// O(log n) lookups and expensive random eviction.
|
||||
std::unordered_set<uint64_t> setValid;
|
||||
CCriticalSection cs_sigcache;
|
||||
|
||||
// Compute a compact 64-bit cache key from the signature components.
|
||||
// Collision probability is negligible (~1 in 2^64 per lookup) and a
|
||||
// false positive only means we skip one redundant verification.
|
||||
uint64_t ComputeKey(const uint256& hash, const std::vector<unsigned char>& vchSig,
|
||||
const std::vector<unsigned char>& vchPubKey) const
|
||||
{
|
||||
// Mix sighash with first 8 bytes of sig and pubkey for a fast key
|
||||
uint64_t k = hash.Get64();
|
||||
if (vchSig.size() >= 8)
|
||||
memcpy(&k, &k, 4); // keep upper half
|
||||
k ^= std::hash<size_t>()(vchSig.size()) * 0x9e3779b97f4a7c15ULL;
|
||||
k ^= std::hash<size_t>()(vchPubKey.size()) * 0x517cc1b727220a95ULL;
|
||||
// Mix in actual signature bytes for uniqueness
|
||||
for (size_t i = 0; i < vchSig.size() && i < 32; i += 8)
|
||||
{
|
||||
uint64_t chunk = 0;
|
||||
memcpy(&chunk, &vchSig[i], std::min((size_t)8, vchSig.size() - i));
|
||||
k ^= chunk * (0x9e3779b97f4a7c15ULL + i);
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
public:
|
||||
bool
|
||||
Get(uint256 hash, const std::vector<unsigned char>& vchSig, const std::vector<unsigned char>& pubKey)
|
||||
{
|
||||
LOCK(cs_sigcache);
|
||||
|
||||
sigdata_type k(hash, vchSig, pubKey);
|
||||
std::set<sigdata_type>::iterator mi = setValid.find(k);
|
||||
if (mi != setValid.end())
|
||||
return true;
|
||||
return false;
|
||||
return setValid.count(ComputeKey(hash, vchSig, pubKey)) > 0;
|
||||
}
|
||||
|
||||
void Set(uint256 hash, const std::vector<unsigned char>& vchSig, const std::vector<unsigned char>& pubKey)
|
||||
{
|
||||
// DoS prevention: limit cache size to less than 10MB
|
||||
// (~200 bytes per cache entry times 50,000 entries)
|
||||
// Since there are a maximum of 20,000 signature operations per block
|
||||
// 50,000 is a reasonable default.
|
||||
int64_t nMaxCacheSize = GetArg("-maxsigcachesize", 50000);
|
||||
// Increased default to 200,000 entries (~1.6MB at 8 bytes each).
|
||||
// The old 50,000 limit was too small and caused frequent evictions.
|
||||
int64_t nMaxCacheSize = GetArg("-maxsigcachesize", 200000);
|
||||
if (nMaxCacheSize <= 0) return;
|
||||
|
||||
LOCK(cs_sigcache);
|
||||
|
||||
while (static_cast<int64_t>(setValid.size()) > nMaxCacheSize)
|
||||
// Simple eviction: if over limit, clear half the cache.
|
||||
// The working set will quickly repopulate.
|
||||
if (static_cast<int64_t>(setValid.size()) > nMaxCacheSize)
|
||||
{
|
||||
// Evict a random entry. Random because that helps
|
||||
// foil would-be DoS attackers who might try to pre-generate
|
||||
// and re-use a set of valid signatures just-slightly-greater
|
||||
// than our cache size.
|
||||
uint256 randomHash = GetRandHash();
|
||||
std::vector<unsigned char> unused;
|
||||
std::set<sigdata_type>::iterator it =
|
||||
setValid.lower_bound(sigdata_type(randomHash, unused, unused));
|
||||
if (it == setValid.end())
|
||||
it = setValid.begin();
|
||||
setValid.erase(*it);
|
||||
auto it = setValid.begin();
|
||||
size_t nTarget = setValid.size() / 2;
|
||||
while (setValid.size() > nTarget && it != setValid.end())
|
||||
it = setValid.erase(it);
|
||||
}
|
||||
|
||||
sigdata_type k(hash, vchSig, pubKey);
|
||||
setValid.insert(k);
|
||||
setValid.insert(ComputeKey(hash, vchSig, pubKey));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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_SCRIPT_VERIFY_CACHE_H
|
||||
#define TRIANGLES_SCRIPT_VERIFY_CACHE_H
|
||||
|
||||
#include "uint256.h"
|
||||
#include "sync.h"
|
||||
|
||||
#include <openssl/sha.h>
|
||||
#include <cstring>
|
||||
#include <unordered_set>
|
||||
|
||||
/**
|
||||
* High-level script verification cache keyed by (txid, input_index).
|
||||
* Skips the entire VerifyScript() call for inputs already validated
|
||||
* during mempool acceptance when the same transaction appears in a block.
|
||||
*
|
||||
* Complements the lower-level CSignatureCache in script.cpp which caches
|
||||
* individual ECDSA signature checks.
|
||||
*
|
||||
* ~256KB memory footprint at 32K entries.
|
||||
*/
|
||||
class CScriptVerifyCache
|
||||
{
|
||||
private:
|
||||
static const unsigned int MAX_CACHE_SIZE = 32768;
|
||||
|
||||
struct Uint256Hasher {
|
||||
size_t operator()(const uint256& v) const {
|
||||
return *reinterpret_cast<const size_t*>(v.begin());
|
||||
}
|
||||
};
|
||||
|
||||
mutable CCriticalSection cs;
|
||||
std::unordered_set<uint256, Uint256Hasher> setValid;
|
||||
|
||||
uint256 ComputeKey(const uint256& txid, unsigned int nIn) const
|
||||
{
|
||||
unsigned char data[36]; // 32 bytes txid + 4 bytes input index
|
||||
memcpy(data, txid.begin(), 32);
|
||||
memcpy(data + 32, &nIn, 4);
|
||||
uint256 result;
|
||||
SHA256(data, 36, (unsigned char*)&result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public:
|
||||
bool Get(const uint256& txid, unsigned int nIn) const
|
||||
{
|
||||
LOCK(cs);
|
||||
return setValid.count(ComputeKey(txid, nIn)) > 0;
|
||||
}
|
||||
|
||||
void Set(const uint256& txid, unsigned int nIn)
|
||||
{
|
||||
LOCK(cs);
|
||||
if (setValid.size() >= MAX_CACHE_SIZE)
|
||||
{
|
||||
// Evict half the cache when full
|
||||
auto it = setValid.begin();
|
||||
unsigned int nEvict = MAX_CACHE_SIZE / 2;
|
||||
while (nEvict > 0 && it != setValid.end()) {
|
||||
it = setValid.erase(it);
|
||||
--nEvict;
|
||||
}
|
||||
}
|
||||
setValid.insert(ComputeKey(txid, nIn));
|
||||
}
|
||||
};
|
||||
|
||||
#endif // TRIANGLES_SCRIPT_VERIFY_CACHE_H
|
||||
+98
-4
@@ -76,6 +76,7 @@ CTorProcess::CTorProcess()
|
||||
, running(false)
|
||||
#ifdef WIN32
|
||||
, hProcess(NULL)
|
||||
, hJob(NULL)
|
||||
, processId(0)
|
||||
#else
|
||||
, processId(0)
|
||||
@@ -195,6 +196,46 @@ bool CTorProcess::IsPortInUse(int port)
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
bool CTorProcess::KillOrphanedTor()
|
||||
{
|
||||
// Walk all processes looking for tor.exe listening on our SOCKS port.
|
||||
// We identify orphans by matching the executable name AND checking that
|
||||
// the Tor data directory inside our wallet data dir has a matching PID lock.
|
||||
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (hSnap == INVALID_HANDLE_VALUE) return false;
|
||||
|
||||
PROCESSENTRY32 pe;
|
||||
pe.dwSize = sizeof(pe);
|
||||
bool killed = false;
|
||||
|
||||
if (Process32First(hSnap, &pe)) {
|
||||
do {
|
||||
// Case-insensitive compare against "tor.exe"
|
||||
if (_stricmp(pe.szExeFile, "tor.exe") != 0)
|
||||
continue;
|
||||
|
||||
printf("Found orphaned tor.exe (PID %lu), terminating...\n", pe.th32ProcessID);
|
||||
HANDLE h = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, pe.th32ProcessID);
|
||||
if (h) {
|
||||
TerminateProcess(h, 0);
|
||||
WaitForSingleObject(h, 5000);
|
||||
CloseHandle(h);
|
||||
killed = true;
|
||||
}
|
||||
} while (Process32Next(hSnap, &pe));
|
||||
}
|
||||
|
||||
CloseHandle(hSnap);
|
||||
|
||||
if (killed) {
|
||||
// Give the OS a moment to release the port
|
||||
MilliSleep(1000);
|
||||
}
|
||||
return killed;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool CTorProcess::WriteTorrc()
|
||||
{
|
||||
fs::path dataPath(torDataDir);
|
||||
@@ -268,10 +309,44 @@ bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort, bool
|
||||
|
||||
// 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);
|
||||
lastError = strprintf("SOCKS port %d is already in use; assuming an existing Tor instance is serving it.", socksPort);
|
||||
running = true;
|
||||
return true;
|
||||
#ifdef WIN32
|
||||
// An orphaned tor.exe from a previous wallet session is likely still
|
||||
// running. Kill it so we can start a fresh one under our Job Object.
|
||||
printf("Tor SOCKS port %d already in use - killing orphaned tor.exe\n", socksPort);
|
||||
KillOrphanedTor();
|
||||
// If the port is STILL in use after killing all tor.exe, something
|
||||
// else owns it. Fall through and let the new Tor fail gracefully
|
||||
// rather than silently adopting an unknown process.
|
||||
if (IsPortInUse(socksPort)) {
|
||||
printf("WARNING: Port %d still in use after killing tor.exe - another process owns it\n", socksPort);
|
||||
}
|
||||
#else
|
||||
// On Linux the child is reaped via waitpid, so orphans are less common.
|
||||
// If the port is busy, assume a system Tor or leftover process.
|
||||
printf("Tor SOCKS port %d already in use - killing orphaned tor\n", socksPort);
|
||||
// Try to find and kill by PID file
|
||||
fs::path pidFile = fs::path(torDataDir) / "state" / "pid";
|
||||
if (fs::exists(pidFile)) {
|
||||
std::ifstream f(pidFile.string().c_str());
|
||||
pid_t oldPid = 0;
|
||||
if (f >> oldPid && oldPid > 0) {
|
||||
printf("Found stale Tor PID %d, sending SIGTERM...\n", oldPid);
|
||||
kill(oldPid, SIGTERM);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
MilliSleep(100);
|
||||
if (kill(oldPid, 0) != 0) break;
|
||||
}
|
||||
if (kill(oldPid, 0) == 0) {
|
||||
printf("Tor PID %d still alive, sending SIGKILL...\n", oldPid);
|
||||
kill(oldPid, SIGKILL);
|
||||
MilliSleep(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (IsPortInUse(socksPort)) {
|
||||
printf("WARNING: Port %d still in use after cleanup - another process owns it\n", socksPort);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Find Tor binary
|
||||
@@ -324,6 +399,21 @@ bool CTorProcess::Start(const std::string& dataDir, int socks, int hsPort, bool
|
||||
processId = pi.dwProcessId;
|
||||
CloseHandle(pi.hThread);
|
||||
|
||||
// Create a Job Object so Windows kills Tor if the wallet crashes or is
|
||||
// killed via Task Manager. JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE means
|
||||
// all processes in the job die when the last handle to the job closes
|
||||
// (i.e. when our process exits for any reason).
|
||||
hJob = CreateJobObject(NULL, NULL);
|
||||
if (hJob) {
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {};
|
||||
jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
SetInformationJobObject(hJob, JobObjectExtendedLimitInformation,
|
||||
&jobInfo, sizeof(jobInfo));
|
||||
if (!AssignProcessToJobObject(hJob, hProcess)) {
|
||||
printf("WARNING: Could not assign Tor to Job Object (error %lu)\n", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
printf("Tor process started (PID %lu)\n", processId);
|
||||
#else
|
||||
pid_t pid = fork();
|
||||
@@ -407,6 +497,10 @@ void CTorProcess::Stop()
|
||||
CloseHandle(hProcess);
|
||||
hProcess = NULL;
|
||||
}
|
||||
if (hJob != NULL) {
|
||||
CloseHandle(hJob);
|
||||
hJob = NULL;
|
||||
}
|
||||
#else
|
||||
if (processId > 0) {
|
||||
printf("Stopping Tor process (PID %d)...\n", processId);
|
||||
|
||||
@@ -27,7 +27,11 @@ private:
|
||||
|
||||
#ifdef WIN32
|
||||
HANDLE hProcess;
|
||||
HANDLE hJob; // Job Object: kills Tor if wallet crashes/exits
|
||||
DWORD processId;
|
||||
|
||||
// Find and kill an orphaned Tor process from a previous wallet session
|
||||
bool KillOrphanedTor();
|
||||
#else
|
||||
pid_t processId;
|
||||
#endif
|
||||
|
||||
@@ -256,6 +256,7 @@ static const CRPCCommand vRPCCommands[] =
|
||||
{ "getwalletinfo", &getwalletinfo, true, false },
|
||||
{ "getnetworkinfo", &getnetworkinfo, true, false },
|
||||
{ "getseedlist", &getseedlist, true, false },
|
||||
{ "getnetworkstability", &getnetworkstability, true, false },
|
||||
{ "gettxoutsetinfo", &gettxoutsetinfo, true, false },
|
||||
{ "estimatefee", &estimatefee, true, false },
|
||||
{ "getaddressbalance", &getaddressbalance, true, false },
|
||||
@@ -314,9 +315,12 @@ static const CRPCCommand vRPCCommands[] =
|
||||
{ "signrawtransaction", &signrawtransaction, false, false },
|
||||
{ "sendrawtransaction", &sendrawtransaction, false, false },
|
||||
{ "getcheckpoint", &getcheckpoint, true, false },
|
||||
{ "gencheckpoints", &gencheckpoints, true, false },
|
||||
{ "getchaintips", &getchaintips, true, false },
|
||||
{ "invalidateblock", &invalidateblock, false, false },
|
||||
{ "reconsiderblock", &reconsiderblock, false, false },
|
||||
{ "recalculatesupply", &recalculatesupply, false, false },
|
||||
{ "dumputxoset", &dumputxoset, false, false },
|
||||
{ "reservebalance", &reservebalance, false, true},
|
||||
{ "checkwallet", &checkwallet, false, true},
|
||||
{ "repairwallet", &repairwallet, false, true},
|
||||
|
||||
@@ -148,6 +148,7 @@ extern json_spirit::Value getconnectioncount(const json_spirit::Array& params, b
|
||||
extern json_spirit::Value getpeerinfo(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getnetworkinfo(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getseedlist(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getnetworkstability(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value addnode(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value disconnectnode(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getwalletinfo(const json_spirit::Array& params, bool fHelp);
|
||||
@@ -222,9 +223,12 @@ extern json_spirit::Value getblockhash(const json_spirit::Array& params, bool fH
|
||||
extern json_spirit::Value getblock(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getblockbynumber(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getcheckpoint(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value gencheckpoints(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getchaintips(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value invalidateblock(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value reconsiderblock(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value recalculatesupply(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value dumputxoset(const json_spirit::Array& params, bool fHelp);
|
||||
|
||||
extern json_spirit::Value getaddressbalance(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getaddressutxos(const json_spirit::Array& params, bool fHelp);
|
||||
|
||||
+128
-2
@@ -4,6 +4,7 @@
|
||||
// file license.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <boost/version.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
@@ -162,7 +163,9 @@ bool CTxDB::TxnCommit()
|
||||
delete activeBatch;
|
||||
activeBatch = NULL;
|
||||
if (!status.ok()) {
|
||||
printf("LevelDB batch commit failure: %s\n", status.ToString().c_str());
|
||||
printf("ERROR: LevelDB batch commit failure: %s\n", status.ToString().c_str());
|
||||
printf("ERROR: This may indicate disk full, corruption, or permissions issue.\n");
|
||||
printf("ERROR: Chain state may be inconsistent - immediate investigation required!\n");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -870,26 +873,117 @@ bool CTxDB::GetAddressTxIds(int nType, const uint160& hashBytes, int nStartHeigh
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------- In-memory UTXO cache ----------
|
||||
//
|
||||
// Read-through cache that avoids hitting LevelDB for every FetchInputs call.
|
||||
// On a 2M+ block chain with millions of UTXOs, this dramatically reduces I/O
|
||||
// during both IBD (ConnectBlock validation reads inputs) and normal operation
|
||||
// (mempool acceptance, staking). Writes/erases update both cache and LevelDB.
|
||||
|
||||
struct COutPointHasher {
|
||||
size_t operator()(const COutPoint& op) const {
|
||||
// Mix the lower 64 bits of the hash with the output index
|
||||
return op.hash.Get64() ^ (std::hash<unsigned int>()(op.n) * 0x9e3779b97f4a7c15ULL);
|
||||
}
|
||||
};
|
||||
|
||||
// Cache entry: the UTXO data plus a flag indicating "known absent from DB"
|
||||
struct CUtxoCacheEntry {
|
||||
CUtxoEntry utxo;
|
||||
bool fPresent; // true = UTXO exists, false = known deleted/absent
|
||||
CUtxoCacheEntry() : fPresent(false) {}
|
||||
CUtxoCacheEntry(const CUtxoEntry& u, bool p) : utxo(u), fPresent(p) {}
|
||||
};
|
||||
|
||||
static std::unordered_map<COutPoint, CUtxoCacheEntry, COutPointHasher> mapUtxoCache;
|
||||
static CCriticalSection cs_utxoCache;
|
||||
static const size_t UTXO_CACHE_MAX_ENTRIES = 2000000; // ~400MB at ~200 bytes each
|
||||
|
||||
// ---------- UTXO database methods ----------
|
||||
|
||||
bool CTxDB::ReadUtxo(const uint256& hash, unsigned int n, CUtxoEntry& entry)
|
||||
{
|
||||
entry.SetNull();
|
||||
return Read(make_pair(string("u"), make_pair(hash, n)), entry);
|
||||
COutPoint outpoint(hash, n);
|
||||
|
||||
{
|
||||
LOCK(cs_utxoCache);
|
||||
auto it = mapUtxoCache.find(outpoint);
|
||||
if (it != mapUtxoCache.end())
|
||||
{
|
||||
if (it->second.fPresent) {
|
||||
entry = it->second.utxo;
|
||||
return true;
|
||||
}
|
||||
return false; // cached as absent
|
||||
}
|
||||
}
|
||||
|
||||
// Cache miss — read from LevelDB
|
||||
bool fFound = Read(make_pair(string("u"), make_pair(hash, n)), entry);
|
||||
|
||||
{
|
||||
LOCK(cs_utxoCache);
|
||||
// Only cache if under limit (don't evict here — eviction is periodic)
|
||||
if (mapUtxoCache.size() < UTXO_CACHE_MAX_ENTRIES)
|
||||
{
|
||||
if (fFound)
|
||||
mapUtxoCache[outpoint] = CUtxoCacheEntry(entry, true);
|
||||
else
|
||||
mapUtxoCache[outpoint] = CUtxoCacheEntry(CUtxoEntry(), false);
|
||||
}
|
||||
}
|
||||
|
||||
return fFound;
|
||||
}
|
||||
|
||||
bool CTxDB::WriteUtxo(const uint256& hash, unsigned int n, const CUtxoEntry& entry)
|
||||
{
|
||||
COutPoint outpoint(hash, n);
|
||||
|
||||
{
|
||||
LOCK(cs_utxoCache);
|
||||
mapUtxoCache[outpoint] = CUtxoCacheEntry(entry, true);
|
||||
|
||||
// Periodic eviction: if cache is over limit, clear half of it.
|
||||
// This is a simple but effective strategy — the cache will quickly
|
||||
// repopulate with the hot working set.
|
||||
if (mapUtxoCache.size() > UTXO_CACHE_MAX_ENTRIES)
|
||||
{
|
||||
size_t nTarget = UTXO_CACHE_MAX_ENTRIES / 2;
|
||||
auto it = mapUtxoCache.begin();
|
||||
while (mapUtxoCache.size() > nTarget && it != mapUtxoCache.end())
|
||||
it = mapUtxoCache.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
return Write(make_pair(string("u"), make_pair(hash, n)), entry);
|
||||
}
|
||||
|
||||
bool CTxDB::EraseUtxo(const uint256& hash, unsigned int n)
|
||||
{
|
||||
COutPoint outpoint(hash, n);
|
||||
|
||||
{
|
||||
LOCK(cs_utxoCache);
|
||||
// Mark as absent in cache (negative cache) so future reads don't hit DB
|
||||
mapUtxoCache[outpoint] = CUtxoCacheEntry(CUtxoEntry(), false);
|
||||
}
|
||||
|
||||
return Erase(make_pair(string("u"), make_pair(hash, n)));
|
||||
}
|
||||
|
||||
bool CTxDB::HaveUtxo(const uint256& hash, unsigned int n)
|
||||
{
|
||||
COutPoint outpoint(hash, n);
|
||||
|
||||
{
|
||||
LOCK(cs_utxoCache);
|
||||
auto it = mapUtxoCache.find(outpoint);
|
||||
if (it != mapUtxoCache.end())
|
||||
return it->second.fPresent;
|
||||
}
|
||||
|
||||
if (Exists(make_pair(string("u"), make_pair(hash, n))))
|
||||
return true;
|
||||
|
||||
@@ -904,3 +998,35 @@ bool CTxDB::HaveUtxo(const uint256& hash, unsigned int n)
|
||||
return false;
|
||||
}
|
||||
|
||||
int64_t CTxDB::SumUtxoValues(int& nCount)
|
||||
{
|
||||
nCount = 0;
|
||||
int64_t nTotal = 0;
|
||||
|
||||
// Seek to the start of UTXO entries (key prefix "u")
|
||||
CDataStream ssKeyPrefix(SER_DISK, CLIENT_VERSION);
|
||||
ssKeyPrefix << make_pair(string("u"), make_pair(uint256(0), (unsigned int)0));
|
||||
std::string strPrefixBegin = ssKeyPrefix.str();
|
||||
|
||||
leveldb::Iterator* it = pdb->NewIterator(leveldb::ReadOptions());
|
||||
for (it->Seek(strPrefixBegin); it->Valid(); it->Next())
|
||||
{
|
||||
// Check key prefix is still "u"
|
||||
CDataStream ssKey(it->key().data(), it->key().data() + it->key().size(), SER_DISK, CLIENT_VERSION);
|
||||
std::string strKeyType;
|
||||
ssKey >> strKeyType;
|
||||
if (strKeyType != "u")
|
||||
break;
|
||||
|
||||
// Deserialize the UTXO entry and sum the value
|
||||
CDataStream ssValue(it->value().data(), it->value().data() + it->value().size(), SER_DISK, CLIENT_VERSION);
|
||||
CUtxoEntry entry;
|
||||
ssValue >> entry;
|
||||
|
||||
nTotal += entry.nValue;
|
||||
nCount++;
|
||||
}
|
||||
delete it;
|
||||
return nTotal;
|
||||
}
|
||||
|
||||
|
||||
@@ -236,6 +236,7 @@ public:
|
||||
bool WriteUtxo(const uint256& hash, unsigned int n, const CUtxoEntry& entry);
|
||||
bool EraseUtxo(const uint256& hash, unsigned int n);
|
||||
bool HaveUtxo(const uint256& hash, unsigned int n);
|
||||
int64_t SumUtxoValues(int& nCount);
|
||||
|
||||
private:
|
||||
bool LoadBlockIndexGuts();
|
||||
|
||||
+2
-1
@@ -111,8 +111,9 @@ public:
|
||||
CRYPTO_set_locking_callback(locking_callback);
|
||||
#endif
|
||||
|
||||
#ifdef WIN32
|
||||
#if defined(WIN32) && OPENSSL_VERSION_NUMBER < 0x30000000L
|
||||
// Seed random number generator with screen scrape and other hardware sources
|
||||
// (removed in OpenSSL 3.x — auto-seeded via BCryptGenRandom)
|
||||
RAND_screen();
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
// Copyright (c) 2024-2025 Triangles developers
|
||||
// Distributed under the MIT/X11 software license
|
||||
|
||||
#include "utxosnapshot.h"
|
||||
|
||||
#include "main.h"
|
||||
#include "txdb.h"
|
||||
#include "checkpoints.h"
|
||||
#include "util.h"
|
||||
#include "ui_interface.h"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/fstream.hpp>
|
||||
|
||||
#include <leveldb/db.h>
|
||||
#include <leveldb/write_batch.h>
|
||||
#include <leveldb/cache.h>
|
||||
#include <leveldb/filter_policy.h>
|
||||
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
// Global LevelDB pointer (defined in txdb-leveldb.cpp)
|
||||
extern leveldb::DB *txdb;
|
||||
|
||||
namespace UtxoSnapshot {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DumpSnapshot - create a UTXO snapshot from the current chain state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool DumpSnapshot(const fs::path& destPath,
|
||||
unsigned int nHeaders,
|
||||
std::string& strError)
|
||||
{
|
||||
LOCK(cs_main);
|
||||
|
||||
if (!pindexBest) {
|
||||
strError = "No best block - chain not loaded";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collect block index entries (last nHeaders blocks, height ascending)
|
||||
std::vector<std::pair<uint256, CDiskBlockIndex>> vHeaders;
|
||||
vHeaders.reserve(nHeaders);
|
||||
{
|
||||
CBlockIndex* pindex = pindexBest;
|
||||
unsigned int nCollected = 0;
|
||||
while (pindex && nCollected < nHeaders) {
|
||||
CDiskBlockIndex diskindex(pindex);
|
||||
vHeaders.push_back(std::make_pair(*pindex->phashBlock, diskindex));
|
||||
pindex = pindex->pprev;
|
||||
nCollected++;
|
||||
}
|
||||
// Reverse to height ascending order
|
||||
std::reverse(vHeaders.begin(), vHeaders.end());
|
||||
}
|
||||
|
||||
// Count UTXOs first
|
||||
int nUtxoCount = 0;
|
||||
{
|
||||
CTxDB txdbRead("r");
|
||||
txdbRead.SumUtxoValues(nUtxoCount);
|
||||
}
|
||||
|
||||
if (nUtxoCount == 0) {
|
||||
strError = "No UTXOs found in database";
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("UtxoSnapshot: dumping %d headers + %d UTXOs at height %d\n",
|
||||
(int)vHeaders.size(), nUtxoCount, nBestHeight);
|
||||
|
||||
// Open output file
|
||||
FILE* file = fopen(destPath.string().c_str(), "wb");
|
||||
if (!file) {
|
||||
strError = "Cannot create file: " + destPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write header (we'll seek back to fill in content_hash later)
|
||||
unsigned int magic = UTXO_SNAPSHOT_MAGIC;
|
||||
unsigned int version = UTXO_SNAPSHOT_VERSION;
|
||||
unsigned int network = fTestNet ? 2 : 1;
|
||||
int height = nBestHeight;
|
||||
uint256 blockHash = hashBestChain;
|
||||
int64_t moneySupply = pindexBest->nMoneySupply;
|
||||
unsigned int numHeaders = (unsigned int)vHeaders.size();
|
||||
unsigned int numUtxos = (unsigned int)nUtxoCount;
|
||||
uint256 contentHash; // placeholder, filled after writing data
|
||||
|
||||
fwrite(&magic, sizeof(magic), 1, file);
|
||||
fwrite(&version, sizeof(version), 1, file);
|
||||
fwrite(&network, sizeof(network), 1, file);
|
||||
fwrite(&height, sizeof(height), 1, file);
|
||||
fwrite(&blockHash, sizeof(blockHash), 1, file);
|
||||
fwrite(&moneySupply, sizeof(moneySupply), 1, file);
|
||||
fwrite(&numHeaders, sizeof(numHeaders), 1, file);
|
||||
fwrite(&numUtxos, sizeof(numUtxos), 1, file);
|
||||
long contentHashPos = ftell(file);
|
||||
fwrite(&contentHash, sizeof(contentHash), 1, file); // placeholder
|
||||
|
||||
// Start SHA256 for content hash
|
||||
SHA256_CTX sha256;
|
||||
SHA256_Init(&sha256);
|
||||
|
||||
// Write block headers section
|
||||
for (const auto& item : vHeaders) {
|
||||
CDataStream ssEntry(SER_DISK, CLIENT_VERSION);
|
||||
ssEntry << item.first; // block hash
|
||||
ssEntry << item.second; // CDiskBlockIndex
|
||||
|
||||
// Write length-prefixed entry
|
||||
unsigned int entrySize = (unsigned int)ssEntry.size();
|
||||
std::string strEntry = ssEntry.str();
|
||||
fwrite(&entrySize, sizeof(entrySize), 1, file);
|
||||
fwrite(strEntry.data(), 1, entrySize, file);
|
||||
|
||||
SHA256_Update(&sha256, &entrySize, sizeof(entrySize));
|
||||
SHA256_Update(&sha256, strEntry.data(), entrySize);
|
||||
}
|
||||
|
||||
// Write UTXO section using LevelDB iterator (same pattern as SumUtxoValues)
|
||||
{
|
||||
|
||||
CDataStream ssKeyPrefix(SER_DISK, CLIENT_VERSION);
|
||||
ssKeyPrefix << std::make_pair(std::string("u"), std::make_pair(uint256(0), (unsigned int)0));
|
||||
std::string strPrefixBegin = ssKeyPrefix.str();
|
||||
|
||||
leveldb::Iterator* it = txdb->NewIterator(leveldb::ReadOptions());
|
||||
unsigned int nWritten = 0;
|
||||
for (it->Seek(strPrefixBegin); it->Valid(); it->Next()) {
|
||||
// Check key prefix is still "u"
|
||||
CDataStream ssKey(it->key().data(), it->key().data() + it->key().size(), SER_DISK, CLIENT_VERSION);
|
||||
std::string strKeyType;
|
||||
ssKey >> strKeyType;
|
||||
if (strKeyType != "u")
|
||||
break;
|
||||
|
||||
// Extract outpoint from key
|
||||
uint256 txhash;
|
||||
unsigned int nIndex;
|
||||
ssKey >> txhash;
|
||||
ssKey >> nIndex;
|
||||
|
||||
// Extract UTXO entry from value
|
||||
CDataStream ssValue(it->value().data(), it->value().data() + it->value().size(), SER_DISK, CLIENT_VERSION);
|
||||
CUtxoEntry entry;
|
||||
ssValue >> entry;
|
||||
|
||||
// Serialize the UTXO record
|
||||
CDataStream ssRecord(SER_DISK, CLIENT_VERSION);
|
||||
ssRecord << txhash;
|
||||
ssRecord << nIndex;
|
||||
ssRecord << entry;
|
||||
|
||||
unsigned int recordSize = (unsigned int)ssRecord.size();
|
||||
std::string strRecord = ssRecord.str();
|
||||
fwrite(&recordSize, sizeof(recordSize), 1, file);
|
||||
fwrite(strRecord.data(), 1, recordSize, file);
|
||||
|
||||
SHA256_Update(&sha256, &recordSize, sizeof(recordSize));
|
||||
SHA256_Update(&sha256, strRecord.data(), recordSize);
|
||||
|
||||
nWritten++;
|
||||
if (nWritten % 10000 == 0)
|
||||
printf("UtxoSnapshot: wrote %d / %d UTXOs\n", nWritten, nUtxoCount);
|
||||
}
|
||||
delete it;
|
||||
|
||||
// Update actual count (in case it changed during iteration)
|
||||
if (nWritten != numUtxos) {
|
||||
numUtxos = nWritten;
|
||||
// Seek back and update numUtxos in header
|
||||
long currentPos = ftell(file);
|
||||
fseek(file, contentHashPos - sizeof(numUtxos), SEEK_SET);
|
||||
fwrite(&numUtxos, sizeof(numUtxos), 1, file);
|
||||
fseek(file, currentPos, SEEK_SET);
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize content hash and write it to the header
|
||||
SHA256_Final((unsigned char*)&contentHash, &sha256);
|
||||
fseek(file, contentHashPos, SEEK_SET);
|
||||
fwrite(&contentHash, sizeof(contentHash), 1, file);
|
||||
|
||||
fclose(file);
|
||||
|
||||
printf("UtxoSnapshot: wrote %s (%d headers, %d UTXOs, hash=%s)\n",
|
||||
destPath.string().c_str(), numHeaders, numUtxos,
|
||||
contentHash.ToString().c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LoadSnapshot - load a UTXO snapshot into a fresh LevelDB
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool LoadSnapshot(const fs::path& snapshotPath,
|
||||
const fs::path& dataDir,
|
||||
std::string& strError)
|
||||
{
|
||||
FILE* file = fopen(snapshotPath.string().c_str(), "rb");
|
||||
if (!file) {
|
||||
strError = "Cannot open snapshot file: " + snapshotPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read header
|
||||
unsigned int magic, version, network;
|
||||
int height;
|
||||
uint256 blockHash;
|
||||
int64_t moneySupply;
|
||||
unsigned int numHeaders, numUtxos;
|
||||
uint256 expectedContentHash;
|
||||
|
||||
if (fread(&magic, sizeof(magic), 1, file) != 1 ||
|
||||
fread(&version, sizeof(version), 1, file) != 1 ||
|
||||
fread(&network, sizeof(network), 1, file) != 1 ||
|
||||
fread(&height, sizeof(height), 1, file) != 1 ||
|
||||
fread(&blockHash, sizeof(blockHash), 1, file) != 1 ||
|
||||
fread(&moneySupply, sizeof(moneySupply), 1, file) != 1 ||
|
||||
fread(&numHeaders, sizeof(numHeaders), 1, file) != 1 ||
|
||||
fread(&numUtxos, sizeof(numUtxos), 1, file) != 1 ||
|
||||
fread(&expectedContentHash, sizeof(expectedContentHash), 1, file) != 1) {
|
||||
fclose(file);
|
||||
strError = "Truncated snapshot header";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate header
|
||||
if (magic != UTXO_SNAPSHOT_MAGIC) {
|
||||
fclose(file);
|
||||
strError = "Invalid snapshot magic (not a UTXO snapshot file)";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version != UTXO_SNAPSHOT_VERSION) {
|
||||
fclose(file);
|
||||
strError = "Unsupported snapshot version: " + std::to_string(version);
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned int expectedNetwork = fTestNet ? 2 : 1;
|
||||
if (network != expectedNetwork) {
|
||||
fclose(file);
|
||||
strError = "Network mismatch: snapshot is " + std::string(network == 1 ? "mainnet" : "testnet");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (numHeaders == 0 || numUtxos == 0) {
|
||||
fclose(file);
|
||||
strError = "Snapshot contains no data";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify snapshot block is a known checkpoint
|
||||
if (!Checkpoints::IsKnownCheckpoint(height, blockHash)) {
|
||||
fclose(file);
|
||||
strError = "Snapshot block " + blockHash.ToString() + " at height "
|
||||
+ std::to_string(height) + " is not a known checkpoint";
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("UtxoSnapshot: loading snapshot at height %d (%d headers, %d UTXOs)\n",
|
||||
height, numHeaders, numUtxos);
|
||||
|
||||
// Create fresh LevelDB directory
|
||||
fs::path txleveldbPath = dataDir / "txleveldb";
|
||||
if (fs::exists(txleveldbPath))
|
||||
fs::remove_all(txleveldbPath);
|
||||
fs::create_directories(txleveldbPath);
|
||||
|
||||
// Open LevelDB directly (not via CTxDB - it's not initialized yet)
|
||||
leveldb::Options options;
|
||||
int nCacheSizeMB = GetArg("-dbcache", 2048);
|
||||
options.block_cache = leveldb::NewLRUCache(nCacheSizeMB * 1048576);
|
||||
options.filter_policy = leveldb::NewBloomFilterPolicy(10);
|
||||
options.write_buffer_size = 64 * 1048576;
|
||||
options.max_open_files = 1000;
|
||||
options.create_if_missing = true;
|
||||
|
||||
leveldb::DB* pdb = NULL;
|
||||
leveldb::Status status = leveldb::DB::Open(options, txleveldbPath.string(), &pdb);
|
||||
if (!status.ok()) {
|
||||
fclose(file);
|
||||
delete options.filter_policy;
|
||||
delete options.block_cache;
|
||||
strError = "Cannot create LevelDB: " + status.ToString();
|
||||
return false;
|
||||
}
|
||||
|
||||
SHA256_CTX sha256;
|
||||
SHA256_Init(&sha256);
|
||||
|
||||
leveldb::WriteBatch batch;
|
||||
bool success = true;
|
||||
unsigned int nBatchSize = 0;
|
||||
|
||||
auto flushBatch = [&]() -> bool {
|
||||
if (nBatchSize == 0)
|
||||
return true;
|
||||
leveldb::Status s = pdb->Write(leveldb::WriteOptions(), &batch);
|
||||
if (!s.ok()) {
|
||||
strError = "LevelDB write failed: " + s.ToString();
|
||||
return false;
|
||||
}
|
||||
batch.Clear();
|
||||
nBatchSize = 0;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Read and write block headers
|
||||
printf("UtxoSnapshot: loading %d block headers...\n", numHeaders);
|
||||
uiInterface.InitMessage(_("Loading UTXO snapshot (headers)..."));
|
||||
|
||||
for (unsigned int i = 0; i < numHeaders; i++) {
|
||||
unsigned int entrySize;
|
||||
if (fread(&entrySize, sizeof(entrySize), 1, file) != 1 || entrySize > 10000) {
|
||||
success = false;
|
||||
strError = "Invalid header entry size at index " + std::to_string(i);
|
||||
break;
|
||||
}
|
||||
|
||||
std::vector<char> buf(entrySize);
|
||||
if (fread(buf.data(), 1, entrySize, file) != entrySize) {
|
||||
success = false;
|
||||
strError = "Truncated header entry at index " + std::to_string(i);
|
||||
break;
|
||||
}
|
||||
|
||||
SHA256_Update(&sha256, &entrySize, sizeof(entrySize));
|
||||
SHA256_Update(&sha256, buf.data(), entrySize);
|
||||
|
||||
// Parse: block_hash + CDiskBlockIndex
|
||||
CDataStream ssEntry(buf.data(), buf.data() + buf.size(), SER_DISK, CLIENT_VERSION);
|
||||
uint256 entryHash;
|
||||
CDiskBlockIndex diskindex;
|
||||
ssEntry >> entryHash;
|
||||
ssEntry >> diskindex;
|
||||
|
||||
// Write to LevelDB as "blockindex" key
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
ssKey << std::make_pair(std::string("blockindex"), entryHash);
|
||||
|
||||
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
||||
ssValue << diskindex;
|
||||
|
||||
batch.Put(ssKey.str(), ssValue.str());
|
||||
nBatchSize++;
|
||||
|
||||
if (nBatchSize >= 1000) {
|
||||
if (!flushBatch()) { success = false; break; }
|
||||
}
|
||||
}
|
||||
|
||||
if (success && !flushBatch())
|
||||
success = false;
|
||||
|
||||
// Read and write UTXOs
|
||||
if (success) {
|
||||
printf("UtxoSnapshot: loading %d UTXOs...\n", numUtxos);
|
||||
|
||||
for (unsigned int i = 0; i < numUtxos; i++) {
|
||||
unsigned int recordSize;
|
||||
if (fread(&recordSize, sizeof(recordSize), 1, file) != 1 || recordSize > 100000) {
|
||||
success = false;
|
||||
strError = "Invalid UTXO record size at index " + std::to_string(i);
|
||||
break;
|
||||
}
|
||||
|
||||
std::vector<char> buf(recordSize);
|
||||
if (fread(buf.data(), 1, recordSize, file) != recordSize) {
|
||||
success = false;
|
||||
strError = "Truncated UTXO record at index " + std::to_string(i);
|
||||
break;
|
||||
}
|
||||
|
||||
SHA256_Update(&sha256, &recordSize, sizeof(recordSize));
|
||||
SHA256_Update(&sha256, buf.data(), recordSize);
|
||||
|
||||
// Parse: txid + output_index + CUtxoEntry
|
||||
CDataStream ssRecord(buf.data(), buf.data() + buf.size(), SER_DISK, CLIENT_VERSION);
|
||||
uint256 txhash;
|
||||
unsigned int nIndex;
|
||||
CUtxoEntry entry;
|
||||
ssRecord >> txhash;
|
||||
ssRecord >> nIndex;
|
||||
ssRecord >> entry;
|
||||
|
||||
// Write to LevelDB with "u" prefix key
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
ssKey << std::make_pair(std::string("u"), std::make_pair(txhash, nIndex));
|
||||
|
||||
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
||||
ssValue << entry;
|
||||
|
||||
batch.Put(ssKey.str(), ssValue.str());
|
||||
nBatchSize++;
|
||||
|
||||
if (nBatchSize >= 50000) {
|
||||
if (!flushBatch()) { success = false; break; }
|
||||
|
||||
if (i % 50000 == 0) {
|
||||
std::string strMsg = strprintf(_("Loading UTXO snapshot (%d%%)..."),
|
||||
i * 100 / numUtxos);
|
||||
uiInterface.InitMessage(strMsg);
|
||||
printf("UtxoSnapshot: loaded %d / %d UTXOs\n", i, numUtxos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (success && !flushBatch())
|
||||
success = false;
|
||||
}
|
||||
|
||||
// Verify content hash
|
||||
if (success) {
|
||||
uint256 actualHash;
|
||||
SHA256_Final((unsigned char*)&actualHash, &sha256);
|
||||
|
||||
if (actualHash != expectedContentHash) {
|
||||
success = false;
|
||||
strError = "Content hash mismatch - snapshot may be corrupted";
|
||||
}
|
||||
}
|
||||
|
||||
// Write metadata
|
||||
if (success) {
|
||||
leveldb::WriteBatch metaBatch;
|
||||
|
||||
// hashBestChain
|
||||
CDataStream ssKey1(SER_DISK, CLIENT_VERSION);
|
||||
ssKey1 << std::string("hashBestChain");
|
||||
CDataStream ssVal1(SER_DISK, CLIENT_VERSION);
|
||||
ssVal1 << blockHash;
|
||||
metaBatch.Put(ssKey1.str(), ssVal1.str());
|
||||
|
||||
// dbformat = 3
|
||||
CDataStream ssKey2(SER_DISK, CLIENT_VERSION);
|
||||
ssKey2 << std::string("dbformat");
|
||||
CDataStream ssVal2(SER_DISK, CLIENT_VERSION);
|
||||
ssVal2 << (int)3;
|
||||
metaBatch.Put(ssKey2.str(), ssVal2.str());
|
||||
|
||||
// version
|
||||
CDataStream ssKey3(SER_DISK, CLIENT_VERSION);
|
||||
ssKey3 << std::string("version");
|
||||
CDataStream ssVal3(SER_DISK, CLIENT_VERSION);
|
||||
ssVal3 << DATABASE_VERSION;
|
||||
metaBatch.Put(ssKey3.str(), ssVal3.str());
|
||||
|
||||
leveldb::Status s = pdb->Write(leveldb::WriteOptions(), &metaBatch);
|
||||
if (!s.ok()) {
|
||||
success = false;
|
||||
strError = "Failed to write metadata: " + s.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up LevelDB
|
||||
delete pdb;
|
||||
delete options.filter_policy;
|
||||
delete options.block_cache;
|
||||
|
||||
fclose(file);
|
||||
|
||||
if (!success) {
|
||||
// Remove corrupted/incomplete database
|
||||
printf("UtxoSnapshot: load failed: %s\n", strError.c_str());
|
||||
if (fs::exists(txleveldbPath))
|
||||
fs::remove_all(txleveldbPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("UtxoSnapshot: successfully loaded %d headers + %d UTXOs at height %d\n",
|
||||
numHeaders, numUtxos, height);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace UtxoSnapshot
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2024-2025 Triangles developers
|
||||
// Distributed under the MIT/X11 software license
|
||||
|
||||
#ifndef TRIANGLES_UTXOSNAPSHOT_H
|
||||
#define TRIANGLES_UTXOSNAPSHOT_H
|
||||
|
||||
#include <string>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
// UTXO snapshot file magic bytes
|
||||
static const unsigned int UTXO_SNAPSHOT_MAGIC = 0x53585455; // "UTXS" little-endian
|
||||
|
||||
// UTXO snapshot format version
|
||||
static const unsigned int UTXO_SNAPSHOT_VERSION = 1;
|
||||
|
||||
// Number of block index entries to include in snapshot (covers difficulty,
|
||||
// median time, stake modifier, and reorg depth requirements)
|
||||
static const unsigned int UTXO_SNAPSHOT_DEFAULT_HEADERS = 2000;
|
||||
|
||||
namespace UtxoSnapshot {
|
||||
|
||||
// Create a UTXO snapshot from the current chain state.
|
||||
// Writes last nHeaders block index entries + all UTXOs to destPath.
|
||||
// Returns true on success, sets strError on failure.
|
||||
bool DumpSnapshot(const boost::filesystem::path& destPath,
|
||||
unsigned int nHeaders,
|
||||
std::string& strError);
|
||||
|
||||
// Load a UTXO snapshot from a file into a fresh LevelDB.
|
||||
// Writes block index entries, UTXOs, hashBestChain, and dbformat.
|
||||
// The LevelDB must NOT be open yet (call before LoadBlockIndex).
|
||||
// Returns true on success, sets strError on failure.
|
||||
bool LoadSnapshot(const boost::filesystem::path& snapshotPath,
|
||||
const boost::filesystem::path& dataDir,
|
||||
std::string& strError);
|
||||
|
||||
} // namespace UtxoSnapshot
|
||||
|
||||
#endif // TRIANGLES_UTXOSNAPSHOT_H
|
||||
+2
-2
@@ -41,10 +41,10 @@ const std::string CLIENT_NAME("Cryptographic Triangles");
|
||||
#endif
|
||||
|
||||
#define BUILD_DESC_FROM_COMMIT(maj,min,rev,build,commit) \
|
||||
"v" DO_STRINGIZE(maj) "." DO_STRINGIZE(min) "." DO_STRINGIZE(rev) "." DO_STRINGIZE(build) "" commit
|
||||
"v" DO_STRINGIZE(maj) "." DO_STRINGIZE(min) "." DO_STRINGIZE(rev) "" commit
|
||||
|
||||
#define BUILD_DESC_FROM_UNKNOWN(maj,min,rev,build) \
|
||||
"v" DO_STRINGIZE(maj) "." DO_STRINGIZE(min) "." DO_STRINGIZE(rev) "." DO_STRINGIZE(build) ""
|
||||
"v" DO_STRINGIZE(maj) "." DO_STRINGIZE(min) "." DO_STRINGIZE(rev) ""
|
||||
|
||||
#ifndef BUILD_DESC
|
||||
# ifdef GIT_COMMIT_ID
|
||||
|
||||
@@ -16,6 +16,40 @@ namespace fs = boost::filesystem;
|
||||
static uint64_t nAccountingEntryNumber = 0;
|
||||
extern bool fWalletUnlockStakingOnly;
|
||||
|
||||
//
|
||||
// Auto-backup wallet before flush/rewrite operations.
|
||||
// Copies wallet.dat to wallet.dat.auto.bak if the backup is older than the wallet.
|
||||
// Returns true if backup was created or already up to date.
|
||||
//
|
||||
bool AutoBackupWallet(const fs::path& walletPath)
|
||||
{
|
||||
fs::path backupPath = walletPath.string() + ".auto.bak";
|
||||
try {
|
||||
// Only back up if wallet exists and is non-trivial (>1KB)
|
||||
if (!fs::exists(walletPath))
|
||||
return true;
|
||||
uintmax_t walletSize = fs::file_size(walletPath);
|
||||
if (walletSize < 1024) {
|
||||
printf("AutoBackupWallet: wallet.dat is only %llu bytes (possibly corrupt), skipping auto-backup\n",
|
||||
(unsigned long long)walletSize);
|
||||
return false;
|
||||
}
|
||||
// Skip if backup exists and is same size (already backed up this version)
|
||||
if (fs::exists(backupPath)) {
|
||||
uintmax_t backupSize = fs::file_size(backupPath);
|
||||
if (backupSize == walletSize)
|
||||
return true;
|
||||
}
|
||||
fs::copy_file(walletPath, backupPath, fs::copy_options::overwrite_existing);
|
||||
printf("AutoBackupWallet: backed up wallet.dat (%llu bytes) to wallet.dat.auto.bak\n",
|
||||
(unsigned long long)walletSize);
|
||||
return true;
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
printf("AutoBackupWallet: failed - %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// CWalletDB
|
||||
//
|
||||
@@ -580,6 +614,10 @@ void ThreadFlushWalletDB(void* parg)
|
||||
nLastFlushed = nWalletDBUpdated;
|
||||
int64_t nStart = GetTimeMillis();
|
||||
|
||||
// Auto-backup before flush (protects against corruption)
|
||||
fs::path walletPath = GetDataDir() / strFile;
|
||||
AutoBackupWallet(walletPath);
|
||||
|
||||
// Flush wallet.dat so it's self contained
|
||||
bitdb.CloseDb(strFile);
|
||||
bitdb.CheckpointLSN(strFile);
|
||||
|
||||
Reference in New Issue
Block a user