Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22e220acaa | |||
| 1c068f4782 | |||
| a671708f0b | |||
| be90d39cd4 | |||
| 4d0478add5 | |||
| 734979c93b | |||
| 00af636aca | |||
| 9377b3a52f | |||
| 1881ff867e | |||
| caddfb1789 | |||
| 6eb25d6b25 | |||
| cd7b68f7cb | |||
| a0e8e74d0d | |||
| 7d62e34868 | |||
| b0e9ca334f |
+1
-1
@@ -6,7 +6,7 @@ if(POLICY CMP0167)
|
||||
endif()
|
||||
|
||||
project(Triangles
|
||||
VERSION 5.7.8.0
|
||||
VERSION 5.8.6
|
||||
DESCRIPTION "Cryptographic Triangles Wallet"
|
||||
LANGUAGES C CXX
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,300 @@
|
||||
# OpenClaw Bootstrap Snapshot Guide
|
||||
|
||||
## Purpose
|
||||
|
||||
This document tells OpenClaw exactly how to update the existing Triangles bootstrap server so new wallets download a ready-to-use snapshot instead of downloading `blk0001.dat` and rebuilding the index locally.
|
||||
|
||||
This guide matches the current wallet code in:
|
||||
|
||||
- `src/bootstrap.cpp`
|
||||
- `src/bootstrap.h`
|
||||
- `src/checkpoints.cpp`
|
||||
- `src/version.h`
|
||||
|
||||
## What The Wallet Actually Does
|
||||
|
||||
When a fresh wallet bootstraps, it:
|
||||
|
||||
1. Downloads `http://bootstrap.cryptographic-triangles.org/bootstrap.tar.gz`
|
||||
2. Extracts it into the data directory
|
||||
3. Requires `blk0001.dat` to exist after extraction
|
||||
4. Looks for `txleveldb/` and `snapshot.manifest`
|
||||
5. Keeps `txleveldb/` only if `snapshot.manifest` passes verification
|
||||
6. Deletes `txleveldb/` if verification fails, then rebuilds from `blk0001.dat`
|
||||
7. Always deletes `database/` from the extracted snapshot
|
||||
|
||||
The verification rules are strict:
|
||||
|
||||
- `format` must be `1`
|
||||
- `network` must be `main` on mainnet
|
||||
- `dbversion` must be `70509`
|
||||
- `height` and `hash` must exactly match a hardcoded checkpoint
|
||||
|
||||
If any of those checks fail, the wallet throws away the shipped `txleveldb/`.
|
||||
|
||||
## Current Hardcoded Mainnet Checkpoint
|
||||
|
||||
As of the current codebase, the latest hardcoded mainnet checkpoint is:
|
||||
|
||||
- Height: `2186940`
|
||||
- Hash: `bd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0`
|
||||
|
||||
OpenClaw must not generate a manifest with an arbitrary tip hash. The manifest only survives if it matches a hardcoded checkpoint from `src/checkpoints.cpp`.
|
||||
|
||||
## Important Limitation
|
||||
|
||||
If the live chain tip is past the latest hardcoded checkpoint, OpenClaw has two valid options:
|
||||
|
||||
1. Publish a snapshot taken exactly at the latest hardcoded checkpoint
|
||||
2. Publish `blk0001.dat` only, without `txleveldb/`, and let clients rebuild locally
|
||||
|
||||
OpenClaw must not publish a `snapshot.manifest` for a height/hash that is not compiled into the wallet.
|
||||
|
||||
## Files OpenClaw Should Publish
|
||||
|
||||
The preferred `bootstrap.tar.gz` should contain:
|
||||
|
||||
- `blk0001.dat`
|
||||
- `txleveldb/`
|
||||
- `snapshot.manifest`
|
||||
- optionally `peers.dat`
|
||||
|
||||
It must not contain:
|
||||
|
||||
- `wallet.dat`
|
||||
- `database/`
|
||||
- `.lock`
|
||||
- pid files
|
||||
- logs
|
||||
- Tor state
|
||||
|
||||
Legacy fallback files should still exist on the web root:
|
||||
|
||||
- `blk0001.dat`
|
||||
- `filelist.txt`
|
||||
|
||||
## Requirements For The Source Node
|
||||
|
||||
Before building a snapshot, the source node should be:
|
||||
|
||||
- fully synced
|
||||
- cleanly shut down before copying files
|
||||
- built from the same code/version expected by clients
|
||||
- using the same LevelDB schema as the client (`DATABASE_VERSION=70509`)
|
||||
|
||||
Recommended node config for the source snapshot node:
|
||||
|
||||
```ini
|
||||
txindex=1
|
||||
addressindex=1
|
||||
daemon=1
|
||||
server=1
|
||||
```
|
||||
|
||||
`addressindex=1` is recommended so clients that enable address index can benefit from faster indexed wallet rescans and address RPCs immediately.
|
||||
|
||||
## OpenClaw Workflow
|
||||
|
||||
### Step 1: Decide Whether A Prebuilt Index Is Allowed
|
||||
|
||||
OpenClaw must first decide whether it can ship `txleveldb/`.
|
||||
|
||||
Rules:
|
||||
|
||||
- If the snapshot node is exactly at checkpoint `2186940`, shipping `txleveldb/` is allowed
|
||||
- If the snapshot node is above `2186940` and the code has not been updated with a newer checkpoint, do not ship `txleveldb/`
|
||||
- In that case, publish a blocks-only bootstrap instead
|
||||
|
||||
### Step 2: Stop The Source Node Cleanly
|
||||
|
||||
Never copy a live LevelDB directory.
|
||||
|
||||
```bash
|
||||
trianglesd stop
|
||||
sleep 10
|
||||
pgrep -af trianglesd || true
|
||||
```
|
||||
|
||||
OpenClaw should confirm the daemon is fully stopped before copying `txleveldb/`.
|
||||
|
||||
### Step 3: Create A Staging Directory
|
||||
|
||||
```bash
|
||||
rm -rf /tmp/triangles-bootstrap-stage
|
||||
mkdir -p /tmp/triangles-bootstrap-stage
|
||||
```
|
||||
|
||||
### Step 4: Copy Snapshot Files
|
||||
|
||||
For a verified snapshot:
|
||||
|
||||
```bash
|
||||
cp ~/.triangles/blk0001.dat /tmp/triangles-bootstrap-stage/
|
||||
cp -a ~/.triangles/txleveldb /tmp/triangles-bootstrap-stage/
|
||||
test -f ~/.triangles/peers.dat && cp ~/.triangles/peers.dat /tmp/triangles-bootstrap-stage/
|
||||
```
|
||||
|
||||
Do not copy:
|
||||
|
||||
```bash
|
||||
rm -rf /tmp/triangles-bootstrap-stage/database
|
||||
rm -f /tmp/triangles-bootstrap-stage/wallet.dat
|
||||
rm -f /tmp/triangles-bootstrap-stage/.lock
|
||||
rm -f /tmp/triangles-bootstrap-stage/*.pid
|
||||
rm -f /tmp/triangles-bootstrap-stage/debug.log
|
||||
```
|
||||
|
||||
### Step 5: Write `snapshot.manifest`
|
||||
|
||||
If OpenClaw is publishing a verified prebuilt index, write:
|
||||
|
||||
```bash
|
||||
cat > /tmp/triangles-bootstrap-stage/snapshot.manifest << 'EOF'
|
||||
format=1
|
||||
network=main
|
||||
height=2186940
|
||||
hash=bd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0
|
||||
dbversion=70509
|
||||
EOF
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `hash` must not include `0x`
|
||||
- `network` must be `main`
|
||||
- `dbversion` must be `70509`
|
||||
- If OpenClaw is publishing blocks-only bootstrap, it should omit `snapshot.manifest` entirely
|
||||
|
||||
### Step 6: Build The Tarball
|
||||
|
||||
```bash
|
||||
cd /tmp/triangles-bootstrap-stage
|
||||
tar czf /tmp/bootstrap.tar.gz .
|
||||
```
|
||||
|
||||
### Step 7: Publish To The Existing Bootstrap Server
|
||||
|
||||
This guide assumes the existing nginx root is:
|
||||
|
||||
- `/var/www/triangles-bootstrap`
|
||||
|
||||
Publish the preferred tarball and the legacy fallback files:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/triangles-bootstrap
|
||||
sudo mv /tmp/bootstrap.tar.gz /var/www/triangles-bootstrap/bootstrap.tar.gz
|
||||
sudo cp ~/.triangles/blk0001.dat /var/www/triangles-bootstrap/blk0001.dat
|
||||
printf "blk0001.dat\n" | sudo tee /var/www/triangles-bootstrap/filelist.txt > /dev/null
|
||||
sudo chown -R www-data:www-data /var/www/triangles-bootstrap
|
||||
```
|
||||
|
||||
If OpenClaw is publishing a blocks-only bootstrap, the commands are the same except the tarball should contain only `blk0001.dat` and optional `peers.dat`.
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before marking the update complete, OpenClaw should verify:
|
||||
|
||||
### Tarball contents
|
||||
|
||||
```bash
|
||||
tar tzf /var/www/triangles-bootstrap/bootstrap.tar.gz | sort
|
||||
```
|
||||
|
||||
Expected for verified snapshot:
|
||||
|
||||
- `./blk0001.dat`
|
||||
- `./txleveldb/...`
|
||||
- `./snapshot.manifest`
|
||||
|
||||
Expected not to exist:
|
||||
|
||||
- `wallet.dat`
|
||||
- `database/`
|
||||
|
||||
### HTTP responses
|
||||
|
||||
```bash
|
||||
curl -I http://localhost/bootstrap.tar.gz
|
||||
curl -I http://localhost/blk0001.dat
|
||||
curl http://localhost/filelist.txt
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- HTTP `200`
|
||||
- `filelist.txt` contains `blk0001.dat`
|
||||
|
||||
### Manifest sanity
|
||||
|
||||
```bash
|
||||
tar xOf /var/www/triangles-bootstrap/bootstrap.tar.gz ./snapshot.manifest
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- `format=1`
|
||||
- `network=main`
|
||||
- `height=2186940`
|
||||
- `hash=bd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0`
|
||||
- `dbversion=70509`
|
||||
|
||||
## Fresh-Client Test
|
||||
|
||||
OpenClaw should test the artifact on a clean machine or clean data directory:
|
||||
|
||||
```bash
|
||||
mv ~/.triangles ~/.triangles.backup.$(date +%s)
|
||||
mkdir -p ~/.triangles
|
||||
trianglesd -bootstrap
|
||||
```
|
||||
|
||||
Then inspect startup logs.
|
||||
|
||||
Successful verified snapshot behavior should include:
|
||||
|
||||
- snapshot downloaded
|
||||
- `snapshot.manifest found`
|
||||
- `manifest verified - keeping pre-built index`
|
||||
- no message about removing extracted `txleveldb/`
|
||||
|
||||
Failure behavior will include:
|
||||
|
||||
- manifest parse or verification failure
|
||||
- `removing extracted txleveldb/`
|
||||
- slow rebuild from `blk0001.dat`
|
||||
|
||||
## Safe Publish Procedure
|
||||
|
||||
OpenClaw should use this order:
|
||||
|
||||
1. Build snapshot in `/tmp`
|
||||
2. Validate tarball contents
|
||||
3. Replace `/var/www/triangles-bootstrap/bootstrap.tar.gz`
|
||||
4. Replace `/var/www/triangles-bootstrap/blk0001.dat`
|
||||
5. Replace `/var/www/triangles-bootstrap/filelist.txt`
|
||||
6. Confirm HTTP `200`
|
||||
|
||||
This avoids serving a half-written tarball.
|
||||
|
||||
## Example Bot Prompt
|
||||
|
||||
Use this exact tasking for OpenClaw:
|
||||
|
||||
```text
|
||||
Update the existing Triangles bootstrap server on bootstrap.cryptographic-triangles.org.
|
||||
|
||||
Rules:
|
||||
- Build the snapshot from a cleanly stopped source node
|
||||
- If the source node is exactly at hardcoded checkpoint 2186940 / bd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0, publish a verified snapshot containing blk0001.dat, txleveldb/, and snapshot.manifest
|
||||
- If the source node is above the latest hardcoded checkpoint, publish a blocks-only bootstrap and do not ship txleveldb/
|
||||
- Do not ship wallet.dat, database/, .lock, pid files, logs, or Tor state
|
||||
- Publish bootstrap.tar.gz, blk0001.dat, and filelist.txt to /var/www/triangles-bootstrap
|
||||
- Verify curl HTTP 200 for bootstrap.tar.gz and blk0001.dat
|
||||
- Report the tarball contents and whether the snapshot is verified or blocks-only
|
||||
```
|
||||
|
||||
## Recommended Next Improvement
|
||||
|
||||
This workflow will stay constrained until the next checkpoint is updated in `src/checkpoints.cpp`.
|
||||
|
||||
If you want OpenClaw to keep shipping prebuilt `txleveldb/` snapshots as the chain advances, the software needs periodic checkpoint updates. Without that, the verified snapshot path will stop at the latest compiled checkpoint and clients will fall back to rebuilds.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Cryptographic Triangles (TRI) - v5.1.5
|
||||
# Cryptographic Triangles (TRI)
|
||||
|
||||
Triangles is a privacy-focused cryptocurrency featuring Proof-of-Stake consensus, Tor v3 onion routing, and built-in encrypted messaging. Originally launched in July 2014, the chain was revived in March 2026 after being frozen since December 2022.
|
||||
|
||||
@@ -16,7 +16,7 @@ Triangles is a privacy-focused cryptocurrency featuring Proof-of-Stake consensus
|
||||
|----------|-------|
|
||||
| Algorithm | Hash9 (PoW blocks 0-9000), PoS from block 9001 |
|
||||
| Block Time | ~120 seconds |
|
||||
| Max Supply | 222,222 TRI |
|
||||
| Max Supply | 2,222,222 TRI |
|
||||
| PoS Reward | 33% annual, coin-age based |
|
||||
| P2P Port | 24112 |
|
||||
| RPC Port | 19112 |
|
||||
@@ -24,48 +24,60 @@ Triangles is a privacy-focused cryptocurrency featuring Proof-of-Stake consensus
|
||||
|
||||
## Network Status
|
||||
|
||||
The Triangles network is live with seed nodes operating on both clearnet and Tor:
|
||||
|
||||
**Clearnet Seeds:**
|
||||
- `194.233.88.206:24112`
|
||||
- `74.208.167.19:24112`
|
||||
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
|
||||
|
||||
Triangles uses CMake. All platforms follow the same build pattern.
|
||||
|
||||
### Dependencies
|
||||
|
||||
| Dependency | Minimum Version |
|
||||
|------------|----------------|
|
||||
| CMake | 3.16+ |
|
||||
| C++ compiler | C++17 support |
|
||||
| OpenSSL | 3.x |
|
||||
| Boost | 1.90+ |
|
||||
| Berkeley DB | 5.3 (with C++ bindings) |
|
||||
| libevent | 2.x |
|
||||
| LevelDB | bundled |
|
||||
|
||||
### Linux (Ubuntu 24.04 / Debian 12+)
|
||||
|
||||
Install dependencies:
|
||||
```bash
|
||||
sudo apt-get install -y build-essential libboost-all-dev libssl-dev \
|
||||
libdb5.3++-dev libevent-dev zlib1g-dev libminiupnpc-dev
|
||||
sudo apt-get install -y build-essential cmake ninja-build \
|
||||
libboost-all-dev libssl-dev libdb5.3++-dev libevent-dev \
|
||||
zlib1g-dev libminiupnpc-dev
|
||||
```
|
||||
|
||||
Build the daemon:
|
||||
For the Qt wallet, also install:
|
||||
```bash
|
||||
cd src/leveldb && make libleveldb.a libmemenv.a && cd ..
|
||||
make -j$(nproc) -f makefile.unix USE_UPNP=0
|
||||
strip trianglesd
|
||||
sudo apt-get install -y qtbase5-dev qt5-qmake libqrencode-dev
|
||||
```
|
||||
|
||||
Run the unit test suite:
|
||||
Build:
|
||||
```bash
|
||||
make -C src -f makefile.unix test
|
||||
cmake -B build -G Ninja -DBUILD_QT=ON
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
### Linux (AlmaLinux 9 / RHEL 9)
|
||||
|
||||
Install dependencies:
|
||||
```bash
|
||||
sudo dnf install -y gcc-c++ make boost-devel openssl-devel libevent-devel \
|
||||
zlib-devel miniupnpc-devel
|
||||
sudo dnf install -y gcc-c++ cmake ninja-build boost-devel openssl-devel \
|
||||
libevent-devel zlib-devel miniupnpc-devel
|
||||
```
|
||||
|
||||
BDB 5.3 C++ bindings must be built from source on RHEL-based systems (the `libdb-devel` package does not include C++ headers). Download BDB 5.3.28 from Oracle and build with `--enable-cxx`.
|
||||
@@ -76,17 +88,27 @@ Then build as above.
|
||||
|
||||
Open an MSYS2 MinGW64 shell and install:
|
||||
```bash
|
||||
pacman -S mingw-w64-x86_64-boost mingw-w64-x86_64-openssl \
|
||||
pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja \
|
||||
mingw-w64-x86_64-boost mingw-w64-x86_64-openssl \
|
||||
mingw-w64-x86_64-db mingw-w64-x86_64-miniupnpc \
|
||||
mingw-w64-x86_64-qt5-base mingw-w64-x86_64-qrencode
|
||||
mingw-w64-x86_64-qt5-base mingw-w64-x86_64-qrencode \
|
||||
mingw-w64-x86_64-libevent
|
||||
```
|
||||
|
||||
Build the Qt wallet:
|
||||
Build:
|
||||
```bash
|
||||
qmake triangles-qt.pro
|
||||
make -j$(nproc)
|
||||
cmake -B build -G Ninja -DBUILD_QT=ON
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
### Build Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `BUILD_QT` | ON | Build the Qt GUI wallet |
|
||||
| `BUILD_DAEMON` | ON | Build the headless daemon |
|
||||
| `BUILD_TESTS` | OFF | Build unit tests |
|
||||
|
||||
## Running
|
||||
|
||||
### First Run
|
||||
@@ -103,15 +125,13 @@ txindex=1
|
||||
listen=1
|
||||
server=1
|
||||
daemon=1
|
||||
addnode=194.233.88.206
|
||||
addnode=74.208.167.19
|
||||
externalip=<your-public-ip>
|
||||
proxy=127.0.0.1:9050
|
||||
EOF
|
||||
|
||||
trianglesd
|
||||
```
|
||||
|
||||
The node will connect to seed nodes and sync the blockchain automatically.
|
||||
The node will connect to seed nodes over Tor and sync the blockchain automatically.
|
||||
|
||||
### Existing Wallet Holders
|
||||
|
||||
@@ -155,7 +175,7 @@ Messages are encrypted end-to-end using AES and distributed through the peer net
|
||||
|
||||
### Tor Support
|
||||
|
||||
To connect through Tor, install the Tor daemon and add to your config:
|
||||
Triangles is designed as a Tor-only network. Install the Tor daemon and configure your proxy:
|
||||
```
|
||||
# triangles.conf
|
||||
proxy=127.0.0.1:9050
|
||||
@@ -199,7 +219,7 @@ Then set `externalip=<your-onion-address>` in `triangles.conf`.
|
||||
- **Block 9001+** - Proof-of-Stake only
|
||||
- **Block 17,651** - V5 hard fork (removed Tor v2, disabled checkpoint master key)
|
||||
- **December 8, 2022** - Chain frozen (all nodes offline)
|
||||
- **March 11, 2026** - Chain revived with v5.0.0.0, staking resumed
|
||||
- **March 11, 2026** - Chain revived, staking resumed
|
||||
|
||||
## Project Structure
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# cmake/GenerateBuildInfoScript.cmake
|
||||
# Called at build time by the custom target in GenerateBuildInfo.cmake.
|
||||
# Replicates the logic of share/genbuild.sh.
|
||||
# Reads the version from clientversion.h (single source of truth) and
|
||||
# appends git commit info for non-release builds.
|
||||
|
||||
# Read existing build.h first line if it exists
|
||||
set(OLD_LINE "")
|
||||
@@ -11,31 +12,69 @@ if(EXISTS "${OUTPUT_FILE}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Try exact tag match first (release builds)
|
||||
# ── Read version from clientversion.h ──
|
||||
file(STRINGS "${SOURCE_DIR}/src/clientversion.h" _ver_lines)
|
||||
foreach(_line ${_ver_lines})
|
||||
if(_line MATCHES "^#define CLIENT_VERSION_MAJOR +([0-9]+)")
|
||||
set(VER_MAJOR "${CMAKE_MATCH_1}")
|
||||
elseif(_line MATCHES "^#define CLIENT_VERSION_MINOR +([0-9]+)")
|
||||
set(VER_MINOR "${CMAKE_MATCH_1}")
|
||||
elseif(_line MATCHES "^#define CLIENT_VERSION_REVISION +([0-9]+)")
|
||||
set(VER_REVISION "${CMAKE_MATCH_1}")
|
||||
elseif(_line MATCHES "^#define CLIENT_VERSION_BUILD +([0-9]+)")
|
||||
set(VER_BUILD "${CMAKE_MATCH_1}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
set(BASE_VERSION "v${VER_MAJOR}.${VER_MINOR}.${VER_REVISION}.${VER_BUILD}")
|
||||
|
||||
# ── Get git commit info (suffix only, not the version number) ──
|
||||
set(GIT_SUFFIX "")
|
||||
|
||||
# Get short commit hash
|
||||
execute_process(
|
||||
COMMAND git describe --tags --exact-match
|
||||
COMMAND git rev-parse --short HEAD
|
||||
WORKING_DIRECTORY "${SOURCE_DIR}"
|
||||
OUTPUT_VARIABLE GIT_DESC
|
||||
OUTPUT_VARIABLE GIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
RESULT_VARIABLE _result
|
||||
)
|
||||
|
||||
# Fall back to tag + commit distance
|
||||
if(NOT _result EQUAL 0)
|
||||
if(_result EQUAL 0 AND GIT_HASH)
|
||||
# Check if working directory is dirty
|
||||
execute_process(
|
||||
COMMAND git describe --tags --dirty
|
||||
COMMAND git diff-index --quiet HEAD --
|
||||
WORKING_DIRECTORY "${SOURCE_DIR}"
|
||||
OUTPUT_VARIABLE GIT_DESC
|
||||
RESULT_VARIABLE _dirty
|
||||
)
|
||||
|
||||
# Check if HEAD is exactly on a tag matching our version
|
||||
execute_process(
|
||||
COMMAND git describe --tags --exact-match HEAD
|
||||
WORKING_DIRECTORY "${SOURCE_DIR}"
|
||||
OUTPUT_VARIABLE GIT_TAG
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
RESULT_VARIABLE _result
|
||||
RESULT_VARIABLE _tag_result
|
||||
)
|
||||
if(NOT _result EQUAL 0)
|
||||
set(GIT_DESC "")
|
||||
|
||||
set(_on_release_tag FALSE)
|
||||
if(_tag_result EQUAL 0 AND GIT_TAG STREQUAL "${BASE_VERSION}")
|
||||
set(_on_release_tag TRUE)
|
||||
endif()
|
||||
|
||||
# Only add git suffix for non-release builds (not on exact version tag, or dirty)
|
||||
if(NOT _on_release_tag OR NOT _dirty EQUAL 0)
|
||||
set(GIT_SUFFIX "-g${GIT_HASH}")
|
||||
if(NOT _dirty EQUAL 0)
|
||||
set(GIT_SUFFIX "${GIT_SUFFIX}-dirty")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(FULL_VERSION "${BASE_VERSION}${GIT_SUFFIX}")
|
||||
|
||||
# Get commit timestamp
|
||||
execute_process(
|
||||
COMMAND git log -n 1 --format=%ci
|
||||
@@ -46,11 +85,7 @@ execute_process(
|
||||
)
|
||||
|
||||
# Build new content
|
||||
if(GIT_DESC)
|
||||
set(NEW_LINE "#define BUILD_DESC \"${GIT_DESC}\"")
|
||||
else()
|
||||
set(NEW_LINE "// No build information available")
|
||||
endif()
|
||||
set(NEW_LINE "#define BUILD_DESC \"${FULL_VERSION}\"")
|
||||
|
||||
# Only write if changed
|
||||
if(NOT "${OLD_LINE}" STREQUAL "${NEW_LINE}")
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# systemd drop-in for trianglesd: enable unlimited core dumps so that
|
||||
# crashes can be diagnosed post-mortem with `coredumpctl gdb`.
|
||||
#
|
||||
# Installation:
|
||||
# sudo mkdir -p /etc/systemd/system/trianglesd.service.d
|
||||
# sudo cp contrib/systemd/coredump.conf /etc/systemd/system/trianglesd.service.d/
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl restart trianglesd
|
||||
#
|
||||
# Verify it took effect:
|
||||
# systemctl show trianglesd | grep -E 'LimitCORE|LimitNOFILE'
|
||||
#
|
||||
# When the next crash happens, retrieve the stack trace with:
|
||||
# coredumpctl list trianglesd
|
||||
# coredumpctl gdb # most recent core; then run `bt full` at the (gdb) prompt
|
||||
#
|
||||
# See contrib/debug/CRASHDUMPS.md for the full playbook.
|
||||
|
||||
[Service]
|
||||
# Allow the kernel to write a full core dump on SIGSEGV/SIGABRT/SIGBUS/SIGFPE.
|
||||
LimitCORE=infinity
|
||||
|
||||
# systemd-coredump compresses and stores cores under /var/lib/systemd/coredump/.
|
||||
# Make sure the package is installed:
|
||||
# apt install systemd-coredump # Debian/Ubuntu
|
||||
# dnf install systemd-coredump # Fedora/RHEL
|
||||
+322
-84
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "bootstrap.h"
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/fstream.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
@@ -12,6 +11,11 @@
|
||||
|
||||
#include "version.h"
|
||||
#include "uint256.h"
|
||||
#include "netbase.h"
|
||||
#include "net.h"
|
||||
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/err.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
@@ -19,12 +23,20 @@
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
|
||||
#ifdef WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
// Forward declarations to avoid pulling in heavy consensus headers
|
||||
extern bool fTestNet;
|
||||
namespace Checkpoints { bool IsKnownCheckpoint(int nHeight, const uint256& hash); }
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
using boost::asio::ip::tcp;
|
||||
|
||||
namespace Bootstrap {
|
||||
|
||||
@@ -33,63 +45,300 @@ bool NeedsBootstrap(const fs::path& dataDir)
|
||||
return !fs::exists(dataDir / "blk0001.dat");
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
boost::asio::io_context io_context;
|
||||
tcp::resolver resolver(io_context);
|
||||
std::string currentHost = host;
|
||||
std::string currentPath = urlPath;
|
||||
int currentPort = PORT;
|
||||
bool useSSL = false;
|
||||
std::string headerData;
|
||||
int redirectCount = 0;
|
||||
const int MAX_REDIRECTS = 5;
|
||||
|
||||
boost::system::error_code resolve_ec;
|
||||
tcp::resolver::results_type endpoints =
|
||||
resolver.resolve(host, std::to_string(PORT), resolve_ec);
|
||||
if (resolve_ec) {
|
||||
strError = "Cannot resolve host: " + host;
|
||||
return false;
|
||||
}
|
||||
HttpConn conn;
|
||||
|
||||
tcp::socket socket(io_context);
|
||||
boost::asio::connect(socket, endpoints);
|
||||
// Connection + redirect loop
|
||||
while (true) {
|
||||
conn.Close(); // clean slate for each attempt
|
||||
|
||||
// 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";
|
||||
boost::asio::write(socket, boost::asio::buffer(request));
|
||||
|
||||
// Read response headers
|
||||
boost::asio::streambuf response_buf;
|
||||
boost::asio::read_until(socket, response_buf, "\r\n\r\n");
|
||||
|
||||
std::istream response_stream(&response_buf);
|
||||
|
||||
// Parse status line
|
||||
std::string http_version;
|
||||
unsigned int status_code = 0;
|
||||
response_stream >> http_version >> status_code;
|
||||
std::string status_message;
|
||||
std::getline(response_stream, status_message);
|
||||
|
||||
if (status_code != 200) {
|
||||
strError = "HTTP error " + std::to_string(status_code) + " for " + urlPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse headers for Content-Length
|
||||
int64_t content_length = 0;
|
||||
std::string header_line;
|
||||
while (std::getline(response_stream, header_line) && header_line != "\r") {
|
||||
std::string lower_header = header_line;
|
||||
std::transform(lower_header.begin(), lower_header.end(),
|
||||
lower_header.begin(), ::tolower);
|
||||
if (lower_header.find("content-length:") == 0) {
|
||||
content_length = std::stoll(header_line.substr(header_line.find(':') + 1));
|
||||
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
|
||||
int64_t content_length = 0;
|
||||
std::string lowerHeaders = headerData;
|
||||
std::transform(lowerHeaders.begin(), lowerHeaders.end(),
|
||||
lowerHeaders.begin(), ::tolower);
|
||||
size_t clPos = lowerHeaders.find("content-length:");
|
||||
if (clPos != std::string::npos) {
|
||||
size_t valStart = clPos + 15;
|
||||
size_t lineEnd = lowerHeaders.find("\r\n", valStart);
|
||||
if (lineEnd != std::string::npos)
|
||||
content_length = std::stoll(headerData.substr(valStart, lineEnd - valStart));
|
||||
}
|
||||
|
||||
// Open output file
|
||||
@@ -99,46 +348,32 @@ bool DownloadFile(const std::string& host, const std::string& urlPath,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read body in chunks
|
||||
int64_t bytes_written = 0;
|
||||
|
||||
// Write any data remaining in the header buffer (body starts here)
|
||||
if (response_buf.size() > 0) {
|
||||
std::istreambuf_iterator<char> eos;
|
||||
std::string remaining(std::istreambuf_iterator<char>(response_stream), eos);
|
||||
if (!remaining.empty()) {
|
||||
fwrite(remaining.data(), 1, remaining.size(), file);
|
||||
bytes_written += remaining.size();
|
||||
}
|
||||
}
|
||||
|
||||
// Read remaining body in chunks
|
||||
std::vector<char> chunk(65536); // 64 KB
|
||||
boost::system::error_code ec;
|
||||
int64_t last_progress = 0;
|
||||
char chunk[65536];
|
||||
|
||||
while (true) {
|
||||
size_t n = socket.read_some(boost::asio::buffer(chunk), ec);
|
||||
if (n > 0) {
|
||||
fwrite(chunk.data(), 1, n, file);
|
||||
bytes_written += n;
|
||||
|
||||
// Report progress every 256 KB
|
||||
if (progressFn && (bytes_written - last_progress >= 262144)) {
|
||||
last_progress = bytes_written;
|
||||
progressFn(bytes_written, content_length);
|
||||
}
|
||||
}
|
||||
if (ec == boost::asio::error::eof)
|
||||
break;
|
||||
if (ec) {
|
||||
int n = conn.Recv(chunk, sizeof(chunk));
|
||||
if (n < 0) {
|
||||
fclose(file);
|
||||
fs::remove(destPath);
|
||||
strError = "Network error: " + ec.message();
|
||||
strError = "Network error during download";
|
||||
return false;
|
||||
}
|
||||
if (n == 0) break; // EOF
|
||||
|
||||
fwrite(chunk, 1, n, file);
|
||||
bytes_written += n;
|
||||
|
||||
if (progressFn && (bytes_written - last_progress >= 262144)) {
|
||||
last_progress = bytes_written;
|
||||
progressFn(bytes_written, content_length);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
// conn destructor handles socket + SSL cleanup
|
||||
|
||||
// Verify download size if Content-Length was provided
|
||||
if (content_length > 0 && bytes_written != content_length) {
|
||||
@@ -158,13 +393,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
|
||||
@@ -433,10 +669,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);
|
||||
@@ -451,7 +689,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
|
||||
@@ -464,7 +702,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;
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -13,7 +13,6 @@ namespace Bootstrap {
|
||||
|
||||
// Bootstrap server configuration
|
||||
static const char* DEFAULT_HOST = "bootstrap.cryptographic-triangles.org";
|
||||
static const char* FALLBACK_HOST = "194.233.88.206";
|
||||
static const char* BASE_PATH = "/";
|
||||
static const int PORT = 80;
|
||||
|
||||
@@ -23,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.
|
||||
|
||||
@@ -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 7
|
||||
#define CLIENT_VERSION_REVISION 8
|
||||
#define CLIENT_VERSION_MINOR 8
|
||||
#define CLIENT_VERSION_REVISION 6
|
||||
#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
|
||||
|
||||
+27
-5
@@ -921,11 +921,6 @@ bool AppInit2()
|
||||
};
|
||||
|
||||
bool success = Bootstrap::DownloadBootstrap(host, dataPath, progressFn, strError);
|
||||
if (!success) {
|
||||
host = Bootstrap::FALLBACK_HOST;
|
||||
printf("\nBootstrap: primary host failed, trying fallback %s...\n", host.c_str());
|
||||
success = Bootstrap::DownloadBootstrap(host, dataPath, progressFn, strError);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
printf("\nBootstrap: failed: %s\n", strError.c_str());
|
||||
@@ -957,6 +952,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();
|
||||
@@ -1046,6 +1053,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)
|
||||
{
|
||||
|
||||
+12
-6
@@ -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);
|
||||
}
|
||||
@@ -334,9 +338,11 @@ bool CheckStakeKernelHash(unsigned int nBits, const CBlock& blockFrom, unsigned
|
||||
// Now check if proof-of-stake hash meets target protocol
|
||||
if (CBigNum(hashProofOfStake) > bnCoinDayWeight * bnTargetPerCoinDay)
|
||||
{
|
||||
// Guard against null pindexBest during early startup / IBD
|
||||
int nCurrentHeight = pindexBest ? pindexBest->nHeight : 0;
|
||||
|
||||
// triangles fix: accept hash to get blockchain moving again with Pharao release (v 4.0.0.1) for first 10 blocks after release
|
||||
//printf(">>>> pindexBest->nHeight %d\n",pindexBest->nHeight);
|
||||
if (pindexBest->nHeight > CRAPCHAIN_CUTOFF_BLOCK)
|
||||
if (nCurrentHeight > CRAPCHAIN_CUTOFF_BLOCK)
|
||||
{
|
||||
if(fDebug)
|
||||
{
|
||||
@@ -349,8 +355,8 @@ bool CheckStakeKernelHash(unsigned int nBits, const CBlock& blockFrom, unsigned
|
||||
else
|
||||
{
|
||||
//accept hash
|
||||
if (pindexBest->nHeight % 10000 == 0 || pindexBest->nHeight > 2186900)
|
||||
printf(">>>> pindexBest->nHeight %d, Pharao release - hash accepted\n", pindexBest->nHeight);
|
||||
if (nCurrentHeight % 10000 == 0 || nCurrentHeight > 2186900)
|
||||
printf(">>>> pindexBest->nHeight %d, Pharao release - hash accepted\n", nCurrentHeight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+466
-75
@@ -20,6 +20,7 @@
|
||||
#include "notificationqueue.h"
|
||||
#include "addressindex.h"
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/fstream.hpp>
|
||||
@@ -70,6 +71,7 @@ uint256 nBestInvalidTrust = 0;
|
||||
|
||||
uint256 hashBestChain = 0;
|
||||
CBlockIndex* pindexBest = NULL;
|
||||
CBlockIndex* pindexFinalized = NULL; // auto-checkpoint: deepest finalized block
|
||||
bool fAddressIndex = false;
|
||||
int64_t nTimeBestReceived = 0;
|
||||
|
||||
@@ -116,8 +118,10 @@ static CCriticalSection cs_PostIbdWork;
|
||||
static bool fPostIbdWorkStarted = false;
|
||||
|
||||
static const unsigned int MAX_HEADER_SYNC_CACHE = 50000;
|
||||
static const unsigned int HEADER_DOWNLOAD_WINDOW = 128;
|
||||
static const unsigned int HEADER_DOWNLOAD_WINDOW = 512; // Increased from 128 for parallel downloads
|
||||
static const unsigned int HEADER_DOWNLOAD_PER_PEER = 64; // Max blocks to request from each peer
|
||||
static const int64_t HEADER_REQUEST_TIMEOUT_MICROS = 30 * 1000000;
|
||||
static const int64_t HEADER_REDUNDANT_REQUEST_MICROS = 10 * 1000000; // Request from another peer after 10s
|
||||
|
||||
static void ThreadPostIbdWork(void* parg)
|
||||
{
|
||||
@@ -247,7 +251,7 @@ static bool AddHeaderSyncNode(const CBlock& header, const uint256& hashHeader)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (header.GetBlockTime() > FutureDrift(GetAdjustedTime()))
|
||||
if (header.GetBlockTime() > GetTime() + 15 * 60)
|
||||
{
|
||||
printf("IBD-DIAG: header rejected (future time) hash=%s time=%u\n",
|
||||
hashHeader.ToString().substr(0,20).c_str(), header.nTime);
|
||||
@@ -405,6 +409,106 @@ static void ContinueHeaderSync(CNode* pfrom, const uint256& hashTip)
|
||||
pfrom->PushMessage("getheaders", locator, uint256(0));
|
||||
}
|
||||
|
||||
// Parallel block downloading: distribute blocks across all available peers
|
||||
static unsigned int QueueHeaderSyncBlocksParallel(unsigned int nWindow)
|
||||
{
|
||||
if (hashBestHeaderSync == 0)
|
||||
return 0;
|
||||
|
||||
const std::vector<uint256> vPath = GetHeaderSyncDownloadPath(hashBestHeaderSync);
|
||||
if (vPath.empty())
|
||||
return 0;
|
||||
|
||||
// Collect eligible peers
|
||||
std::vector<CNode*> vEligiblePeers;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
{
|
||||
if (!pnode->fClient && pnode->nVersion != 0 && !pnode->fDisconnect)
|
||||
vEligiblePeers.push_back(pnode);
|
||||
}
|
||||
}
|
||||
|
||||
if (vEligiblePeers.empty())
|
||||
return 0;
|
||||
|
||||
const int64_t nNow = GetTime() * 1000000;
|
||||
unsigned int nInFlight = CountHeaderSyncInFlight();
|
||||
unsigned int nQueued = 0;
|
||||
unsigned int nPeerIndex = 0;
|
||||
|
||||
// Sort peers by blocks delivered (descending) for speed-weighted assignment.
|
||||
// Faster peers get more blocks assigned to them, improving IBD throughput
|
||||
// on Tor networks where latency varies significantly between peers.
|
||||
std::sort(vEligiblePeers.begin(), vEligiblePeers.end(),
|
||||
[](const CNode* a, const CNode* b) {
|
||||
return a->nBlocksDelivered > b->nBlocksDelivered;
|
||||
});
|
||||
|
||||
// Build a weighted distribution: top peer gets 3 slots per round, second gets 2, rest get 1.
|
||||
std::vector<CNode*> vWeightedPeers;
|
||||
for (size_t i = 0; i < vEligiblePeers.size(); i++)
|
||||
{
|
||||
int nWeight = (i == 0) ? 3 : (i == 1) ? 2 : 1;
|
||||
for (int w = 0; w < nWeight; w++)
|
||||
vWeightedPeers.push_back(vEligiblePeers[i]);
|
||||
}
|
||||
|
||||
// Distribute blocks across peers using speed-weighted assignment
|
||||
for (std::vector<uint256>::const_iterator it = vPath.begin(); it != vPath.end(); ++it)
|
||||
{
|
||||
if (nInFlight + nQueued >= nWindow)
|
||||
break;
|
||||
|
||||
std::map<uint256, CHeaderSyncNode>::iterator mi = mapHeaderSync.find(*it);
|
||||
if (mi == mapHeaderSync.end())
|
||||
continue;
|
||||
|
||||
// Check if already requested recently
|
||||
bool fNeedsRequest = false;
|
||||
if (!mi->second.fRequested)
|
||||
{
|
||||
// Never requested - request now
|
||||
fNeedsRequest = true;
|
||||
}
|
||||
else if (nNow - mi->second.nLastRequestTime >= HEADER_REQUEST_TIMEOUT_MICROS)
|
||||
{
|
||||
// Timeout expired - retry
|
||||
fNeedsRequest = true;
|
||||
}
|
||||
else if (nNow - mi->second.nLastRequestTime >= HEADER_REDUNDANT_REQUEST_MICROS)
|
||||
{
|
||||
// Redundant request: ask another peer if original is slow
|
||||
// This creates parallel downloads for slow blocks
|
||||
fNeedsRequest = true;
|
||||
}
|
||||
|
||||
if (!fNeedsRequest)
|
||||
continue;
|
||||
|
||||
// Speed-weighted assignment across peers
|
||||
CNode* pnode = vWeightedPeers[nPeerIndex % vWeightedPeers.size()];
|
||||
pnode->AskFor(CInv(MSG_BLOCK, *it));
|
||||
|
||||
// Update tracking (only on first request, not redundant)
|
||||
if (!mi->second.fRequested || nNow - mi->second.nLastRequestTime >= HEADER_REQUEST_TIMEOUT_MICROS)
|
||||
{
|
||||
mi->second.fRequested = true;
|
||||
mi->second.nLastRequestTime = nNow;
|
||||
}
|
||||
|
||||
++nQueued;
|
||||
++nPeerIndex;
|
||||
}
|
||||
|
||||
if (nQueued > 0)
|
||||
printf("IBD-DIAG: parallel queue distributed %u blocks across %zu peers (window=%u, inflight=%u)\n",
|
||||
nQueued, vEligiblePeers.size(), nWindow, nInFlight);
|
||||
|
||||
return nQueued;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
@@ -1322,6 +1426,57 @@ uint256 WantedByOrphan(const CBlock* pblockOrphan)
|
||||
return pblockOrphan->hashPrevBlock;
|
||||
}
|
||||
|
||||
// Track orphan insertion order for smart eviction (oldest first)
|
||||
static std::deque<uint256> dequeOrphanOrder;
|
||||
|
||||
// Evict excess orphan blocks when limit is exceeded.
|
||||
// Evicts oldest orphans first (FIFO) instead of random — this ensures
|
||||
// legitimate out-of-order blocks from recent parallel downloads survive,
|
||||
// while stale orphans that will likely never connect get cleaned up.
|
||||
unsigned int LimitOrphanBlocks(unsigned int nMaxOrphans)
|
||||
{
|
||||
unsigned int nEvicted = 0;
|
||||
while (mapOrphanBlocks.size() > nMaxOrphans)
|
||||
{
|
||||
// Evict the oldest orphan (front of insertion queue)
|
||||
while (!dequeOrphanOrder.empty() && !mapOrphanBlocks.count(dequeOrphanOrder.front()))
|
||||
dequeOrphanOrder.pop_front(); // skip already-removed entries
|
||||
|
||||
if (dequeOrphanOrder.empty())
|
||||
break;
|
||||
|
||||
uint256 evictHash = dequeOrphanOrder.front();
|
||||
dequeOrphanOrder.pop_front();
|
||||
|
||||
auto it = mapOrphanBlocks.find(evictHash);
|
||||
if (it == mapOrphanBlocks.end())
|
||||
continue;
|
||||
|
||||
CBlock* pblockEvict = it->second;
|
||||
|
||||
// Remove from by-prev index
|
||||
for (auto range = mapOrphanBlocksByPrev.equal_range(pblockEvict->hashPrevBlock);
|
||||
range.first != range.second; ++range.first)
|
||||
{
|
||||
if (range.first->second == pblockEvict) {
|
||||
mapOrphanBlocksByPrev.erase(range.first);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setStakeSeenOrphan.erase(pblockEvict->GetProofOfStake());
|
||||
delete pblockEvict;
|
||||
mapOrphanBlocks.erase(evictHash);
|
||||
nEvicted++;
|
||||
}
|
||||
|
||||
if (nEvicted > 0)
|
||||
printf("LimitOrphanBlocks: evicted %u oldest orphan(s), %u remain\n",
|
||||
nEvicted, (unsigned int)mapOrphanBlocks.size());
|
||||
|
||||
return nEvicted;
|
||||
}
|
||||
|
||||
// miner's coin base reward
|
||||
int64_t GetProofOfWorkReward(int64_t nFees)
|
||||
{
|
||||
@@ -1415,9 +1570,13 @@ static unsigned int GetNextTargetRequired_(const CBlockIndex* pindexLast, bool f
|
||||
return bnTargetLimit.GetCompact(); // genesis block
|
||||
|
||||
const CBlockIndex* pindexPrev = GetLastBlockIndex(pindexLast, fProofOfStake);
|
||||
if (pindexPrev == NULL)
|
||||
return bnTargetLimit.GetCompact(); // no previous block of this type
|
||||
if (pindexPrev->pprev == NULL)
|
||||
return bnTargetLimit.GetCompact(); // first block
|
||||
const CBlockIndex* pindexPrevPrev = GetLastBlockIndex(pindexPrev->pprev, fProofOfStake);
|
||||
if (pindexPrevPrev == NULL)
|
||||
return bnTargetLimit.GetCompact(); // no second previous block of this type
|
||||
if (pindexPrevPrev->pprev == NULL)
|
||||
return bnTargetLimit.GetCompact(); // second block
|
||||
|
||||
@@ -1504,8 +1663,12 @@ void static InvalidChainFound(CBlockIndex* pindexNew)
|
||||
uiInterface.NotifyBlocksChanged();
|
||||
}
|
||||
|
||||
uint256 nBestInvalidBlockTrust = pindexNew->nChainTrust - pindexNew->pprev->nChainTrust;
|
||||
uint256 nBestBlockTrust = pindexBest->nHeight != 0 ? (pindexBest->nChainTrust - pindexBest->pprev->nChainTrust) : pindexBest->nChainTrust;
|
||||
uint256 nBestInvalidBlockTrust = pindexNew->pprev
|
||||
? pindexNew->nChainTrust - pindexNew->pprev->nChainTrust
|
||||
: pindexNew->nChainTrust;
|
||||
uint256 nBestBlockTrust = (pindexBest && pindexBest->nHeight != 0 && pindexBest->pprev)
|
||||
? (pindexBest->nChainTrust - pindexBest->pprev->nChainTrust)
|
||||
: (pindexBest ? pindexBest->nChainTrust : uint256(0));
|
||||
|
||||
printf("InvalidChainFound: invalid block=%s height=%d trust=%s blocktrust=%" PRId64 " date=%s\n",
|
||||
pindexNew->GetBlockHash().ToString().substr(0,20).c_str(), pindexNew->nHeight,
|
||||
@@ -1513,9 +1676,9 @@ void static InvalidChainFound(CBlockIndex* pindexNew)
|
||||
DateTimeStrFormat("%x %H:%M:%S", pindexNew->GetBlockTime()).c_str());
|
||||
printf("InvalidChainFound: current best=%s height=%d trust=%s blocktrust=%" PRId64 " date=%s\n",
|
||||
hashBestChain.ToString().substr(0,20).c_str(), nBestHeight,
|
||||
CBigNum(pindexBest->nChainTrust).ToString().c_str(),
|
||||
pindexBest ? CBigNum(pindexBest->nChainTrust).ToString().c_str() : "0",
|
||||
nBestBlockTrust.Get64(),
|
||||
DateTimeStrFormat("%x %H:%M:%S", pindexBest->GetBlockTime()).c_str());
|
||||
pindexBest ? DateTimeStrFormat("%x %H:%M:%S", pindexBest->GetBlockTime()).c_str() : "unknown");
|
||||
}
|
||||
|
||||
|
||||
@@ -2034,6 +2197,31 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
|
||||
// Track pending UTXOs so later txs in the same block can find inputs.
|
||||
if (fAssumeValid)
|
||||
{
|
||||
// Track money supply from input/output values
|
||||
int64_t nTxValueOut = tx.GetValueOut();
|
||||
nValueOut += nTxValueOut;
|
||||
|
||||
if (!tx.IsCoinBase())
|
||||
{
|
||||
int64_t nTxValueIn = 0;
|
||||
for (const CTxIn& txin : tx.vin)
|
||||
{
|
||||
// Check in-block pending UTXOs first, then UTXO database
|
||||
MapPrevTx::iterator it = mapPendingUtxos.find(txin.prevout);
|
||||
if (it != mapPendingUtxos.end())
|
||||
nTxValueIn += it->second.nValue;
|
||||
else
|
||||
{
|
||||
CUtxoEntry utxo;
|
||||
if (txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, utxo))
|
||||
nTxValueIn += utxo.nValue;
|
||||
}
|
||||
}
|
||||
nValueIn += nTxValueIn;
|
||||
if (!tx.IsCoinStake())
|
||||
nFees += nTxValueIn - nTxValueOut;
|
||||
}
|
||||
|
||||
// Add outputs to pending UTXOs
|
||||
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
||||
{
|
||||
@@ -2295,7 +2483,15 @@ bool CBlock::ConnectBlock(CTxDB& txdb, CBlockIndex* pindex, bool fJustCheck)
|
||||
|
||||
bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
{
|
||||
printf("REORGANIZE\n");
|
||||
printf("REORGANIZE: Switching chains\n");
|
||||
printf(" Old tip: %s height %d trust %s\n",
|
||||
pindexBest->GetBlockHash().ToString().substr(0,20).c_str(),
|
||||
pindexBest->nHeight,
|
||||
CBigNum(pindexBest->nChainTrust).ToString().c_str());
|
||||
printf(" New tip: %s height %d trust %s\n",
|
||||
pindexNew->GetBlockHash().ToString().substr(0,20).c_str(),
|
||||
pindexNew->nHeight,
|
||||
CBigNum(pindexNew->nChainTrust).ToString().c_str());
|
||||
|
||||
// Find the fork
|
||||
CBlockIndex* pfork = pindexBest;
|
||||
@@ -2311,6 +2507,27 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
return error("Reorganize() : pfork->pprev is null");
|
||||
}
|
||||
|
||||
// Finality: reject reorgs that go below the auto-checkpoint or
|
||||
// exceed MAX_REORG_DEPTH blocks. During IBD we allow deep reorgs
|
||||
// since we haven't settled on a tip yet.
|
||||
if (!IsInitialBlockDownload())
|
||||
{
|
||||
if (pindexFinalized && pfork->nHeight < pindexFinalized->nHeight)
|
||||
{
|
||||
printf("REORGANIZE: REJECTED — fork at %d is below finalized block %d\n",
|
||||
pfork->nHeight, pindexFinalized->nHeight);
|
||||
return error("Reorganize() : fork point %d below auto-checkpoint %d",
|
||||
pfork->nHeight, pindexFinalized->nHeight);
|
||||
}
|
||||
unsigned int nDisconnectDepth = pindexBest->nHeight - pfork->nHeight;
|
||||
if (nDisconnectDepth > MAX_REORG_DEPTH)
|
||||
{
|
||||
printf("REORGANIZE: REJECTED — depth %u exceeds finality limit %u (fork at %d)\n",
|
||||
nDisconnectDepth, MAX_REORG_DEPTH, pfork->nHeight);
|
||||
return error("Reorganize() : reorg depth %u exceeds maximum %u", nDisconnectDepth, MAX_REORG_DEPTH);
|
||||
}
|
||||
}
|
||||
|
||||
// List of what to disconnect
|
||||
vector<CBlockIndex*> vDisconnect;
|
||||
for (CBlockIndex* pindex = pindexBest; pindex != pfork; pindex = pindex->pprev)
|
||||
@@ -2322,8 +2539,17 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
vConnect.push_back(pindex);
|
||||
reverse(vConnect.begin(), vConnect.end());
|
||||
|
||||
printf("REORGANIZE: Disconnect %" PRIszu " blocks; %s..%s\n", vDisconnect.size(), pfork->GetBlockHash().ToString().substr(0,20).c_str(), pindexBest->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
printf("REORGANIZE: Connect %" PRIszu " blocks; %s..%s\n", vConnect.size(), pfork->GetBlockHash().ToString().substr(0,20).c_str(), pindexNew->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
printf("REORGANIZE: Fork point at height %d: %s\n",
|
||||
pfork->nHeight,
|
||||
pfork->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
printf("REORGANIZE: Disconnect %" PRIszu " blocks (heights %d..%d)\n",
|
||||
vDisconnect.size(),
|
||||
pfork->nHeight + 1,
|
||||
pindexBest->nHeight);
|
||||
printf("REORGANIZE: Connect %" PRIszu " blocks (heights %d..%d)\n",
|
||||
vConnect.size(),
|
||||
pfork->nHeight + 1,
|
||||
pindexNew->nHeight);
|
||||
|
||||
// Disconnect shorter branch
|
||||
vector<CTransaction> vResurrect;
|
||||
@@ -2341,12 +2567,6 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
vResurrect.push_back(tx);
|
||||
}
|
||||
|
||||
// Remove disconnected PoS blocks from setStakeSeen so they don't
|
||||
// block acceptance of valid blocks on the winning chain.
|
||||
for (CBlockIndex* pindex : vDisconnect)
|
||||
if (pindex->IsProofOfStake())
|
||||
setStakeSeen.erase(make_pair(pindex->prevoutStake, pindex->nStakeTime));
|
||||
|
||||
// Connect longer branch
|
||||
vector<CTransaction> vDelete;
|
||||
for (unsigned int i = 0; i < vConnect.size(); i++)
|
||||
@@ -2374,19 +2594,37 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
if (!txdb.TxnCommit())
|
||||
return error("Reorganize() : TxnCommit failed");
|
||||
|
||||
// Disconnect shorter branch
|
||||
// ======================================================================
|
||||
// CRITICAL: All operations below this point must be in-memory only and
|
||||
// should never fail. The DB transaction is committed, so we cannot abort.
|
||||
// ======================================================================
|
||||
|
||||
// Disconnect shorter branch (in-memory only)
|
||||
for (CBlockIndex* pindex : vDisconnect)
|
||||
if (pindex->pprev)
|
||||
pindex->pprev->pnext = NULL;
|
||||
|
||||
// Connect longer branch
|
||||
// Connect longer branch (in-memory only)
|
||||
for (CBlockIndex* pindex : vConnect)
|
||||
if (pindex->pprev)
|
||||
pindex->pprev->pnext = pindex;
|
||||
|
||||
// Remove disconnected PoS blocks from setStakeSeen so they don't
|
||||
// block acceptance of valid blocks on the winning chain.
|
||||
// This MUST happen after commit to maintain consistency.
|
||||
for (CBlockIndex* pindex : vDisconnect)
|
||||
if (pindex->IsProofOfStake())
|
||||
setStakeSeen.erase(make_pair(pindex->prevoutStake, pindex->nStakeTime));
|
||||
|
||||
// Resurrect memory transactions that were in the disconnected branch
|
||||
unsigned int nResurrected = 0;
|
||||
for (CTransaction& tx : vResurrect)
|
||||
tx.AcceptToMemoryPool(txdb, false);
|
||||
{
|
||||
if (tx.AcceptToMemoryPool(txdb, false))
|
||||
nResurrected++;
|
||||
}
|
||||
if (nResurrected > 0)
|
||||
printf("REORGANIZE: resurrected %u transactions to mempool\n", nResurrected);
|
||||
|
||||
// Delete redundant memory transactions that are in the connected branch
|
||||
for (CTransaction& tx : vDelete) {
|
||||
@@ -2394,7 +2632,8 @@ bool static Reorganize(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
mempool.removeConflicts(tx);
|
||||
}
|
||||
|
||||
printf("REORGANIZE: done\n");
|
||||
printf("REORGANIZE: done (fork at height %d, %zu disconnected, %zu connected)\n",
|
||||
pfork->nHeight, vDisconnect.size(), vConnect.size());
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -2511,7 +2750,24 @@ bool CBlock::SetBestChain(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
nTimeBestReceived = GetTime();
|
||||
nTransactionsUpdated++;
|
||||
|
||||
uint256 nBestBlockTrust = pindexBest->nHeight != 0 ? (pindexBest->nChainTrust - pindexBest->pprev->nChainTrust) : pindexBest->nChainTrust;
|
||||
// Auto-checkpoint: finalize the block at depth MAX_REORG_DEPTH.
|
||||
// Only set when fully synced (not IBD) so we don't lock in a
|
||||
// potentially wrong chain during initial sync.
|
||||
if (!IsInitialBlockDownload() && nBestHeight > (int)MAX_REORG_DEPTH)
|
||||
{
|
||||
CBlockIndex* pcandidate = pindexBest;
|
||||
for (int i = 0; i < (int)MAX_REORG_DEPTH && pcandidate; i++)
|
||||
pcandidate = pcandidate->pprev;
|
||||
if (pcandidate && pcandidate != pindexFinalized)
|
||||
{
|
||||
pindexFinalized = pcandidate;
|
||||
printf("AUTO-CHECKPOINT: block %d (%s) is now finalized\n",
|
||||
pindexFinalized->nHeight,
|
||||
pindexFinalized->GetBlockHash().ToString().substr(0,20).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
uint256 nBestBlockTrust = (pindexBest->nHeight != 0 && pindexBest->pprev) ? (pindexBest->nChainTrust - pindexBest->pprev->nChainTrust) : pindexBest->nChainTrust;
|
||||
|
||||
// Log every 5000 blocks during sync, every block once caught up
|
||||
if (nBestHeight % 5000 == 0 || !IsInitialBlockDownload())
|
||||
@@ -2584,6 +2840,9 @@ bool CBlock::SetBestChain(CTxDB& txdb, CBlockIndex* pindexNew)
|
||||
{
|
||||
printf("*** Initial block download complete at height %d ***\n", nBestHeight);
|
||||
|
||||
// Trim orphan blocks to normal limit now that IBD is done
|
||||
LimitOrphanBlocks(MAX_ORPHAN_BLOCKS);
|
||||
|
||||
// Update wallet best chain locator now that IBD is done
|
||||
const CBlockLocator locator(pindexBest);
|
||||
::SetBestChain(locator);
|
||||
@@ -2773,22 +3032,62 @@ bool CBlock::AddToBlockIndex(unsigned int nFile, unsigned int nBlockPos, const u
|
||||
// writes to the same transaction, cutting the per-block commit count in half.
|
||||
//
|
||||
// Chain selection rules:
|
||||
// 1. Strictly greater trust always wins (normal case).
|
||||
// 2. Equal trust with shallow fork (parent in main chain): use
|
||||
// deterministic hash tiebreaker — lower tip hash wins. This
|
||||
// resolves single-block PoS races where two stakers find valid
|
||||
// blocks at the same height with identical difficulty.
|
||||
// 3. Equal trust with deep fork (parent NOT in main chain): do NOT
|
||||
// reorg. Without this rule, Tor-latency-induced multi-block
|
||||
// forks cause nodes to oscillate between competing chains of
|
||||
// similar trust, preventing convergence.
|
||||
// 1. Linear extension: always accept (no reorg needed).
|
||||
// 2. Side-chain reorg: require 10% more cumulative trust than current
|
||||
// best chain. This gives a strong "first-seen" advantage and
|
||||
// prevents endless fork-thrashing on a small network.
|
||||
// During IBD the delta is waived so the heaviest chain wins.
|
||||
// 3. Equal trust: deterministic tiebreaker with timestamp preference.
|
||||
// First prefer the block with the earlier timestamp (lower nTime),
|
||||
// then break remaining ties by lower hash. This converges faster
|
||||
// because the earlier block is more likely to have propagated first.
|
||||
// Rate-limited to one equal-trust reorg per 2 minutes.
|
||||
bool fNewBest = false;
|
||||
static int64_t nLastEqualTrustReorg = 0;
|
||||
if (pindexNew->nChainTrust > nBestChainTrust)
|
||||
fNewBest = true;
|
||||
{
|
||||
bool fLinearExtension = (pindexNew->pprev == pindexBest);
|
||||
if (fLinearExtension || IsInitialBlockDownload())
|
||||
{
|
||||
fNewBest = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Side-chain reorg: require 10% more trust.
|
||||
// new * 10 > best * 11 ⟺ new > best * 1.1
|
||||
CBigNum bnNewTrust(pindexNew->nChainTrust);
|
||||
CBigNum bnBestTrust(nBestChainTrust);
|
||||
if (bnNewTrust * 10 > bnBestTrust * 11)
|
||||
{
|
||||
fNewBest = true;
|
||||
printf("CHAIN: Side-chain reorg accepted (trust delta sufficient)\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("CHAIN: Side-chain at height %d REJECTED — insufficient trust delta "
|
||||
"(need >10%% more, have %s vs %s)\n",
|
||||
pindexNew->nHeight,
|
||||
bnNewTrust.ToString().c_str(),
|
||||
bnBestTrust.ToString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (pindexNew->nChainTrust == nBestChainTrust && pindexBest &&
|
||||
pindexNew->pprev && pindexNew->pprev->IsInMainChain() &&
|
||||
pindexNew->GetBlockHash() < pindexBest->GetBlockHash())
|
||||
fNewBest = true;
|
||||
GetTime() - nLastEqualTrustReorg > 2 * 60)
|
||||
{
|
||||
// Prefer earlier timestamp, then lower hash as final tiebreaker
|
||||
bool fPreferNew = false;
|
||||
if (pindexNew->nTime < pindexBest->nTime)
|
||||
fPreferNew = true;
|
||||
else if (pindexNew->nTime == pindexBest->nTime)
|
||||
fPreferNew = (pindexNew->GetBlockHash() < pindexBest->GetBlockHash());
|
||||
|
||||
if (fPreferNew)
|
||||
{
|
||||
fNewBest = true;
|
||||
nLastEqualTrustReorg = GetTime();
|
||||
}
|
||||
}
|
||||
|
||||
if (fNewBest)
|
||||
{
|
||||
@@ -2829,8 +3128,14 @@ bool CBlock::CheckBlock(bool fCheckPOW, bool fCheckMerkleRoot, bool fCheckSig) c
|
||||
if (fCheckPOW && IsProofOfWork() && !CheckProofOfWork(GetHash(), nBits))
|
||||
return DoS(50, error("CheckBlock() : proof of work failed"));
|
||||
|
||||
// Check timestamp
|
||||
if (GetBlockTime() > FutureDrift(GetAdjustedTime()))
|
||||
// Check timestamp: reject blocks obviously too far in the future.
|
||||
// Use a generous 15-minute window from the raw system clock.
|
||||
// GetAdjustedTime() is NOT used here because it incorporates peer-reported
|
||||
// time offsets that differ between Tor nodes, causing nondeterministic
|
||||
// block rejection — the primary cause of persistent chain splits.
|
||||
// The deterministic timestamp checks in AcceptBlock (median-time-past,
|
||||
// prev-block-time with 3-min drift) still enforce tight rules.
|
||||
if (GetBlockTime() > GetTime() + 15 * 60)
|
||||
return error("CheckBlock() : block timestamp too far in the future");
|
||||
|
||||
// First transaction must be coinbase, the rest must not be
|
||||
@@ -2990,14 +3295,20 @@ bool CBlock::AcceptBlock()
|
||||
if (!AddToBlockIndex(nFile, nBlockPos, hashProofOfStake))
|
||||
return error("AcceptBlock() : AddToBlockIndex failed");
|
||||
|
||||
// Relay inventory, but don't relay old inventory during initial block download
|
||||
int nBlockEstimate = Checkpoints::GetTotalBlocksEstimate();
|
||||
// Push new tip block directly to peers that are near our tip.
|
||||
// On a small Tor-only network the inv->getdata->block round-trip adds
|
||||
// 1-2 seconds of latency per hop. Pushing immediately cuts propagation
|
||||
// to a single hop. Only push to peers within 10 blocks of our tip —
|
||||
// pushing full blocks to syncing peers wastes bandwidth and slows IBD.
|
||||
if (hashBestChain == hash)
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
if (nBestHeight > (pnode->nStartingHeight != -1 ? pnode->nStartingHeight - 2000 : nBlockEstimate))
|
||||
pnode->PushInventory(CInv(MSG_BLOCK, hash));
|
||||
if (pnode->nStartingHeight >= nBestHeight - 10)
|
||||
{
|
||||
pnode->PushMessage("block", *this);
|
||||
pnode->AddInventoryKnown(CInv(MSG_BLOCK, hash));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -3103,37 +3414,16 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
||||
}
|
||||
mapOrphanBlocks.insert(make_pair(hash, pblock2));
|
||||
mapOrphanBlocksByPrev.insert(make_pair(pblock2->hashPrevBlock, pblock2));
|
||||
dequeOrphanOrder.push_back(hash); // track insertion order for FIFO eviction
|
||||
|
||||
// Limit orphan blocks to prevent memory exhaustion.
|
||||
// Allow more orphans during IBD so out-of-order blocks from parallel
|
||||
// downloads don't get evicted and re-requested.
|
||||
unsigned int nMaxOrphans = IsInitialBlockDownload() ? MAX_ORPHAN_BLOCKS_IBD : MAX_ORPHAN_BLOCKS;
|
||||
if (mapOrphanBlocks.size() > nMaxOrphans)
|
||||
{
|
||||
// Evict a random orphan
|
||||
uint256 randomhash = GetRandHash();
|
||||
auto it = mapOrphanBlocks.lower_bound(randomhash);
|
||||
if (it == mapOrphanBlocks.end())
|
||||
it = mapOrphanBlocks.begin();
|
||||
CBlock* pblockEvict = it->second;
|
||||
uint256 evictHash = it->first;
|
||||
// Remove from by-prev index
|
||||
for (auto range = mapOrphanBlocksByPrev.equal_range(pblockEvict->hashPrevBlock);
|
||||
range.first != range.second; ++range.first)
|
||||
{
|
||||
if (range.first->second == pblockEvict) {
|
||||
mapOrphanBlocksByPrev.erase(range.first);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setStakeSeenOrphan.erase(pblockEvict->GetProofOfStake());
|
||||
delete pblockEvict;
|
||||
mapOrphanBlocks.erase(evictHash);
|
||||
printf("ProcessBlock: orphan eviction, %u orphans remain\n", (unsigned int)mapOrphanBlocks.size());
|
||||
}
|
||||
LimitOrphanBlocks(nMaxOrphans);
|
||||
|
||||
// Ask this guy to fill in what we're missing
|
||||
if (pfrom)
|
||||
if (pfrom && pindexBest)
|
||||
{
|
||||
pfrom->PushGetBlocks(pindexBest, GetOrphanRoot(pblock2));
|
||||
// triangles: getblocks may not obtain the ancestor block rejected
|
||||
@@ -3176,9 +3466,10 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
||||
if (nBestHeight % 5000 == 0 || !IsInitialBlockDownload())
|
||||
printf("ProcessBlock: ACCEPTED block %d\n", nBestHeight);
|
||||
|
||||
if (pfrom && hashBestHeaderSync != 0)
|
||||
if (hashBestHeaderSync != 0)
|
||||
{
|
||||
const unsigned int nQueued = QueueHeaderSyncBlocks(pfrom, HEADER_DOWNLOAD_WINDOW);
|
||||
// Use parallel queue to distribute across all peers
|
||||
const unsigned int nQueued = QueueHeaderSyncBlocksParallel(HEADER_DOWNLOAD_WINDOW);
|
||||
if (nQueued > 0)
|
||||
printf("IBD-DIAG: queued %u more blocks from header planner after accepting %s\n",
|
||||
nQueued, hash.ToString().substr(0,20).c_str());
|
||||
@@ -3729,10 +4020,6 @@ bool FastImportBlockFile()
|
||||
}
|
||||
pindexNew->nStakeModifierChecksum = GetStakeModifierChecksum(pindexNew);
|
||||
|
||||
// Money supply tracking
|
||||
pindexNew->nMint = 0;
|
||||
pindexNew->nMoneySupply = (pindexNew->pprev ? pindexNew->pprev->nMoneySupply : 0);
|
||||
|
||||
// PoS stake seen set
|
||||
if (pindexNew->IsProofOfStake())
|
||||
setStakeSeen.insert(make_pair(pindexNew->prevoutStake, pindexNew->nStakeTime));
|
||||
@@ -3745,10 +4032,10 @@ bool FastImportBlockFile()
|
||||
if (pindexNew->pprev)
|
||||
pindexNew->pprev->pnext = pindexNew;
|
||||
|
||||
// Write block index to batch
|
||||
txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew));
|
||||
|
||||
// Build tx index + UTXO entries
|
||||
// Build tx index + UTXO entries, tracking money supply
|
||||
int64_t nBlockValueIn = 0;
|
||||
int64_t nBlockValueOut = 0;
|
||||
int64_t nFees = 0;
|
||||
unsigned int nTxPos = nBlockPos + ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION)
|
||||
- (2 * GetSizeOfCompactSize(0)) + GetSizeOfCompactSize(block.vtx.size());
|
||||
for (unsigned int i = 0; i < block.vtx.size(); i++)
|
||||
@@ -3759,11 +4046,23 @@ bool FastImportBlockFile()
|
||||
txdb.UpdateTxIndex(hashTx, CTxIndex(posThisTx, tx.vout.size()));
|
||||
nTxPos += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
|
||||
|
||||
// UTXO entries
|
||||
int64_t nTxValueOut = tx.GetValueOut();
|
||||
nBlockValueOut += nTxValueOut;
|
||||
|
||||
// UTXO entries — read input values before erasing for money supply
|
||||
if (!tx.IsCoinBase())
|
||||
{
|
||||
int64_t nTxValueIn = 0;
|
||||
for (const CTxIn& txin : tx.vin)
|
||||
{
|
||||
CUtxoEntry utxo;
|
||||
if (txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, utxo))
|
||||
nTxValueIn += utxo.nValue;
|
||||
txdb.EraseUtxo(txin.prevout.hash, txin.prevout.n);
|
||||
}
|
||||
nBlockValueIn += nTxValueIn;
|
||||
if (!tx.IsCoinStake())
|
||||
nFees += nTxValueIn - nTxValueOut;
|
||||
}
|
||||
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
||||
{
|
||||
@@ -3781,6 +4080,13 @@ bool FastImportBlockFile()
|
||||
}
|
||||
}
|
||||
|
||||
// Money supply tracking — matches ConnectBlock formula
|
||||
pindexNew->nMint = nBlockValueOut - nBlockValueIn + nFees;
|
||||
pindexNew->nMoneySupply = (pindexNew->pprev ? pindexNew->pprev->nMoneySupply : 0) + nBlockValueOut - nBlockValueIn;
|
||||
|
||||
// Write block index to batch
|
||||
txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew));
|
||||
|
||||
// Update best chain
|
||||
if (pindexNew->nChainTrust > nBestChainTrust)
|
||||
{
|
||||
@@ -4343,6 +4649,30 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
||||
// Find the last block the caller has in the main chain
|
||||
CBlockIndex* pindex = locator.GetBlockIndex();
|
||||
|
||||
// Detect incompatible fork: peer sent a locator with entries but
|
||||
// GetBlockIndex() fell through to genesis (no locator hash matched
|
||||
// our main chain). If the peer's tip isn't our genesis,
|
||||
// they're on a completely different fork.
|
||||
if (!locator.IsNull() && pindex == pindexGenesisBlock &&
|
||||
pindexGenesisBlock && locator.GetTipHash() != pindexGenesisBlock->GetBlockHash())
|
||||
{
|
||||
pfrom->nIncompatibleGetblocks++;
|
||||
if (pfrom->nIncompatibleGetblocks >= 3)
|
||||
{
|
||||
printf("WARNING: peer %s sent %d getblocks with no common blocks — disconnecting (incompatible fork)\n",
|
||||
pfrom->addr.ToString().c_str(), pfrom->nIncompatibleGetblocks);
|
||||
pfrom->Misbehaving(100);
|
||||
return true;
|
||||
}
|
||||
printf("WARNING: peer %s getblocks locator has no common blocks (%d/3 before ban)\n",
|
||||
pfrom->addr.ToString().c_str(), pfrom->nIncompatibleGetblocks);
|
||||
}
|
||||
else if (pindex && pindex != pindexGenesisBlock)
|
||||
{
|
||||
// Peer matched a non-genesis block — they share our chain
|
||||
pfrom->nIncompatibleGetblocks = 0;
|
||||
}
|
||||
|
||||
// Send the rest of the chain
|
||||
if (pindex)
|
||||
pindex = pindex->pnext;
|
||||
@@ -4472,7 +4802,7 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
||||
|
||||
int nRequested = 0;
|
||||
if (hashBestHeaderSync != 0)
|
||||
nRequested = QueueHeaderSyncBlocks(pfrom, HEADER_DOWNLOAD_WINDOW);
|
||||
nRequested = QueueHeaderSyncBlocksParallel(HEADER_DOWNLOAD_WINDOW);
|
||||
|
||||
if (nNewHeaders > 0 || nRequested > 0)
|
||||
printf("IBD-DIAG: accepted %d new headers, queued %d blocks from %zu headers (peer=%s bestHeader=%s)\n",
|
||||
@@ -4580,6 +4910,9 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
||||
CInv inv(MSG_BLOCK, hashBlock);
|
||||
pfrom->AddInventoryKnown(inv);
|
||||
|
||||
// Track block delivery for peer latency scoring
|
||||
pfrom->nBlocksDelivered++;
|
||||
|
||||
if (ProcessBlock(pfrom, &block))
|
||||
{
|
||||
mapAlreadyAskedFor.erase(inv);
|
||||
@@ -5114,6 +5447,64 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
|
||||
pto->PushMessage("inv", vInv);
|
||||
|
||||
|
||||
//
|
||||
// Periodic chain-tip sync: every 45 seconds, ask each peer if they
|
||||
// have blocks we don't. On a small Tor-only network, transient
|
||||
// partitions can cause forks that persist silently — this ensures
|
||||
// nodes discover the longer chain even without explicit announcement.
|
||||
//
|
||||
if (!IsInitialBlockDownload() && !pto->fClient && pindexBest &&
|
||||
GetTime() - pto->nLastTipCheck > 45)
|
||||
{
|
||||
pto->nLastTipCheck = GetTime();
|
||||
pto->pindexLastGetBlocksBegin = NULL; // reset dedup to force request
|
||||
pto->PushGetBlocks(pindexBest, uint256(0));
|
||||
}
|
||||
|
||||
//
|
||||
// Slow-peer eviction: every 5 minutes during sync, identify the
|
||||
// outbound peer with the fewest blocks delivered and disconnect it
|
||||
// to free the slot for a potentially faster peer. This is critical
|
||||
// on Tor networks with high latency variance.
|
||||
//
|
||||
if (nBestHeight < GetNumBlocksOfPeers() && !pto->fClient && !pto->fInbound)
|
||||
{
|
||||
static int64_t nLastEvictionCheck = 0;
|
||||
if (GetTime() - nLastEvictionCheck > 5 * 60)
|
||||
{
|
||||
nLastEvictionCheck = GetTime();
|
||||
CNode* pWorst = NULL;
|
||||
int nWorstBlocks = INT_MAX;
|
||||
int nOutbound = 0;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
{
|
||||
if (pnode->fInbound || pnode->fDisconnect || pnode->fClient)
|
||||
continue;
|
||||
nOutbound++;
|
||||
// Only consider peers connected for at least 3 minutes
|
||||
if (GetTime() - pnode->nTimeConnected < 3 * 60)
|
||||
continue;
|
||||
if (pnode->nBlocksDelivered < nWorstBlocks)
|
||||
{
|
||||
nWorstBlocks = pnode->nBlocksDelivered;
|
||||
pWorst = pnode;
|
||||
}
|
||||
}
|
||||
// Only evict if we have at least 3 outbound peers and the worst
|
||||
// peer has delivered significantly fewer blocks than average
|
||||
if (pWorst && nOutbound >= 3 && nWorstBlocks == 0)
|
||||
{
|
||||
printf("PEER-EVICT: disconnecting slow peer %s (0 blocks delivered in %ds)\n",
|
||||
pWorst->addr.ToString().c_str(),
|
||||
(int)(GetTime() - pWorst->nTimeConnected));
|
||||
pWorst->fDisconnect = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Stall detection: if we're still catching up and no new blocks for
|
||||
// a while, re-request. Active during IBD (5s timeout) and also
|
||||
|
||||
+12
-3
@@ -37,8 +37,9 @@ static const unsigned int MAX_BLOCK_SIZE = 1000000;
|
||||
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 = 2000;
|
||||
static const unsigned int MAX_ORPHAN_BLOCKS_IBD = 4000;
|
||||
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 +60,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 +84,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 +138,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 +1562,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();
|
||||
|
||||
+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);
|
||||
|
||||
+87
-22
@@ -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);
|
||||
|
||||
|
||||
@@ -1381,7 +1381,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,16 +1394,78 @@ 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: seeding complete\n");
|
||||
printf("ThreadOnionSeed: initial seeding complete\n");
|
||||
|
||||
// Periodic re-seeding for isolated or under-connected nodes.
|
||||
// Check every 2 minutes, re-seed when < 2 outbound peers.
|
||||
// First re-seed after 5 min cooldown, then 15 min for subsequent.
|
||||
int64_t nLastReseed = GetTime();
|
||||
bool bFirstReseed = true;
|
||||
while (!fShutdown) {
|
||||
for (int i = 0; i < 120 && !fShutdown; i++) // sleep 2 minutes
|
||||
MilliSleep(1000);
|
||||
|
||||
if (fShutdown) break;
|
||||
|
||||
int nOutbound = 0;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
if (!pnode->fInbound)
|
||||
nOutbound++;
|
||||
}
|
||||
|
||||
int64_t nCooldown = bFirstReseed ? 5 * 60 : 15 * 60;
|
||||
if (nOutbound < 2 && GetTime() - nLastReseed > nCooldown) {
|
||||
printf("ThreadOnionSeed: low outbound peers (%d), re-seeding...\n", nOutbound);
|
||||
ThreadHTTPSeedFetch2(NULL);
|
||||
|
||||
// Also 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1461,7 +1523,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";
|
||||
@@ -1490,7 +1552,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
|
||||
@@ -1498,7 +1560,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
|
||||
@@ -1510,7 +1572,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)
|
||||
@@ -1527,7 +1589,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());
|
||||
@@ -1550,7 +1612,7 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
SSL_free(ssl);
|
||||
SSL_CTX_free(ctx);
|
||||
closesocket(hSocket);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
nSent += nBytes;
|
||||
}
|
||||
@@ -1575,21 +1637,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);
|
||||
@@ -1601,7 +1663,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'))
|
||||
@@ -1622,12 +1684,8 @@ void ThreadHTTPSeedFetch2(void* parg)
|
||||
port = atoi(addrStr.substr(onionPos + 7).c_str());
|
||||
addrStr = addrStr.substr(0, onionPos + 6); // keep ".onion"
|
||||
} else if (addrStr.find(".onion") == std::string::npos) {
|
||||
// Clearnet address - find last colon for port
|
||||
size_t colonPos = addrStr.rfind(':');
|
||||
if (colonPos != std::string::npos) {
|
||||
port = atoi(addrStr.substr(colonPos + 1).c_str());
|
||||
addrStr = addrStr.substr(0, colonPos);
|
||||
}
|
||||
// Tor-native: skip non-.onion addresses
|
||||
continue;
|
||||
}
|
||||
|
||||
if (port <= 0 || port > 65535)
|
||||
@@ -1646,17 +1704,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -272,6 +272,10 @@ 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 nIncompatibleGetblocks; // count of getblocks with no common blocks (fork detection)
|
||||
|
||||
// flood relay
|
||||
std::vector<CAddress> vAddrToSend;
|
||||
@@ -319,6 +323,10 @@ public:
|
||||
pindexLastGetHeadersBegin = 0;
|
||||
hashLastGetHeadersEnd = 0;
|
||||
nStartingHeight = -1;
|
||||
nLastTipCheck = 0;
|
||||
nAvgBlockLatencyUs = 0;
|
||||
nBlocksDelivered = 0;
|
||||
nIncompatibleGetblocks = 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"},
|
||||
|
||||
@@ -292,12 +292,6 @@ bool IntroDialog::pickDataDirectory()
|
||||
};
|
||||
|
||||
bool success = Bootstrap::DownloadBootstrap(host, dataDirPath, progressFn, strError);
|
||||
if (!success) {
|
||||
host = Bootstrap::FALLBACK_HOST;
|
||||
progress.setValue(0);
|
||||
success = Bootstrap::DownloadBootstrap(host, dataDirPath, progressFn, strError);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
QMessageBox::warning(0, "Triangles",
|
||||
QString("Could not download blockchain snapshot:\n%1\n\n"
|
||||
|
||||
@@ -389,7 +389,7 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(const QList<SendCoinsRecipie
|
||||
for (it = mapStealthNarr.begin(); it != mapStealthNarr.end(); ++it)
|
||||
{
|
||||
char key[64];
|
||||
if (snprintf(key, sizeof(key), "n_%u") < 1)
|
||||
if (snprintf(key, sizeof(key), "n_%u", it->first) < 1)
|
||||
{
|
||||
printf("CreateStealthTransaction(): Error creating narration key.");
|
||||
continue;
|
||||
|
||||
@@ -28,6 +28,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 +363,49 @@ Value gettxoutsetinfo(const Array& params, bool fHelp)
|
||||
return obj;
|
||||
}
|
||||
|
||||
Value recalculatesupply(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 0)
|
||||
throw runtime_error(
|
||||
"recalculatesupply\n"
|
||||
"Recalculates the money supply by summing all unspent transaction outputs.\n"
|
||||
"Updates the stored money supply value at the chain tip and persists it to disk.\n"
|
||||
"Returns the old and new supply values for comparison.\n"
|
||||
"\nWARNING: This modifies blockchain index state. Only use if money supply is incorrect.");
|
||||
|
||||
if (!pindexBest)
|
||||
throw runtime_error("recalculatesupply: no best block");
|
||||
|
||||
int nUtxoCount = 0;
|
||||
CTxDB txdb;
|
||||
int64_t nCalculatedSupply = txdb.SumUtxoValues(nUtxoCount);
|
||||
int64_t nOldSupply = pindexBest->nMoneySupply;
|
||||
int64_t nDifference = nCalculatedSupply - nOldSupply;
|
||||
|
||||
// Sanity check: difference should be reasonable (not millions of TRI)
|
||||
// Max supply is 2,222,222 TRI, so any difference > 1M TRI is suspicious
|
||||
if (abs64(nDifference) > 1000000 * COIN)
|
||||
throw runtime_error(strprintf(
|
||||
"recalculatesupply: calculated supply differs by %s TRI - this is abnormal, refusing to update",
|
||||
FormatMoney(abs64(nDifference)).c_str()));
|
||||
|
||||
// Update the chain tip's money supply
|
||||
pindexBest->nMoneySupply = nCalculatedSupply;
|
||||
|
||||
// Persist to LevelDB
|
||||
CTxDB txdbWrite;
|
||||
if (!txdbWrite.WriteBlockIndex(CDiskBlockIndex(pindexBest)))
|
||||
throw runtime_error("recalculatesupply: failed to write updated block index");
|
||||
|
||||
Object result;
|
||||
result.push_back(Pair("height", (int)nBestHeight));
|
||||
result.push_back(Pair("old_supply", ValueFromAmount(nOldSupply)));
|
||||
result.push_back(Pair("new_supply", ValueFromAmount(nCalculatedSupply)));
|
||||
result.push_back(Pair("difference", ValueFromAmount(nDifference)));
|
||||
result.push_back(Pair("utxo_count", nUtxoCount));
|
||||
return result;
|
||||
}
|
||||
|
||||
// triangles: get information of sync-checkpoint
|
||||
Value getcheckpoint(const Array& params, bool fHelp)
|
||||
{
|
||||
@@ -416,6 +462,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
|
||||
// ============================================================================
|
||||
@@ -598,3 +686,166 @@ Value getaddresstxids(const Array& params, bool fHelp)
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Value getchaintips(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 0)
|
||||
throw runtime_error(
|
||||
"getchaintips\n"
|
||||
"Return information about all known tips in the block tree,\n"
|
||||
"including the main chain as well as orphaned branches.\n"
|
||||
"Essential for diagnosing chain forks.");
|
||||
|
||||
// Collect all block indices that are tips (nothing points to them as pprev)
|
||||
set<CBlockIndex*> setTips;
|
||||
|
||||
{
|
||||
LOCK(cs_main);
|
||||
for (const auto& item : mapBlockIndex)
|
||||
setTips.insert(item.second);
|
||||
|
||||
for (const auto& item : mapBlockIndex) {
|
||||
if (item.second->pprev)
|
||||
setTips.erase(item.second->pprev);
|
||||
}
|
||||
}
|
||||
|
||||
Array res;
|
||||
LOCK(cs_main);
|
||||
for (CBlockIndex* tip : setTips)
|
||||
{
|
||||
Object obj;
|
||||
obj.push_back(Pair("height", tip->nHeight));
|
||||
obj.push_back(Pair("hash", tip->GetBlockHash().GetHex()));
|
||||
obj.push_back(Pair("chaintrust", tip->nChainTrust.GetHex()));
|
||||
|
||||
int branchLen = 0;
|
||||
CBlockIndex* pWalk = tip;
|
||||
while (pWalk && !pWalk->IsInMainChain()) {
|
||||
branchLen++;
|
||||
pWalk = pWalk->pprev;
|
||||
}
|
||||
|
||||
string status;
|
||||
if (tip == pindexBest)
|
||||
status = "active";
|
||||
else if (branchLen > 0)
|
||||
status = "valid-fork";
|
||||
else
|
||||
status = "unknown";
|
||||
|
||||
obj.push_back(Pair("branchlen", branchLen));
|
||||
obj.push_back(Pair("status", status));
|
||||
|
||||
if (pWalk && !tip->IsInMainChain())
|
||||
obj.push_back(Pair("forkpoint", pWalk->GetBlockHash().GetHex()));
|
||||
|
||||
res.push_back(obj);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Value invalidateblock(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"invalidateblock <hash>\n"
|
||||
"Permanently marks a block as invalid and rewinds the chain.\n"
|
||||
"This forces the node to reorganize to the parent chain.\n"
|
||||
"Use reconsiderblock to undo.");
|
||||
|
||||
string strHash = params[0].get_str();
|
||||
uint256 hash(strHash);
|
||||
|
||||
LOCK(cs_main);
|
||||
|
||||
if (mapBlockIndex.count(hash) == 0)
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
|
||||
|
||||
CBlockIndex* pindex = mapBlockIndex[hash];
|
||||
|
||||
if (pindex->IsInMainChain())
|
||||
{
|
||||
CTxDB txdb;
|
||||
if (!txdb.TxnBegin())
|
||||
throw runtime_error("Failed to begin transaction.");
|
||||
|
||||
CBlockIndex* pindexWalk = pindexBest;
|
||||
|
||||
// Disconnect blocks from best back to (but not including) pindex's parent
|
||||
while (pindexWalk && pindexWalk != pindex->pprev)
|
||||
{
|
||||
CBlock block;
|
||||
if (!block.ReadFromDisk(pindexWalk))
|
||||
throw runtime_error("Failed to read block from disk during invalidation.");
|
||||
|
||||
if (!block.DisconnectBlock(txdb, pindexWalk))
|
||||
throw runtime_error("Failed to disconnect block during invalidation.");
|
||||
|
||||
// Remove disconnected PoS blocks from setStakeSeen
|
||||
if (pindexWalk->IsProofOfStake())
|
||||
{
|
||||
extern set<pair<COutPoint, unsigned int> > setStakeSeen;
|
||||
setStakeSeen.erase(make_pair(pindexWalk->prevoutStake, pindexWalk->nStakeTime));
|
||||
}
|
||||
|
||||
pindexWalk->pprev->pnext = NULL;
|
||||
pindexWalk = pindexWalk->pprev;
|
||||
}
|
||||
|
||||
// Update best block to the fork point
|
||||
if (pindex->pprev) {
|
||||
pindexBest = pindex->pprev;
|
||||
extern uint256 nBestChainTrust;
|
||||
nBestChainTrust = pindexBest->nChainTrust;
|
||||
nBestHeight = pindexBest->nHeight;
|
||||
txdb.WriteHashBestChain(pindexBest->GetBlockHash());
|
||||
if (!txdb.TxnCommit())
|
||||
throw runtime_error("Failed to commit transaction.");
|
||||
printf("invalidateblock: rewound chain to height %d hash %s\n",
|
||||
pindexBest->nHeight, pindexBest->GetBlockHash().ToString().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
Value reconsiderblock(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"reconsiderblock <hash>\n"
|
||||
"Reconsiders a previously invalidated block for activation.\n"
|
||||
"If it has more chain trust than current best, triggers a reorg.");
|
||||
|
||||
string strHash = params[0].get_str();
|
||||
uint256 hash(strHash);
|
||||
|
||||
LOCK(cs_main);
|
||||
|
||||
if (mapBlockIndex.count(hash) == 0)
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
|
||||
|
||||
CBlockIndex* pindex = mapBlockIndex[hash];
|
||||
|
||||
extern uint256 nBestChainTrust;
|
||||
if (pindex->nChainTrust > nBestChainTrust)
|
||||
{
|
||||
CBlock block;
|
||||
if (!block.ReadFromDisk(pindex))
|
||||
throw runtime_error("Failed to read block from disk.");
|
||||
|
||||
CTxDB txdb;
|
||||
block.SetBestChain(txdb, pindex);
|
||||
printf("reconsiderblock: reconsidered block %s at height %d, new best height=%d\n",
|
||||
hash.ToString().c_str(), pindex->nHeight, nBestHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("reconsiderblock: block %s does not have more trust than current best\n",
|
||||
hash.ToString().c_str());
|
||||
}
|
||||
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <algorithm>
|
||||
#include "net.h"
|
||||
#include "addrman.h"
|
||||
#include "trianglesrpc.h"
|
||||
@@ -169,6 +170,76 @@ Value sendalert(const Array& params, bool fHelp)
|
||||
return result;
|
||||
}
|
||||
|
||||
Value addnode(const Array& params, bool fHelp)
|
||||
{
|
||||
string strCommand;
|
||||
if (params.size() == 2)
|
||||
strCommand = params[1].get_str();
|
||||
if (fHelp || params.size() != 2 ||
|
||||
(strCommand != "onetry" && strCommand != "add" && strCommand != "remove"))
|
||||
throw runtime_error(
|
||||
"addnode <node> <add|remove|onetry>\n"
|
||||
"Attempts to add or remove a node from the addnode list,\n"
|
||||
"or try a connection to a node once.\n"
|
||||
"<node> must be a .onion address (Tor-native network).");
|
||||
|
||||
string strNode = params[0].get_str();
|
||||
|
||||
// Tor-native: require .onion addresses
|
||||
if (strNode.find(".onion") == string::npos)
|
||||
throw runtime_error("Only .onion addresses are supported on this network.");
|
||||
|
||||
if (strCommand == "onetry")
|
||||
{
|
||||
CAddress addr;
|
||||
CNode* pnode = ConnectNode(addr, strNode.c_str());
|
||||
if (!pnode)
|
||||
throw runtime_error("Failed to connect to node (may already be connected or unreachable).");
|
||||
pnode->Release();
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
// For add/remove, manipulate the -addnode list that ThreadOpenAddedConnections uses
|
||||
LOCK(cs_vNodes);
|
||||
vector<string>& vAddedNodes = mapMultiArgs["-addnode"];
|
||||
|
||||
if (strCommand == "add")
|
||||
{
|
||||
for (const string& existing : vAddedNodes)
|
||||
if (existing == strNode)
|
||||
throw runtime_error("Node already added.");
|
||||
vAddedNodes.push_back(strNode);
|
||||
}
|
||||
else if (strCommand == "remove")
|
||||
{
|
||||
auto it = std::find(vAddedNodes.begin(), vAddedNodes.end(), strNode);
|
||||
if (it == vAddedNodes.end())
|
||||
throw runtime_error("Node not found in addnode list.");
|
||||
vAddedNodes.erase(it);
|
||||
}
|
||||
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
Value disconnectnode(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"disconnectnode <node>\n"
|
||||
"Immediately disconnects from the specified node.");
|
||||
|
||||
string strNode = params[0].get_str();
|
||||
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes) {
|
||||
if (pnode->addrName == strNode || pnode->addr.ToString() == strNode) {
|
||||
pnode->CloseSocketDisconnect();
|
||||
return Value::null;
|
||||
}
|
||||
}
|
||||
throw runtime_error("Node not found.");
|
||||
}
|
||||
|
||||
Value getseedlist(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 0)
|
||||
|
||||
+2
-1
@@ -1944,7 +1944,8 @@ Value clearwallettransactions(const Array& params, bool fHelp)
|
||||
if (datKey.get_data() == NULL || datValue.get_data() == NULL
|
||||
|| ret != 0)
|
||||
{
|
||||
snprintf(cbuf, sizeof(cbuf), "wallet DB error %d, %s", ret, db_strerror(ret));
|
||||
const char* dbErr = db_strerror(ret);
|
||||
snprintf(cbuf, sizeof(cbuf), "wallet DB error %d, %s", ret, dbErr ? dbErr : "unknown");
|
||||
throw runtime_error(cbuf);
|
||||
};
|
||||
|
||||
|
||||
+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));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -248,6 +248,8 @@ static const CRPCCommand vRPCCommands[] =
|
||||
{ "getblockcount", &getblockcount, true, false },
|
||||
{ "getconnectioncount", &getconnectioncount, true, false },
|
||||
{ "getpeerinfo", &getpeerinfo, true, false },
|
||||
{ "addnode", &addnode, true, false },
|
||||
{ "disconnectnode", &disconnectnode, true, false },
|
||||
{ "getdifficulty", &getdifficulty, true, false },
|
||||
{ "getblockheader", &getblockheader, true, false },
|
||||
{ "getblockchaininfo", &getblockchaininfo, true, false },
|
||||
@@ -312,6 +314,11 @@ 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 },
|
||||
{ "reservebalance", &reservebalance, false, true},
|
||||
{ "checkwallet", &checkwallet, false, true},
|
||||
{ "repairwallet", &repairwallet, false, true},
|
||||
|
||||
@@ -148,6 +148,8 @@ 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 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);
|
||||
extern json_spirit::Value dumpwallet(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value importwallet(const json_spirit::Array& params, bool fHelp);
|
||||
@@ -220,6 +222,11 @@ 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 getaddressbalance(const json_spirit::Array& params, bool fHelp);
|
||||
extern json_spirit::Value getaddressutxos(const json_spirit::Array& params, bool fHelp);
|
||||
|
||||
+172
-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;
|
||||
@@ -613,6 +616,50 @@ bool CTxDB::LoadBlockIndex()
|
||||
hashBestChain.ToString().substr(0,20).c_str(), nBestHeight, CBigNum(nBestChainTrust).ToString().c_str(),
|
||||
DateTimeStrFormat("%x %H:%M:%S", pindexBest->GetBlockTime()).c_str());
|
||||
|
||||
// Re-evaluate best chain: scan for competing tips with equal or greater trust.
|
||||
// This fixes nodes stuck on the wrong fork after consensus rule changes.
|
||||
{
|
||||
CBlockIndex* pindexBetter = NULL;
|
||||
for (const auto& item : mapBlockIndex)
|
||||
{
|
||||
CBlockIndex* pindex = item.second;
|
||||
if (pindex == pindexBest)
|
||||
continue;
|
||||
if (pindex->nChainTrust > nBestChainTrust)
|
||||
{
|
||||
pindexBetter = pindex;
|
||||
break;
|
||||
}
|
||||
if (pindex->nChainTrust == nBestChainTrust &&
|
||||
pindex->GetBlockHash() < pindexBest->GetBlockHash())
|
||||
{
|
||||
if (!pindexBetter || pindex->GetBlockHash() < pindexBetter->GetBlockHash())
|
||||
pindexBetter = pindex;
|
||||
}
|
||||
}
|
||||
if (pindexBetter)
|
||||
{
|
||||
printf("LoadBlockIndex(): found better chain tip %s at height %d (trust %s vs %s)\n",
|
||||
pindexBetter->GetBlockHash().ToString().substr(0,20).c_str(),
|
||||
pindexBetter->nHeight,
|
||||
CBigNum(pindexBetter->nChainTrust).ToString().c_str(),
|
||||
CBigNum(nBestChainTrust).ToString().c_str());
|
||||
CBlock block;
|
||||
if (block.ReadFromDisk(pindexBetter))
|
||||
{
|
||||
CTxDB txdb2;
|
||||
if (block.SetBestChain(txdb2, pindexBetter))
|
||||
{
|
||||
hashBestChain = pindexBetter->GetBlockHash();
|
||||
pindexBest = pindexBetter;
|
||||
nBestHeight = pindexBetter->nHeight;
|
||||
nBestChainTrust = pindexBetter->nChainTrust;
|
||||
printf("LoadBlockIndex(): switched to better chain tip\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// triangles: load hashSyncCheckpoint (best-effort, non-fatal)
|
||||
if (!ReadSyncCheckpoint(Checkpoints::hashSyncCheckpoint))
|
||||
printf("LoadBlockIndex(): no sync checkpoint in DB, using default\n");
|
||||
@@ -826,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;
|
||||
|
||||
@@ -860,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();
|
||||
|
||||
+4
-4
@@ -41,16 +41,16 @@ 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
|
||||
# define BUILD_DESC BUILD_DESC_FROM_COMMIT(DISPLAY_VERSION_MAJOR, DISPLAY_VERSION_MINOR, DISPLAY_VERSION_REVISION, DISPLAY_VERSION_BUILD, GIT_COMMIT_ID)
|
||||
# define BUILD_DESC BUILD_DESC_FROM_COMMIT(CLIENT_VERSION_MAJOR, CLIENT_VERSION_MINOR, CLIENT_VERSION_REVISION, CLIENT_VERSION_BUILD, GIT_COMMIT_ID)
|
||||
# else
|
||||
# define BUILD_DESC BUILD_DESC_FROM_UNKNOWN(DISPLAY_VERSION_MAJOR, DISPLAY_VERSION_MINOR, DISPLAY_VERSION_REVISION, DISPLAY_VERSION_BUILD)
|
||||
# define BUILD_DESC BUILD_DESC_FROM_UNKNOWN(CLIENT_VERSION_MAJOR, CLIENT_VERSION_MINOR, CLIENT_VERSION_REVISION, CLIENT_VERSION_BUILD)
|
||||
# endif
|
||||
#endif
|
||||
|
||||
|
||||
@@ -51,9 +51,4 @@ static const int BIP0031_VERSION = 60000;
|
||||
// "mempool" command, enhanced "getdata" behavior starts with this version:
|
||||
static const int MEMPOOL_GD_VERSION = 60002;
|
||||
|
||||
#define DISPLAY_VERSION_MAJOR 5
|
||||
#define DISPLAY_VERSION_MINOR 7
|
||||
#define DISPLAY_VERSION_REVISION 6
|
||||
#define DISPLAY_VERSION_BUILD 0
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
## TRI Node Upgrade to v5.8.0 - April 14, 2026
|
||||
|
||||
This document outlines the process and results of upgrading the TRI network nodes to version 5.8.0.
|
||||
|
||||
### Initial State
|
||||
|
||||
- **DNS2:** `v5.7.9` @ block `2,203,611`
|
||||
- **DNS3:** `v5.7.5` @ block `2,204,954`
|
||||
- **Contabo Seeds:** `v5.7.9` @ block `2,203,594`
|
||||
|
||||
Nodes were on multiple versions and forks.
|
||||
|
||||
### Upgrade Process
|
||||
|
||||
1. **Version Confirmation:** Verified `v5.8.0` was available on GitHub.
|
||||
2. **Upgrades:**
|
||||
- DNS2 upgraded to `v5.8.0` via `dpkg`.
|
||||
- DNS3 upgraded to `v5.8.0` via `dpkg`.
|
||||
- Contabo seeds (`tri-seed-1` to `4`) upgraded to `v5.8.0` via `dpkg` inside their containers.
|
||||
3. **Chain Reset:** To resolve forks, the chain data (blocks, chainstate, peers) was wiped on DNS2 and all Contabo seeds. Wallets and configs were preserved. DNS3 was left as the canonical chain source.
|
||||
|
||||
### Current Status
|
||||
|
||||
- All nodes are now running `v5.8.0`.
|
||||
- Nodes are currently re-syncing to the canonical chain. Monitoring is in progress.
|
||||
|
||||
### DNS2 Wallet Corruption and Recovery
|
||||
|
||||
- **Symptom:** `triangles.service` on DNS2 was in a crash loop. Logs showed a recurring `CDB() : can't open database file wallet.dat, error -30973` error.
|
||||
- **Diagnosis:** `wallet.dat` file was corrupted.
|
||||
- **Recovery:**
|
||||
1. The corrupted wallet was moved to `wallet.dat.corrupted` for safety.
|
||||
2. The latest wallet backup (`dns2-wallet_20260414_031501.dat`) was restored from Dropbox.
|
||||
3. The `triangles.service` was restarted.
|
||||
|
||||
This restored the wallet to a healthy state.
|
||||
Reference in New Issue
Block a user