Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c312bb7da | |||
| 41ba9f8bc9 | |||
| cbb189aade | |||
| 5635cb5e57 | |||
| b6feab8e94 | |||
| 333f7abfc0 | |||
| eb20edf890 | |||
| ff7a7d3b6b | |||
| fb5f1be032 | |||
| 83a66814b0 | |||
| ae7beb0df7 | |||
| d4d0ddf849 | |||
| 3566eed9e1 | |||
| 3473e80876 | |||
| a48fb88e4c | |||
| bfdb399772 | |||
| c577fb2ff5 | |||
| b6b3f3877f | |||
| 9ed79d53a6 | |||
| 8615e6b46d | |||
| e694a189f8 | |||
| 2aeae07d0b | |||
| fcfa3b9938 | |||
| 6cf30350ea | |||
| c1c9f19870 | |||
| 77b05a84f2 | |||
| 2e19d85b18 | |||
| b9ce72d39a | |||
| 6defb54300 | |||
| 43eaa96bc9 |
@@ -175,6 +175,7 @@ jobs:
|
||||
mingw-w64-x86_64-miniupnpc
|
||||
mingw-w64-x86_64-zlib
|
||||
mingw-w64-x86_64-rocksdb
|
||||
mingw-w64-x86_64-sqlite3
|
||||
mingw-w64-x86_64-autotools
|
||||
|
||||
- name: Set VERSION
|
||||
@@ -182,9 +183,9 @@ jobs:
|
||||
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
||||
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||
else
|
||||
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | awk '{print $3}')
|
||||
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | awk '{print $3}')
|
||||
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | awk '{print $3}')
|
||||
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
|
||||
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
|
||||
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | tr -d '\r' | awk '{print $3}')
|
||||
echo "VERSION=${MAJOR}.${MINOR}.${REV}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
@@ -352,6 +353,7 @@ jobs:
|
||||
mingw-w64-x86_64-miniupnpc
|
||||
mingw-w64-x86_64-zlib
|
||||
mingw-w64-x86_64-rocksdb
|
||||
mingw-w64-x86_64-sqlite3
|
||||
mingw-w64-x86_64-autotools
|
||||
|
||||
- name: Configure
|
||||
@@ -413,9 +415,9 @@ jobs:
|
||||
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
||||
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||
else
|
||||
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | awk '{print $3}')
|
||||
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | awk '{print $3}')
|
||||
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | awk '{print $3}')
|
||||
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
|
||||
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
|
||||
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | tr -d '\r' | awk '{print $3}')
|
||||
echo "VERSION=${MAJOR}.${MINOR}.${REV}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
@@ -549,9 +551,9 @@ jobs:
|
||||
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
||||
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||
else
|
||||
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | awk '{print $3}')
|
||||
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | awk '{print $3}')
|
||||
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | awk '{print $3}')
|
||||
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
|
||||
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
|
||||
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | tr -d '\r' | awk '{print $3}')
|
||||
echo "VERSION=${MAJOR}.${MINOR}.${REV}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
@@ -620,9 +622,9 @@ jobs:
|
||||
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
|
||||
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||
else
|
||||
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | awk '{print $3}')
|
||||
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | awk '{print $3}')
|
||||
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | awk '{print $3}')
|
||||
MAJOR=$(grep 'CLIENT_VERSION_MAJOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
|
||||
MINOR=$(grep 'CLIENT_VERSION_MINOR' src/clientversion.h | tr -d '\r' | awk '{print $3}')
|
||||
REV=$(grep 'CLIENT_VERSION_REVISION' src/clientversion.h | tr -d '\r' | awk '{print $3}')
|
||||
echo "VERSION=${MAJOR}.${MINOR}.${REV}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Boost removal — progress
|
||||
|
||||
Goal: drop the Boost dependency in favor of C++17 std. No consensus or wire
|
||||
behavior changes.
|
||||
|
||||
## Done
|
||||
|
||||
**Triangles' own code (daemon + GUI) is now completely Boost-free.** All nine
|
||||
translation units that used Boost have been migrated. The only remaining Boost
|
||||
usage in the tree is (1) the Boost.Test unit-test framework under `src/test/`,
|
||||
and (2) Boost as a *transitive link dependency of the bundled embedded i2pd
|
||||
router* (`libi2pd.a`) — not of any Triangles source. See "Remaining" below.
|
||||
|
||||
| File | Boost removed | Replacement |
|
||||
|------|---------------|-------------|
|
||||
| `txdb-leveldb.cpp` | `boost/version.hpp` (unused include) | deleted |
|
||||
| `txdb-rocksdb.cpp` | `boost/version.hpp` (unused include) | deleted |
|
||||
| `walletdb.cpp` | `boost/version.hpp` + `BOOST_VERSION` guard | unconditional `std::filesystem` branch |
|
||||
| `util.cpp` | `boost::program_options` config-file parser + `to_internal` workaround | small C++17 INI parser in `ReadConfigFile` |
|
||||
| `init.cpp` | `boost::interprocess::file_lock` + `using namespace boost` | portable `LockDataDirectory()` (`flock` POSIX / `LockFileEx` Win32) |
|
||||
| `rpcdump.cpp` | `boost::posix_time` + `boost::gregorian` | `std::get_time` + `timegm`/`_mkgmtime` |
|
||||
|
||||
`wallet.cpp` and `triangles-cli.cpp` only ever *mentioned* Boost in comments —
|
||||
no code change needed.
|
||||
|
||||
### Behavior notes for review
|
||||
- **Config parser**: `name = value`; a line whose first non-whitespace char is
|
||||
`#` is a comment; blank lines ignored; inline `#` is NOT a comment (so
|
||||
`rpcpassword` may contain `#`). First value wins for single-valued settings;
|
||||
`-name` keying and `nofoo=` negative-setting interpretation preserved.
|
||||
- **File lock**: exclusive, non-blocking; the fd/handle is held for process
|
||||
lifetime and released by the OS on exit (matches the old file_lock lifetime).
|
||||
- **Dump time parser**: same five accepted formats, parsed as UTC.
|
||||
|
||||
### CMake note
|
||||
`program_options` is no longer used by any source file and can be dropped from
|
||||
the `find_package(Boost ... COMPONENTS ...)` list once the remaining two files
|
||||
are migrated. It is left in place for now because removing it before the Asio
|
||||
migration provides no benefit and the component is harmless if installed.
|
||||
|
||||
### RPC server (done — `trianglesrpc.cpp`)
|
||||
|
||||
The JSON-RPC/HTTP server previously used `boost::asio` (async sockets +
|
||||
`boost::asio::ssl`), `boost::bind`, `boost::iostreams`,
|
||||
`boost::shared_ptr`/`weak_ptr`, and `boost::system::error_code`. It was
|
||||
rewritten onto **raw BSD sockets** behind a small `std::iostream`
|
||||
(`src/rpc_httpsocket.h`), preserving the thread-per-connection model so the
|
||||
HTTP parser, JSON-RPC dispatch, REST handler, and the blocking SSE handler are
|
||||
all unchanged.
|
||||
|
||||
- New `src/rpc_httpsocket.h`: `CSocketIOStream` (a `std::iostream` over a
|
||||
`SOCKET`), `ConnectRPCSocket()`, `BindRPCSockets()` (separate IPv4/IPv6
|
||||
listeners, loopback unless `-rpcallowip`), `SockaddrToString()`.
|
||||
- `ThreadRPCServer2` now binds sockets and runs a `select()`-based accept loop
|
||||
that spawns `ThreadRPCServer3` per connection.
|
||||
- `ClientAllowed` takes a numeric IP string.
|
||||
- `CallRPC` connects via a raw socket.
|
||||
- **`-rpcssl` is removed.** RPC TLS was a rarely used Asio::ssl feature; for
|
||||
remote access, front the port with stunnel/nginx or reach it over SSH/Tor
|
||||
(the same decision Bitcoin Core made). A warning is logged if `-rpcssl` is set.
|
||||
|
||||
### Qt URI handler (done — `qt/qtipcserver.cpp`)
|
||||
|
||||
The `triangles:` single-instance URI handoff used
|
||||
`boost::interprocess::message_queue` + `boost::posix_time`. Rewritten onto
|
||||
`QLocalServer` / `QLocalSocket` (QtNetwork), keeping the existing polling-thread
|
||||
model via the blocking `waitForNewConnection` / `waitForReadyRead` /
|
||||
`waitForConnected` methods (no Qt event loop required). `Qt5::Network` added to
|
||||
the Qt find_package and the `triangles-qt` link.
|
||||
|
||||
### CMake
|
||||
- `Boost::program_options`, `Boost::thread`, `Boost::chrono` removed from the
|
||||
`triangles_common` link — Triangles' own objects reference no Boost symbols.
|
||||
|
||||
## Remaining
|
||||
|
||||
Two things still pull Boost into the build; neither is Triangles source:
|
||||
|
||||
1. **Embedded i2pd router.** When built with the embedded I2P router, the
|
||||
bundled `libi2pd.a` / `libi2pdclient.a` link Boost
|
||||
(`program_options`, `thread`, `chrono`, `filesystem`, `system`). The
|
||||
i2pd-specific link block (and the top-level `find_package(Boost ...)`) are
|
||||
therefore left intact. Fully dropping Boost from the build requires either a
|
||||
Boost-free i2pd build or disabling the embedded router. This is an upstream
|
||||
i2pd concern, not Triangles code.
|
||||
|
||||
2. **Unit tests.** `src/test/*` use the Boost.Test framework
|
||||
(`Boost::unit_test_framework`). Optional follow-up: port to a header-only
|
||||
framework (e.g. Catch2/doctest) to remove the last first-party Boost use.
|
||||
|
||||
When both are addressed, `find_package(Boost ...)` can be removed entirely.
|
||||
+4
-1
@@ -154,6 +154,9 @@ if(NOT TARGET RocksDB::rocksdb AND NOT TARGET PkgConfig::RocksDB)
|
||||
message(STATUS "Found RocksDB (manual probe): ${ROCKSDB_LIBRARY}")
|
||||
endif()
|
||||
|
||||
# Modernization: SQLite3 for the new wallet DB backend.
|
||||
find_package(SQLite3 REQUIRED)
|
||||
|
||||
# Triangles uses RocksDB features that only exist in 7.4+ (XXH3 per-block
|
||||
# checksum, type 4). Building against an older RocksDB produces a binary
|
||||
# whose smsgDB Open() fails on any SST file written by RocksDB 7.4+ —
|
||||
@@ -248,7 +251,7 @@ set(SECP256K1_ENABLE_MODULE_ELLSWIFT OFF CACHE INTERNAL "")
|
||||
add_subdirectory(src/secp256k1 EXCLUDE_FROM_ALL)
|
||||
|
||||
if(BUILD_QT)
|
||||
find_package(Qt5 5.9 REQUIRED COMPONENTS Core Gui Widgets)
|
||||
find_package(Qt5 5.9 REQUIRED COMPONENTS Core Gui Widgets Network)
|
||||
find_package(Qt5 COMPONENTS LinguistTools QUIET)
|
||||
if(USE_DBUS AND UNIX AND NOT APPLE)
|
||||
find_package(Qt5 COMPONENTS DBus QUIET)
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ FROM ubuntu:22.04
|
||||
|
||||
LABEL maintainer="Cryptographic Triangles Team"
|
||||
LABEL description="Cryptographic Triangles (TRI) headless daemon"
|
||||
LABEL version="5.9.24"
|
||||
LABEL version="6.1.0"
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
|
||||
@@ -1,250 +1,272 @@
|
||||
# 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.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Proof-of-Stake** - Energy-efficient block production with 33% annual staking rewards (coin-age based)
|
||||
- **Hash9 Algorithm** - Unique 13-step hash cascade (Fugue, Hamsi, Groestl, Blake, BMW, Skein, Keccak, Shavite, JH, Luffa, Cubehash, Echo, SIMD)
|
||||
- **Encrypted Messaging** - Send and receive encrypted messages directly through the wallet
|
||||
- **Tor v3 Integration** - Connect and transact over the Tor network with v3 onion hidden services
|
||||
- **120-second Block Time** - Fast confirmations with 2-minute target spacing
|
||||
|
||||
## Specifications
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Algorithm | Hash9 (PoW blocks 0-9000), PoS from block 9001 |
|
||||
| Block Time | ~120 seconds |
|
||||
| Max Supply | 2,222,222 TRI |
|
||||
| PoS Reward | 33% annual, coin-age based |
|
||||
| P2P Port | 24112 |
|
||||
| RPC Port | 19112 |
|
||||
| Protocol | 70205 |
|
||||
|
||||
## Network Status
|
||||
|
||||
The Triangles network operates exclusively over Tor for privacy:
|
||||
|
||||
**Tor v3 Seeds:**
|
||||
- `jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112`
|
||||
- `uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112`
|
||||
- `el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112`
|
||||
- `sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112`
|
||||
- `i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112`
|
||||
|
||||
**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 cmake ninja-build \
|
||||
libboost-all-dev libssl-dev libdb5.3++-dev libevent-dev \
|
||||
zlib1g-dev libminiupnpc-dev
|
||||
```
|
||||
|
||||
For the Qt wallet, also install:
|
||||
```bash
|
||||
sudo apt-get install -y qtbase5-dev qt5-qmake libqrencode-dev
|
||||
```
|
||||
|
||||
Build:
|
||||
```bash
|
||||
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++ 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`.
|
||||
|
||||
Then build as above.
|
||||
|
||||
### Windows (MSYS2 MinGW64)
|
||||
|
||||
Open an MSYS2 MinGW64 shell and install:
|
||||
```bash
|
||||
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-libevent
|
||||
```
|
||||
|
||||
Build:
|
||||
```bash
|
||||
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
|
||||
```bash
|
||||
mkdir -p ~/.triangles
|
||||
cat > ~/.triangles/triangles.conf << 'EOF'
|
||||
port=24112
|
||||
rpcport=19112
|
||||
rpcuser=trianglesrpc
|
||||
rpcpassword=<generate-a-strong-password>
|
||||
rpcallowip=127.0.0.1
|
||||
staking=1
|
||||
txindex=1
|
||||
listen=1
|
||||
server=1
|
||||
daemon=1
|
||||
proxy=127.0.0.1:9050
|
||||
EOF
|
||||
|
||||
trianglesd
|
||||
```
|
||||
|
||||
The node will connect to seed nodes over Tor and sync the blockchain automatically.
|
||||
|
||||
### Existing Wallet Holders
|
||||
|
||||
If you have a `wallet.dat` from the original Triangles network:
|
||||
|
||||
1. Place your `wallet.dat` in `~/.triangles/` (Linux) or `%APPDATA%\triangles\` (Windows)
|
||||
2. Start the wallet - it will sync the blockchain and your balance will appear automatically
|
||||
3. No migration or special action is needed - all keys and balances are preserved
|
||||
|
||||
### Staking
|
||||
|
||||
To stake, your wallet must be:
|
||||
- Running with `staking=1` in the config
|
||||
- Connected to at least one peer
|
||||
- Containing coins with sufficient coin-age (mature inputs)
|
||||
|
||||
Check staking status:
|
||||
```bash
|
||||
trianglesd getstakinginfo
|
||||
```
|
||||
|
||||
### Encrypted Messaging
|
||||
|
||||
Send and receive encrypted messages between wallet addresses:
|
||||
|
||||
```bash
|
||||
# Enable messaging
|
||||
trianglesd smsgenable
|
||||
|
||||
# Send a message
|
||||
trianglesd smsgsend <your-address> <recipient-address> "Hello from Triangles!"
|
||||
|
||||
# Check inbox
|
||||
trianglesd smsginbox all
|
||||
|
||||
# Send anonymous message
|
||||
trianglesd smsgsendanon <recipient-address> "Anonymous message"
|
||||
```
|
||||
|
||||
Messages are encrypted end-to-end using AES and distributed through the peer network in time-bucketed batches.
|
||||
|
||||
### Tor Support
|
||||
|
||||
Triangles is designed as a Tor-only network. Install the Tor daemon and configure your proxy:
|
||||
```
|
||||
# triangles.conf
|
||||
proxy=127.0.0.1:9050
|
||||
```
|
||||
|
||||
To run your own hidden service, add to `/etc/tor/torrc`:
|
||||
```
|
||||
HiddenServiceDir /var/lib/tor/triangles/
|
||||
HiddenServiceVersion 3
|
||||
HiddenServicePort 24112 127.0.0.1:24112
|
||||
```
|
||||
|
||||
Then set `externalip=<your-onion-address>` in `triangles.conf`.
|
||||
|
||||
## RPC Commands
|
||||
|
||||
### General
|
||||
- `getinfo` - Node status, balance, block height, connections
|
||||
- `getpeerinfo` - Connected peer details
|
||||
- `getstakinginfo` - Staking status and weight
|
||||
|
||||
### Wallet
|
||||
- `getbalance` - Current balance
|
||||
- `listunspent` - Unspent transaction outputs
|
||||
- `sendtoaddress <addr> <amount>` - Send TRI
|
||||
- `getnewaddress` - Generate new receiving address
|
||||
|
||||
### Messaging
|
||||
- `smsgenable` / `smsgdisable` - Toggle secure messaging
|
||||
- `smsgsend <from> <to> <message>` - Send encrypted message
|
||||
- `smsgsendanon <to> <message>` - Send anonymous message
|
||||
- `smsginbox [all|unread|clear]` - View received messages
|
||||
- `smsgoutbox [all|clear]` - View sent messages
|
||||
- `smsglocalkeys` - List messaging-enabled addresses
|
||||
- `smsgscanchain` - Scan blockchain for public keys
|
||||
|
||||
## Chain History
|
||||
|
||||
- **July 16, 2014** - Genesis block
|
||||
- **Block 0-9000** - Proof-of-Work mining phase (Hash9)
|
||||
- **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, staking resumed
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
main.cpp - Core blockchain logic, block/tx validation, message routing
|
||||
miner.cpp - Staking miner thread
|
||||
net.cpp - P2P networking
|
||||
init.cpp - Daemon initialization
|
||||
wallet.cpp - Wallet management
|
||||
smessage.cpp/h - Encrypted messaging system
|
||||
kernel.cpp - PoS kernel (stake validation)
|
||||
checkpoints.cpp - Hardcoded checkpoints
|
||||
net_bootstrap.h - DNS/IP seed configuration
|
||||
onionseed.h - Tor v3 onion seed addresses
|
||||
tor/
|
||||
onion_v3.cpp/h - Tor v3 hidden service management
|
||||
tor_crypto_compat.h - Ed25519/SHA3 crypto compatibility
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the MIT/X11 software license. See `COPYING` for details.
|
||||
|
||||
## Links
|
||||
|
||||
- Website: [cryptographic-triangles.org](https://cryptographic-triangles.org)
|
||||
- Explorer: [blocks.cryptographic-triangles.org](https://blocks.cryptographic-triangles.org)
|
||||
# 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.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Proof-of-Stake** - Energy-efficient block production with 33% annual staking rewards (coin-age based)
|
||||
- **Hash9 Algorithm** - Unique 13-step hash cascade (Fugue, Hamsi, Groestl, Blake, BMW, Skein, Keccak, Shavite, JH, Luffa, Cubehash, Echo, SIMD)
|
||||
- **Encrypted Messaging** - Send and receive encrypted messages directly through the wallet
|
||||
- **Tor v3 Integration** - Connect and transact over the Tor network with v3 onion hidden services
|
||||
- **120-second Block Time** - Fast confirmations with 2-minute target spacing
|
||||
|
||||
## Specifications
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Algorithm | Hash9 (PoW blocks 0-9000), PoS from block 9001 |
|
||||
| Block Time | ~120 seconds |
|
||||
| Max Supply | 2,222,222 TRI |
|
||||
| PoS Reward | 33% annual, coin-age based |
|
||||
| P2P Port | 24112 |
|
||||
| RPC Port | 19112 |
|
||||
| Protocol | 70205 |
|
||||
|
||||
## Network Status
|
||||
|
||||
The Triangles network operates exclusively over Tor for privacy:
|
||||
|
||||
**Tor v3 Seeds:**
|
||||
- `jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112`
|
||||
- `uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112`
|
||||
- `el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112`
|
||||
- `sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112`
|
||||
- `i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112`
|
||||
|
||||
**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+ |
|
||||
| SQLite | 3.x (default wallet database backend) |
|
||||
| Berkeley DB | 5.3 with C++ bindings (legacy wallet backend, used for migration) |
|
||||
| libevent | 2.x |
|
||||
| RocksDB | 7.4+ (default chain database backend) |
|
||||
| LevelDB | bundled (legacy chain DB backend, used for migration) |
|
||||
|
||||
### Linux (Ubuntu 24.04 / Debian 12+)
|
||||
|
||||
Install dependencies:
|
||||
```bash
|
||||
sudo apt-get install -y build-essential cmake ninja-build \
|
||||
libboost-all-dev libssl-dev libdb5.3++-dev libevent-dev \
|
||||
zlib1g-dev libminiupnpc-dev
|
||||
```
|
||||
|
||||
For the Qt wallet, also install:
|
||||
```bash
|
||||
sudo apt-get install -y qtbase5-dev qt5-qmake libqrencode-dev
|
||||
```
|
||||
|
||||
Build:
|
||||
```bash
|
||||
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++ 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`.
|
||||
|
||||
Then build as above.
|
||||
|
||||
### Windows (MSYS2 MinGW64)
|
||||
|
||||
Open an MSYS2 MinGW64 shell and install:
|
||||
```bash
|
||||
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-libevent
|
||||
```
|
||||
|
||||
Build:
|
||||
```bash
|
||||
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
|
||||
```bash
|
||||
mkdir -p ~/.triangles
|
||||
cat > ~/.triangles/triangles.conf << 'EOF'
|
||||
port=24112
|
||||
rpcport=19112
|
||||
rpcuser=trianglesrpc
|
||||
rpcpassword=<generate-a-strong-password>
|
||||
rpcallowip=127.0.0.1
|
||||
staking=1
|
||||
txindex=1
|
||||
listen=1
|
||||
server=1
|
||||
daemon=1
|
||||
proxy=127.0.0.1:9050
|
||||
EOF
|
||||
|
||||
trianglesd
|
||||
```
|
||||
|
||||
The node will connect to seed nodes over Tor and sync the blockchain automatically.
|
||||
|
||||
### Chain Database (RocksDB)
|
||||
|
||||
The chain database (block index, transaction index, UTXO set, address index) uses **RocksDB by default**. RocksDB gives faster sync and lookups than the legacy LevelDB backend through parallel compaction, bloom filters, and a larger write buffer and block cache (tunable with `-dbcache=<MB>`).
|
||||
|
||||
If you are upgrading a node that already has a LevelDB chain database (`txleveldb/` in your data directory), it is migrated automatically on first launch: the chain state is copied into a new `rocksdb/` directory and verified (record count, UTXO count and value, best-chain hash, and DB format must all match) before use. The original `txleveldb/` directory is left untouched as a fallback and is never modified.
|
||||
|
||||
To select a backend explicitly:
|
||||
|
||||
```bash
|
||||
trianglesd -chaindb=rocksdb # default
|
||||
trianglesd -chaindb=leveldb # legacy backend (retained for fallback/migration)
|
||||
```
|
||||
|
||||
Migration can also be triggered or forced manually:
|
||||
|
||||
```bash
|
||||
trianglesd -migratechaindb # migrate txleveldb -> rocksdb if not already done
|
||||
trianglesd -migratechaindbforce # re-migrate, replacing any existing rocksdb/
|
||||
```
|
||||
|
||||
### Existing Wallet Holders
|
||||
|
||||
If you have a `wallet.dat` from the original Triangles network:
|
||||
|
||||
1. Place your `wallet.dat` in `~/.triangles/` (Linux) or `%APPDATA%\triangles\` (Windows)
|
||||
2. Start the wallet - it will sync the blockchain and your balance will appear automatically
|
||||
3. No migration or special action is needed - all keys and balances are preserved
|
||||
|
||||
### Staking
|
||||
|
||||
To stake, your wallet must be:
|
||||
- Running with `staking=1` in the config
|
||||
- Connected to at least one peer
|
||||
- Containing coins with sufficient coin-age (mature inputs)
|
||||
|
||||
Check staking status:
|
||||
```bash
|
||||
trianglesd getstakinginfo
|
||||
```
|
||||
|
||||
### Encrypted Messaging
|
||||
|
||||
Send and receive encrypted messages between wallet addresses:
|
||||
|
||||
```bash
|
||||
# Enable messaging
|
||||
trianglesd smsgenable
|
||||
|
||||
# Send a message
|
||||
trianglesd smsgsend <your-address> <recipient-address> "Hello from Triangles!"
|
||||
|
||||
# Check inbox
|
||||
trianglesd smsginbox all
|
||||
|
||||
# Send anonymous message
|
||||
trianglesd smsgsendanon <recipient-address> "Anonymous message"
|
||||
```
|
||||
|
||||
Messages are encrypted end-to-end using AES and distributed through the peer network in time-bucketed batches.
|
||||
|
||||
### Tor Support
|
||||
|
||||
Triangles is designed as a Tor-only network. Install the Tor daemon and configure your proxy:
|
||||
```
|
||||
# triangles.conf
|
||||
proxy=127.0.0.1:9050
|
||||
```
|
||||
|
||||
To run your own hidden service, add to `/etc/tor/torrc`:
|
||||
```
|
||||
HiddenServiceDir /var/lib/tor/triangles/
|
||||
HiddenServiceVersion 3
|
||||
HiddenServicePort 24112 127.0.0.1:24112
|
||||
```
|
||||
|
||||
Then set `externalip=<your-onion-address>` in `triangles.conf`.
|
||||
|
||||
## RPC Commands
|
||||
|
||||
### General
|
||||
- `getinfo` - Node status, balance, block height, connections
|
||||
- `getpeerinfo` - Connected peer details
|
||||
- `getstakinginfo` - Staking status and weight
|
||||
|
||||
### Wallet
|
||||
- `getbalance` - Current balance
|
||||
- `listunspent` - Unspent transaction outputs
|
||||
- `sendtoaddress <addr> <amount>` - Send TRI
|
||||
- `getnewaddress` - Generate new receiving address
|
||||
|
||||
### Messaging
|
||||
- `smsgenable` / `smsgdisable` - Toggle secure messaging
|
||||
- `smsgsend <from> <to> <message>` - Send encrypted message
|
||||
- `smsgsendanon <to> <message>` - Send anonymous message
|
||||
- `smsginbox [all|unread|clear]` - View received messages
|
||||
- `smsgoutbox [all|clear]` - View sent messages
|
||||
- `smsglocalkeys` - List messaging-enabled addresses
|
||||
- `smsgscanchain` - Scan blockchain for public keys
|
||||
|
||||
## Chain History
|
||||
|
||||
- **July 16, 2014** - Genesis block
|
||||
- **Block 0-9000** - Proof-of-Work mining phase (Hash9)
|
||||
- **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, staking resumed
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
main.cpp - Core blockchain logic, block/tx validation, message routing
|
||||
miner.cpp - Staking miner thread
|
||||
net.cpp - P2P networking
|
||||
init.cpp - Daemon initialization
|
||||
wallet.cpp - Wallet management
|
||||
smessage.cpp/h - Encrypted messaging system
|
||||
kernel.cpp - PoS kernel (stake validation)
|
||||
checkpoints.cpp - Hardcoded checkpoints
|
||||
net_bootstrap.h - DNS/IP seed configuration
|
||||
onionseed.h - Tor v3 onion seed addresses
|
||||
tor/
|
||||
onion_v3.cpp/h - Tor v3 hidden service management
|
||||
tor_crypto_compat.h - Ed25519/SHA3 crypto compatibility
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the MIT/X11 software license. See `COPYING` for details.
|
||||
|
||||
## Links
|
||||
|
||||
- Website: [cryptographic-triangles.org](https://cryptographic-triangles.org)
|
||||
- Explorer: [blocks.cryptographic-triangles.org](https://blocks.cryptographic-triangles.org)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# RocksDB as the default chain database backend
|
||||
|
||||
This change finishes the RocksDB chain-database backend, makes it the default,
|
||||
and provides a transparent migration path off LevelDB. **No consensus rules
|
||||
change** — only how the block index / tx index / UTXO set / address index are
|
||||
stored on disk. On-disk key bytes remain identical across both backends, which
|
||||
is what the migration and the dual-backend equivalence tests rely on.
|
||||
|
||||
## What changed
|
||||
|
||||
### 1. Fixed the column-family iteration bug (the real "unfinished" blocker)
|
||||
|
||||
The RocksDB backend routed keys into per-prefix **column families**
|
||||
(`blockindex`, `txindex`, `utxo`, `addrindex`) on write, but the read path —
|
||||
both `CRocksTxDB::NewIterator()` and `CRocksTxDB::LoadBlockIndex()` — only ever
|
||||
iterated the **default** column family. With column families enabled:
|
||||
|
||||
- `LoadBlockIndex()` loaded **zero** blocks (block-index records were in a
|
||||
non-default CF the loader never scanned),
|
||||
- UTXO snapshot dumps and address-index range scans saw nothing, and
|
||||
- the migration verifier `CollectStats()` reported a record-count mismatch.
|
||||
|
||||
This is why `-chaindb=rocksdb` "compiled clean but was never runtime-valid."
|
||||
|
||||
**Fix:** column-family partitioning is disabled. `GetCF()` now always returns
|
||||
the default CF, so writes, point reads, `Exists`, `Erase`, and full-keyspace
|
||||
iteration are mutually consistent — and byte-identical to the single-keyspace
|
||||
LevelDB backend. New databases are created single-CF; pre-existing experimental
|
||||
multi-CF databases are still opened (for compatibility) but should be
|
||||
re-migrated or reindexed. RocksDB still delivers its performance win from
|
||||
parallel compaction, bloom filters, large write buffer, and block cache — the
|
||||
CF split was a premature optimization, not the source of the speedup.
|
||||
|
||||
Re-introducing column families is a tracked follow-up that first requires
|
||||
CF-aware iterators (a multiplexed merge across CFs) in `NewIterator()` /
|
||||
`LoadBlockIndex()`.
|
||||
|
||||
### 2. Automatic LevelDB -> RocksDB migration on startup
|
||||
|
||||
`init.cpp` now runs the migration automatically when RocksDB is the active
|
||||
backend and the only chain DB present is a legacy `txleveldb/` (no `rocksdb/`
|
||||
yet). `MaybeMigrateLevelDbToRocksDb()` is a no-op when there is nothing to
|
||||
migrate, so it is safe on every launch. The LevelDB source is never modified;
|
||||
it remains a fallback.
|
||||
|
||||
### 3. RocksDB is now the default backend
|
||||
|
||||
`-chaindb` defaults to `rocksdb` (was `leveldb`). LevelDB stays selectable with
|
||||
`-chaindb=leveldb` and is retained as migration source + fallback. Full removal
|
||||
of LevelDB is deferred to a later phase, after live-chain validation.
|
||||
|
||||
### 4. Fixed `NeedsBootstrap()` to recognize the RocksDB directory
|
||||
|
||||
`Bootstrap::NeedsBootstrap()` checked for `txleveldb/` but not `rocksdb/`. With
|
||||
RocksDB as default, a fully-synced rocksdb-only node would have been treated as
|
||||
"fresh" and could have triggered a bootstrap download over a healthy chain on
|
||||
every restart. It now treats a `rocksdb/` directory as an existing chain DB.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `src/txdb-rocksdb.cpp` — disable CF routing; single-CF open; remove dead CF tables
|
||||
- `src/txdb-rocksdb.h` — update CF member docs
|
||||
- `src/txdb-factory.cpp` — default backend `leveldb` -> `rocksdb`
|
||||
- `src/txdb.h` — update factory doc comment
|
||||
- `src/init.cpp` — auto-migrate on startup when RocksDB active + legacy LevelDB present
|
||||
- `src/bootstrap.cpp` — `NeedsBootstrap()` recognizes `rocksdb/`
|
||||
- `src/test/chaindb_runtime_tests.cpp` — update default-backend expectations
|
||||
- `README.md` — document RocksDB default + migration
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cmake -B build -G Ninja -DBUILD_QT=ON -DBUILD_TESTS=ON
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
RocksDB is required (`librocksdb-dev` >= 7.4 on Debian/Ubuntu,
|
||||
`mingw-w64-x86_64-rocksdb` on MSYS2, `rocksdb` on Homebrew).
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
# RocksDB wrapper runtime smoke tests (the class the daemon uses at runtime)
|
||||
./build/bin/test_chaindb_runtime
|
||||
|
||||
# LevelDB/RocksDB byte-for-byte migration equivalence
|
||||
./build/bin/test_chaindb_equivalence
|
||||
|
||||
# Full unit suite
|
||||
./build/bin/test_triangles
|
||||
```
|
||||
|
||||
Expected after this change:
|
||||
- `get_chain_data_dir_default_is_rocksdb` passes (default resolves to rocksdb).
|
||||
- `iterator_walks_every_key_in_sorted_order` passes (the `"banana"` key, which
|
||||
previously routed to a non-default CF the iterator never read, now lives in
|
||||
the default CF and is iterated).
|
||||
- Migration verification (`CollectStats` / `StatsMatch`) passes end-to-end.
|
||||
|
||||
## Live-chain validation checklist (V6 task T010)
|
||||
|
||||
This is the step that cannot be done without real chain data and must be run on
|
||||
a node before release:
|
||||
|
||||
1. **Migrate a real chain.** On a node with an existing `txleveldb/`, launch the
|
||||
new binary (default backend). Confirm the log shows
|
||||
`ChainDB: RocksDB backend active with a legacy LevelDB present; migrating
|
||||
automatically.` followed by `ChainDB migration: verified N records ... best=<hash>`.
|
||||
2. **Verify block index loads.** Confirm `LoadBlockIndex()` reports the correct
|
||||
`height=` and `hashBestChain=` (matching the prior LevelDB tip), not 0.
|
||||
3. **Compare RPC output.** `getinfo`, `getblockcount`, `getbestblockhash`, and a
|
||||
spot-check of `gettxout` / address-index queries must match a LevelDB run of
|
||||
the same datadir (`-chaindb=leveldb`).
|
||||
4. **Restart twice.** Confirm no spurious bootstrap download fires and the tip is
|
||||
stable across restarts.
|
||||
5. **Sync new blocks.** Let the node accept and stake new blocks; confirm UTXO
|
||||
set and money supply stay consistent.
|
||||
6. **Benchmark.** Use `contrib/bench/bench-chaindb.sh --backends=rocksdb` vs
|
||||
`leveldb` to confirm the speedup on this hardware.
|
||||
|
||||
## Rollback
|
||||
|
||||
Set `-chaindb=leveldb` in `triangles.conf` (or on the command line). The
|
||||
original `txleveldb/` is untouched by migration, so reverting is immediate.
|
||||
|
||||
## Remaining follow-ups
|
||||
|
||||
- CF-aware iteration, then re-enable column-family partitioning for independent
|
||||
compaction/caching.
|
||||
- Retire LevelDB entirely (remove `txdb-leveldb.*`, drop the `-chaindb=leveldb`
|
||||
option and the bundled LevelDB dependency) once RocksDB is validated in
|
||||
production for at least one release cycle.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Wallet storage: Berkeley DB → SQLite
|
||||
|
||||
Goal: retire Berkeley DB as the wallet store and make **SQLite the default**
|
||||
wallet backend, with a transparent, non-destructive migration of existing
|
||||
`wallet.dat` files. This removes the single ugliest build dependency (BDB 5.3
|
||||
with C++ bindings, hand-built on RHEL/MSYS2) and gives the wallet a modern,
|
||||
maintainable, single-file store — the kind exchanges expect.
|
||||
|
||||
No consensus or wire behavior changes. The on-disk *record encoding* is
|
||||
unchanged: keys and values are the exact `SER_DISK / CLIENT_VERSION` bytes
|
||||
`CWalletDB` already produces, just stored as `(key BLOB, value BLOB)` rows in
|
||||
SQLite instead of Berkeley B-tree entries. That byte-for-byte identity is what
|
||||
makes migration a verbatim copy.
|
||||
|
||||
## Delivered in this pass
|
||||
|
||||
New, self-contained modules (do not disturb the working Berkeley path):
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/walletdb-base.h` | Backend-agnostic seam: `WalletDatabase`, `WalletBatch` (raw byte Read/Write/Erase/Has + cursor + txn), `WalletCursor`; `ResolveWalletDbKind()` / `MakeWalletDatabase()` declarations. |
|
||||
| `src/walletdb-sqlite.h/.cpp` | `SQLiteDatabase` / `SQLiteBatch` — single `main(key BLOB PRIMARY KEY, value BLOB)` table, `synchronous=FULL`, prepared statements, transactions, cursor, online-backup, `integrity_check`. App-id/user-version stamping to reject foreign DBs. |
|
||||
| `src/walletmigrate.h/.cpp` | `MaybeMigrateBerkeleyWalletToSQLite()` — detects a Berkeley `wallet.dat`, copies every record verbatim into a temp SQLite file, verifies the row count, backs up the original to `wallet.dat.bdb.bak`, then swaps SQLite into place. Idempotent and non-destructive. |
|
||||
| `src/walletdb-factory.cpp` | `ResolveWalletDbKind()` (default **sqlite**, `-walletdb=bdb` fallback) and `MakeWalletDatabase()` (SQLite implemented). |
|
||||
| `src/walletdb-batch.h` | `CWalletBatchTyped` — typed Read/Write/Erase/Exists + cursor over `WalletBatch`, byte-identical to the old `CDB` templates. The drop-in base for `CWalletDB`. |
|
||||
|
||||
Build wiring:
|
||||
- `find_package(SQLite3 REQUIRED)` in the top-level `CMakeLists.txt`.
|
||||
- `SQLite::SQLite3` linked into `triangles_common`; the new sources added to `CORE_SOURCES`.
|
||||
|
||||
## Remaining integration (compile-in-the-loop)
|
||||
|
||||
The new modules are complete but `CWalletDB` is not yet routed through the seam
|
||||
— it still inherits Berkeley `CDB`. This is the mechanical-but-careful step that
|
||||
needs a compiler in the loop. **It must be done and landed as one unit** (it
|
||||
touches `walletdb.h`, `walletdb.cpp`, `wallet.cpp`, `db.cpp`, and `init.cpp`):
|
||||
re-basing ~800 lines of funds-critical code is exactly the kind of change that
|
||||
should be compiled and run against a real `wallet.dat` rather than committed
|
||||
blind.
|
||||
|
||||
1. **Typed wrappers over the batch — DONE.** `src/walletdb-batch.h`
|
||||
(`CWalletBatchTyped`) provides `Read/Write/Erase/Exists` + cursor over a
|
||||
`WalletBatch`, byte-identical to `CDB`'s templates. `CWalletDB` derives from
|
||||
it instead of `CDB`.
|
||||
2. **Re-base `CWalletDB`.** Hold a `std::unique_ptr<WalletDatabase>` +
|
||||
`WalletBatch` obtained from `MakeWalletDatabase("wallet.dat", err)` instead of
|
||||
deriving from `CDB`. Route `TxnBegin/Commit/Abort` to the batch.
|
||||
3. **Cursors.** Replace `GetAtCursor` / `GetTxnCursor` / `ReadAtCursor`
|
||||
(Berkeley `Dbc*`, `DB_NEXT`) in `walletdb.cpp` (`LoadWallet`,
|
||||
`ReorderTransactions`) with `WalletBatch::GetNewCursor()` + `WalletCursor::Next()`.
|
||||
4. **Berkeley-specific call sites.**
|
||||
- `BackupWallet()` / `AutoBackupWallet()` → `WalletDatabase::Backup()`.
|
||||
- `CDB::Rewrite()` (used by `CWallet::EncryptWallet`) → `WalletDatabase::Rewrite()`
|
||||
(VACUUM). Unencrypted-key cleanup already happens via explicit `Erase`.
|
||||
- `bitdb.Flush()` / env shutdown in `init.cpp` → `WalletDatabase::Flush()/Close()`
|
||||
(no-op for SQLite).
|
||||
5. **Berkeley behind the same seam (optional but recommended).** Add a thin
|
||||
`BerkeleyDatabase`/`BerkeleyBatch` adapter wrapping the existing `CDBEnv`/`CDB`
|
||||
so `-walletdb=bdb` routes through `MakeWalletDatabase` too, instead of the
|
||||
legacy path. Keeps one code path for one release, then delete BDB entirely.
|
||||
6. **Run the migration on startup.** In `init.cpp`, before the wallet is loaded
|
||||
and when the backend is SQLite, call
|
||||
`MaybeMigrateBerkeleyWalletToSQLite(GetDataDir()/strWalletFileName, err)`.
|
||||
|
||||
## Gating
|
||||
|
||||
```
|
||||
trianglesd # SQLite (default)
|
||||
trianglesd -walletdb=bdb # Berkeley fallback (retained for one release)
|
||||
```
|
||||
|
||||
## Validation checklist (must pass before release)
|
||||
|
||||
Cannot be verified without a build + a real wallet. Run on a node:
|
||||
|
||||
1. **Build** with `-DBUILD_TESTS=ON`; confirm SQLite is found and linked.
|
||||
2. **Fresh wallet**: start with no wallet → a SQLite `wallet.dat` is created;
|
||||
`getnewaddress`, `getinfo` work; restart preserves keys/balance.
|
||||
3. **Migration**: copy a real Berkeley `wallet.dat` into the datadir, start the
|
||||
node. Confirm: `wallet.dat.bdb.bak` is created, `wallet.dat` is now SQLite
|
||||
(`sqlite3 wallet.dat "PRAGMA integrity_check;"` → `ok`), and
|
||||
`listaddressgroupings` / `getbalance` / `dumpwallet` match a `-walletdb=bdb`
|
||||
run against the `.bdb.bak` original.
|
||||
4. **Key parity**: `dumpwallet` before (bdb) and after (sqlite); diff must be
|
||||
empty (same keys, labels, metadata, HD seed).
|
||||
5. **Encryption**: `encryptwallet`, restart, `walletpassphrase`, sign/spend.
|
||||
6. **Backup/restore**: `backupwallet`, restore into a fresh datadir, verify
|
||||
balance and spend.
|
||||
7. **Send/receive + staking** over a few blocks; confirm new keys/txns persist
|
||||
across restart.
|
||||
8. **Crash safety**: kill -9 mid-write; restart; `integrity_check` ok, no loss.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Add `test_wallet_sqlite` unit tests (round-trip, migration parity, cursor).
|
||||
- Once SQLite is validated for a release, remove `-walletdb=bdb`, delete
|
||||
`db.cpp`/`walletdb`'s Berkeley code, and drop the `BerkeleyDB` CMake
|
||||
dependency — completing the retirement.
|
||||
@@ -47,6 +47,30 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "i[3-6]86")
|
||||
add_compile_options(-msse2)
|
||||
endif()
|
||||
|
||||
# ── x86-64 baseline ISA (portability across CPU vendors/models) ──
|
||||
# CRITICAL: Without this, GCC on Intel CI runners (Skylake-X, Ice Lake,
|
||||
# Sapphire Rapids) emits AVX-512 / AVX10 instructions (vmovdqu8, vpcompressd,
|
||||
# vpopcntd, etc.) for std::string / memcpy inlining that CRASH with SIGILL
|
||||
# on AMD EPYC (Milan, Genoa) and older Intel without AVX-512/AVX10.
|
||||
# x86-64-v2 = baseline from ~2009 (Nehalem): SSE4.2 + POPCNT + CMPXCHG16B.
|
||||
# Supported on EVERY x86_64 CPU Triangles runs on in production (DNS2, DNS3,
|
||||
# Hetzner ARM64 excluded — that's a different build). Do NOT raise to v3
|
||||
# (AVX2) without re-testing on every supported CPU; v3 is fine for most
|
||||
# modern hardware but adds risk on edge cases (early Ryzen, Atom).
|
||||
# Override with -DCMAKE_X86_64_BASELINE=OFF to disable (not recommended).
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$" AND NOT WIN32 AND NOT APPLE)
|
||||
option(CMAKE_X86_64_BASELINE
|
||||
"Compile with -march=x86-64-v2 (SSE4.2 baseline) for portability across CPU vendors"
|
||||
ON)
|
||||
if(CMAKE_X86_64_BASELINE)
|
||||
add_compile_options(-march=x86-64-v2)
|
||||
# -mtune=generic tells GCC the binary will run on CPUs other than the
|
||||
# build host. Combined with -march=x86-64-v2 above, the scheduler
|
||||
# picks instructions from the v2 subset only — no AVX-512 leaks.
|
||||
add_compile_options(-mtune=generic)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ── Platform: Windows (MSYS2 MinGW64) ──
|
||||
if(WIN32)
|
||||
add_compile_options(-Wa,-mbig-obj)
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
# I2P support (SAM v3)
|
||||
|
||||
Triangles runs over I2P in addition to Tor, giving the wallet a second
|
||||
anonymous network and a `.b32.i2p` address shown directly above the `.onion`
|
||||
address in the status bar.
|
||||
|
||||
I2P is **on by default** and works the same way as the embedded Tor: the wallet
|
||||
auto-launches a bundled **i2pd** router as a managed child process, enables its
|
||||
SAM bridge, and connects to it. The user does not have to install or configure
|
||||
anything — provided the i2pd binary ships with the wallet.
|
||||
|
||||
## Shipping the i2pd binary
|
||||
|
||||
Like `tor.exe`, the wallet looks for an `i2pd` executable in several places and
|
||||
launches the first one it finds:
|
||||
|
||||
1. Next to the wallet executable (recommended): `i2pd.exe` (Windows) / `i2pd`
|
||||
(Linux/macOS), or in an `i2pd/` subfolder beside it.
|
||||
2. In the data directory (or its `i2pd/` subfolder).
|
||||
3. Common system locations (`/usr/bin/i2pd`, Homebrew, `C:\i2pd\…`, etc.).
|
||||
|
||||
Get i2pd from https://i2pd.website/ (or your package manager) and place the
|
||||
binary next to the wallet in your build/packaging step. That's the only manual
|
||||
part, and it's a packaging concern, not something the end user does.
|
||||
|
||||
If no i2pd binary is found, the wallet logs a notice and continues with **Tor
|
||||
only** — I2P is strictly additive and never blocks start-up.
|
||||
|
||||
## What happens at start-up
|
||||
|
||||
1. If a SAM bridge is already listening on `127.0.0.1:7656` (e.g. you run your
|
||||
own router), the wallet uses it and does **not** launch its own.
|
||||
2. Otherwise it writes `i2pd.conf` into `<datadir>/i2pd/` (SAM enabled, other
|
||||
services off), launches i2pd, and waits for the SAM bridge to come up.
|
||||
3. The SAM client then loads/creates a persistent destination
|
||||
(`<datadir>/i2p_private_key`), opens a STREAM session, derives the
|
||||
`.b32.i2p` address (`base32(SHA-256(destination))`), accepts inbound I2P
|
||||
streams, and dials outbound `.b32.i2p` peers.
|
||||
4. On wallet exit, the SAM session is closed and the i2pd child process is
|
||||
terminated (an external router you started yourself is left running).
|
||||
|
||||
The first session takes a little longer while i2pd builds tunnels; the address
|
||||
appears once the bridge is ready.
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
-i2p Enable I2P; auto-launches bundled i2pd (default: 1; -i2p=0 to disable)
|
||||
-i2psam=<ip:port> SAM bridge address (default: 127.0.0.1:7656).
|
||||
A non-loopback address disables the bundled router and
|
||||
connects to that external bridge instead.
|
||||
```
|
||||
|
||||
## Checking it
|
||||
|
||||
* GUI: the `.b32.i2p` address sits on top of the `.onion` in the status bar;
|
||||
click either to copy.
|
||||
* RPC: `getinfo` shows `toraddress` and `i2paddress`; `getnetworkinfo` shows
|
||||
`toraddress` and an `i2p` object (`enabled`, `active`, `address`, `peers`).
|
||||
|
||||
## Notes / limitations
|
||||
|
||||
* The address serialization format carries a flag for I2P addresses, so **all
|
||||
nodes must run this build** to exchange I2P peers; an old `peers.dat` is
|
||||
discarded.
|
||||
* `i2p_private_key` is your stable I2P identity — back it up, don't delete it.
|
||||
* This was implemented without a build/CI environment here; build and test
|
||||
against a real i2pd before relying on it.
|
||||
@@ -3,7 +3,7 @@
|
||||
# Run on a Linux x64 system with appimagetool installed
|
||||
set -e
|
||||
|
||||
VERSION="5.9.24"
|
||||
VERSION="6.1.0"
|
||||
APPDIR="Triangles-x86_64.AppDir"
|
||||
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Run from the packaging/debian directory
|
||||
set -e
|
||||
|
||||
VERSION="5.9.24"
|
||||
VERSION="6.1.0"
|
||||
PKGDIR="triangles_${VERSION}-1_amd64"
|
||||
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM ubuntu:22.04 AS builder
|
||||
|
||||
ARG VERSION=5.9.24
|
||||
ARG VERSION=6.1.0
|
||||
ARG DEB_URL=https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}/cryptographic-triangles-daemon_${VERSION}_amd64.deb
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
@@ -13,11 +13,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
# ---------- Runtime ----------
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ARG VERSION=5.9.24
|
||||
ARG VERSION=6.1.0
|
||||
|
||||
LABEL maintainer="Cryptographic Triangles Team"
|
||||
LABEL description="Cryptographic Triangles (TRI) headless daemon"
|
||||
LABEL version="5.9.24"
|
||||
LABEL version="6.1.0"
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
|
||||
@@ -3,7 +3,7 @@ version: "3.8"
|
||||
services:
|
||||
trianglesd:
|
||||
build: .
|
||||
image: cryptographic-triangles/trianglesd:5.9.24
|
||||
image: cryptographic-triangles/trianglesd:6.1.0
|
||||
container_name: trianglesd
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
@@ -25,7 +25,7 @@ modules:
|
||||
- install -Dm644 org.cryptographic_triangles.TrianglesQt.metainfo.xml /app/share/metainfo/org.cryptographic_triangles.TrianglesQt.metainfo.xml
|
||||
sources:
|
||||
- type: file
|
||||
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.24/Cryptographic-Triangles-v5.9.24-linux-x64-qt
|
||||
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.1.0/Cryptographic-Triangles-v6.1.0-linux-x64-qt
|
||||
sha256: ed220eb8d0b403f62cdac28988541fd1a27864491e233216f9c00a4c2537b4a3
|
||||
dest-filename: triangles-qt-linux
|
||||
- type: file
|
||||
@@ -55,6 +55,6 @@ modules:
|
||||
- install -Dm755 trianglesd-linux /app/bin/trianglesd
|
||||
sources:
|
||||
- type: file
|
||||
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.24/Cryptographic-Triangles-v5.9.24-linux-x64-daemon
|
||||
url: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.1.0/Cryptographic-Triangles-v6.1.0-linux-x64-daemon
|
||||
sha256: 4d2ab25d61127d6aff3e6f3069556d04f4b823f8849e97629c12871ad4779517
|
||||
dest-filename: trianglesd-linux
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# Install build tools: sudo dnf install rpm-build rpmdevtools
|
||||
set -e
|
||||
|
||||
VERSION="5.9.24"
|
||||
VERSION="6.1.0"
|
||||
RELEASE_URL="https://github.com/SamiAhmed7777/triangles_v5/releases/download/v${VERSION}"
|
||||
|
||||
echo "Building RPM for Triangles v${VERSION}..."
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: triangles
|
||||
Version: 5.9.24
|
||||
Version: 6.1.0
|
||||
Release: 1%{?dist}
|
||||
Summary: Cryptographic Triangles (TRI) cryptocurrency wallet
|
||||
License: MIT
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"version": "5.9.24",
|
||||
"version": "6.1.0",
|
||||
"description": "Cryptographic Triangles (TRI) cryptocurrency wallet with PoS staking and encrypted messaging",
|
||||
"homepage": "https://cryptographic-triangles.org",
|
||||
"license": "MIT",
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.24/Cryptographic-Triangles-5.9.24-win-x64.zip",
|
||||
"url": "https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.1.0/Cryptographic-Triangles-6.1.0-win-x64.zip",
|
||||
"hash": "6f002a669a7e92aaf3d8dd7b1ae80f06a086c99a15ca05cf107665009ffc06b7"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
PackageIdentifier: CryptographicTriangles.TrianglesQt
|
||||
PackageVersion: 5.9.24
|
||||
PackageVersion: 6.1.0
|
||||
PackageLocale: en-US
|
||||
Publisher: Cryptographic Triangles
|
||||
PublisherUrl: https://cryptographic-triangles.org
|
||||
@@ -27,7 +27,7 @@ Installers:
|
||||
- RelativeFilePath: triangles-qt.exe
|
||||
PortableCommandAlias: triangles-qt
|
||||
ArchiveBinariesDependOnPath: true
|
||||
InstallerUrl: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.24/Cryptographic-Triangles-5.9.24-win-x64.zip
|
||||
InstallerUrl: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.1.0/Cryptographic-Triangles-6.1.0-win-x64.zip
|
||||
InstallerSha256: 6F002A669A7E92AAF3D8DD7B1AE80F06A086C99A15CA05CF107665009FFC06B7
|
||||
ManifestType: singleton
|
||||
ManifestVersion: 1.6.0
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
name: triangles
|
||||
base: core22
|
||||
version: '5.9.24'
|
||||
version: '6.1.0'
|
||||
summary: Cryptographic Triangles (TRI) cryptocurrency wallet
|
||||
description: |
|
||||
Privacy-focused cryptocurrency featuring Proof-of-Stake consensus,
|
||||
@@ -51,10 +51,10 @@ apps:
|
||||
parts:
|
||||
triangles:
|
||||
plugin: dump
|
||||
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.24/Cryptographic-Triangles-v5.9.24-linux-x64-qt
|
||||
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.1.0/Cryptographic-Triangles-v6.1.0-linux-x64-qt
|
||||
source-type: file
|
||||
organize:
|
||||
Cryptographic-Triangles-v5.9.24-linux-x64-qt: bin/triangles-qt
|
||||
Cryptographic-Triangles-v6.1.0-linux-x64-qt: bin/triangles-qt
|
||||
stage-packages:
|
||||
- libqt5widgets5
|
||||
- libqt5gui5
|
||||
@@ -73,10 +73,10 @@ parts:
|
||||
|
||||
trianglesd:
|
||||
plugin: dump
|
||||
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v5.9.24/Cryptographic-Triangles-v5.9.24-linux-x64-daemon
|
||||
source: https://github.com/SamiAhmed7777/triangles_v5/releases/download/v6.1.0/Cryptographic-Triangles-v6.1.0-linux-x64-daemon
|
||||
source-type: file
|
||||
organize:
|
||||
Cryptographic-Triangles-v5.9.24-linux-x64-daemon: bin/trianglesd
|
||||
Cryptographic-Triangles-v6.1.0-linux-x64-daemon: bin/trianglesd
|
||||
|
||||
desktop-entry:
|
||||
plugin: dump
|
||||
|
||||
+11
-3
@@ -108,6 +108,15 @@ endif()
|
||||
# for the rationale — RocksDB also backs the smessage store).
|
||||
list(APPEND CORE_SOURCES txdb-rocksdb.cpp)
|
||||
|
||||
# Modernization: SQLite wallet DB backend + Berkeley→SQLite migration.
|
||||
# Built unconditionally; selection happens at runtime via -walletdb.
|
||||
list(APPEND CORE_SOURCES
|
||||
walletdb-factory.cpp
|
||||
walletdb-sqlite.cpp
|
||||
walletdb-recover.cpp
|
||||
walletmigrate.cpp
|
||||
)
|
||||
|
||||
add_library(triangles_common OBJECT ${CORE_SOURCES})
|
||||
|
||||
target_include_directories(triangles_common PUBLIC
|
||||
@@ -126,13 +135,11 @@ target_link_libraries(triangles_common PUBLIC
|
||||
leveldb_bundled
|
||||
OpenSSL::SSL
|
||||
OpenSSL::Crypto
|
||||
Boost::program_options
|
||||
Boost::thread
|
||||
Boost::chrono
|
||||
BerkeleyDB::BerkeleyDB
|
||||
Libevent::Libevent
|
||||
ZLIB::ZLIB
|
||||
Threads::Threads
|
||||
SQLite::SQLite3
|
||||
)
|
||||
|
||||
# Optional: UPnP
|
||||
@@ -522,6 +529,7 @@ if(BUILD_QT)
|
||||
Qt5::Core
|
||||
Qt5::Gui
|
||||
Qt5::Widgets
|
||||
Qt5::Network
|
||||
)
|
||||
|
||||
# Optional: D-Bus notifications (Linux)
|
||||
|
||||
+468
-453
@@ -1,453 +1,468 @@
|
||||
// Copyright (c) 2009-2012 The Bitcoin developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include "checkpoints.h"
|
||||
|
||||
#include "txdb.h"
|
||||
#include "main.h"
|
||||
#include "uint256.h"
|
||||
|
||||
namespace Checkpoints
|
||||
{
|
||||
typedef std::map<int, uint256> MapCheckpoints;
|
||||
|
||||
//
|
||||
// What makes a good checkpoint block?
|
||||
// + Is surrounded by blocks with reasonable timestamps
|
||||
// (no blocks before with a timestamp after, none after with
|
||||
// timestamp before)
|
||||
// + Contains no strange transactions
|
||||
//
|
||||
static MapCheckpoints mapCheckpoints = {
|
||||
{ 0, hashGenesisBlockOfficial },
|
||||
{ 2000, uint256("0x0000000000b5f20078bf46ebdf1500813bb6b2cb482065aa93b89e073b2c6467")},
|
||||
{ 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")},
|
||||
{ 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")},
|
||||
{ 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")},
|
||||
{ 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")},
|
||||
{ 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")},
|
||||
{ 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")},
|
||||
{ 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")},
|
||||
{ 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")},
|
||||
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
|
||||
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
|
||||
// Recent finality pin (PoS era). Closes the long unchecked span from
|
||||
// 17650 to the live tip so stale-bootstrap / low-trust forks below
|
||||
// this height are rejected outright. Hash from the canonical chain.
|
||||
{ 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")},
|
||||
{ 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")},
|
||||
};
|
||||
|
||||
// Published UTXO snapshot file SHA256, keyed by snapshot height.
|
||||
// Each entry binds height -> SHA256 of the canonical snapshot file produced by
|
||||
// UtxoSnapshot::DumpSnapshot at that height. Used by SnapshotNet to verify
|
||||
// P2P-delivered snapshots without trusting any peer.
|
||||
//
|
||||
// Maintainers: after producing a snapshot, sha256 the file and add an entry
|
||||
// here. The corresponding (height, blockhash) must already exist in
|
||||
// mapCheckpoints / mapCheckpointsTestnet.
|
||||
static std::map<int, uint256> mapSnapshotHashes = {
|
||||
{ 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")},
|
||||
};
|
||||
|
||||
static std::map<int, uint256> mapSnapshotHashesTestnet = {
|
||||
};
|
||||
|
||||
static MapCheckpoints mapCheckpointsTestnet = {
|
||||
{ 0, hashGenesisBlockTestNet },
|
||||
{ 2000, uint256("0x0000000000b5f20078bf46ebdf1500813bb6b2cb482065aa93b89e073b2c6467")},
|
||||
{ 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")},
|
||||
{ 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")},
|
||||
{ 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")},
|
||||
{ 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")},
|
||||
{ 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")},
|
||||
{ 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")},
|
||||
{ 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")},
|
||||
{ 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")},
|
||||
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
|
||||
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
|
||||
};
|
||||
|
||||
bool CheckHardened(int nHeight, const uint256& hash)
|
||||
{
|
||||
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
|
||||
|
||||
MapCheckpoints::const_iterator i = checkpoints.find(nHeight);
|
||||
if (i == checkpoints.end()) return true;
|
||||
return hash == i->second;
|
||||
}
|
||||
|
||||
bool IsKnownCheckpoint(int nHeight, const uint256& hash)
|
||||
{
|
||||
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
|
||||
MapCheckpoints::const_iterator i = checkpoints.find(nHeight);
|
||||
if (i == checkpoints.end()) return false;
|
||||
return hash == i->second;
|
||||
}
|
||||
|
||||
int GetTotalBlocksEstimate()
|
||||
{
|
||||
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
|
||||
|
||||
return checkpoints.rbegin()->first;
|
||||
}
|
||||
|
||||
int GetBestSnapshotHeight()
|
||||
{
|
||||
std::map<int, uint256>& snaps = (fTestNet ? mapSnapshotHashesTestnet : mapSnapshotHashes);
|
||||
if (snaps.empty()) return 0;
|
||||
return snaps.rbegin()->first;
|
||||
}
|
||||
|
||||
bool GetSnapshotHash(int nHeight, uint256& fileHashOut)
|
||||
{
|
||||
std::map<int, uint256>& snaps = (fTestNet ? mapSnapshotHashesTestnet : mapSnapshotHashes);
|
||||
auto it = snaps.find(nHeight);
|
||||
if (it == snaps.end()) return false;
|
||||
fileHashOut = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
CBlockIndex* GetLastCheckpoint(const std::map<uint256, CBlockIndex*>& mapBlockIndex)
|
||||
{
|
||||
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
|
||||
|
||||
for (auto it = checkpoints.rbegin(); it != checkpoints.rend(); ++it)
|
||||
{
|
||||
const uint256& hash = it->second;
|
||||
std::map<uint256, CBlockIndex*>::const_iterator t = mapBlockIndex.find(hash);
|
||||
if (t != mapBlockIndex.end())
|
||||
return t->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// triangles: synchronized checkpoint (centrally broadcasted)
|
||||
uint256 hashSyncCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
|
||||
uint256 hashPendingCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
|
||||
CSyncCheckpoint checkpointMessage;
|
||||
CSyncCheckpoint checkpointMessagePending;
|
||||
uint256 hashInvalidCheckpoint = 0;
|
||||
CCriticalSection cs_hashSyncCheckpoint;
|
||||
|
||||
// triangles: get last synchronized checkpoint
|
||||
CBlockIndex* GetLastSyncCheckpoint()
|
||||
{
|
||||
LOCK(cs_hashSyncCheckpoint);
|
||||
if (!mapBlockIndex.count(hashSyncCheckpoint))
|
||||
error("GetSyncCheckpoint: block index missing for current sync-checkpoint %s", hashSyncCheckpoint.ToString().c_str());
|
||||
else
|
||||
return mapBlockIndex[hashSyncCheckpoint];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// triangles: only descendant of current sync-checkpoint is allowed
|
||||
bool ValidateSyncCheckpoint(uint256 hashCheckpoint)
|
||||
{
|
||||
if (!mapBlockIndex.count(hashSyncCheckpoint))
|
||||
return error("ValidateSyncCheckpoint: block index missing for current sync-checkpoint %s", hashSyncCheckpoint.ToString().c_str());
|
||||
if (!mapBlockIndex.count(hashCheckpoint))
|
||||
return error("ValidateSyncCheckpoint: block index missing for received sync-checkpoint %s", hashCheckpoint.ToString().c_str());
|
||||
|
||||
CBlockIndex* pindexSyncCheckpoint = mapBlockIndex[hashSyncCheckpoint];
|
||||
CBlockIndex* pindexCheckpointRecv = mapBlockIndex[hashCheckpoint];
|
||||
|
||||
if (pindexCheckpointRecv->nHeight <= pindexSyncCheckpoint->nHeight)
|
||||
{
|
||||
// Received an older checkpoint, trace back from current checkpoint
|
||||
// to the same height of the received checkpoint to verify
|
||||
// that current checkpoint should be a descendant block
|
||||
CBlockIndex* pindex = pindexSyncCheckpoint;
|
||||
while (pindex->nHeight > pindexCheckpointRecv->nHeight)
|
||||
if (!(pindex = pindex->pprev))
|
||||
return error("ValidateSyncCheckpoint: pprev null - block index structure failure");
|
||||
if (pindex->GetBlockHash() != hashCheckpoint)
|
||||
{
|
||||
hashInvalidCheckpoint = hashCheckpoint;
|
||||
return error("ValidateSyncCheckpoint: new sync-checkpoint %s is conflicting with current sync-checkpoint %s", hashCheckpoint.ToString().c_str(), hashSyncCheckpoint.ToString().c_str());
|
||||
}
|
||||
return false; // ignore older checkpoint
|
||||
}
|
||||
|
||||
// Received checkpoint should be a descendant block of the current
|
||||
// checkpoint. Trace back to the same height of current checkpoint
|
||||
// to verify.
|
||||
CBlockIndex* pindex = pindexCheckpointRecv;
|
||||
while (pindex->nHeight > pindexSyncCheckpoint->nHeight)
|
||||
if (!(pindex = pindex->pprev))
|
||||
return error("ValidateSyncCheckpoint: pprev2 null - block index structure failure");
|
||||
if (pindex->GetBlockHash() != hashSyncCheckpoint)
|
||||
{
|
||||
hashInvalidCheckpoint = hashCheckpoint;
|
||||
return error("ValidateSyncCheckpoint: new sync-checkpoint %s is not a descendant of current sync-checkpoint %s", hashCheckpoint.ToString().c_str(), hashSyncCheckpoint.ToString().c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteSyncCheckpoint(const uint256& hashCheckpoint)
|
||||
{
|
||||
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
|
||||
txdb.TxnBegin();
|
||||
if (!txdb.WriteSyncCheckpoint(hashCheckpoint))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("WriteSyncCheckpoint(): failed to write to db sync checkpoint %s", hashCheckpoint.ToString().c_str());
|
||||
}
|
||||
if (!txdb.TxnCommit())
|
||||
return error("WriteSyncCheckpoint(): failed to commit to db sync checkpoint %s", hashCheckpoint.ToString().c_str());
|
||||
|
||||
Checkpoints::hashSyncCheckpoint = hashCheckpoint;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AcceptPendingSyncCheckpoint()
|
||||
{
|
||||
LOCK(cs_hashSyncCheckpoint);
|
||||
if (hashPendingCheckpoint != 0 && mapBlockIndex.count(hashPendingCheckpoint))
|
||||
{
|
||||
if (!ValidateSyncCheckpoint(hashPendingCheckpoint))
|
||||
{
|
||||
hashPendingCheckpoint = 0;
|
||||
checkpointMessagePending.SetNull();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
|
||||
CBlockIndex* pindexCheckpoint = mapBlockIndex[hashPendingCheckpoint];
|
||||
if (!pindexCheckpoint->IsInMainChain())
|
||||
{
|
||||
CBlock block;
|
||||
if (!block.ReadFromDisk(pindexCheckpoint))
|
||||
return error("AcceptPendingSyncCheckpoint: ReadFromDisk failed for sync checkpoint %s", hashPendingCheckpoint.ToString().c_str());
|
||||
if (!block.SetBestChain(txdb, pindexCheckpoint))
|
||||
{
|
||||
hashInvalidCheckpoint = hashPendingCheckpoint;
|
||||
return error("AcceptPendingSyncCheckpoint: SetBestChain failed for sync checkpoint %s", hashPendingCheckpoint.ToString().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!WriteSyncCheckpoint(hashPendingCheckpoint))
|
||||
return error("AcceptPendingSyncCheckpoint(): failed to write sync checkpoint %s", hashPendingCheckpoint.ToString().c_str());
|
||||
hashPendingCheckpoint = 0;
|
||||
checkpointMessage = checkpointMessagePending;
|
||||
checkpointMessagePending.SetNull();
|
||||
printf("AcceptPendingSyncCheckpoint : sync-checkpoint at %s\n", hashSyncCheckpoint.ToString().c_str());
|
||||
// relay the checkpoint
|
||||
if (!checkpointMessage.IsNull())
|
||||
{
|
||||
for (CNode* pnode : vNodes)
|
||||
checkpointMessage.RelayTo(pnode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Automatically select a suitable sync-checkpoint
|
||||
uint256 AutoSelectSyncCheckpoint()
|
||||
{
|
||||
const CBlockIndex *pindex = pindexBest;
|
||||
// Search backward for a block within max span and maturity window
|
||||
while (pindex->pprev && (pindex->GetBlockTime() + CHECKPOINT_MAX_SPAN > pindexBest->GetBlockTime() || pindex->nHeight + 8 > pindexBest->nHeight))
|
||||
pindex = pindex->pprev;
|
||||
return pindex->GetBlockHash();
|
||||
}
|
||||
|
||||
// Check against synchronized checkpoint
|
||||
// Disabled: master key removed in V5, no new sync checkpoints possible.
|
||||
// Always returns true to prevent stale DB-persisted checkpoints from blocking IBD.
|
||||
bool CheckSync(const uint256& hashBlock, const CBlockIndex* pindexPrev)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WantedByPendingSyncCheckpoint(uint256 hashBlock)
|
||||
{
|
||||
LOCK(cs_hashSyncCheckpoint);
|
||||
if (hashPendingCheckpoint == 0)
|
||||
return false;
|
||||
if (hashBlock == hashPendingCheckpoint)
|
||||
return true;
|
||||
if (mapOrphanBlocks.count(hashPendingCheckpoint)
|
||||
&& hashBlock == WantedByOrphan(mapOrphanBlocks[hashPendingCheckpoint].get()))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// triangles: reset synchronized checkpoint to last hardened checkpoint
|
||||
bool ResetSyncCheckpoint()
|
||||
{
|
||||
LOCK(cs_hashSyncCheckpoint);
|
||||
const uint256& hash = mapCheckpoints.rbegin()->second;
|
||||
if (mapBlockIndex.count(hash) && !mapBlockIndex[hash]->IsInMainChain())
|
||||
{
|
||||
// checkpoint block accepted but not yet in main chain
|
||||
printf("ResetSyncCheckpoint: SetBestChain to hardened checkpoint %s\n", hash.ToString().c_str());
|
||||
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
|
||||
CBlock block;
|
||||
if (!block.ReadFromDisk(mapBlockIndex[hash]))
|
||||
return error("ResetSyncCheckpoint: ReadFromDisk failed for hardened checkpoint %s", hash.ToString().c_str());
|
||||
if (!block.SetBestChain(txdb, mapBlockIndex[hash]))
|
||||
{
|
||||
return error("ResetSyncCheckpoint: SetBestChain failed for hardened checkpoint %s", hash.ToString().c_str());
|
||||
}
|
||||
}
|
||||
else if(!mapBlockIndex.count(hash))
|
||||
{
|
||||
// checkpoint block not yet accepted
|
||||
hashPendingCheckpoint = hash;
|
||||
checkpointMessagePending.SetNull();
|
||||
printf("ResetSyncCheckpoint: pending for sync-checkpoint %s\n", hashPendingCheckpoint.ToString().c_str());
|
||||
}
|
||||
|
||||
for (auto it = mapCheckpoints.rbegin(); it != mapCheckpoints.rend(); ++it)
|
||||
{
|
||||
const uint256& hash = it->second;
|
||||
if (mapBlockIndex.count(hash) && mapBlockIndex[hash]->IsInMainChain())
|
||||
{
|
||||
if (!WriteSyncCheckpoint(hash))
|
||||
return error("ResetSyncCheckpoint: failed to write sync checkpoint %s", hash.ToString().c_str());
|
||||
printf("ResetSyncCheckpoint: sync-checkpoint reset to %s\n", hashSyncCheckpoint.ToString().c_str());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AskForPendingSyncCheckpoint(CNode* pfrom)
|
||||
{
|
||||
LOCK(cs_hashSyncCheckpoint);
|
||||
if (pfrom && hashPendingCheckpoint != 0 && (!mapBlockIndex.count(hashPendingCheckpoint)) && (!mapOrphanBlocks.count(hashPendingCheckpoint)))
|
||||
pfrom->AskFor(CInv(MSG_BLOCK, hashPendingCheckpoint));
|
||||
}
|
||||
|
||||
bool SetCheckpointPrivKey(std::string strPrivKey)
|
||||
{
|
||||
// Test signing a sync-checkpoint with genesis block
|
||||
CSyncCheckpoint checkpoint;
|
||||
checkpoint.hashCheckpoint = !fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet;
|
||||
CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION);
|
||||
sMsg << (CUnsignedSyncCheckpoint)checkpoint;
|
||||
checkpoint.vchMsg = std::vector<unsigned char>(sMsg.begin(), sMsg.end());
|
||||
|
||||
std::vector<unsigned char> vchPrivKey = ParseHex(strPrivKey);
|
||||
CKey key;
|
||||
key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash
|
||||
if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig))
|
||||
return false;
|
||||
|
||||
// Test signing successful, proceed
|
||||
CSyncCheckpoint::strMasterPrivKey = strPrivKey;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SendSyncCheckpoint(uint256 hashCheckpoint)
|
||||
{
|
||||
CSyncCheckpoint checkpoint;
|
||||
checkpoint.hashCheckpoint = hashCheckpoint;
|
||||
CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION);
|
||||
sMsg << (CUnsignedSyncCheckpoint)checkpoint;
|
||||
checkpoint.vchMsg = std::vector<unsigned char>(sMsg.begin(), sMsg.end());
|
||||
|
||||
if (CSyncCheckpoint::strMasterPrivKey.empty())
|
||||
return error("SendSyncCheckpoint: Checkpoint master key unavailable.");
|
||||
std::vector<unsigned char> vchPrivKey = ParseHex(CSyncCheckpoint::strMasterPrivKey);
|
||||
CKey key;
|
||||
key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash
|
||||
if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig))
|
||||
return error("SendSyncCheckpoint: Unable to sign checkpoint, check private key?");
|
||||
|
||||
if(!checkpoint.ProcessSyncCheckpoint(nullptr))
|
||||
{
|
||||
printf("WARNING: SendSyncCheckpoint: Failed to process checkpoint.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Relay checkpoint
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
checkpoint.RelayTo(pnode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is the sync-checkpoint outside maturity window?
|
||||
bool IsMatureSyncCheckpoint()
|
||||
{
|
||||
LOCK(cs_hashSyncCheckpoint);
|
||||
if (!mapBlockIndex.count(hashSyncCheckpoint))
|
||||
return true; // no valid sync checkpoint, treat as mature
|
||||
const CBlockIndex* pindexSync = mapBlockIndex[hashSyncCheckpoint];
|
||||
return (nBestHeight >= pindexSync->nHeight + nCoinbaseMaturity ||
|
||||
pindexSync->GetBlockTime() + nStakeMinAge < GetAdjustedTime());
|
||||
}
|
||||
}
|
||||
|
||||
// triangles: sync-checkpoint master key (DISABLED for decentralization - v5 hard fork)
|
||||
const std::string CSyncCheckpoint::strMasterPubKey = "";
|
||||
|
||||
std::string CSyncCheckpoint::strMasterPrivKey = "";
|
||||
|
||||
// triangles: verify signature of sync-checkpoint message
|
||||
// Master key system disabled - checkpoint signatures are no longer required
|
||||
bool CSyncCheckpoint::CheckSignature()
|
||||
{
|
||||
// Deserialize the checkpoint data without signature verification
|
||||
CDataStream sMsg(vchMsg, SER_NETWORK, PROTOCOL_VERSION);
|
||||
sMsg >> *(CUnsignedSyncCheckpoint*)this;
|
||||
return true;
|
||||
}
|
||||
|
||||
// triangles: process synchronized checkpoint
|
||||
bool CSyncCheckpoint::ProcessSyncCheckpoint(CNode* pfrom)
|
||||
{
|
||||
if (!CheckSignature())
|
||||
return false;
|
||||
|
||||
LOCK(Checkpoints::cs_hashSyncCheckpoint);
|
||||
if (!mapBlockIndex.count(hashCheckpoint))
|
||||
{
|
||||
// We haven't received the checkpoint chain, keep the checkpoint as pending
|
||||
Checkpoints::hashPendingCheckpoint = hashCheckpoint;
|
||||
Checkpoints::checkpointMessagePending = *this;
|
||||
printf("ProcessSyncCheckpoint: pending for sync-checkpoint %s\n", hashCheckpoint.ToString().c_str());
|
||||
// Ask this guy to fill in what we're missing
|
||||
if (pfrom)
|
||||
{
|
||||
pfrom->PushGetBlocks(pindexBest, hashCheckpoint);
|
||||
// ask directly as well in case rejected earlier by duplicate
|
||||
// proof-of-stake because getblocks may not get it this time
|
||||
pfrom->AskFor(CInv(MSG_BLOCK, mapOrphanBlocks.count(hashCheckpoint)? WantedByOrphan(mapOrphanBlocks[hashCheckpoint].get()) : hashCheckpoint));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Checkpoints::ValidateSyncCheckpoint(hashCheckpoint))
|
||||
return false;
|
||||
|
||||
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
|
||||
CBlockIndex* pindexCheckpoint = mapBlockIndex[hashCheckpoint];
|
||||
if (!pindexCheckpoint->IsInMainChain())
|
||||
{
|
||||
// checkpoint chain received but not yet main chain
|
||||
CBlock block;
|
||||
if (!block.ReadFromDisk(pindexCheckpoint))
|
||||
return error("ProcessSyncCheckpoint: ReadFromDisk failed for sync checkpoint %s", hashCheckpoint.ToString().c_str());
|
||||
if (!block.SetBestChain(txdb, pindexCheckpoint))
|
||||
{
|
||||
Checkpoints::hashInvalidCheckpoint = hashCheckpoint;
|
||||
return error("ProcessSyncCheckpoint: SetBestChain failed for sync checkpoint %s", hashCheckpoint.ToString().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!Checkpoints::WriteSyncCheckpoint(hashCheckpoint))
|
||||
return error("ProcessSyncCheckpoint(): failed to write sync checkpoint %s", hashCheckpoint.ToString().c_str());
|
||||
Checkpoints::checkpointMessage = *this;
|
||||
Checkpoints::hashPendingCheckpoint = 0;
|
||||
Checkpoints::checkpointMessagePending.SetNull();
|
||||
printf("ProcessSyncCheckpoint: sync-checkpoint at %s\n", hashCheckpoint.ToString().c_str());
|
||||
return true;
|
||||
}
|
||||
// Copyright (c) 2009-2012 The Bitcoin developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include "checkpoints.h"
|
||||
|
||||
#include "txdb.h"
|
||||
#include "main.h"
|
||||
#include "uint256.h"
|
||||
|
||||
namespace Checkpoints
|
||||
{
|
||||
typedef std::map<int, uint256> MapCheckpoints;
|
||||
|
||||
//
|
||||
// What makes a good checkpoint block?
|
||||
// + Is surrounded by blocks with reasonable timestamps
|
||||
// (no blocks before with a timestamp after, none after with
|
||||
// timestamp before)
|
||||
// + Contains no strange transactions
|
||||
//
|
||||
static MapCheckpoints mapCheckpoints = {
|
||||
{ 0, hashGenesisBlockOfficial },
|
||||
{ 2000, uint256("0x0000000000b5f20078bf46ebdf1500813bb6b2cb482065aa93b89e073b2c6467")},
|
||||
{ 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")},
|
||||
{ 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")},
|
||||
{ 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")},
|
||||
{ 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")},
|
||||
{ 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")},
|
||||
{ 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")},
|
||||
{ 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")},
|
||||
{ 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")},
|
||||
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
|
||||
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
|
||||
// Recent finality pin (PoS era). Closes the long unchecked span from
|
||||
// 17650 to the live tip so stale-bootstrap / low-trust forks below
|
||||
// this height are rejected outright. Hash from the canonical chain.
|
||||
{ 2205000, uint256("0x6bdd3c5e5a32e1dd9a70e705f1a28d1dd84929f89579bd2696d41bc87f39446f")},
|
||||
{ 2206004, uint256("0xb34e8e6a7bb7f52167d81aaad4d26f87a876898fdd0fce860916fc1aaf9a2a46")},
|
||||
// Continuous finality pins: every 1000 blocks from 2206500 onward so the
|
||||
// gap between the last hardcoded checkpoint and the live tip stays bounded.
|
||||
// Without these, a fresh node syncing from zero (no snapshot) has 8,400+
|
||||
// unverified blocks at tip — a peer feeding fork blocks at those heights
|
||||
// could trick an IBD node into accepting a divergent chain. With these
|
||||
// pins, any divergence >1000 blocks is rejected at AcceptBlock time.
|
||||
// All hashes verified against the canonical chain on 2026-07-01.
|
||||
{ 2206500, uint256("0x707ea288242227e9b36ceeeecd5a16a6c918f8b6f7e6375128cba908ebfcbf27")},
|
||||
{ 2207000, uint256("0x7af1cc23fdffb3a9ed2eb9aa5a8697e8af2f98c67c4f6baa9f4d7899cbfaf4ca")},
|
||||
{ 2210000, uint256("0xe2dc2e55c6e1b3d2ea9d8a1f2b274bf64053ddd6a61335dc6896aa9c056956be")},
|
||||
{ 2211000, uint256("0x61c8a179c928a1f0bbffa029b4f1aea67b04a98227a6d02e6137280404ed29dc")},
|
||||
{ 2212000, uint256("0xf4df2b5d0d1de326b97ed5a3eeefef307a51791e03af401373e142f00453a9a8")},
|
||||
{ 2213000, uint256("0x7bc9652d423676c52ba8b0a287e0b46e1eca6e8eecc51d3f30e0d665d3b236f5")},
|
||||
{ 2214000, uint256("0x17e61ceb45db36358aaabe91b094a77ecba32370a467185fa9af75eef6c8e414")},
|
||||
{ 2214400, uint256("0x8ebb818f7280850c5a3916b7c8a2bca603f7c4f9926d3cdc2262f726035d96ed")},
|
||||
};
|
||||
|
||||
// Published UTXO snapshot file SHA256, keyed by snapshot height.
|
||||
// Each entry binds height -> SHA256 of the canonical snapshot file produced by
|
||||
// UtxoSnapshot::DumpSnapshot at that height. Used by SnapshotNet to verify
|
||||
// P2P-delivered snapshots without trusting any peer.
|
||||
//
|
||||
// Maintainers: after producing a snapshot, sha256 the file and add an entry
|
||||
// here. The corresponding (height, blockhash) must already exist in
|
||||
// mapCheckpoints / mapCheckpointsTestnet.
|
||||
static std::map<int, uint256> mapSnapshotHashes = {
|
||||
{ 2206004, uint256("0x1419282dae817315ee1b955543f6248233fe5800f5e8488734a0ece5bd6781ea")},
|
||||
};
|
||||
|
||||
static std::map<int, uint256> mapSnapshotHashesTestnet = {
|
||||
};
|
||||
|
||||
static MapCheckpoints mapCheckpointsTestnet = {
|
||||
{ 0, hashGenesisBlockTestNet },
|
||||
{ 2000, uint256("0x0000000000b5f20078bf46ebdf1500813bb6b2cb482065aa93b89e073b2c6467")},
|
||||
{ 2101, uint256("0xd4ea1ac45b63c8162a7fc8033cec441db8d532ba988202849d7831e32fe2d059")},
|
||||
{ 2847, uint256("0xb5015e2835f13fd3bb6135cff9b31ac33310c9b77d694bdb592b8680d98d018e")},
|
||||
{ 3589, uint256("0xb12a2ca3db4e288cada98aa2139768532bd4474c49dd7b8158031032dac08d51")},
|
||||
{ 3935, uint256("0xe16290c9757d1368b8d7c35de07d4f8f70c2c9f9c785b667df0c3bff85086ca6")},
|
||||
{ 5703, uint256("0x587db07bb2172ad7db72c5fabc2518262a1b27f503f99417510b2c6fafa6557b")},
|
||||
{ 9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db")},
|
||||
{ 9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007")},
|
||||
{ 9002, uint256("0xa1e20fb1d44688b763690cf74d6aefe859e4cc32981f9e3f2b2ae9702bbcf249")},
|
||||
{ 10881, uint256("0x4b6554c45e1e6764a6f3c309c47baf53c9edd81f624e52b072518cd15da237e6")},
|
||||
{ 17650, uint256("0x224940e1f986a202209b8e762728d1452ab45870c308abf84905674acf326a47")},
|
||||
};
|
||||
|
||||
bool CheckHardened(int nHeight, const uint256& hash)
|
||||
{
|
||||
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
|
||||
|
||||
MapCheckpoints::const_iterator i = checkpoints.find(nHeight);
|
||||
if (i == checkpoints.end()) return true;
|
||||
return hash == i->second;
|
||||
}
|
||||
|
||||
bool IsKnownCheckpoint(int nHeight, const uint256& hash)
|
||||
{
|
||||
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
|
||||
MapCheckpoints::const_iterator i = checkpoints.find(nHeight);
|
||||
if (i == checkpoints.end()) return false;
|
||||
return hash == i->second;
|
||||
}
|
||||
|
||||
int GetTotalBlocksEstimate()
|
||||
{
|
||||
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
|
||||
|
||||
return checkpoints.rbegin()->first;
|
||||
}
|
||||
|
||||
int GetBestSnapshotHeight()
|
||||
{
|
||||
std::map<int, uint256>& snaps = (fTestNet ? mapSnapshotHashesTestnet : mapSnapshotHashes);
|
||||
if (snaps.empty()) return 0;
|
||||
return snaps.rbegin()->first;
|
||||
}
|
||||
|
||||
bool GetSnapshotHash(int nHeight, uint256& fileHashOut)
|
||||
{
|
||||
std::map<int, uint256>& snaps = (fTestNet ? mapSnapshotHashesTestnet : mapSnapshotHashes);
|
||||
auto it = snaps.find(nHeight);
|
||||
if (it == snaps.end()) return false;
|
||||
fileHashOut = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
CBlockIndex* GetLastCheckpoint(const std::map<uint256, CBlockIndex*>& mapBlockIndex)
|
||||
{
|
||||
MapCheckpoints& checkpoints = (fTestNet ? mapCheckpointsTestnet : mapCheckpoints);
|
||||
|
||||
for (auto it = checkpoints.rbegin(); it != checkpoints.rend(); ++it)
|
||||
{
|
||||
const uint256& hash = it->second;
|
||||
std::map<uint256, CBlockIndex*>::const_iterator t = mapBlockIndex.find(hash);
|
||||
if (t != mapBlockIndex.end())
|
||||
return t->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// triangles: synchronized checkpoint (centrally broadcasted)
|
||||
uint256 hashSyncCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
|
||||
uint256 hashPendingCheckpoint = uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021");
|
||||
CSyncCheckpoint checkpointMessage;
|
||||
CSyncCheckpoint checkpointMessagePending;
|
||||
uint256 hashInvalidCheckpoint = 0;
|
||||
CCriticalSection cs_hashSyncCheckpoint;
|
||||
|
||||
// triangles: get last synchronized checkpoint
|
||||
CBlockIndex* GetLastSyncCheckpoint()
|
||||
{
|
||||
LOCK(cs_hashSyncCheckpoint);
|
||||
if (!mapBlockIndex.count(hashSyncCheckpoint))
|
||||
error("GetSyncCheckpoint: block index missing for current sync-checkpoint %s", hashSyncCheckpoint.ToString().c_str());
|
||||
else
|
||||
return mapBlockIndex[hashSyncCheckpoint];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// triangles: only descendant of current sync-checkpoint is allowed
|
||||
bool ValidateSyncCheckpoint(uint256 hashCheckpoint)
|
||||
{
|
||||
if (!mapBlockIndex.count(hashSyncCheckpoint))
|
||||
return error("ValidateSyncCheckpoint: block index missing for current sync-checkpoint %s", hashSyncCheckpoint.ToString().c_str());
|
||||
if (!mapBlockIndex.count(hashCheckpoint))
|
||||
return error("ValidateSyncCheckpoint: block index missing for received sync-checkpoint %s", hashCheckpoint.ToString().c_str());
|
||||
|
||||
CBlockIndex* pindexSyncCheckpoint = mapBlockIndex[hashSyncCheckpoint];
|
||||
CBlockIndex* pindexCheckpointRecv = mapBlockIndex[hashCheckpoint];
|
||||
|
||||
if (pindexCheckpointRecv->nHeight <= pindexSyncCheckpoint->nHeight)
|
||||
{
|
||||
// Received an older checkpoint, trace back from current checkpoint
|
||||
// to the same height of the received checkpoint to verify
|
||||
// that current checkpoint should be a descendant block
|
||||
CBlockIndex* pindex = pindexSyncCheckpoint;
|
||||
while (pindex->nHeight > pindexCheckpointRecv->nHeight)
|
||||
if (!(pindex = pindex->pprev))
|
||||
return error("ValidateSyncCheckpoint: pprev null - block index structure failure");
|
||||
if (pindex->GetBlockHash() != hashCheckpoint)
|
||||
{
|
||||
hashInvalidCheckpoint = hashCheckpoint;
|
||||
return error("ValidateSyncCheckpoint: new sync-checkpoint %s is conflicting with current sync-checkpoint %s", hashCheckpoint.ToString().c_str(), hashSyncCheckpoint.ToString().c_str());
|
||||
}
|
||||
return false; // ignore older checkpoint
|
||||
}
|
||||
|
||||
// Received checkpoint should be a descendant block of the current
|
||||
// checkpoint. Trace back to the same height of current checkpoint
|
||||
// to verify.
|
||||
CBlockIndex* pindex = pindexCheckpointRecv;
|
||||
while (pindex->nHeight > pindexSyncCheckpoint->nHeight)
|
||||
if (!(pindex = pindex->pprev))
|
||||
return error("ValidateSyncCheckpoint: pprev2 null - block index structure failure");
|
||||
if (pindex->GetBlockHash() != hashSyncCheckpoint)
|
||||
{
|
||||
hashInvalidCheckpoint = hashCheckpoint;
|
||||
return error("ValidateSyncCheckpoint: new sync-checkpoint %s is not a descendant of current sync-checkpoint %s", hashCheckpoint.ToString().c_str(), hashSyncCheckpoint.ToString().c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteSyncCheckpoint(const uint256& hashCheckpoint)
|
||||
{
|
||||
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
|
||||
txdb.TxnBegin();
|
||||
if (!txdb.WriteSyncCheckpoint(hashCheckpoint))
|
||||
{
|
||||
txdb.TxnAbort();
|
||||
return error("WriteSyncCheckpoint(): failed to write to db sync checkpoint %s", hashCheckpoint.ToString().c_str());
|
||||
}
|
||||
if (!txdb.TxnCommit())
|
||||
return error("WriteSyncCheckpoint(): failed to commit to db sync checkpoint %s", hashCheckpoint.ToString().c_str());
|
||||
|
||||
Checkpoints::hashSyncCheckpoint = hashCheckpoint;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AcceptPendingSyncCheckpoint()
|
||||
{
|
||||
LOCK(cs_hashSyncCheckpoint);
|
||||
if (hashPendingCheckpoint != 0 && mapBlockIndex.count(hashPendingCheckpoint))
|
||||
{
|
||||
if (!ValidateSyncCheckpoint(hashPendingCheckpoint))
|
||||
{
|
||||
hashPendingCheckpoint = 0;
|
||||
checkpointMessagePending.SetNull();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
|
||||
CBlockIndex* pindexCheckpoint = mapBlockIndex[hashPendingCheckpoint];
|
||||
if (!pindexCheckpoint->IsInMainChain())
|
||||
{
|
||||
CBlock block;
|
||||
if (!block.ReadFromDisk(pindexCheckpoint))
|
||||
return error("AcceptPendingSyncCheckpoint: ReadFromDisk failed for sync checkpoint %s", hashPendingCheckpoint.ToString().c_str());
|
||||
if (!block.SetBestChain(txdb, pindexCheckpoint))
|
||||
{
|
||||
hashInvalidCheckpoint = hashPendingCheckpoint;
|
||||
return error("AcceptPendingSyncCheckpoint: SetBestChain failed for sync checkpoint %s", hashPendingCheckpoint.ToString().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!WriteSyncCheckpoint(hashPendingCheckpoint))
|
||||
return error("AcceptPendingSyncCheckpoint(): failed to write sync checkpoint %s", hashPendingCheckpoint.ToString().c_str());
|
||||
hashPendingCheckpoint = 0;
|
||||
checkpointMessage = checkpointMessagePending;
|
||||
checkpointMessagePending.SetNull();
|
||||
printf("AcceptPendingSyncCheckpoint : sync-checkpoint at %s\n", hashSyncCheckpoint.ToString().c_str());
|
||||
// relay the checkpoint
|
||||
if (!checkpointMessage.IsNull())
|
||||
{
|
||||
for (CNode* pnode : vNodes)
|
||||
checkpointMessage.RelayTo(pnode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Automatically select a suitable sync-checkpoint
|
||||
uint256 AutoSelectSyncCheckpoint()
|
||||
{
|
||||
const CBlockIndex *pindex = pindexBest;
|
||||
// Search backward for a block within max span and maturity window
|
||||
while (pindex->pprev && (pindex->GetBlockTime() + CHECKPOINT_MAX_SPAN > pindexBest->GetBlockTime() || pindex->nHeight + 8 > pindexBest->nHeight))
|
||||
pindex = pindex->pprev;
|
||||
return pindex->GetBlockHash();
|
||||
}
|
||||
|
||||
// Check against synchronized checkpoint
|
||||
// Disabled: master key removed in V5, no new sync checkpoints possible.
|
||||
// Always returns true to prevent stale DB-persisted checkpoints from blocking IBD.
|
||||
bool CheckSync(const uint256& hashBlock, const CBlockIndex* pindexPrev)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WantedByPendingSyncCheckpoint(uint256 hashBlock)
|
||||
{
|
||||
LOCK(cs_hashSyncCheckpoint);
|
||||
if (hashPendingCheckpoint == 0)
|
||||
return false;
|
||||
if (hashBlock == hashPendingCheckpoint)
|
||||
return true;
|
||||
if (mapOrphanBlocks.count(hashPendingCheckpoint)
|
||||
&& hashBlock == WantedByOrphan(mapOrphanBlocks[hashPendingCheckpoint].get()))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// triangles: reset synchronized checkpoint to last hardened checkpoint
|
||||
bool ResetSyncCheckpoint()
|
||||
{
|
||||
LOCK(cs_hashSyncCheckpoint);
|
||||
const uint256& hash = mapCheckpoints.rbegin()->second;
|
||||
if (mapBlockIndex.count(hash) && !mapBlockIndex[hash]->IsInMainChain())
|
||||
{
|
||||
// checkpoint block accepted but not yet in main chain
|
||||
printf("ResetSyncCheckpoint: SetBestChain to hardened checkpoint %s\n", hash.ToString().c_str());
|
||||
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
|
||||
CBlock block;
|
||||
if (!block.ReadFromDisk(mapBlockIndex[hash]))
|
||||
return error("ResetSyncCheckpoint: ReadFromDisk failed for hardened checkpoint %s", hash.ToString().c_str());
|
||||
if (!block.SetBestChain(txdb, mapBlockIndex[hash]))
|
||||
{
|
||||
return error("ResetSyncCheckpoint: SetBestChain failed for hardened checkpoint %s", hash.ToString().c_str());
|
||||
}
|
||||
}
|
||||
else if(!mapBlockIndex.count(hash))
|
||||
{
|
||||
// checkpoint block not yet accepted
|
||||
hashPendingCheckpoint = hash;
|
||||
checkpointMessagePending.SetNull();
|
||||
printf("ResetSyncCheckpoint: pending for sync-checkpoint %s\n", hashPendingCheckpoint.ToString().c_str());
|
||||
}
|
||||
|
||||
for (auto it = mapCheckpoints.rbegin(); it != mapCheckpoints.rend(); ++it)
|
||||
{
|
||||
const uint256& hash = it->second;
|
||||
if (mapBlockIndex.count(hash) && mapBlockIndex[hash]->IsInMainChain())
|
||||
{
|
||||
if (!WriteSyncCheckpoint(hash))
|
||||
return error("ResetSyncCheckpoint: failed to write sync checkpoint %s", hash.ToString().c_str());
|
||||
printf("ResetSyncCheckpoint: sync-checkpoint reset to %s\n", hashSyncCheckpoint.ToString().c_str());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AskForPendingSyncCheckpoint(CNode* pfrom)
|
||||
{
|
||||
LOCK(cs_hashSyncCheckpoint);
|
||||
if (pfrom && hashPendingCheckpoint != 0 && (!mapBlockIndex.count(hashPendingCheckpoint)) && (!mapOrphanBlocks.count(hashPendingCheckpoint)))
|
||||
pfrom->AskFor(CInv(MSG_BLOCK, hashPendingCheckpoint));
|
||||
}
|
||||
|
||||
bool SetCheckpointPrivKey(std::string strPrivKey)
|
||||
{
|
||||
// Test signing a sync-checkpoint with genesis block
|
||||
CSyncCheckpoint checkpoint;
|
||||
checkpoint.hashCheckpoint = !fTestNet ? hashGenesisBlockOfficial : hashGenesisBlockTestNet;
|
||||
CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION);
|
||||
sMsg << (CUnsignedSyncCheckpoint)checkpoint;
|
||||
checkpoint.vchMsg = std::vector<unsigned char>(sMsg.begin(), sMsg.end());
|
||||
|
||||
std::vector<unsigned char> vchPrivKey = ParseHex(strPrivKey);
|
||||
CKey key;
|
||||
key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash
|
||||
if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig))
|
||||
return false;
|
||||
|
||||
// Test signing successful, proceed
|
||||
CSyncCheckpoint::strMasterPrivKey = strPrivKey;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SendSyncCheckpoint(uint256 hashCheckpoint)
|
||||
{
|
||||
CSyncCheckpoint checkpoint;
|
||||
checkpoint.hashCheckpoint = hashCheckpoint;
|
||||
CDataStream sMsg(SER_NETWORK, PROTOCOL_VERSION);
|
||||
sMsg << (CUnsignedSyncCheckpoint)checkpoint;
|
||||
checkpoint.vchMsg = std::vector<unsigned char>(sMsg.begin(), sMsg.end());
|
||||
|
||||
if (CSyncCheckpoint::strMasterPrivKey.empty())
|
||||
return error("SendSyncCheckpoint: Checkpoint master key unavailable.");
|
||||
std::vector<unsigned char> vchPrivKey = ParseHex(CSyncCheckpoint::strMasterPrivKey);
|
||||
CKey key;
|
||||
key.SetPrivKey(CPrivKey(vchPrivKey.begin(), vchPrivKey.end())); // if key is not correct openssl may crash
|
||||
if (!key.Sign(Hash(checkpoint.vchMsg.begin(), checkpoint.vchMsg.end()), checkpoint.vchSig))
|
||||
return error("SendSyncCheckpoint: Unable to sign checkpoint, check private key?");
|
||||
|
||||
if(!checkpoint.ProcessSyncCheckpoint(nullptr))
|
||||
{
|
||||
printf("WARNING: SendSyncCheckpoint: Failed to process checkpoint.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Relay checkpoint
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
checkpoint.RelayTo(pnode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is the sync-checkpoint outside maturity window?
|
||||
bool IsMatureSyncCheckpoint()
|
||||
{
|
||||
LOCK(cs_hashSyncCheckpoint);
|
||||
if (!mapBlockIndex.count(hashSyncCheckpoint))
|
||||
return true; // no valid sync checkpoint, treat as mature
|
||||
const CBlockIndex* pindexSync = mapBlockIndex[hashSyncCheckpoint];
|
||||
return (nBestHeight >= pindexSync->nHeight + nCoinbaseMaturity ||
|
||||
pindexSync->GetBlockTime() + nStakeMinAge < GetAdjustedTime());
|
||||
}
|
||||
}
|
||||
|
||||
// triangles: sync-checkpoint master key (DISABLED for decentralization - v5 hard fork)
|
||||
const std::string CSyncCheckpoint::strMasterPubKey = "";
|
||||
|
||||
std::string CSyncCheckpoint::strMasterPrivKey = "";
|
||||
|
||||
// triangles: verify signature of sync-checkpoint message
|
||||
// Master key system disabled - checkpoint signatures are no longer required
|
||||
bool CSyncCheckpoint::CheckSignature()
|
||||
{
|
||||
// Deserialize the checkpoint data without signature verification
|
||||
CDataStream sMsg(vchMsg, SER_NETWORK, PROTOCOL_VERSION);
|
||||
sMsg >> *(CUnsignedSyncCheckpoint*)this;
|
||||
return true;
|
||||
}
|
||||
|
||||
// triangles: process synchronized checkpoint
|
||||
bool CSyncCheckpoint::ProcessSyncCheckpoint(CNode* pfrom)
|
||||
{
|
||||
if (!CheckSignature())
|
||||
return false;
|
||||
|
||||
LOCK(Checkpoints::cs_hashSyncCheckpoint);
|
||||
if (!mapBlockIndex.count(hashCheckpoint))
|
||||
{
|
||||
// We haven't received the checkpoint chain, keep the checkpoint as pending
|
||||
Checkpoints::hashPendingCheckpoint = hashCheckpoint;
|
||||
Checkpoints::checkpointMessagePending = *this;
|
||||
printf("ProcessSyncCheckpoint: pending for sync-checkpoint %s\n", hashCheckpoint.ToString().c_str());
|
||||
// Ask this guy to fill in what we're missing
|
||||
if (pfrom)
|
||||
{
|
||||
pfrom->PushGetBlocks(pindexBest, hashCheckpoint);
|
||||
// ask directly as well in case rejected earlier by duplicate
|
||||
// proof-of-stake because getblocks may not get it this time
|
||||
pfrom->AskFor(CInv(MSG_BLOCK, mapOrphanBlocks.count(hashCheckpoint)? WantedByOrphan(mapOrphanBlocks[hashCheckpoint].get()) : hashCheckpoint));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Checkpoints::ValidateSyncCheckpoint(hashCheckpoint))
|
||||
return false;
|
||||
|
||||
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
|
||||
CBlockIndex* pindexCheckpoint = mapBlockIndex[hashCheckpoint];
|
||||
if (!pindexCheckpoint->IsInMainChain())
|
||||
{
|
||||
// checkpoint chain received but not yet main chain
|
||||
CBlock block;
|
||||
if (!block.ReadFromDisk(pindexCheckpoint))
|
||||
return error("ProcessSyncCheckpoint: ReadFromDisk failed for sync checkpoint %s", hashCheckpoint.ToString().c_str());
|
||||
if (!block.SetBestChain(txdb, pindexCheckpoint))
|
||||
{
|
||||
Checkpoints::hashInvalidCheckpoint = hashCheckpoint;
|
||||
return error("ProcessSyncCheckpoint: SetBestChain failed for sync checkpoint %s", hashCheckpoint.ToString().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!Checkpoints::WriteSyncCheckpoint(hashCheckpoint))
|
||||
return error("ProcessSyncCheckpoint(): failed to write sync checkpoint %s", hashCheckpoint.ToString().c_str());
|
||||
Checkpoints::checkpointMessage = *this;
|
||||
Checkpoints::hashPendingCheckpoint = 0;
|
||||
Checkpoints::checkpointMessagePending.SetNull();
|
||||
printf("ProcessSyncCheckpoint: sync-checkpoint at %s\n", hashCheckpoint.ToString().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
+17
-17
@@ -1,19 +1,19 @@
|
||||
#ifndef CLIENTVERSION_H
|
||||
#define CLIENTVERSION_H
|
||||
|
||||
//
|
||||
// client versioning
|
||||
//
|
||||
|
||||
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
|
||||
#ifndef CLIENTVERSION_H
|
||||
#define CLIENTVERSION_H
|
||||
|
||||
//
|
||||
// client versioning
|
||||
//
|
||||
|
||||
// These need to be macros, as version.cpp's and triangles-qt.rc's voodoo requires it
|
||||
#define CLIENT_VERSION_MAJOR 6
|
||||
#define CLIENT_VERSION_MINOR 0
|
||||
#define CLIENT_VERSION_REVISION 0
|
||||
#define CLIENT_VERSION_MINOR 1
|
||||
#define CLIENT_VERSION_REVISION 1
|
||||
#define CLIENT_VERSION_BUILD 0
|
||||
|
||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||
// Don't merge these into one macro!
|
||||
#define STRINGIZE(X) DO_STRINGIZE(X)
|
||||
#define DO_STRINGIZE(X) #X
|
||||
|
||||
#endif // CLIENTVERSION_H
|
||||
|
||||
// Converts the parameter X to a string after macro replacement on X has been performed.
|
||||
// Don't merge these into one macro!
|
||||
#define STRINGIZE(X) DO_STRINGIZE(X)
|
||||
#define DO_STRINGIZE(X) #X
|
||||
|
||||
#endif // CLIENTVERSION_H
|
||||
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
// Copyright (c) 2024 Triangles developers
|
||||
// I2P (SAM v3) transport support
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include "i2p.h"
|
||||
|
||||
#include "util.h"
|
||||
#include "netbase.h"
|
||||
#include "protocol.h" // CAddress
|
||||
#include "net.h" // AddI2PInboundNode(), GetListenPort()
|
||||
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
#ifdef WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#ifndef closesocket
|
||||
#define closesocket close
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// I2P uses a base64 variant where '+' -> '-' and '/' -> '~'.
|
||||
static const char* pI2PBase64 =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~";
|
||||
|
||||
static std::vector<unsigned char> DecodeI2PBase64(const std::string& str)
|
||||
{
|
||||
int table[256];
|
||||
for (int i = 0; i < 256; i++) table[i] = -1;
|
||||
for (int i = 0; i < 64; i++) table[(unsigned char)pI2PBase64[i]] = i;
|
||||
|
||||
std::vector<unsigned char> out;
|
||||
int bits = 0; uint32_t buf = 0;
|
||||
for (char c : str) {
|
||||
if (c == '=' || c == '\r' || c == '\n') continue;
|
||||
int v = table[(unsigned char)c];
|
||||
if (v < 0) continue; // skip anything unexpected
|
||||
buf = (buf << 6) | v;
|
||||
bits += 6;
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out.push_back((unsigned char)((buf >> bits) & 0xFF));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
CI2PSession* CI2PSession::GetInstance()
|
||||
{
|
||||
static CI2PSession instance;
|
||||
return &instance;
|
||||
}
|
||||
|
||||
CI2PSession::CI2PSession()
|
||||
: samHost(I2P_DEFAULT_SAM_HOST), samPort(I2P_DEFAULT_SAM_PORT),
|
||||
hSession(INVALID_SOCKET), fEnabled(false), fActive(false), fShutdown(false)
|
||||
{
|
||||
}
|
||||
|
||||
CI2PSession::~CI2PSession()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
std::string CI2PSession::GetB32Address()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(cs);
|
||||
return b32Address;
|
||||
}
|
||||
|
||||
// --- low level SAM helpers -------------------------------------------------
|
||||
|
||||
bool CI2PSession::SamConnect(SOCKET& hSocketRet)
|
||||
{
|
||||
SOCKET hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (hSocket == INVALID_SOCKET)
|
||||
return false;
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons((unsigned short)samPort);
|
||||
addr.sin_addr.s_addr = inet_addr(samHost.c_str());
|
||||
|
||||
if (connect(hSocket, (struct sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {
|
||||
closesocket(hSocket);
|
||||
return false;
|
||||
}
|
||||
|
||||
hSocketRet = hSocket;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CI2PSession::SamSendLine(SOCKET hSocket, const std::string& strLine)
|
||||
{
|
||||
std::string out = strLine + "\n";
|
||||
const char* p = out.c_str();
|
||||
size_t left = out.size();
|
||||
while (left > 0) {
|
||||
int n = send(hSocket, p, (int)left, MSG_NOSIGNAL);
|
||||
if (n <= 0)
|
||||
return false;
|
||||
p += n;
|
||||
left -= n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CI2PSession::SamRecvLine(SOCKET hSocket, std::string& strLineRet)
|
||||
{
|
||||
strLineRet.clear();
|
||||
char c;
|
||||
// SAM replies are newline terminated; read one byte at a time so we stop
|
||||
// exactly at the boundary and leave any following stream data untouched.
|
||||
for (int i = 0; i < 16384; i++) {
|
||||
int n = recv(hSocket, &c, 1, 0);
|
||||
if (n <= 0)
|
||||
return false;
|
||||
if (c == '\n')
|
||||
return true;
|
||||
if (c != '\r')
|
||||
strLineRet += c;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string CI2PSession::SamGetValue(const std::string& strReply, const std::string& strKey)
|
||||
{
|
||||
// Tokens are space separated KEY=VALUE pairs. VALUE runs to the next space.
|
||||
std::string needle = strKey + "=";
|
||||
size_t pos = strReply.find(needle);
|
||||
if (pos == std::string::npos)
|
||||
return "";
|
||||
pos += needle.size();
|
||||
size_t end = strReply.find(' ', pos);
|
||||
if (end == std::string::npos)
|
||||
end = strReply.size();
|
||||
return strReply.substr(pos, end - pos);
|
||||
}
|
||||
|
||||
bool CI2PSession::SamHandshake(SOCKET hSocket)
|
||||
{
|
||||
if (!SamSendLine(hSocket, "HELLO VERSION MIN=3.1 MAX=3.3"))
|
||||
return false;
|
||||
std::string reply;
|
||||
if (!SamRecvLine(hSocket, reply))
|
||||
return false;
|
||||
if (SamGetValue(reply, "RESULT") != "OK") {
|
||||
printf("I2P: SAM handshake failed: %s\n", reply.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string CI2PSession::DestToB32(const std::string& strB64Dest)
|
||||
{
|
||||
std::vector<unsigned char> dest = DecodeI2PBase64(strB64Dest);
|
||||
if (dest.empty())
|
||||
return "";
|
||||
unsigned char hash[SHA256_DIGEST_LENGTH];
|
||||
SHA256(dest.data(), dest.size(), hash);
|
||||
std::string b32 = EncodeBase32(hash, SHA256_DIGEST_LENGTH);
|
||||
// I2P b32 addresses are unpadded.
|
||||
while (!b32.empty() && b32[b32.size() - 1] == '=')
|
||||
b32.erase(b32.size() - 1);
|
||||
return b32 + ".b32.i2p";
|
||||
}
|
||||
|
||||
// --- session bring-up ------------------------------------------------------
|
||||
|
||||
bool CI2PSession::LoadOrCreateDestination(std::string& strPrivKeyRet)
|
||||
{
|
||||
fs::path keyPath = GetDataDir() / "i2p_private_key";
|
||||
|
||||
// Reuse an existing persistent destination if we have one.
|
||||
{
|
||||
std::ifstream f(keyPath.string().c_str());
|
||||
if (f.is_open()) {
|
||||
std::string line;
|
||||
std::getline(f, line);
|
||||
while (!line.empty() &&
|
||||
(line[line.size() - 1] == '\r' || line[line.size() - 1] == '\n'))
|
||||
line.erase(line.size() - 1);
|
||||
if (!line.empty()) {
|
||||
strPrivKeyRet = line;
|
||||
printf("I2P: loaded persistent destination from %s\n",
|
||||
keyPath.string().c_str());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a fresh destination via the bridge (Ed25519, SIGNATURE_TYPE=7).
|
||||
SOCKET hSocket = INVALID_SOCKET;
|
||||
if (!SamConnect(hSocket) || !SamHandshake(hSocket)) {
|
||||
if (hSocket != INVALID_SOCKET) closesocket(hSocket);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
if (SamSendLine(hSocket, "DEST GENERATE SIGNATURE_TYPE=7")) {
|
||||
std::string reply;
|
||||
if (SamRecvLine(hSocket, reply)) {
|
||||
std::string priv = SamGetValue(reply, "PRIV");
|
||||
if (!priv.empty()) {
|
||||
strPrivKeyRet = priv;
|
||||
std::ofstream out(keyPath.string().c_str(), std::ios::trunc);
|
||||
if (out.is_open()) {
|
||||
out << priv << std::endl;
|
||||
out.close();
|
||||
printf("I2P: generated and saved new persistent destination\n");
|
||||
ok = true;
|
||||
} else {
|
||||
printf("I2P: WARNING could not write %s\n", keyPath.string().c_str());
|
||||
ok = true; // still usable for this run
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
closesocket(hSocket);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool CI2PSession::CreateSession()
|
||||
{
|
||||
if (!SamConnect(hSession))
|
||||
return false;
|
||||
if (!SamHandshake(hSession))
|
||||
return false;
|
||||
|
||||
std::ostringstream id;
|
||||
id << "triangles-" << (uint64_t)GetTime() << "-" << (uint64_t)(GetRand(1000000));
|
||||
sessionId = id.str();
|
||||
|
||||
std::string cmd = "SESSION CREATE STYLE=STREAM ID=" + sessionId +
|
||||
" DESTINATION=" + privateKey + " SIGNATURE_TYPE=7";
|
||||
if (!SamSendLine(hSession, cmd))
|
||||
return false;
|
||||
|
||||
std::string reply;
|
||||
if (!SamRecvLine(hSession, reply))
|
||||
return false;
|
||||
|
||||
if (SamGetValue(reply, "RESULT") != "OK") {
|
||||
printf("I2P: SESSION CREATE failed: %s\n", reply.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// The bridge echoes the (possibly newly assigned) private key back.
|
||||
std::string echoed = SamGetValue(reply, "DESTINATION");
|
||||
if (!echoed.empty())
|
||||
privateKey = echoed;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CI2PSession::ResolveMyB32()
|
||||
{
|
||||
SOCKET hSocket = INVALID_SOCKET;
|
||||
if (!SamConnect(hSocket) || !SamHandshake(hSocket)) {
|
||||
if (hSocket != INVALID_SOCKET) closesocket(hSocket);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
if (SamSendLine(hSocket, "NAMING LOOKUP NAME=ME")) {
|
||||
std::string reply;
|
||||
if (SamRecvLine(hSocket, reply) && SamGetValue(reply, "RESULT") == "OK") {
|
||||
std::string dest = SamGetValue(reply, "VALUE");
|
||||
std::string b32 = DestToB32(dest);
|
||||
if (!b32.empty()) {
|
||||
std::lock_guard<std::mutex> lock(cs);
|
||||
b32Address = b32;
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
closesocket(hSocket);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool CI2PSession::Start()
|
||||
{
|
||||
if (!GetBoolArg("-i2p", true)) {
|
||||
printf("I2P: disabled (-i2p=0)\n");
|
||||
return false;
|
||||
}
|
||||
fEnabled.store(true);
|
||||
|
||||
// -i2psam=host:port overrides the default SAM bridge endpoint.
|
||||
std::string sam = GetArg("-i2psam", "");
|
||||
if (!sam.empty()) {
|
||||
int port = I2P_DEFAULT_SAM_PORT;
|
||||
std::string host;
|
||||
SplitHostPort(sam, port, host);
|
||||
if (!host.empty()) samHost = host;
|
||||
if (port > 0) samPort = port;
|
||||
}
|
||||
|
||||
printf("I2P: connecting to SAM bridge at %s:%d\n", samHost.c_str(), samPort);
|
||||
|
||||
if (!LoadOrCreateDestination(privateKey)) {
|
||||
printf("I2P: ERROR could not obtain a destination. Is an I2P router with "
|
||||
"the SAM bridge enabled running at %s:%d?\n", samHost.c_str(), samPort);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CreateSession()) {
|
||||
printf("I2P: ERROR failed to create SAM STREAM session\n");
|
||||
if (hSession != INVALID_SOCKET) { closesocket(hSession); hSession = INVALID_SOCKET; }
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ResolveMyB32())
|
||||
printf("I2P: WARNING could not resolve our own .b32.i2p address yet\n");
|
||||
|
||||
fActive.store(true);
|
||||
fShutdown.store(false);
|
||||
|
||||
printf("I2P: session active. Our address: %s\n", GetB32Address().c_str());
|
||||
|
||||
// Register our I2P address as a local address so peers can learn it.
|
||||
CService meI2P;
|
||||
if (!b32Address.empty() && meI2P.SetSpecial(b32Address)) {
|
||||
meI2P.SetPort((unsigned short)GetListenPort());
|
||||
AddLocal(meI2P, LOCAL_MANUAL);
|
||||
}
|
||||
|
||||
acceptThread = std::thread(&CI2PSession::AcceptLoop, this);
|
||||
return true;
|
||||
}
|
||||
|
||||
void CI2PSession::Stop()
|
||||
{
|
||||
if (!fEnabled.load())
|
||||
return;
|
||||
fShutdown.store(true);
|
||||
fActive.store(false);
|
||||
|
||||
if (hSession != INVALID_SOCKET) {
|
||||
closesocket(hSession);
|
||||
hSession = INVALID_SOCKET;
|
||||
}
|
||||
if (acceptThread.joinable())
|
||||
acceptThread.join();
|
||||
fEnabled.store(false);
|
||||
printf("I2P: session stopped\n");
|
||||
}
|
||||
|
||||
// --- inbound ---------------------------------------------------------------
|
||||
|
||||
void CI2PSession::AcceptLoop()
|
||||
{
|
||||
while (!fShutdown.load()) {
|
||||
SOCKET hSocket = INVALID_SOCKET;
|
||||
if (!SamConnect(hSocket) || !SamHandshake(hSocket)) {
|
||||
if (hSocket != INVALID_SOCKET) closesocket(hSocket);
|
||||
if (fShutdown.load()) break;
|
||||
MilliSleep(2000);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Block here until a peer dials us; the router then streams the remote
|
||||
// destination on its own line, after which the socket carries data.
|
||||
if (!SamSendLine(hSocket, "STREAM ACCEPT ID=" + sessionId + " SILENT=false")) {
|
||||
closesocket(hSocket);
|
||||
MilliSleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string status;
|
||||
if (!SamRecvLine(hSocket, status) || SamGetValue(status, "RESULT") != "OK") {
|
||||
if (!fShutdown.load())
|
||||
printf("I2P: STREAM ACCEPT rejected: %s\n", status.c_str());
|
||||
closesocket(hSocket);
|
||||
MilliSleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string remoteDest;
|
||||
if (!SamRecvLine(hSocket, remoteDest)) {
|
||||
closesocket(hSocket);
|
||||
continue;
|
||||
}
|
||||
if (fShutdown.load()) {
|
||||
closesocket(hSocket);
|
||||
break;
|
||||
}
|
||||
|
||||
// The first token is the remote full destination (base64).
|
||||
std::string destTok = remoteDest;
|
||||
size_t sp = destTok.find(' ');
|
||||
if (sp != std::string::npos)
|
||||
destTok = destTok.substr(0, sp);
|
||||
|
||||
std::string b32 = DestToB32(destTok);
|
||||
CAddress addr;
|
||||
if (b32.empty() || !addr.SetSpecial(b32)) {
|
||||
printf("I2P: could not parse inbound remote destination\n");
|
||||
closesocket(hSocket);
|
||||
continue;
|
||||
}
|
||||
addr.nServices = 0;
|
||||
addr.nTime = GetTime();
|
||||
|
||||
// Hand the live data socket to the net layer as an inbound peer.
|
||||
printf("I2P: inbound connection from %s\n", b32.c_str());
|
||||
AddI2PInboundNode(hSocket, addr);
|
||||
}
|
||||
}
|
||||
|
||||
// --- outbound --------------------------------------------------------------
|
||||
|
||||
bool CI2PSession::Connect(const std::string& strDest, SOCKET& hSocketRet)
|
||||
{
|
||||
if (!fActive.load())
|
||||
return false;
|
||||
|
||||
SOCKET hSocket = INVALID_SOCKET;
|
||||
if (!SamConnect(hSocket) || !SamHandshake(hSocket)) {
|
||||
if (hSocket != INVALID_SOCKET) closesocket(hSocket);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SamSendLine(hSocket, "STREAM CONNECT ID=" + sessionId +
|
||||
" DESTINATION=" + strDest + " SILENT=false")) {
|
||||
closesocket(hSocket);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string status;
|
||||
if (!SamRecvLine(hSocket, status) || SamGetValue(status, "RESULT") != "OK") {
|
||||
printf("I2P: STREAM CONNECT to %s failed: %s\n", strDest.c_str(), status.c_str());
|
||||
closesocket(hSocket);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Socket is now a bidirectional stream to the peer.
|
||||
hSocketRet = hSocket;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StartI2P()
|
||||
{
|
||||
return CI2PSession::GetInstance()->Start();
|
||||
}
|
||||
|
||||
void StopI2P()
|
||||
{
|
||||
CI2PSession::GetInstance()->Stop();
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2024 Triangles developers
|
||||
// I2P (SAM v3) transport support
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
//
|
||||
// This module gives Triangles real I2P connectivity that mirrors the existing
|
||||
// embedded-Tor design: instead of a SOCKS proxy it talks the SAM v3 protocol
|
||||
// to a locally running I2P router (i2pd or Java I2P) and obtains a persistent
|
||||
// I2P destination whose ".b32.i2p" address is shown alongside the .onion
|
||||
// address. The wallet:
|
||||
// * creates / loads a persistent destination (i2p_private_key in datadir),
|
||||
// * runs a STREAM session so peers can dial us,
|
||||
// * accepts inbound I2P streams and feeds them to the net layer,
|
||||
// * dials outbound ".b32.i2p" peers through the same session.
|
||||
//
|
||||
// A running I2P router with its SAM bridge enabled (default 127.0.0.1:7656) is
|
||||
// required; nothing is bundled. Enable with -i2p and optionally -i2psam=host:port.
|
||||
|
||||
#ifndef TRIANGLES_I2P_H
|
||||
#define TRIANGLES_I2P_H
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "compat.h" // SOCKET / INVALID_SOCKET
|
||||
|
||||
// Default SAM bridge endpoint exposed by i2pd / Java I2P.
|
||||
#define I2P_DEFAULT_SAM_HOST "127.0.0.1"
|
||||
#define I2P_DEFAULT_SAM_PORT 7656
|
||||
|
||||
// Manages a single persistent I2P STREAM session over SAM v3.
|
||||
class CI2PSession
|
||||
{
|
||||
public:
|
||||
static CI2PSession* GetInstance();
|
||||
|
||||
// Bring the session up: connect to the SAM bridge, load/generate the
|
||||
// persistent destination and start accepting inbound streams.
|
||||
// Returns false (and logs) if no router/SAM bridge is reachable.
|
||||
bool Start();
|
||||
|
||||
// Tear the session down and stop the accept loop.
|
||||
void Stop();
|
||||
|
||||
bool IsEnabled() const { return fEnabled.load(); }
|
||||
bool IsActive() const { return fActive.load(); }
|
||||
|
||||
// Our own ".b32.i2p" address (empty until the session is up).
|
||||
std::string GetB32Address();
|
||||
|
||||
// Dial a remote ".b32.i2p" (or full base64 destination) through the
|
||||
// session. On success hSocketRet is a connected, blocking data socket the
|
||||
// caller can hand to a CNode. The caller takes ownership of the socket.
|
||||
bool Connect(const std::string& strDest, SOCKET& hSocketRet);
|
||||
|
||||
private:
|
||||
CI2PSession();
|
||||
~CI2PSession();
|
||||
|
||||
// --- low level SAM helpers ---
|
||||
bool SamConnect(SOCKET& hSocketRet); // raw TCP to the bridge
|
||||
bool SamHandshake(SOCKET hSocket); // HELLO VERSION
|
||||
bool SamSendLine(SOCKET hSocket, const std::string& strLine);
|
||||
bool SamRecvLine(SOCKET hSocket, std::string& strLineRet);
|
||||
static std::string SamGetValue(const std::string& strReply, const std::string& strKey);
|
||||
|
||||
bool LoadOrCreateDestination(std::string& strPrivKeyRet);
|
||||
bool CreateSession(); // SESSION CREATE
|
||||
bool ResolveMyB32(); // NAMING LOOKUP ME
|
||||
void AcceptLoop(); // inbound STREAM ACCEPT
|
||||
|
||||
// Compute the ".b32.i2p" address from a base64 (I2P alphabet) destination.
|
||||
static std::string DestToB32(const std::string& strB64Dest);
|
||||
|
||||
std::string samHost;
|
||||
int samPort;
|
||||
std::string sessionId;
|
||||
std::string privateKey; // persistent destination private key (base64)
|
||||
std::string b32Address; // our own .b32.i2p
|
||||
SOCKET hSession; // long-lived control socket owning the session
|
||||
|
||||
std::atomic<bool> fEnabled;
|
||||
std::atomic<bool> fActive;
|
||||
std::atomic<bool> fShutdown;
|
||||
std::thread acceptThread;
|
||||
std::mutex cs;
|
||||
};
|
||||
|
||||
// Convenience: start/stop from init.cpp.
|
||||
bool StartI2P();
|
||||
void StopI2P();
|
||||
|
||||
#endif // TRIANGLES_I2P_H
|
||||
+106
-74
@@ -495,105 +495,137 @@ bool CI2PEmbedded::Start(int socks, int sam, int server)
|
||||
argvPtrs.push_back(nullptr);
|
||||
|
||||
try {
|
||||
// Initialize i2pd: config parse, filesystem, crypto, router context
|
||||
// ----------------------------------------------------------------
|
||||
// Phase 1 (synchronous, < 1s): config parse, crypto, router context
|
||||
// ----------------------------------------------------------------
|
||||
i2p::api::InitI2P((int)(argvPtrs.size() - 1), argvPtrs.data(), "triangles-i2pd");
|
||||
fflush(stdout);
|
||||
|
||||
// Start the I2P router: netdb, transports, tunnels, router context
|
||||
// Redirect i2pd logs to our stdout/stderr
|
||||
auto logStream = std::make_shared<std::ostream>(std::cout.rdbuf());
|
||||
i2p::api::StartI2P(logStream);
|
||||
|
||||
printf("Embedded I2P: router started, starting client services...\n");
|
||||
|
||||
// Start the client context — this initializes SAM bridge, SOCKS proxy,
|
||||
// and tunnels based on config. The client context reads the conf we
|
||||
// wrote above to determine which services to start.
|
||||
i2p::client::context.Start();
|
||||
|
||||
// Mark running immediately so Qt UI shows I2P as active.
|
||||
running.store(true);
|
||||
printf("Embedded I2P: SOCKS proxy at 127.0.0.1:%d, SAM at 127.0.0.1:%d\n",
|
||||
socksPort, samPort);
|
||||
|
||||
// Wait for i2pd's SOCKS proxy AND SAM bridge to become available
|
||||
// (up to 120s — I2P bootstrap is slower than Tor due to floodfill
|
||||
// lookup and tunnel build).
|
||||
printf("Embedded I2P: waiting for SOCKS proxy and SAM bridge...\n");
|
||||
bool socksReady = false;
|
||||
bool samReady = false;
|
||||
// ----------------------------------------------------------------
|
||||
// Phase 2 (background thread): StartI2P + client context + bootstrap
|
||||
//
|
||||
// i2p::api::StartI2P() → NetDb::Start() → Reseed() can block for
|
||||
// up to 180s on first run (empty netDb → HTTPS download from public
|
||||
// I2P reseed servers). Running this on the main init thread freezes
|
||||
// the GUI splash screen ("Starting embedded I2P router...").
|
||||
//
|
||||
// The background thread handles:
|
||||
// 1. StartI2P (router, netdb, transports, tunnels, reseed)
|
||||
// 2. client::context.Start (SAM bridge, SOCKS proxy, server tunnel)
|
||||
// 3. Polling for SOCKS/SAM port readiness (up to 300s)
|
||||
// 4. .b32.i2p address population
|
||||
//
|
||||
// Meanwhile, the main init proceeds immediately. Tor-only mode
|
||||
// works in the meantime; I2P connectivity comes up asynchronously.
|
||||
// ----------------------------------------------------------------
|
||||
printf("Embedded I2P: launching router in background thread...\n");
|
||||
fflush(stdout);
|
||||
|
||||
for (int i = 0; i < 120; i++) {
|
||||
MilliSleep(1000);
|
||||
if (fShutdown) {
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
std::thread([this]() {
|
||||
try {
|
||||
// Start the I2P router (netdb, transports, tunnels, reseed)
|
||||
auto logStream = std::make_shared<std::ostream>(std::cout.rdbuf());
|
||||
i2p::api::StartI2P(logStream);
|
||||
fflush(stdout);
|
||||
|
||||
// --- Check SOCKS proxy readiness ---
|
||||
if (!socksReady) {
|
||||
printf("Embedded I2P: router started, starting client services...\n");
|
||||
fflush(stdout);
|
||||
|
||||
// Start SAM bridge, SOCKS proxy, and server tunnel
|
||||
i2p::client::context.Start();
|
||||
|
||||
printf("Embedded I2P: SOCKS proxy at 127.0.0.1:%d, SAM at 127.0.0.1:%d\n",
|
||||
socksPort, samPort);
|
||||
fflush(stdout);
|
||||
|
||||
// Wait for SOCKS proxy + SAM bridge to become available
|
||||
printf("Embedded I2P: waiting for SOCKS proxy and SAM bridge...\n");
|
||||
bool socksReady = false;
|
||||
bool samReady = false;
|
||||
|
||||
for (int i = 0; i < 300; i++) {
|
||||
MilliSleep(1000);
|
||||
if (fShutdown) {
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!socksReady) {
|
||||
#ifdef WIN32
|
||||
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock != INVALID_SOCKET) {
|
||||
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock != INVALID_SOCKET) {
|
||||
#else
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock >= 0) {
|
||||
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock >= 0) {
|
||||
#endif
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
addr.sin_port = htons(socksPort);
|
||||
bool up = (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0);
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
addr.sin_port = htons(socksPort);
|
||||
bool up = (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) == 0);
|
||||
#ifdef WIN32
|
||||
closesocket(sock);
|
||||
closesocket(sock);
|
||||
#else
|
||||
close(sock);
|
||||
close(sock);
|
||||
#endif
|
||||
if (up) {
|
||||
socksReady = true;
|
||||
printf("Embedded I2P: SOCKS proxy ready on port %d (took %ds)\n",
|
||||
socksPort, i + 1);
|
||||
if (up) {
|
||||
socksReady = true;
|
||||
printf("Embedded I2P: SOCKS proxy ready on port %d (took %ds)\n",
|
||||
socksPort, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!samReady) {
|
||||
samReady = IsSamAvailable();
|
||||
if (samReady) {
|
||||
printf("Embedded I2P: SAM v3 bridge ready on port %d (took %ds)\n",
|
||||
samPort, i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (socksReady && samReady) {
|
||||
printf("Embedded I2P: all I2P endpoints ready (SOCKS %d + SAM %d)\n",
|
||||
socksPort, samPort);
|
||||
break;
|
||||
}
|
||||
|
||||
if (i > 0 && i % 30 == 0) {
|
||||
printf("Embedded I2P: still bootstrapping (%ds elapsed, SOCKS:%s SAM:%s)...\n",
|
||||
i, socksReady ? "ready" : "wait",
|
||||
samReady ? "ready" : "wait");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Check SAM bridge readiness ---
|
||||
if (!samReady) {
|
||||
samReady = IsSamAvailable();
|
||||
if (samReady) {
|
||||
printf("Embedded I2P: SAM v3 bridge ready on port %d (took %ds)\n",
|
||||
samPort, i + 1);
|
||||
// Populate .b32.i2p address
|
||||
try {
|
||||
auto identHash = i2p::context.GetRouterInfo().GetIdentHash();
|
||||
i2pHostname = identHash.ToBase32() + ".b32.i2p";
|
||||
printf("Embedded I2P: router address = %s\n", i2pHostname.c_str());
|
||||
} catch (...) {
|
||||
printf("Embedded I2P: .b32.i2p address not yet available, Qt timer will retry\n");
|
||||
}
|
||||
}
|
||||
fflush(stdout);
|
||||
|
||||
// Both endpoints are up — router is fully bootstrapped
|
||||
if (socksReady && samReady) {
|
||||
printf("Embedded I2P: all I2P endpoints ready (SOCKS %d + SAM %d)\n",
|
||||
socksPort, samPort);
|
||||
break;
|
||||
} catch (const std::exception& e) {
|
||||
printf("ERROR: Embedded I2P background init failed: %s\n", e.what());
|
||||
fflush(stdout);
|
||||
}
|
||||
}).detach();
|
||||
|
||||
if (i > 0 && i % 30 == 0) {
|
||||
printf("Embedded I2P: still bootstrapping (%ds elapsed, SOCKS:%s SAM:%s)...\n",
|
||||
i, socksReady ? "ready" : "wait",
|
||||
samReady ? "ready" : "wait");
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the .b32.i2p address from the router's identity hash.
|
||||
// This is the I2P address that appears in the Qt status bar.
|
||||
try {
|
||||
auto identHash = i2p::context.GetRouterInfo().GetIdentHash();
|
||||
i2pHostname = identHash.ToBase32() + ".b32.i2p";
|
||||
printf("Embedded I2P: router address = %s\n", i2pHostname.c_str());
|
||||
} catch (...) {
|
||||
printf("Embedded I2P: could not retrieve router address yet\n");
|
||||
}
|
||||
printf("Embedded I2P: router init delegated to background thread\n");
|
||||
fflush(stdout);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
lastError = std::string("i2pd initialization failed: ") + e.what();
|
||||
printf("ERROR: Embedded I2P startup failed: %s\n", e.what());
|
||||
running.store(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// Dynamic seeds will also be available at:
|
||||
// https://seeds.cryptographic-triangles.org/i2p-seeds.txt
|
||||
static const char *strMainNetI2PSeed[][1] = {
|
||||
// SAMI-PC - authoritative wallet node (main PC)
|
||||
{"fecv4pomdm47epuadgrpkvxzjqfqwsjfc7t7xadwaac5bislyrhq.b32.i2p"},
|
||||
// DNS2 - primary bootstrap server (194.233.88.206)
|
||||
// Generated by embedded i2pd on first run, keys persist in i2p_data/
|
||||
{"hnupgkbtcn4hlo6sunhbp6uuz4k6bkgsa5jtcruyyt7y6q7qsoda.b32.i2p"},
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
// Copyright (c) 2024 Triangles developers
|
||||
// I2P Router Process Manager - launches and manages a bundled i2pd binary
|
||||
// Distributed under the MIT/X11 software license
|
||||
|
||||
#ifdef WIN32
|
||||
#define NOMINMAX
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef _WIN32_WINNT
|
||||
#define _WIN32_WINNT 0x0600
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include "i2p_process.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#ifdef WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static CI2PProcess* i2pProcessInstance = nullptr;
|
||||
|
||||
CI2PProcess* CI2PProcess::GetInstance()
|
||||
{
|
||||
if (!i2pProcessInstance)
|
||||
i2pProcessInstance = new CI2PProcess();
|
||||
return i2pProcessInstance;
|
||||
}
|
||||
|
||||
CI2PProcess::CI2PProcess()
|
||||
: samPort(7656)
|
||||
, running(false)
|
||||
, fExternal(false)
|
||||
#ifdef WIN32
|
||||
, hProcess(nullptr)
|
||||
, hJob(nullptr)
|
||||
, processId(0)
|
||||
#else
|
||||
, processId(0)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
CI2PProcess::~CI2PProcess()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
// Try a quick TCP connect; success means something is already listening
|
||||
// (e.g. the SAM bridge is up, or an external router is running).
|
||||
bool CI2PProcess::CanConnect(const std::string& host, int port)
|
||||
{
|
||||
#ifdef WIN32
|
||||
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (s == INVALID_SOCKET) return false;
|
||||
#else
|
||||
int s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (s < 0) return false;
|
||||
#endif
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons((unsigned short)port);
|
||||
addr.sin_addr.s_addr = inet_addr(host.c_str());
|
||||
bool ok = (connect(s, (struct sockaddr*)&addr, sizeof(addr)) == 0);
|
||||
#ifdef WIN32
|
||||
closesocket(s);
|
||||
#else
|
||||
close(s);
|
||||
#endif
|
||||
return ok;
|
||||
}
|
||||
|
||||
std::string CI2PProcess::FindI2pdBinary()
|
||||
{
|
||||
std::vector<std::string> candidates;
|
||||
|
||||
#ifdef WIN32
|
||||
const char* exeName = "i2pd.exe";
|
||||
#else
|
||||
const char* exeName = "i2pd";
|
||||
#endif
|
||||
|
||||
// 1. Next to the wallet executable (this is how tor.exe is shipped).
|
||||
try {
|
||||
fs::path exeDir;
|
||||
#ifdef WIN32
|
||||
char buf[MAX_PATH];
|
||||
if (GetModuleFileNameA(nullptr, buf, MAX_PATH) > 0)
|
||||
exeDir = fs::path(buf).parent_path();
|
||||
#else
|
||||
exeDir = fs::current_path();
|
||||
#endif
|
||||
if (!exeDir.empty()) {
|
||||
candidates.push_back((exeDir / exeName).string());
|
||||
candidates.push_back((exeDir / "i2pd" / exeName).string());
|
||||
candidates.push_back((exeDir / "I2P" / exeName).string());
|
||||
}
|
||||
} catch (...) {}
|
||||
|
||||
// 2. In / next to the data directory.
|
||||
candidates.push_back((GetDataDir() / exeName).string());
|
||||
candidates.push_back((GetDataDir() / "i2pd" / exeName).string());
|
||||
|
||||
// 3. Common system locations.
|
||||
#ifdef WIN32
|
||||
if (const char* pf = getenv("ProgramFiles"))
|
||||
candidates.push_back(std::string(pf) + "\\i2pd\\" + exeName);
|
||||
if (const char* pfx = getenv("ProgramFiles(x86)"))
|
||||
candidates.push_back(std::string(pfx) + "\\i2pd\\" + exeName);
|
||||
candidates.push_back(std::string("C:\\i2pd\\") + exeName);
|
||||
#else
|
||||
candidates.push_back("/usr/bin/i2pd");
|
||||
candidates.push_back("/usr/local/bin/i2pd");
|
||||
candidates.push_back("/opt/i2pd/bin/i2pd");
|
||||
candidates.push_back("/opt/homebrew/bin/i2pd");
|
||||
candidates.push_back("/usr/local/opt/i2pd/bin/i2pd");
|
||||
#endif
|
||||
|
||||
for (const std::string& c : candidates) {
|
||||
try {
|
||||
if (fs::exists(c) && fs::is_regular_file(c)) {
|
||||
printf("I2P: found i2pd binary at %s\n", c.c_str());
|
||||
return c;
|
||||
}
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool CI2PProcess::WriteConfig()
|
||||
{
|
||||
fs::path dir(dataDir);
|
||||
try {
|
||||
fs::create_directories(dir);
|
||||
} catch (const std::exception& e) {
|
||||
lastError = std::string("Cannot create i2pd data directory: ") + e.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
confPath = (dir / "i2pd.conf").string();
|
||||
fs::path logPath = dir / "i2pd.log";
|
||||
|
||||
std::ofstream conf(confPath.c_str(), std::ios::trunc);
|
||||
if (!conf.is_open()) {
|
||||
lastError = "Cannot write i2pd.conf to " + confPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
conf << "# Triangles Wallet I2P configuration (auto-generated)\n";
|
||||
conf << "# Do not edit - this file is overwritten on startup\n\n";
|
||||
conf << "daemon = false\n";
|
||||
conf << "log = file\n";
|
||||
conf << "logfile = " << logPath.string() << "\n";
|
||||
conf << "datadir = " << dir.string() << "\n\n";
|
||||
|
||||
// The bridge our SAM client talks to.
|
||||
conf << "[sam]\n";
|
||||
conf << "enabled = true\n";
|
||||
conf << "address = 127.0.0.1\n";
|
||||
conf << "port = " << samPort << "\n\n";
|
||||
|
||||
// We only need SAM; keep everything else off to minimise footprint.
|
||||
conf << "[httpproxy]\nenabled = false\n\n";
|
||||
conf << "[socksproxy]\nenabled = false\n\n";
|
||||
conf << "[http]\nenabled = false\n\n";
|
||||
conf << "[i2pcontrol]\nenabled = false\n";
|
||||
|
||||
conf.close();
|
||||
printf("I2P: wrote i2pd config to %s (SAM port %d)\n", confPath.c_str(), samPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CI2PProcess::Start(const std::string& dataDirIn, int samPortIn)
|
||||
{
|
||||
dataDir = dataDirIn;
|
||||
samPort = samPortIn;
|
||||
fExternal = false;
|
||||
lastError.clear();
|
||||
|
||||
// If a SAM bridge is already up, use it instead of launching our own.
|
||||
if (CanConnect("127.0.0.1", samPort)) {
|
||||
printf("I2P: detected an I2P router already listening on SAM port %d; using it\n", samPort);
|
||||
fExternal = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
binaryPath = FindI2pdBinary();
|
||||
if (binaryPath.empty()) {
|
||||
lastError = "No i2pd binary found (ship i2pd alongside the wallet, like tor)";
|
||||
printf("I2P: %s\n", lastError.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!WriteConfig())
|
||||
return false;
|
||||
|
||||
printf("I2P: starting i2pd: %s --conf %s\n", binaryPath.c_str(), confPath.c_str());
|
||||
|
||||
#ifdef WIN32
|
||||
STARTUPINFOA si;
|
||||
PROCESS_INFORMATION pi;
|
||||
ZeroMemory(&si, sizeof(si));
|
||||
si.cb = sizeof(si);
|
||||
si.dwFlags = STARTF_USESHOWWINDOW;
|
||||
si.wShowWindow = SW_HIDE;
|
||||
ZeroMemory(&pi, sizeof(pi));
|
||||
|
||||
std::string cmdLine = "\"" + binaryPath + "\" --conf \"" + confPath + "\"";
|
||||
|
||||
if (!CreateProcessA(nullptr, (LPSTR)cmdLine.c_str(), nullptr, nullptr,
|
||||
FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) {
|
||||
DWORD err = ::GetLastError();
|
||||
lastError = strprintf("CreateProcess failed for i2pd '%s' (Windows error %lu)", binaryPath.c_str(), err);
|
||||
printf("I2P: ERROR %s\n", lastError.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
hProcess = pi.hProcess;
|
||||
processId = pi.dwProcessId;
|
||||
CloseHandle(pi.hThread);
|
||||
|
||||
// Kill i2pd if the wallet dies (matches the embedded Tor behaviour).
|
||||
hJob = CreateJobObject(nullptr, nullptr);
|
||||
if (hJob) {
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {};
|
||||
jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo));
|
||||
if (!AssignProcessToJobObject(hJob, hProcess))
|
||||
printf("I2P: WARNING could not assign i2pd to Job Object (error %lu)\n", GetLastError());
|
||||
}
|
||||
|
||||
printf("I2P: i2pd started (PID %lu)\n", processId);
|
||||
#else
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
lastError = "Failed to fork for i2pd process";
|
||||
printf("I2P: ERROR %s\n", lastError.c_str());
|
||||
return false;
|
||||
}
|
||||
if (pid == 0) {
|
||||
freopen("/dev/null", "w", stdout);
|
||||
freopen("/dev/null", "w", stderr);
|
||||
execl(binaryPath.c_str(), binaryPath.c_str(),
|
||||
"--conf", confPath.c_str(), (char*)nullptr);
|
||||
_exit(1);
|
||||
}
|
||||
processId = pid;
|
||||
printf("I2P: i2pd started (PID %d)\n", processId);
|
||||
#endif
|
||||
|
||||
running = true;
|
||||
|
||||
// Wait for the SAM bridge to come up. The bridge opens quickly; tunnel
|
||||
// build (needed for actual connectivity) continues in the background.
|
||||
printf("I2P: waiting for SAM bridge on port %d...\n", samPort);
|
||||
for (int i = 0; i < 45; i++) {
|
||||
MilliSleep(1000);
|
||||
if (fShutdown) {
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
if (CanConnect("127.0.0.1", samPort)) {
|
||||
printf("I2P: SAM bridge ready on port %d (took %ds)\n", samPort, i + 1);
|
||||
return true;
|
||||
}
|
||||
if (!IsRunning()) {
|
||||
lastError = "i2pd exited during start-up before the SAM bridge became ready";
|
||||
printf("I2P: ERROR %s\n", lastError.c_str());
|
||||
running = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
lastError = strprintf("i2pd started but SAM port %d not ready after 45s", samPort);
|
||||
printf("I2P: WARNING %s (it may still be building tunnels)\n", lastError.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
void CI2PProcess::Stop()
|
||||
{
|
||||
if (fExternal) {
|
||||
// We never launched it; leave the user's router running.
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
if (!running) return;
|
||||
|
||||
#ifdef WIN32
|
||||
if (hProcess != nullptr) {
|
||||
printf("I2P: stopping i2pd (PID %lu)...\n", processId);
|
||||
TerminateProcess(hProcess, 0);
|
||||
WaitForSingleObject(hProcess, 5000);
|
||||
CloseHandle(hProcess);
|
||||
hProcess = nullptr;
|
||||
}
|
||||
if (hJob != nullptr) {
|
||||
CloseHandle(hJob);
|
||||
hJob = nullptr;
|
||||
}
|
||||
#else
|
||||
if (processId > 0) {
|
||||
printf("I2P: stopping i2pd (PID %d)...\n", processId);
|
||||
kill(processId, SIGTERM);
|
||||
for (int i = 0; i < 50; i++) {
|
||||
int status;
|
||||
pid_t result = waitpid(processId, &status, WNOHANG);
|
||||
if (result != 0) break;
|
||||
MilliSleep(100);
|
||||
}
|
||||
kill(processId, SIGKILL);
|
||||
waitpid(processId, nullptr, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
processId = 0;
|
||||
running = false;
|
||||
printf("I2P: i2pd stopped\n");
|
||||
}
|
||||
|
||||
bool CI2PProcess::IsRunning()
|
||||
{
|
||||
if (fExternal) return true;
|
||||
if (!running) return false;
|
||||
|
||||
#ifdef WIN32
|
||||
if (hProcess == nullptr) return false;
|
||||
DWORD exitCode;
|
||||
if (GetExitCodeProcess(hProcess, &exitCode))
|
||||
return (exitCode == STILL_ACTIVE);
|
||||
return false;
|
||||
#else
|
||||
if (processId <= 0) return false;
|
||||
int status;
|
||||
pid_t result = waitpid(processId, &status, WNOHANG);
|
||||
return (result == 0); // 0 => still running
|
||||
#endif
|
||||
}
|
||||
|
||||
bool StartEmbeddedI2P(const std::string& dataDir, int samPort)
|
||||
{
|
||||
return CI2PProcess::GetInstance()->Start(dataDir, samPort);
|
||||
}
|
||||
|
||||
void StopEmbeddedI2P()
|
||||
{
|
||||
CI2PProcess::GetInstance()->Stop();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2024 Triangles developers
|
||||
// I2P Router Process Manager - launches and manages a bundled i2pd binary
|
||||
// Distributed under the MIT/X11 software license
|
||||
//
|
||||
// Mirrors tor_process.cpp: locate an i2pd executable shipped alongside the
|
||||
// wallet (or installed on the system), write an auto-generated config that
|
||||
// enables the SAM bridge, launch it as a managed child process, and shut it
|
||||
// down when the wallet exits. The SAM session in i2p.cpp then connects to it,
|
||||
// so the user does not have to install or run a separate I2P router.
|
||||
|
||||
#ifndef TRIANGLES_I2P_PROCESS_H
|
||||
#define TRIANGLES_I2P_PROCESS_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#ifdef WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
class CI2PProcess
|
||||
{
|
||||
public:
|
||||
static CI2PProcess* GetInstance();
|
||||
|
||||
CI2PProcess();
|
||||
~CI2PProcess();
|
||||
|
||||
// Bring up the router. If something is already listening on the SAM port we
|
||||
// assume an external router and do not launch our own (fExternal=true).
|
||||
// Returns true if a SAM bridge is (or will shortly be) reachable.
|
||||
bool Start(const std::string& dataDir, int samPort = 7656);
|
||||
|
||||
// Terminate the launched router (no-op for an external one).
|
||||
void Stop();
|
||||
|
||||
bool IsRunning();
|
||||
bool IsExternal() const { return fExternal; }
|
||||
std::string GetLastError() const { return lastError; }
|
||||
std::string GetBinaryPath() const { return binaryPath; }
|
||||
|
||||
private:
|
||||
std::string FindI2pdBinary();
|
||||
bool WriteConfig();
|
||||
static bool CanConnect(const std::string& host, int port);
|
||||
|
||||
int samPort;
|
||||
bool running;
|
||||
bool fExternal;
|
||||
std::string dataDir;
|
||||
std::string binaryPath;
|
||||
std::string confPath;
|
||||
std::string lastError;
|
||||
|
||||
#ifdef WIN32
|
||||
HANDLE hProcess;
|
||||
HANDLE hJob;
|
||||
DWORD processId;
|
||||
#else
|
||||
int processId;
|
||||
#endif
|
||||
};
|
||||
|
||||
// Convenience wrappers for init.cpp.
|
||||
bool StartEmbeddedI2P(const std::string& dataDir, int samPort);
|
||||
void StopEmbeddedI2P();
|
||||
|
||||
#endif // TRIANGLES_I2P_PROCESS_H
|
||||
+150
-28
@@ -4,6 +4,8 @@
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
#include "txdb.h"
|
||||
#include "walletdb.h"
|
||||
#include "walletdb-recover.h" // BerkeleyRecoverWallet / BerkeleyZapWalletTx
|
||||
#include "walletmigrate.h" // MaybeMigrateBerkeleyWalletToSQLite / IsSQLiteFile
|
||||
#include "trianglesrpc.h"
|
||||
#include "net.h"
|
||||
#include "netbase.h"
|
||||
@@ -37,12 +39,16 @@ static bool InitError(const std::string& str);
|
||||
static bool InitWarning(const std::string& str);
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <boost/interprocess/sync/file_lock.hpp>
|
||||
#include <algorithm>
|
||||
#include <openssl/crypto.h>
|
||||
|
||||
#ifndef WIN32
|
||||
#include <signal.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
// Windows.h (transitively included) defines these as macros, clobbering Checkpoints:: enum values.
|
||||
@@ -57,9 +63,41 @@ static bool InitWarning(const std::string& str);
|
||||
#endif
|
||||
|
||||
using namespace std;
|
||||
using namespace boost;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
// Acquire an exclusive, non-blocking advisory lock on the datadir .lock file
|
||||
// and hold it for the lifetime of the process. Replaces
|
||||
// boost::interprocess::file_lock. The descriptor/handle is intentionally never
|
||||
// released — the OS drops the lock automatically when the process exits.
|
||||
bool LockDataDirectory(const std::filesystem::path& pathLockFile)
|
||||
{
|
||||
#ifdef WIN32
|
||||
HANDLE hFile = CreateFileA(pathLockFile.string().c_str(),
|
||||
GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ,
|
||||
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hFile == INVALID_HANDLE_VALUE)
|
||||
return false;
|
||||
OVERLAPPED ov = {};
|
||||
if (!LockFileEx(hFile, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
|
||||
0, MAXDWORD, MAXDWORD, &ov)) {
|
||||
CloseHandle(hFile);
|
||||
return false;
|
||||
}
|
||||
return true; // handle held until process exit
|
||||
#else
|
||||
int fd = open(pathLockFile.string().c_str(), O_RDWR | O_CREAT, 0644);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
return true; // fd held until process exit
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<CWallet> pwalletMain;
|
||||
CClientUIInterface uiInterface;
|
||||
std::string strWalletFileName;
|
||||
@@ -340,10 +378,12 @@ void Shutdown(void* parg)
|
||||
pScriptCheckQueue.reset();
|
||||
}
|
||||
|
||||
// Stop the embedded I2P router.
|
||||
StopEmbeddedI2P();
|
||||
|
||||
// NOW safe to destroy Tor state - all threads have stopped
|
||||
ShutdownTorV3();
|
||||
StopEmbeddedTor();
|
||||
StopEmbeddedI2P();
|
||||
|
||||
#ifdef ENABLE_ZMQ
|
||||
if (pzmqNotifier)
|
||||
@@ -884,8 +924,7 @@ bool AppInit2()
|
||||
fs::path pathLockFile = GetDataDir() / ".lock";
|
||||
FILE* file = fopen(pathLockFile.string().c_str(), "a"); // empty lock file; created if it doesn't exist.
|
||||
if (file) fclose(file);
|
||||
static boost::interprocess::file_lock lock(pathLockFile.string().c_str());
|
||||
if (!lock.try_lock())
|
||||
if (!LockDataDirectory(pathLockFile))
|
||||
return InitError(strprintf(_("Cannot obtain a lock on data directory %s. Triangles is probably already running."), strDataDir.c_str()));
|
||||
|
||||
#if !defined(WIN32) && !defined(QT_GUI)
|
||||
@@ -931,6 +970,21 @@ bool AppInit2()
|
||||
uiInterface.InitMessage(_("Verifying database integrity..."));
|
||||
nStart = GetTimeMillis();
|
||||
|
||||
// The pre-rebase Berkeley-only paths (salvagewallet, zapwallettxes,
|
||||
// bitdb.Verify, and the Berkeley→SQLite migration hook itself) only
|
||||
// apply to a wallet.dat that is still a Berkeley DB file. Once the
|
||||
// migration has run — or if the user is starting with a wallet that was
|
||||
// already SQLite — those steps would either no-op or (worse) misinterpret
|
||||
// the SQLite file as a corrupt Berkeley file and abort startup.
|
||||
//
|
||||
// The SQLite backend runs its own PRAGMA integrity_check in
|
||||
// SQLiteDatabase::Open(), so the wallet is validated against the SQLite
|
||||
// schema before the wallet handle is ever constructed downstream.
|
||||
//
|
||||
// Note: the snapshot is taken AFTER any migration hook below, so that
|
||||
// post-migration the verify/salvage paths are skipped automatically.
|
||||
bool walletIsSqlite = false;
|
||||
|
||||
if (!bitdb.Open(GetDataDir()))
|
||||
{
|
||||
string msg = strprintf(_("Error initializing database environment %s!"
|
||||
@@ -941,33 +995,63 @@ bool AppInit2()
|
||||
|
||||
if (GetBoolArg("-salvagewallet"))
|
||||
{
|
||||
// Recover readable keypairs:
|
||||
if (!CWalletDB::Recover(bitdb, strWalletFileName, true))
|
||||
// Recover readable keypairs (Berkeley path; only relevant for legacy
|
||||
// wallet.dat files that haven't been migrated to SQLite yet):
|
||||
if (!BerkeleyRecoverWallet(bitdb, strWalletFileName, true))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GetBoolArg("-zapwallettxes") && fs::exists(GetDataDir() / strWalletFileName))
|
||||
{
|
||||
uiInterface.InitMessage(_("Zapping all transactions from wallet..."));
|
||||
if (!CWalletDB::ZapWalletTx(strWalletFileName))
|
||||
if (!BerkeleyZapWalletTx(strWalletFileName))
|
||||
return InitError(_("Error: could not zap wallet transactions"));
|
||||
}
|
||||
|
||||
if (fs::exists(GetDataDir() / strWalletFileName))
|
||||
// ── Wallet backend migration ──────────────────────────────────────────────
|
||||
// The daemon now defaults to SQLite (-walletdb=sqlite). If the wallet file
|
||||
// on disk is still a Berkeley DB, convert it non-destructively to a SQLite
|
||||
// wallet here, before the CWalletDB handle is opened downstream. The
|
||||
// Berkeley original is preserved as "<name>.bdb.bak" alongside.
|
||||
if (ResolveWalletDbKind() == WalletDbKind::SQLite &&
|
||||
fs::exists(GetDataDir() / strWalletFileName) &&
|
||||
!IsSQLiteFile(GetDataDir() / strWalletFileName))
|
||||
{
|
||||
CDBEnv::VerifyResult r = bitdb.Verify(strWalletFileName, CWalletDB::Recover);
|
||||
if (r == CDBEnv::RECOVER_OK)
|
||||
{
|
||||
string msg = strprintf(_("Warning: wallet.dat corrupt, data salvaged!"
|
||||
" Original wallet.dat saved as wallet.{timestamp}.bak in %s; if"
|
||||
" your balance or transactions are incorrect you should"
|
||||
" restore from a backup."), strDataDir.c_str());
|
||||
uiInterface.ThreadSafeMessageBox(msg, _("Triangles"), CClientUIInterface::OK | CClientUIInterface::ICON_EXCLAMATION | CClientUIInterface::MODAL);
|
||||
}
|
||||
if (r == CDBEnv::RECOVER_FAIL)
|
||||
return InitError(_("wallet.dat corrupt, salvage failed"));
|
||||
uiInterface.InitMessage(_("Migrating wallet from Berkeley DB to SQLite..."));
|
||||
std::string migErr;
|
||||
if (!MaybeMigrateBerkeleyWalletToSQLite(GetDataDir() / strWalletFileName, migErr))
|
||||
return InitError(_("Wallet migration failed: ") + migErr);
|
||||
// Snapshot AFTER migration so the post-migration verify step below
|
||||
// is skipped automatically when the wallet is now SQLite.
|
||||
walletIsSqlite =
|
||||
fs::exists(GetDataDir() / strWalletFileName) &&
|
||||
IsSQLiteFile(GetDataDir() / strWalletFileName);
|
||||
}
|
||||
StartupPerfLog("verify_db", GetTimeMillis() - nStart, strprintf("wallet=%s", strWalletFileName.c_str()));
|
||||
else
|
||||
{
|
||||
walletIsSqlite =
|
||||
fs::exists(GetDataDir() / strWalletFileName) &&
|
||||
IsSQLiteFile(GetDataDir() / strWalletFileName);
|
||||
}
|
||||
|
||||
if (!walletIsSqlite)
|
||||
{
|
||||
if (fs::exists(GetDataDir() / strWalletFileName))
|
||||
{
|
||||
CDBEnv::VerifyResult r = bitdb.Verify(strWalletFileName, BerkeleyRecoverWallet);
|
||||
if (r == CDBEnv::RECOVER_OK)
|
||||
{
|
||||
string msg = strprintf(_("Warning: wallet.dat corrupt, data salvaged!"
|
||||
" Original wallet.dat saved as wallet.{timestamp}.bak in %s; if"
|
||||
" your balance or transactions are incorrect you should"
|
||||
" restore from a backup."), strDataDir.c_str());
|
||||
uiInterface.ThreadSafeMessageBox(msg, _("Triangles"), CClientUIInterface::OK | CClientUIInterface::ICON_EXCLAMATION | CClientUIInterface::MODAL);
|
||||
}
|
||||
if (r == CDBEnv::RECOVER_FAIL)
|
||||
return InitError(_("wallet.dat corrupt, salvage failed"));
|
||||
}
|
||||
}
|
||||
StartupPerfLog("verify_db", GetTimeMillis() - nStart, strprintf("wallet=%s wallet_is_sqlite=%d", strWalletFileName.c_str(), (int)walletIsSqlite));
|
||||
|
||||
// ********************************************************* Step 6: network initialization
|
||||
nStart = GetTimeMillis();
|
||||
@@ -1183,14 +1267,32 @@ bool AppInit2()
|
||||
}
|
||||
}
|
||||
|
||||
// ********************************************************* Step 6d: optional LevelDB -> RocksDB chain DB migration
|
||||
if (GetBoolArg("-migratechaindb", false) || GetBoolArg("-migratechaindbforce", false))
|
||||
// ********************************************************* Step 6d: LevelDB -> RocksDB chain DB migration
|
||||
// Runs when explicitly requested (-migratechaindb[force]) OR automatically
|
||||
// when RocksDB is the active backend and the only chain DB present is a
|
||||
// legacy LevelDB (txleveldb). This makes the RocksDB default transparent
|
||||
// for existing nodes: their chain state is copied (and verified) into a new
|
||||
// rocksdb/ directory on first launch, leaving the LevelDB source untouched
|
||||
// as a fallback. MaybeMigrateLevelDbToRocksDb() is a no-op when there is no
|
||||
// LevelDB source or a RocksDB directory already exists, so it is safe to
|
||||
// call on every startup.
|
||||
{
|
||||
uiInterface.InitMessage(_("Migrating chain database to RocksDB..."));
|
||||
std::string strMigrateError;
|
||||
bool fForce = GetBoolArg("-migratechaindbforce", false);
|
||||
if (!MaybeMigrateLevelDbToRocksDb(fForce, strMigrateError))
|
||||
return InitError(strprintf(_("Chain DB migration failed: %s"), strMigrateError.c_str()));
|
||||
bool fExplicit = GetBoolArg("-migratechaindb", false) ||
|
||||
GetBoolArg("-migratechaindbforce", false);
|
||||
bool fAuto = IsRocksDbChainBackend() &&
|
||||
fs::exists(GetDataDir() / "txleveldb") &&
|
||||
!fs::exists(GetDataDir() / "rocksdb");
|
||||
if (fExplicit || fAuto)
|
||||
{
|
||||
uiInterface.InitMessage(_("Migrating chain database to RocksDB..."));
|
||||
if (fAuto && !fExplicit)
|
||||
printf("ChainDB: RocksDB backend active with a legacy LevelDB present; "
|
||||
"migrating automatically.\n");
|
||||
std::string strMigrateError;
|
||||
bool fForce = GetBoolArg("-migratechaindbforce", false);
|
||||
if (!MaybeMigrateLevelDbToRocksDb(fForce, strMigrateError))
|
||||
return InitError(strprintf(_("Chain DB migration failed: %s"), strMigrateError.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
// ********************************************************* Step 7: load blockchain
|
||||
@@ -1670,6 +1772,26 @@ bool AppInit2()
|
||||
if (!NewThread(ThreadTorMaintenance, nullptr))
|
||||
printf("Warning: ThreadTorMaintenance could not be started\n");
|
||||
}
|
||||
|
||||
// Bring up I2P (SAM) transport alongside Tor so the wallet has both a
|
||||
// .onion and a .b32.i2p address. On by default; disable with -i2p=0.
|
||||
// A bundled i2pd router is launched automatically (mirroring embedded
|
||||
// Tor); if -i2psam points at a non-loopback bridge, or a router is
|
||||
// already running, we use that instead.
|
||||
if (GetBoolArg("-i2p", true)) {
|
||||
int64_t nI2PStart = GetTimeMillis();
|
||||
|
||||
uiInterface.InitMessage(_("Starting the I2P router..."));
|
||||
bool i2pStarted = StartEmbeddedI2P();
|
||||
StartupPerfLog("i2p_start", GetTimeMillis() - nI2PStart, strprintf("started=%d", i2pStarted));
|
||||
if (i2pStarted) {
|
||||
SetReachable(NET_I2P, true);
|
||||
std::string i2pAddr = CI2PEmbedded::GetInstance()->GetI2PAddress();
|
||||
printf("I2P network enabled. Our address: %s\n", i2pAddr.c_str());
|
||||
} else {
|
||||
printf("NOTICE: I2P not available this session; continuing with Tor only\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ********************************************************* Step 9: import blocks
|
||||
|
||||
+302
-2
@@ -3478,10 +3478,26 @@ bool ProcessBlock(CNode* pfrom, CBlock* pblock)
|
||||
bnRequired.SetCompact(ComputeMinWork(pindexLastPow->nBits, deltaTime));
|
||||
}
|
||||
|
||||
if (bnRequired != 0 && bnNewBlock > bnRequired)
|
||||
// bnNewBlock is the difficulty of the candidate block (compact bits -> target).
|
||||
// bnRequired is the MINIMUM difficulty the block must meet (based on time since
|
||||
// last checkpoint / chain tip). If the candidate's target is SMALLER than required
|
||||
// (i.e. block is harder than allowed), it's "too much" difficulty and we reject.
|
||||
// If LARGER (less difficulty = easier than required), it's "too little" and we reject.
|
||||
// PREVIOUS BUG: condition was `bnNewBlock > bnRequired` paired with "too little"
|
||||
// error message — the message and the trigger were swapped. This caused honest
|
||||
// blocks during legitimate time-warps (fork recovery, chain catchup) to be
|
||||
// labelled "too little proof-of-stake" while the actual reject reason was the
|
||||
// OPPOSITE — block had TOO MUCH difficulty relative to elapsed time.
|
||||
// Fixed: condition now matches the message (block too easy => reject).
|
||||
if (bnRequired != 0 && bnNewBlock < bnRequired)
|
||||
{
|
||||
// Anti-spam is a soft scoring signal, NOT a hard ban trigger. A single
|
||||
// violation should log + score modestly, not 24-hour-ban honest peers
|
||||
// (which is what happened during the 2026-06-23 DNS2 clearnet-fork
|
||||
// incident — `Misbehaving(100)` crossed the banscore threshold on the
|
||||
// FIRST block, instantly banning every honest peer feeding us fork blocks).
|
||||
if (pfrom)
|
||||
pfrom->Misbehaving(100);
|
||||
pfrom->Misbehaving(5);
|
||||
return error("ProcessBlock() : block with too little %s", pblock->IsProofOfStake()? "proof-of-stake" : "proof-of-work");
|
||||
}
|
||||
}
|
||||
@@ -4010,6 +4026,290 @@ bool LoadExternalBlockFile(FILE* fileIn)
|
||||
return nLoaded > 0;
|
||||
}
|
||||
|
||||
|
||||
bool FastImportBlockFile()
|
||||
{
|
||||
// Fast block import: reads blk0001.dat and builds the block index
|
||||
// directly without re-writing block data. LevelDB writes are batched
|
||||
// every 200K blocks for speed. Only used for trusted bootstrap data
|
||||
// (blocks below the hardcoded checkpoint).
|
||||
|
||||
fs::path blkPath = GetDataDir() / "blk0001.dat";
|
||||
if (!fs::exists(blkPath))
|
||||
return false;
|
||||
|
||||
printf("FastImportBlockFile: starting from %s\n", blkPath.string().c_str());
|
||||
int64_t nStart = GetTimeMillis();
|
||||
|
||||
FILE* fileIn = fopen(blkPath.string().c_str(), "rb");
|
||||
if (!fileIn)
|
||||
return false;
|
||||
|
||||
// Get file size for progress
|
||||
fseek(fileIn, 0, SEEK_END);
|
||||
int64_t nFileSize = ftell(fileIn);
|
||||
fseek(fileIn, 0, SEEK_SET);
|
||||
|
||||
int nLoaded = 0;
|
||||
int64_t nLastProgressReport = 0;
|
||||
|
||||
{
|
||||
LOCK(cs_main);
|
||||
CAutoFile blkdat(fileIn, SER_DISK, CLIENT_VERSION);
|
||||
|
||||
auto txdb_holder = MakeChainDB(); CTxDBBase& txdb = *txdb_holder;
|
||||
txdb.TxnBegin();
|
||||
|
||||
unsigned int nPos = 0;
|
||||
while (nPos != (unsigned int)-1 && blkdat.good() && !fRequestShutdown)
|
||||
{
|
||||
// Find message start bytes (same scan as LoadExternalBlockFile)
|
||||
unsigned char pchData[65536];
|
||||
do {
|
||||
fseek(blkdat, nPos, SEEK_SET);
|
||||
int nRead = fread(pchData, 1, sizeof(pchData), blkdat);
|
||||
if (nRead <= 8)
|
||||
{
|
||||
nPos = (unsigned int)-1;
|
||||
break;
|
||||
}
|
||||
void* nFind = memchr(pchData, pchMessageStart[0], nRead+1-sizeof(pchMessageStart));
|
||||
if (nFind)
|
||||
{
|
||||
if (memcmp(nFind, pchMessageStart, sizeof(pchMessageStart))==0)
|
||||
{
|
||||
nPos += ((unsigned char*)nFind - pchData) + sizeof(pchMessageStart);
|
||||
break;
|
||||
}
|
||||
nPos += ((unsigned char*)nFind - pchData) + 1;
|
||||
}
|
||||
else
|
||||
nPos += sizeof(pchData) - sizeof(pchMessageStart) + 1;
|
||||
} while(!fRequestShutdown);
|
||||
|
||||
if (nPos == (unsigned int)-1)
|
||||
break;
|
||||
|
||||
fseek(blkdat, nPos, SEEK_SET);
|
||||
unsigned int nSize;
|
||||
blkdat >> nSize;
|
||||
|
||||
if (nSize == 0 || nSize > MAX_BLOCK_SIZE)
|
||||
{
|
||||
nPos += 4 + nSize;
|
||||
continue;
|
||||
}
|
||||
|
||||
// nBlockPos = file position where the block data starts
|
||||
// (after 4-byte message start + 4-byte size)
|
||||
unsigned int nBlockPos = nPos + 4;
|
||||
|
||||
CBlock block;
|
||||
blkdat >> block;
|
||||
|
||||
uint256 hash = block.GetHash();
|
||||
if (mapBlockIndex.count(hash))
|
||||
{
|
||||
nPos += 4 + nSize;
|
||||
continue; // already indexed
|
||||
}
|
||||
|
||||
// Create CBlockIndex
|
||||
CBlockIndex* pindexNew = new CBlockIndex(1, nBlockPos, block);
|
||||
if (!pindexNew)
|
||||
break;
|
||||
|
||||
// Link to previous block
|
||||
auto miPrev = mapBlockIndex.find(block.hashPrevBlock);
|
||||
if (miPrev != mapBlockIndex.end())
|
||||
{
|
||||
pindexNew->pprev = miPrev->second;
|
||||
pindexNew->nHeight = pindexNew->pprev->nHeight + 1;
|
||||
}
|
||||
|
||||
// Chain trust
|
||||
pindexNew->nChainTrust = (pindexNew->pprev ? pindexNew->pprev->nChainTrust : 0) + pindexNew->GetBlockTrust();
|
||||
|
||||
// Stake entropy bit
|
||||
pindexNew->SetStakeEntropyBit(block.GetStakeEntropyBit());
|
||||
|
||||
// Stake modifier (minimal for blocks far below checkpoint)
|
||||
int nCheckpointHeight = Checkpoints::GetTotalBlocksEstimate();
|
||||
if (pindexNew->nHeight >= nCheckpointHeight - 1000)
|
||||
{
|
||||
uint64_t nStakeModifier = 0;
|
||||
bool fGeneratedStakeModifier = false;
|
||||
ComputeNextStakeModifier(pindexNew->pprev, nStakeModifier, fGeneratedStakeModifier);
|
||||
pindexNew->SetStakeModifier(nStakeModifier, fGeneratedStakeModifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
pindexNew->SetStakeModifier(0, pindexNew->nHeight == 0);
|
||||
}
|
||||
pindexNew->nStakeModifierChecksum = GetStakeModifierChecksum(pindexNew);
|
||||
|
||||
// PoS stake seen set
|
||||
if (pindexNew->IsProofOfStake())
|
||||
setStakeSeen.insert(make_pair(pindexNew->prevoutStake, pindexNew->nStakeTime));
|
||||
|
||||
// Insert into mapBlockIndex
|
||||
auto mi = mapBlockIndex.insert(make_pair(hash, pindexNew)).first;
|
||||
pindexNew->phashBlock = &mi->first;
|
||||
|
||||
// Link pnext for previous block
|
||||
if (pindexNew->pprev)
|
||||
pindexNew->pprev->pnext = pindexNew;
|
||||
|
||||
// NOTE: tx-index, UTXO-set and money-supply application are
|
||||
// DEFERRED to a second pass over the active (best-trust) chain
|
||||
// only — see the pass after this loop. Applying them here, for
|
||||
// every block read from the file (which permanently retains
|
||||
// ORPHANED side-chain blocks), wrote those orphans' outputs into
|
||||
// the UTXO set as phantom coins and over-counted nMoneySupply.
|
||||
// That was the root cause of UTXO-set / supply inflation on every
|
||||
// reindex. Here we only build the block index for all blocks so
|
||||
// best-chain selection by trust still works.
|
||||
txdb.WriteBlockIndex(CDiskBlockIndex(pindexNew));
|
||||
|
||||
// Update best chain
|
||||
if (pindexNew->nChainTrust > nBestChainTrust)
|
||||
{
|
||||
hashBestChain = hash;
|
||||
pindexBest = pindexNew;
|
||||
pblockindexFBBHLast = nullptr;
|
||||
nBestHeight = pindexNew->nHeight;
|
||||
nBestChainTrust = pindexNew->nChainTrust;
|
||||
nTimeBestReceived = GetTime();
|
||||
}
|
||||
|
||||
// Set genesis block
|
||||
if (pindexGenesisBlock == nullptr && pindexNew->nHeight == 0)
|
||||
pindexGenesisBlock = pindexNew;
|
||||
|
||||
nLoaded++;
|
||||
nPos += 4 + nSize;
|
||||
|
||||
// Batch commit every 200K blocks for LevelDB efficiency
|
||||
if (nLoaded % 200000 == 0)
|
||||
{
|
||||
txdb.WriteHashBestChain(hashBestChain);
|
||||
txdb.TxnCommit();
|
||||
txdb.TxnBegin();
|
||||
}
|
||||
|
||||
// Report progress every 5000 blocks to keep GUI responsive.
|
||||
// AppInit2 runs on the GUI thread, so uiInterface.InitMessage
|
||||
// triggers processEvents() which prevents the window from freezing.
|
||||
if (nLoaded % 5000 == 0)
|
||||
{
|
||||
int pct = (nFileSize > 0) ? (int)((int64_t)nPos * 100 / nFileSize) : 0;
|
||||
printf("FastImport: %d blocks indexed (%d%%)\n", nLoaded, pct);
|
||||
uiInterface.InitMessage(strprintf(_("Importing blocks... %d indexed (%d%%)"), nLoaded, pct));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Pass 2: apply tx-index, UTXO set and money supply along the
|
||||
// ACTIVE (best-trust) chain ONLY. The file-order pass above indexed
|
||||
// every block including orphaned side-chain blocks; replaying only
|
||||
// the main chain here keeps the UTXO set and money supply exactly in
|
||||
// consensus and prevents orphan outputs becoming phantom coins. ----
|
||||
if (pindexBest)
|
||||
{
|
||||
std::vector<CBlockIndex*> vMain;
|
||||
for (CBlockIndex* p = pindexBest; p; p = p->pprev)
|
||||
vMain.push_back(p);
|
||||
std::reverse(vMain.begin(), vMain.end());
|
||||
printf("FastImportBlockFile: applying UTXO/supply along %d main-chain blocks...\n", (int)vMain.size());
|
||||
uiInterface.InitMessage(_("Building UTXO set (main chain)..."));
|
||||
|
||||
int64_t nRunningSupply = 0;
|
||||
int nApplied = 0;
|
||||
for (CBlockIndex* pindex : vMain)
|
||||
{
|
||||
// Genesis (height 0) is a hardcoded special block that is not
|
||||
// re-read from disk this way; it contributes nothing to supply
|
||||
// and the genesis-walk audit skips it identically. Carry the
|
||||
// running supply (0) forward and move on.
|
||||
if (pindex->nHeight == 0)
|
||||
{
|
||||
pindex->nMint = 0;
|
||||
pindex->nMoneySupply = nRunningSupply; // still 0 here
|
||||
txdb.WriteBlockIndex(CDiskBlockIndex(pindex));
|
||||
continue;
|
||||
}
|
||||
|
||||
CBlock blockMain;
|
||||
if (!blockMain.ReadFromDisk(pindex))
|
||||
return error("FastImportBlockFile: ReadFromDisk failed at height %d", pindex->nHeight);
|
||||
|
||||
int64_t nBlockValueIn = 0;
|
||||
int64_t nBlockValueOut = 0;
|
||||
unsigned int nTxPos2 = pindex->nBlockPos + ::GetSerializeSize(CBlock(), SER_DISK, CLIENT_VERSION)
|
||||
- (2 * GetSizeOfCompactSize(0)) + GetSizeOfCompactSize(blockMain.vtx.size());
|
||||
for (const CTransaction& tx : blockMain.vtx)
|
||||
{
|
||||
uint256 hashTx = tx.GetHash();
|
||||
CDiskTxPos posThisTx(1, pindex->nBlockPos, nTxPos2);
|
||||
txdb.UpdateTxIndex(hashTx, CTxIndex(posThisTx, tx.vout.size()));
|
||||
nTxPos2 += ::GetSerializeSize(tx, SER_DISK, CLIENT_VERSION);
|
||||
|
||||
nBlockValueOut += tx.GetValueOut();
|
||||
if (!tx.IsCoinBase())
|
||||
{
|
||||
for (const CTxIn& txin : tx.vin)
|
||||
{
|
||||
CUtxoEntry uprev;
|
||||
if (txdb.ReadUtxo(txin.prevout.hash, txin.prevout.n, uprev))
|
||||
nBlockValueIn += uprev.nValue;
|
||||
txdb.EraseUtxo(txin.prevout.hash, txin.prevout.n);
|
||||
}
|
||||
}
|
||||
for (unsigned int k = 0; k < tx.vout.size(); k++)
|
||||
{
|
||||
if (tx.vout[k].IsEmpty())
|
||||
continue;
|
||||
CUtxoEntry utxo;
|
||||
utxo.nValue = tx.vout[k].nValue;
|
||||
utxo.nHeight = pindex->nHeight;
|
||||
utxo.scriptPubKey = tx.vout[k].scriptPubKey;
|
||||
utxo.fCoinBase = tx.IsCoinBase();
|
||||
utxo.fCoinStake = tx.IsCoinStake();
|
||||
utxo.nTxTime = tx.nTime;
|
||||
txdb.WriteUtxo(hashTx, k, utxo);
|
||||
}
|
||||
}
|
||||
|
||||
pindex->nMint = nBlockValueOut - nBlockValueIn;
|
||||
nRunningSupply += (nBlockValueOut - nBlockValueIn);
|
||||
pindex->nMoneySupply = nRunningSupply;
|
||||
txdb.WriteBlockIndex(CDiskBlockIndex(pindex));
|
||||
|
||||
if (++nApplied % 200000 == 0) { txdb.TxnCommit(); txdb.TxnBegin(); }
|
||||
if (nApplied % 5000 == 0)
|
||||
{
|
||||
int pct2 = (int)((int64_t)nApplied * 100 / (vMain.empty() ? 1 : vMain.size()));
|
||||
printf("FastImport UTXO apply: %d/%d main-chain blocks (%d%%)\n", nApplied, (int)vMain.size(), pct2);
|
||||
uiInterface.InitMessage(strprintf(_("Building UTXO set... %d%%"), pct2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final commit
|
||||
if (pindexBest)
|
||||
{
|
||||
txdb.WriteHashBestChain(hashBestChain);
|
||||
|
||||
// Write sync checkpoint
|
||||
Checkpoints::WriteSyncCheckpoint(hashBestChain);
|
||||
}
|
||||
txdb.TxnCommit();
|
||||
}
|
||||
|
||||
nTransactionsUpdated++;
|
||||
printf("FastImportBlockFile: indexed %d blocks in %" PRId64 "ms\n", nLoaded, GetTimeMillis() - nStart);
|
||||
return nLoaded > 0;
|
||||
}
|
||||
|
||||
string GetWarnings(string strFor)
|
||||
{
|
||||
string strStatusBar;
|
||||
|
||||
+48
@@ -650,6 +650,54 @@ CNode* ConnectNode(CAddress addrConnect, const char *pszDest)
|
||||
}
|
||||
}
|
||||
|
||||
// Adopt a connected I2P SAM data socket (from the accept loop in i2p.cpp) as an
|
||||
// inbound peer. The socket arrives in blocking mode; switch it to non-blocking
|
||||
// to match the rest of the socket handler, then register the node.
|
||||
void AddI2PInboundNode(SOCKET hSocket, const CAddress& addr)
|
||||
{
|
||||
if (hSocket == INVALID_SOCKET)
|
||||
return;
|
||||
|
||||
if (CNode::IsBanned(addr)) {
|
||||
printf("I2P inbound from %s dropped (banned)\n", addr.ToString().c_str());
|
||||
closesocket(hSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
// Honour the inbound connection limit.
|
||||
int nInbound = 0;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
if (pnode->fInbound)
|
||||
nInbound++;
|
||||
}
|
||||
int nMaxInbound = GetArg("-maxconnections", 125) - MAX_OUTBOUND_CONNECTIONS;
|
||||
if (nInbound >= nMaxInbound) {
|
||||
printf("I2P inbound from %s dropped (too many inbound)\n", addr.ToString().c_str());
|
||||
closesocket(hSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
u_long nOne = 1;
|
||||
if (ioctlsocket(hSocket, FIONBIO, &nOne) == SOCKET_ERROR)
|
||||
printf("AddI2PInboundNode() : ioctlsocket non-blocking setting failed, error %d\n", WSAGetLastError());
|
||||
#else
|
||||
if (fcntl(hSocket, F_SETFL, O_NONBLOCK) == SOCKET_ERROR)
|
||||
printf("AddI2PInboundNode() : fcntl non-blocking setting failed, error %d\n", errno);
|
||||
#endif
|
||||
|
||||
printf("accepted I2P connection %s\n", addr.ToString().c_str());
|
||||
CNode* pnode = new CNode(hSocket, addr, "", true);
|
||||
pnode->AddRef();
|
||||
pnode->nTimeConnected = GetTime();
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
vNodes.push_back(pnode);
|
||||
}
|
||||
}
|
||||
|
||||
void CNode::CloseSocketDisconnect()
|
||||
{
|
||||
fDisconnect = true;
|
||||
|
||||
@@ -37,6 +37,8 @@ void AddressCurrentlyConnected(const CService& addr);
|
||||
CNode* FindNode(const CNetAddr& ip);
|
||||
CNode* FindNode(const CService& ip);
|
||||
CNode* ConnectNode(CAddress addrConnect, const char *strDest = nullptr);
|
||||
// Adopt a connected I2P SAM data socket as an inbound peer (called from i2p.cpp).
|
||||
void AddI2PInboundNode(SOCKET hSocket, const CAddress& addr);
|
||||
void MapPort();
|
||||
unsigned short GetListenPort();
|
||||
bool BindListenPort(const CService &bindAddr, std::string& strError=REF(std::string()));
|
||||
|
||||
+45
-3
@@ -672,6 +672,7 @@ void CNetAddr::Init()
|
||||
memset(ip, 0, sizeof(ip));
|
||||
memset(tor_v3_pubkey, 0, sizeof(tor_v3_pubkey));
|
||||
m_is_tor_v3 = false;
|
||||
m_is_i2p = false;
|
||||
}
|
||||
|
||||
void CNetAddr::SetIP(const CNetAddr& ipIn)
|
||||
@@ -679,6 +680,7 @@ void CNetAddr::SetIP(const CNetAddr& ipIn)
|
||||
memcpy(ip, ipIn.ip, sizeof(ip));
|
||||
memcpy(tor_v3_pubkey, ipIn.tor_v3_pubkey, sizeof(tor_v3_pubkey));
|
||||
m_is_tor_v3 = ipIn.m_is_tor_v3;
|
||||
m_is_i2p = ipIn.m_is_i2p;
|
||||
}
|
||||
|
||||
static const unsigned char pchOnionCat[] = {0xFD,0x87,0xD8,0x7E,0xEB,0x43};
|
||||
@@ -734,6 +736,22 @@ bool CNetAddr::SetSpecial(const std::string &strName)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Modern I2P base32 address: 52 base32 chars = SHA-256(destination) (32 bytes)
|
||||
// rendered as "<b32>.b32.i2p". Store the hash and flag this as an I2P address.
|
||||
if (strName.size()>8 && strName.substr(strName.size() - 8, 8) == ".b32.i2p") {
|
||||
std::string addrPart = strName.substr(0, strName.size() - 8);
|
||||
std::vector<unsigned char> vchAddr = DecodeBase32(addrPart.c_str());
|
||||
if (vchAddr.size() != 32)
|
||||
return false;
|
||||
// Keep the GarliCat prefix in ip[] so legacy reachability checks that
|
||||
// look for unique-local space still treat this as a routable overlay.
|
||||
memcpy(ip, pchGarliCat, sizeof(pchGarliCat));
|
||||
memset(ip + sizeof(pchGarliCat), 0, 16 - sizeof(pchGarliCat));
|
||||
memcpy(tor_v3_pubkey, vchAddr.data(), 32);
|
||||
m_is_i2p = true;
|
||||
m_is_tor_v3 = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -856,7 +874,7 @@ bool CNetAddr::IsTorV3() const
|
||||
|
||||
bool CNetAddr::IsI2P() const
|
||||
{
|
||||
return (memcmp(ip, pchGarliCat, sizeof(pchGarliCat)) == 0);
|
||||
return m_is_i2p || (memcmp(ip, pchGarliCat, sizeof(pchGarliCat)) == 0);
|
||||
}
|
||||
|
||||
bool CNetAddr::IsLocal() const
|
||||
@@ -962,6 +980,13 @@ std::string CNetAddr::ToStringIP() const
|
||||
}
|
||||
if (IsTor())
|
||||
return EncodeBase32(&ip[6], 10) + ".onion";
|
||||
if (m_is_i2p) {
|
||||
// Modern I2P: base32 of the 32-byte destination hash, unpadded.
|
||||
std::string b32 = EncodeBase32(tor_v3_pubkey, 32);
|
||||
while (!b32.empty() && b32[b32.size() - 1] == '=')
|
||||
b32.erase(b32.size() - 1);
|
||||
return b32 + ".b32.i2p";
|
||||
}
|
||||
if (IsI2P())
|
||||
return EncodeBase32(&ip[6], 10) + ".b32.i2p";
|
||||
CService serv(*this, 0);
|
||||
@@ -995,12 +1020,14 @@ bool operator==(const CNetAddr& a, const CNetAddr& b)
|
||||
{
|
||||
if (a.m_is_tor_v3 || b.m_is_tor_v3)
|
||||
return a.m_is_tor_v3 == b.m_is_tor_v3 && memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) == 0;
|
||||
if (a.m_is_i2p || b.m_is_i2p)
|
||||
return a.m_is_i2p == b.m_is_i2p && memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) == 0;
|
||||
return (memcmp(a.ip, b.ip, 16) == 0);
|
||||
}
|
||||
|
||||
bool operator!=(const CNetAddr& a, const CNetAddr& b)
|
||||
{
|
||||
return (memcmp(a.ip, b.ip, 16) != 0);
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
bool operator<(const CNetAddr& a, const CNetAddr& b)
|
||||
@@ -1009,6 +1036,10 @@ bool operator<(const CNetAddr& a, const CNetAddr& b)
|
||||
return !a.m_is_tor_v3; // non-v3 sorts before v3
|
||||
if (a.m_is_tor_v3)
|
||||
return memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) < 0;
|
||||
if (a.m_is_i2p != b.m_is_i2p)
|
||||
return !a.m_is_i2p; // non-i2p sorts before i2p
|
||||
if (a.m_is_i2p)
|
||||
return memcmp(a.tor_v3_pubkey, b.tor_v3_pubkey, 32) < 0;
|
||||
return (memcmp(a.ip, b.ip, 16) < 0);
|
||||
}
|
||||
|
||||
@@ -1032,6 +1063,17 @@ bool CNetAddr::GetIn6Addr(struct in6_addr* pipv6Addr) const
|
||||
// no two connections will be attempted to addresses with the same group
|
||||
std::vector<unsigned char> CNetAddr::GetGroup() const
|
||||
{
|
||||
// Modern I2P addresses keep their identifying bytes in the 32-byte
|
||||
// destination-hash field (ip[] only holds the overlay prefix), so derive
|
||||
// the group from the hash to keep peers in distinct groups.
|
||||
if (m_is_i2p) {
|
||||
std::vector<unsigned char> vch;
|
||||
vch.push_back(NET_I2P);
|
||||
vch.push_back(tor_v3_pubkey[0]);
|
||||
vch.push_back(tor_v3_pubkey[1]);
|
||||
return vch;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> vchRet;
|
||||
int nClass = NET_IPV6;
|
||||
int nStartByte = 0;
|
||||
@@ -1106,7 +1148,7 @@ std::vector<unsigned char> CNetAddr::GetGroup() const
|
||||
uint64_t CNetAddr::GetHash() const
|
||||
{
|
||||
uint256 hash;
|
||||
if (m_is_tor_v3)
|
||||
if (m_is_tor_v3 || m_is_i2p)
|
||||
hash = Hash(&tor_v3_pubkey[0], &tor_v3_pubkey[32]);
|
||||
else
|
||||
hash = Hash(&ip[0], &ip[16]);
|
||||
|
||||
+7
-1
@@ -106,8 +106,12 @@ class CNetAddr
|
||||
{
|
||||
protected:
|
||||
unsigned char ip[16]; // in network byte order
|
||||
unsigned char tor_v3_pubkey[32]; // Ed25519 public key for Tor v3 onion addresses
|
||||
// For Tor v3 this holds the 32-byte Ed25519 public key. When m_is_i2p is
|
||||
// set it instead holds the 32-byte SHA-256 of the I2P destination (the
|
||||
// value rendered as the ".b32.i2p" address). A CNetAddr is never both.
|
||||
unsigned char tor_v3_pubkey[32];
|
||||
bool m_is_tor_v3;
|
||||
bool m_is_i2p;
|
||||
|
||||
public:
|
||||
CNetAddr();
|
||||
@@ -160,6 +164,7 @@ class CNetAddr
|
||||
READWRITE(FLATDATA(ip));
|
||||
READWRITE(FLATDATA(tor_v3_pubkey));
|
||||
READWRITE(m_is_tor_v3);
|
||||
READWRITE(m_is_i2p);
|
||||
)
|
||||
};
|
||||
|
||||
@@ -203,6 +208,7 @@ class CService : public CNetAddr
|
||||
READWRITE(FLATDATA(ip));
|
||||
READWRITE(FLATDATA(tor_v3_pubkey));
|
||||
READWRITE(m_is_tor_v3);
|
||||
READWRITE(m_is_i2p);
|
||||
unsigned short portN = htons(port);
|
||||
READWRITE(portN);
|
||||
if (fRead)
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
// Hardcoded onion seed nodes for initial peer discovery.
|
||||
// Also fetched dynamically via https://seeds.cryptographic-triangles.org/seeds.txt
|
||||
static const char *strMainNetOnionSeed[][1] = {
|
||||
// SAMI-PC - authoritative wallet node (main PC)
|
||||
{"6ygpphp2qsucwvhwefv6h6ehvk6zjf7b7zdp4ggkzjjwe76cg6jwm7id.onion"},
|
||||
// DNS2 - primary bootstrap server (194.233.88.206)
|
||||
{"gxvrhv3qitnc6kobrhsrse46bmcfitnybapor3or3oczzuxn6hfzxyid.onion"},
|
||||
// DNS3 - canonical chain reference (74.208.167.19)
|
||||
|
||||
+89
-82
@@ -1,32 +1,27 @@
|
||||
// Copyright (c) 2009-2012 The Bitcoin developers
|
||||
// Copyright (c) 2026 The Triangles developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <boost/version.hpp>
|
||||
#if defined(WIN32) && BOOST_VERSION == 104900
|
||||
#define BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME
|
||||
#define BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME
|
||||
#endif
|
||||
//
|
||||
// Single-instance "triangles:" URI handoff. When the wallet is launched with a
|
||||
// URI argument and an instance is already running, the URI is relayed to the
|
||||
// running instance over a local socket; otherwise this instance becomes the
|
||||
// listener. Reworked from Boost.Interprocess message queues onto Qt's
|
||||
// QLocalServer/QLocalSocket (QtNetwork) — no Boost dependency.
|
||||
|
||||
#include "qtipcserver.h"
|
||||
#include "guiconstants.h"
|
||||
#include "ui_interface.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
||||
#include <boost/interprocess/ipc/message_queue.hpp>
|
||||
#include <boost/version.hpp>
|
||||
|
||||
#if defined(WIN32) && (!defined(BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME) || !defined(BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME) || BOOST_VERSION < 104900)
|
||||
#warning Compiling without BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME and BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME uncommented in boost/interprocess/detail/tmp_dir_helpers.hpp or using a boost version before 1.49 may have unintended results see svn.boost.org/trac/boost/ticket/5392
|
||||
#endif
|
||||
|
||||
using namespace boost;
|
||||
using namespace boost::interprocess;
|
||||
using namespace boost::posix_time;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QLocalServer>
|
||||
#include <QLocalSocket>
|
||||
#include <QString>
|
||||
|
||||
#if defined MAC_OSX || defined __FreeBSD__
|
||||
// URI handling not implemented on OSX yet
|
||||
@@ -36,33 +31,47 @@ void ipcInit(int argc, char *argv[]) { }
|
||||
|
||||
#else
|
||||
|
||||
// Local-socket server name. QLocalServer maps this to a named pipe on Windows
|
||||
// and a filesystem socket on Unix.
|
||||
static const QString IPC_SERVER_NAME = QStringLiteral(TRIANGLESURI_QUEUE_NAME);
|
||||
|
||||
static void ipcThread2(void* pArg);
|
||||
|
||||
static bool IsTrianglesURI(const char* arg)
|
||||
{
|
||||
// Case-insensitive match of the "Triangles:" scheme prefix.
|
||||
return std::equal(std::begin("Triangles:"), std::end("Triangles:") - 1, arg,
|
||||
[](char a, char b) {
|
||||
return std::tolower(static_cast<unsigned char>(a)) ==
|
||||
std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
}
|
||||
|
||||
static bool ipcScanCmd(int argc, char *argv[], bool fRelay)
|
||||
{
|
||||
// Check for URI in argv
|
||||
// Check for URI in argv and relay it to a running instance, if any.
|
||||
bool fSent = false;
|
||||
for (int i = 1; i < argc; i++)
|
||||
{
|
||||
if (std::equal(std::begin("Triangles:"), std::end("Triangles:") - 1, argv[i], [](char a, char b) { return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b)); }))
|
||||
if (!IsTrianglesURI(argv[i]))
|
||||
continue;
|
||||
|
||||
const char *strURI = argv[i];
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(IPC_SERVER_NAME);
|
||||
if (socket.waitForConnected(1000))
|
||||
{
|
||||
const char *strURI = argv[i];
|
||||
try {
|
||||
boost::interprocess::message_queue mq(boost::interprocess::open_only, TRIANGLESURI_QUEUE_NAME);
|
||||
if (mq.try_send(strURI, strlen(strURI), 0))
|
||||
fSent = true;
|
||||
else if (fRelay)
|
||||
break;
|
||||
}
|
||||
catch (boost::interprocess::interprocess_exception &ex) {
|
||||
// don't log the "file not found" exception, because that's normal for
|
||||
// the first start of the first instance
|
||||
if (ex.get_error_code() != boost::interprocess::not_found_error || !fRelay)
|
||||
{
|
||||
printf("main() - boost interprocess exception #%d: %s\n", ex.get_error_code(), ex.what());
|
||||
break;
|
||||
}
|
||||
}
|
||||
socket.write(strURI, static_cast<qint64>(strlen(strURI)));
|
||||
socket.flush();
|
||||
socket.waitForBytesWritten(1000);
|
||||
socket.disconnectFromServer();
|
||||
fSent = true;
|
||||
}
|
||||
else if (fRelay)
|
||||
{
|
||||
// No running instance accepted the URI; this process should become
|
||||
// the listener instead of relaying.
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fSent;
|
||||
@@ -78,7 +87,7 @@ static void ipcThread(void* pArg)
|
||||
{
|
||||
// Make this thread recognisable as the GUI-IPC thread
|
||||
RenameThread("Triangles-gui-ipc");
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
ipcThread2(pArg);
|
||||
@@ -95,69 +104,67 @@ static void ipcThread2(void* pArg)
|
||||
{
|
||||
printf("ipcThread started\n");
|
||||
|
||||
message_queue* mq = (message_queue*)pArg;
|
||||
char buffer[MAX_URI_LENGTH + 1] = "";
|
||||
size_t nSize = 0;
|
||||
unsigned int nPriority = 0;
|
||||
QLocalServer* server = static_cast<QLocalServer*>(pArg);
|
||||
|
||||
// Poll for inbound connections without requiring a Qt event loop:
|
||||
// waitForNewConnection(timeout) pumps the socket internally.
|
||||
while (true)
|
||||
{
|
||||
ptime d = boost::posix_time::microsec_clock::universal_time() + millisec(100);
|
||||
if (mq->timed_receive(&buffer, sizeof(buffer), nSize, nPriority, d))
|
||||
if (server->waitForNewConnection(100))
|
||||
{
|
||||
uiInterface.ThreadSafeHandleURI(std::string(buffer, nSize));
|
||||
MilliSleep(1000);
|
||||
QLocalSocket* client = server->nextPendingConnection();
|
||||
if (client)
|
||||
{
|
||||
if (client->waitForReadyRead(1000))
|
||||
{
|
||||
QByteArray data = client->readAll();
|
||||
if (data.size() > MAX_URI_LENGTH)
|
||||
data.truncate(MAX_URI_LENGTH);
|
||||
uiInterface.ThreadSafeHandleURI(std::string(data.constData(), data.size()));
|
||||
MilliSleep(1000);
|
||||
}
|
||||
client->disconnectFromServer();
|
||||
delete client;
|
||||
}
|
||||
}
|
||||
|
||||
if (fShutdown)
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove message queue
|
||||
message_queue::remove(TRIANGLESURI_QUEUE_NAME);
|
||||
// Cleanup allocated memory
|
||||
delete mq;
|
||||
server->close();
|
||||
delete server;
|
||||
}
|
||||
|
||||
void ipcInit(int argc, char *argv[])
|
||||
{
|
||||
message_queue* mq = NULL;
|
||||
char buffer[MAX_URI_LENGTH + 1] = "";
|
||||
size_t nSize = 0;
|
||||
unsigned int nPriority = 0;
|
||||
// Clear any stale socket/pipe left by a previous crashed instance, then
|
||||
// listen. If listen() fails, another instance already owns the name — in
|
||||
// that case relay our own URI args (below) and don't start a server.
|
||||
QLocalServer::removeServer(IPC_SERVER_NAME);
|
||||
|
||||
try {
|
||||
mq = new message_queue(open_or_create, TRIANGLESURI_QUEUE_NAME, 2, MAX_URI_LENGTH);
|
||||
|
||||
// Make sure we don't lose any Triangles: URIs
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
ptime d = boost::posix_time::microsec_clock::universal_time() + millisec(1);
|
||||
if (mq->timed_receive(&buffer, sizeof(buffer), nSize, nPriority, d))
|
||||
{
|
||||
uiInterface.ThreadSafeHandleURI(std::string(buffer, nSize));
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// Make sure only one Triangles instance is listening
|
||||
message_queue::remove(TRIANGLESURI_QUEUE_NAME);
|
||||
delete mq;
|
||||
|
||||
mq = new message_queue(open_or_create, TRIANGLESURI_QUEUE_NAME, 2, MAX_URI_LENGTH);
|
||||
}
|
||||
catch (interprocess_exception &ex) {
|
||||
printf("ipcInit() - boost interprocess exception #%d: %s\n", ex.get_error_code(), ex.what());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NewThread(ipcThread, mq))
|
||||
QLocalServer* server = new QLocalServer();
|
||||
server->setSocketOptions(QLocalServer::UserAccessOption); // owner-only access
|
||||
if (!server->listen(IPC_SERVER_NAME))
|
||||
{
|
||||
delete mq;
|
||||
printf("ipcInit() - QLocalServer listen failed: %s\n",
|
||||
server->errorString().toUtf8().constData());
|
||||
delete server;
|
||||
// Still try to relay any URI passed on our command line to whoever is
|
||||
// listening.
|
||||
ipcScanCmd(argc, argv, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NewThread(ipcThread, server))
|
||||
{
|
||||
server->close();
|
||||
delete server;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle a URI passed on our own command line (relayed to the server we
|
||||
// just started).
|
||||
ipcScanCmd(argc, argv, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -349,6 +349,12 @@ TrianglesGUI::TrianglesGUI(bool fIsTestnet, QWidget *parent):
|
||||
labelOnionAddress->setCursor(Qt::PointingHandCursor);
|
||||
labelOnionAddress->installEventFilter(this);
|
||||
|
||||
// I2P address, stacked directly above the .onion address (click to copy)
|
||||
labelI2PAddress = ui->label_i2p;
|
||||
labelI2PAddress->setVisible(false);
|
||||
labelI2PAddress->setCursor(Qt::PointingHandCursor);
|
||||
labelI2PAddress->installEventFilter(this);
|
||||
|
||||
// V3 indicator next to staking icon (hidden until onion is active)
|
||||
labelV3Icon = ui->label_v3;
|
||||
labelV3Icon->setVisible(false);
|
||||
@@ -370,7 +376,6 @@ TrianglesGUI::TrianglesGUI(bool fIsTestnet, QWidget *parent):
|
||||
|
||||
labelI2PIcon = ui->label_i2p_icon;
|
||||
labelI2PIcon->setVisible(false);
|
||||
|
||||
QTimer *timerI2P = new QTimer(this);
|
||||
connect(timerI2P, SIGNAL(timeout()), this, SLOT(updateI2PAddress()));
|
||||
timerI2P->start(5000);
|
||||
|
||||
@@ -110,8 +110,8 @@ private:
|
||||
QLabel *labelConnectionsIcon;
|
||||
QLabel *labelBlocksIcon;
|
||||
QLabel *labelOnionAddress;
|
||||
QLabel *labelV3Icon;
|
||||
QLabel *labelI2PAddress;
|
||||
QLabel *labelV3Icon;
|
||||
QLabel *labelI2PIcon;
|
||||
QLabel *labelTorIcon;
|
||||
QLabel *progressBarLabel;
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
//
|
||||
// Raw-socket transport for the JSON-RPC / REST HTTP server, replacing the
|
||||
// previous Boost.Asio implementation. Provides:
|
||||
//
|
||||
// - CSocketIOStream : a std::iostream backed by a connected SOCKET, so the
|
||||
// existing HTTP/JSON/SSE/REST code (which reads and writes std::iostream)
|
||||
// is unchanged.
|
||||
// - ConnectRPCSocket() : client-side connect (used by CallRPC).
|
||||
// - BindRPCSockets() : create listening sockets for the RPC server.
|
||||
// - SockaddrToString() : numeric host string for a peer address.
|
||||
//
|
||||
// TLS for the RPC port is intentionally not supported here (it was a rarely
|
||||
// used Boost.Asio::ssl feature). For remote access, front the RPC port with a
|
||||
// TLS terminator (stunnel / nginx) or reach it over SSH / Tor — the same
|
||||
// guidance Bitcoin Core adopted when it moved its RPC server off Boost.Asio.
|
||||
|
||||
#ifndef TRIANGLES_RPC_HTTPSOCKET_H
|
||||
#define TRIANGLES_RPC_HTTPSOCKET_H
|
||||
|
||||
#include "compat.h" // SOCKET, closesocket, INVALID_SOCKET, MSG_NOSIGNAL
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <streambuf>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef WIN32
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#endif
|
||||
|
||||
// ── std::streambuf over a connected socket ──────────────────────────────────
|
||||
class CSocketStreamBuf : public std::streambuf
|
||||
{
|
||||
public:
|
||||
explicit CSocketStreamBuf(SOCKET s) : m_socket(s)
|
||||
{
|
||||
setg(m_in, m_in, m_in); // empty get area to start
|
||||
}
|
||||
|
||||
protected:
|
||||
// Refill the get area with one recv().
|
||||
int_type underflow() override
|
||||
{
|
||||
if (gptr() < egptr())
|
||||
return traits_type::to_int_type(*gptr());
|
||||
int n = ::recv(m_socket, m_in, static_cast<int>(sizeof(m_in)), 0);
|
||||
if (n <= 0)
|
||||
return traits_type::eof(); // peer closed or error
|
||||
setg(m_in, m_in, m_in + n);
|
||||
return traits_type::to_int_type(*gptr());
|
||||
}
|
||||
|
||||
// Bulk write (operator<< on strings lands here).
|
||||
std::streamsize xsputn(const char* s, std::streamsize n) override
|
||||
{
|
||||
return SendAll(s, n) ? n : 0;
|
||||
}
|
||||
|
||||
int_type overflow(int_type ch) override
|
||||
{
|
||||
if (traits_type::eq_int_type(ch, traits_type::eof()))
|
||||
return traits_type::not_eof(ch);
|
||||
char c = static_cast<char>(ch);
|
||||
return SendAll(&c, 1) ? ch : traits_type::eof();
|
||||
}
|
||||
|
||||
int sync() override { return 0; } // sends are immediate; nothing buffered
|
||||
|
||||
private:
|
||||
bool SendAll(const char* s, std::streamsize n)
|
||||
{
|
||||
std::streamsize sent = 0;
|
||||
while (sent < n) {
|
||||
int r = ::send(m_socket, s + sent, static_cast<int>(n - sent), MSG_NOSIGNAL);
|
||||
if (r <= 0)
|
||||
return false;
|
||||
sent += r;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
SOCKET m_socket;
|
||||
char m_in[8192];
|
||||
};
|
||||
|
||||
// std::iostream that owns a CSocketStreamBuf bound to a socket. The socket
|
||||
// itself is owned by the caller (AcceptedConnection / CallRPC), not closed here.
|
||||
class CSocketIOStream : public std::iostream
|
||||
{
|
||||
public:
|
||||
explicit CSocketIOStream(SOCKET s) : std::iostream(nullptr), m_buf(s)
|
||||
{
|
||||
rdbuf(&m_buf);
|
||||
}
|
||||
|
||||
private:
|
||||
CSocketStreamBuf m_buf;
|
||||
};
|
||||
|
||||
// Numeric (no DNS) host string for a peer sockaddr, e.g. "127.0.0.1" or "::1".
|
||||
inline std::string SockaddrToString(const struct sockaddr* sa, socklen_t salen)
|
||||
{
|
||||
char host[NI_MAXHOST] = {0};
|
||||
if (::getnameinfo(sa, salen, host, sizeof(host), nullptr, 0, NI_NUMERICHOST) != 0)
|
||||
return "unknown";
|
||||
return std::string(host);
|
||||
}
|
||||
|
||||
// Client connect to host:port. Returns INVALID_SOCKET on failure.
|
||||
inline SOCKET ConnectRPCSocket(const std::string& host, int port)
|
||||
{
|
||||
struct addrinfo hints;
|
||||
std::memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
struct addrinfo* res = nullptr;
|
||||
const std::string portStr = std::to_string(port);
|
||||
if (::getaddrinfo(host.c_str(), portStr.c_str(), &hints, &res) != 0)
|
||||
return INVALID_SOCKET;
|
||||
|
||||
SOCKET hSocket = INVALID_SOCKET;
|
||||
for (struct addrinfo* rp = res; rp != nullptr; 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, static_cast<int>(rp->ai_addrlen)) == 0)
|
||||
break;
|
||||
closesocket(hSocket);
|
||||
hSocket = INVALID_SOCKET;
|
||||
}
|
||||
::freeaddrinfo(res);
|
||||
return hSocket;
|
||||
}
|
||||
|
||||
// Create listening sockets for the RPC server. When loopbackOnly is true the
|
||||
// server binds the loopback interface(s) only; otherwise it binds the wildcard
|
||||
// address(es). IPv4 and IPv6 are bound on separate sockets (IPV6_V6ONLY) so the
|
||||
// two never conflict. Returns the bound, listening sockets; empty + strError on
|
||||
// total failure (partial success — e.g. only IPv4 — is returned as success).
|
||||
inline std::vector<SOCKET> BindRPCSockets(int port, bool loopbackOnly, std::string& strError)
|
||||
{
|
||||
std::vector<SOCKET> vListen;
|
||||
|
||||
struct addrinfo hints;
|
||||
std::memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_flags = AI_PASSIVE; // wildcard when node == nullptr
|
||||
|
||||
struct addrinfo* res = nullptr;
|
||||
const std::string portStr = std::to_string(port);
|
||||
// "localhost" resolves to the loopback addresses (127.0.0.1 and ::1);
|
||||
// nullptr + AI_PASSIVE yields the wildcard addresses.
|
||||
const char* node = loopbackOnly ? "localhost" : nullptr;
|
||||
int gai = ::getaddrinfo(node, portStr.c_str(), &hints, &res);
|
||||
if (gai != 0) {
|
||||
strError = std::string("RPC bind: getaddrinfo failed: ") + gai_strerror(gai);
|
||||
return vListen;
|
||||
}
|
||||
|
||||
for (struct addrinfo* rp = res; rp != nullptr; rp = rp->ai_next) {
|
||||
if (rp->ai_family != AF_INET && rp->ai_family != AF_INET6)
|
||||
continue;
|
||||
SOCKET s = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
if (s == INVALID_SOCKET)
|
||||
continue;
|
||||
|
||||
int one = 1;
|
||||
::setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
|
||||
reinterpret_cast<const char*>(&one), sizeof(one));
|
||||
if (rp->ai_family == AF_INET6) {
|
||||
// Keep IPv6 sockets v6-only so a separate IPv4 socket can also bind.
|
||||
::setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY,
|
||||
reinterpret_cast<const char*>(&one), sizeof(one));
|
||||
}
|
||||
|
||||
if (::bind(s, rp->ai_addr, static_cast<int>(rp->ai_addrlen)) != 0 ||
|
||||
::listen(s, SOMAXCONN) != 0) {
|
||||
closesocket(s);
|
||||
continue;
|
||||
}
|
||||
vListen.push_back(s);
|
||||
}
|
||||
::freeaddrinfo(res);
|
||||
|
||||
if (vListen.empty())
|
||||
strError = "RPC bind: could not bind any address (port in use?)";
|
||||
return vListen;
|
||||
}
|
||||
|
||||
#endif // TRIANGLES_RPC_HTTPSOCKET_H
|
||||
+315
-319
@@ -1,319 +1,315 @@
|
||||
// Copyright (c) 2009-2012 Bitcoin Developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
|
||||
#include "init.h" // for pwalletMain
|
||||
#include "trianglesrpc.h"
|
||||
#include "ui_interface.h"
|
||||
#include "base58.h"
|
||||
|
||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
||||
|
||||
#define printf OutputDebugStringF
|
||||
|
||||
using namespace json_spirit;
|
||||
using namespace std;
|
||||
|
||||
void EnsureWalletIsUnlocked();
|
||||
|
||||
namespace bt = boost::posix_time;
|
||||
|
||||
// Extended DecodeDumpTime implementation, see this page for details:
|
||||
// http://stackoverflow.com/questions/3786201/parsing-of-date-time-from-string-boost
|
||||
const std::locale formats[] = {
|
||||
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%dT%H:%M:%SZ")),
|
||||
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%d %H:%M:%S")),
|
||||
std::locale(std::locale::classic(),new bt::time_input_facet("%Y/%m/%d %H:%M:%S")),
|
||||
std::locale(std::locale::classic(),new bt::time_input_facet("%d.%m.%Y %H:%M:%S")),
|
||||
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%d"))
|
||||
};
|
||||
|
||||
const size_t formats_n = sizeof(formats)/sizeof(formats[0]);
|
||||
|
||||
std::time_t pt_to_time_t(const bt::ptime& pt)
|
||||
{
|
||||
bt::ptime timet_start(boost::gregorian::date(1970,1,1));
|
||||
bt::time_duration diff = pt - timet_start;
|
||||
return diff.ticks()/bt::time_duration::rep_type::ticks_per_second;
|
||||
}
|
||||
|
||||
int64_t DecodeDumpTime(const std::string& s)
|
||||
{
|
||||
bt::ptime pt;
|
||||
|
||||
for(size_t i=0; i<formats_n; ++i)
|
||||
{
|
||||
std::istringstream is(s);
|
||||
is.imbue(formats[i]);
|
||||
is >> pt;
|
||||
if(pt != bt::ptime()) break;
|
||||
}
|
||||
|
||||
return pt_to_time_t(pt);
|
||||
}
|
||||
|
||||
std::string static EncodeDumpTime(int64_t nTime) {
|
||||
return DateTimeStrFormat("%Y-%m-%dT%H:%M:%SZ", nTime);
|
||||
}
|
||||
|
||||
std::string static EncodeDumpString(const std::string &str) {
|
||||
std::stringstream ret;
|
||||
for (unsigned char c : str) {
|
||||
if (c <= 32 || c >= 128 || c == '%') {
|
||||
ret << '%' << HexStr(&c, &c + 1);
|
||||
} else {
|
||||
ret << c;
|
||||
}
|
||||
}
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
std::string DecodeDumpString(const std::string &str) {
|
||||
std::stringstream ret;
|
||||
for (unsigned int pos = 0; pos < str.length(); pos++) {
|
||||
unsigned char c = str[pos];
|
||||
if (c == '%' && pos+2 < str.length()) {
|
||||
c = (((str[pos+1]>>6)*9+((str[pos+1]-'0')&15)) << 4) |
|
||||
((str[pos+2]>>6)*9+((str[pos+2]-'0')&15));
|
||||
pos += 2;
|
||||
}
|
||||
ret << c;
|
||||
}
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
class CTxDump
|
||||
{
|
||||
public:
|
||||
CBlockIndex *pindex;
|
||||
int64_t nValue;
|
||||
bool fSpent;
|
||||
CWalletTx* ptx;
|
||||
int nOut;
|
||||
CTxDump(CWalletTx* ptx = nullptr, int nOut = -1)
|
||||
{
|
||||
pindex = nullptr;
|
||||
nValue = 0;
|
||||
fSpent = false;
|
||||
this->ptx = ptx;
|
||||
this->nOut = nOut;
|
||||
}
|
||||
};
|
||||
|
||||
Value importprivkey(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() < 1 || params.size() > 2)
|
||||
throw runtime_error(
|
||||
"importprivkey <Trianglesprivkey> [label]\n"
|
||||
"Adds a private key (as returned by dumpprivkey) to your wallet.");
|
||||
|
||||
string strSecret = params[0].get_str();
|
||||
string strLabel = "";
|
||||
if (params.size() > 1)
|
||||
strLabel = params[1].get_str();
|
||||
CTrianglesSecret vchSecret;
|
||||
bool fGood = vchSecret.SetString(strSecret);
|
||||
|
||||
if (!fGood) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key");
|
||||
if (fWalletUnlockStakingOnly)
|
||||
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
|
||||
|
||||
CKey key;
|
||||
bool fCompressed;
|
||||
CSecret secret = vchSecret.GetSecret(fCompressed);
|
||||
key.SetSecret(secret, fCompressed);
|
||||
CKeyID vchAddress = key.GetPubKey().GetID();
|
||||
{
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
|
||||
pwalletMain->MarkDirty();
|
||||
pwalletMain->SetAddressBookName(vchAddress, strLabel);
|
||||
|
||||
if (!pwalletMain->AddKey(key))
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet");
|
||||
|
||||
pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
|
||||
pwalletMain->ReacceptWalletTransactions();
|
||||
}
|
||||
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
Value importwallet(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"importwallet <filename>\n"
|
||||
"Imports keys from a wallet dump file (see dumpwallet).");
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
ifstream file;
|
||||
file.open(params[0].get_str().c_str());
|
||||
if (!file.is_open())
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
|
||||
|
||||
int64_t nTimeBegin = pindexBest->nTime;
|
||||
|
||||
bool fGood = true;
|
||||
|
||||
while (file.good()) {
|
||||
std::string line;
|
||||
std::getline(file, line);
|
||||
if (line.empty() || line[0] == '#')
|
||||
continue;
|
||||
|
||||
auto vstr = SplitString(line, ' ');
|
||||
if (vstr.size() < 2)
|
||||
continue;
|
||||
CTrianglesSecret vchSecret;
|
||||
if (!vchSecret.SetString(vstr[0]))
|
||||
continue;
|
||||
|
||||
bool fCompressed;
|
||||
CKey key;
|
||||
CSecret secret = vchSecret.GetSecret(fCompressed);
|
||||
key.SetSecret(secret, fCompressed);
|
||||
CKeyID keyid = key.GetPubKey().GetID();
|
||||
|
||||
if (pwalletMain->HaveKey(keyid)) {
|
||||
printf("Skipping import of %s (key already present)\n", CTrianglesAddress(keyid).ToString().c_str());
|
||||
continue;
|
||||
}
|
||||
int64_t nTime = DecodeDumpTime(vstr[1]);
|
||||
std::string strLabel;
|
||||
bool fLabel = true;
|
||||
for (unsigned int nStr = 2; nStr < vstr.size(); nStr++) {
|
||||
if (vstr[nStr].starts_with("#"))
|
||||
break;
|
||||
if (vstr[nStr] == "change=1")
|
||||
fLabel = false;
|
||||
if (vstr[nStr] == "reserve=1")
|
||||
fLabel = false;
|
||||
if (vstr[nStr].starts_with("label=")) {
|
||||
strLabel = DecodeDumpString(vstr[nStr].substr(6));
|
||||
fLabel = true;
|
||||
}
|
||||
}
|
||||
printf("Importing %s...\n", CTrianglesAddress(keyid).ToString().c_str());
|
||||
if (!pwalletMain->AddKey(key)) {
|
||||
fGood = false;
|
||||
continue;
|
||||
}
|
||||
pwalletMain->mapKeyMetadata[keyid].nCreateTime = nTime;
|
||||
if (fLabel)
|
||||
pwalletMain->SetAddressBookName(keyid, strLabel);
|
||||
nTimeBegin = std::min(nTimeBegin, nTime);
|
||||
}
|
||||
file.close();
|
||||
|
||||
CBlockIndex *pindex = pindexBest;
|
||||
while (pindex && pindex->pprev && pindex->nTime > nTimeBegin - 7200)
|
||||
pindex = pindex->pprev;
|
||||
|
||||
if (!pwalletMain->nTimeFirstKey || nTimeBegin < pwalletMain->nTimeFirstKey)
|
||||
pwalletMain->nTimeFirstKey = nTimeBegin;
|
||||
|
||||
printf("Rescanning last %i blocks\n", pindexBest->nHeight - pindex->nHeight + 1);
|
||||
pwalletMain->ScanForWalletTransactions(pindex);
|
||||
pwalletMain->ReacceptWalletTransactions();
|
||||
pwalletMain->MarkDirty();
|
||||
|
||||
if (!fGood)
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding some keys to wallet");
|
||||
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
|
||||
Value dumpprivkey(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"dumpprivkey <Trianglesaddress>\n"
|
||||
"Reveals the private key corresponding to <Trianglesaddress>.");
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
string strAddress = params[0].get_str();
|
||||
CTrianglesAddress address;
|
||||
if (!address.SetString(strAddress))
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Triangles address");
|
||||
if (fWalletUnlockStakingOnly)
|
||||
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
|
||||
CKeyID keyID;
|
||||
if (!address.GetKeyID(keyID))
|
||||
throw JSONRPCError(RPC_TYPE_ERROR, "Address does not refer to a key");
|
||||
CSecret vchSecret;
|
||||
bool fCompressed;
|
||||
if (!pwalletMain->GetSecret(keyID, vchSecret, fCompressed))
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Private key for address " + strAddress + " is not known");
|
||||
return CTrianglesSecret(vchSecret, fCompressed).ToString();
|
||||
}
|
||||
|
||||
Value dumpwallet(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"dumpwallet <filename>\n"
|
||||
"Dumps all wallet keys in a human-readable format.");
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
ofstream file;
|
||||
file.open(params[0].get_str().c_str());
|
||||
if (!file.is_open())
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
|
||||
|
||||
std::map<CKeyID, int64_t> mapKeyBirth;
|
||||
|
||||
std::set<CKeyID> setKeyPool;
|
||||
|
||||
pwalletMain->GetKeyBirthTimes(mapKeyBirth);
|
||||
|
||||
pwalletMain->GetAllReserveKeys(setKeyPool);
|
||||
|
||||
// sort time/key pairs
|
||||
std::vector<std::pair<int64_t, CKeyID> > vKeyBirth;
|
||||
for (std::map<CKeyID, int64_t>::const_iterator it = mapKeyBirth.begin(); it != mapKeyBirth.end(); it++) {
|
||||
vKeyBirth.push_back({it->second, it->first});
|
||||
}
|
||||
mapKeyBirth.clear();
|
||||
std::sort(vKeyBirth.begin(), vKeyBirth.end());
|
||||
|
||||
// produce output
|
||||
file << strprintf("# Wallet dump created by Triangles %s (%s)\n", CLIENT_BUILD.c_str(), CLIENT_DATE.c_str());
|
||||
file << strprintf("# * Created on %s\n", EncodeDumpTime(GetTime()).c_str());
|
||||
file << strprintf("# * Best block at time of backup was %i (%s),\n", nBestHeight, hashBestChain.ToString().c_str());
|
||||
file << strprintf("# mined on %s\n", EncodeDumpTime(pindexBest->nTime).c_str());
|
||||
file << "\n";
|
||||
for (std::vector<std::pair<int64_t, CKeyID> >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) {
|
||||
const CKeyID &keyid = it->second;
|
||||
std::string strTime = EncodeDumpTime(it->first);
|
||||
std::string strAddr = CTrianglesAddress(keyid).ToString();
|
||||
bool IsCompressed;
|
||||
|
||||
CKey key;
|
||||
if (pwalletMain->GetKey(keyid, key)) {
|
||||
if (pwalletMain->mapAddressBook.count(keyid)) {
|
||||
CSecret secret = key.GetSecret(IsCompressed);
|
||||
file << strprintf("%s %s label=%s # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), EncodeDumpString(pwalletMain->mapAddressBook[keyid]).c_str(), strAddr.c_str());
|
||||
} else if (setKeyPool.count(keyid)) {
|
||||
CSecret secret = key.GetSecret(IsCompressed);
|
||||
file << strprintf("%s %s reserve=1 # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), strAddr.c_str());
|
||||
} else {
|
||||
CSecret secret = key.GetSecret(IsCompressed);
|
||||
file << strprintf("%s %s change=1 # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), strAddr.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
file << "\n";
|
||||
file << "# End of dump\n";
|
||||
file.close();
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
|
||||
// Copyright (c) 2009-2012 Bitcoin Developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <ctime>
|
||||
|
||||
#include "init.h" // for pwalletMain
|
||||
#include "trianglesrpc.h"
|
||||
#include "ui_interface.h"
|
||||
#include "base58.h"
|
||||
|
||||
#define printf OutputDebugStringF
|
||||
|
||||
using namespace json_spirit;
|
||||
using namespace std;
|
||||
|
||||
void EnsureWalletIsUnlocked();
|
||||
|
||||
// Accepted timestamp formats, tried in order. Replaces the boost::posix_time
|
||||
// parser; std::get_time is portable (C++11) and parses against each format.
|
||||
static const char* const dumptime_formats[] = {
|
||||
"%Y-%m-%dT%H:%M:%SZ",
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y/%m/%d %H:%M:%S",
|
||||
"%d.%m.%Y %H:%M:%S",
|
||||
"%Y-%m-%d",
|
||||
};
|
||||
|
||||
int64_t DecodeDumpTime(const std::string& s)
|
||||
{
|
||||
for (const char* fmt : dumptime_formats)
|
||||
{
|
||||
std::tm tm = {};
|
||||
std::istringstream is(s);
|
||||
is >> std::get_time(&tm, fmt);
|
||||
if (is.fail())
|
||||
continue;
|
||||
// Interpret the parsed broken-down time as UTC.
|
||||
#ifdef WIN32
|
||||
std::time_t t = _mkgmtime(&tm);
|
||||
#else
|
||||
std::time_t t = timegm(&tm);
|
||||
#endif
|
||||
if (t != static_cast<std::time_t>(-1))
|
||||
return static_cast<int64_t>(t);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string static EncodeDumpTime(int64_t nTime) {
|
||||
return DateTimeStrFormat("%Y-%m-%dT%H:%M:%SZ", nTime);
|
||||
}
|
||||
|
||||
std::string static EncodeDumpString(const std::string &str) {
|
||||
std::stringstream ret;
|
||||
for (unsigned char c : str) {
|
||||
if (c <= 32 || c >= 128 || c == '%') {
|
||||
ret << '%' << HexStr(&c, &c + 1);
|
||||
} else {
|
||||
ret << c;
|
||||
}
|
||||
}
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
std::string DecodeDumpString(const std::string &str) {
|
||||
std::stringstream ret;
|
||||
for (unsigned int pos = 0; pos < str.length(); pos++) {
|
||||
unsigned char c = str[pos];
|
||||
if (c == '%' && pos+2 < str.length()) {
|
||||
c = (((str[pos+1]>>6)*9+((str[pos+1]-'0')&15)) << 4) |
|
||||
((str[pos+2]>>6)*9+((str[pos+2]-'0')&15));
|
||||
pos += 2;
|
||||
}
|
||||
ret << c;
|
||||
}
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
class CTxDump
|
||||
{
|
||||
public:
|
||||
CBlockIndex *pindex;
|
||||
int64_t nValue;
|
||||
bool fSpent;
|
||||
CWalletTx* ptx;
|
||||
int nOut;
|
||||
CTxDump(CWalletTx* ptx = nullptr, int nOut = -1)
|
||||
{
|
||||
pindex = nullptr;
|
||||
nValue = 0;
|
||||
fSpent = false;
|
||||
this->ptx = ptx;
|
||||
this->nOut = nOut;
|
||||
}
|
||||
};
|
||||
|
||||
Value importprivkey(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() < 1 || params.size() > 2)
|
||||
throw runtime_error(
|
||||
"importprivkey <Trianglesprivkey> [label]\n"
|
||||
"Adds a private key (as returned by dumpprivkey) to your wallet.");
|
||||
|
||||
string strSecret = params[0].get_str();
|
||||
string strLabel = "";
|
||||
if (params.size() > 1)
|
||||
strLabel = params[1].get_str();
|
||||
CTrianglesSecret vchSecret;
|
||||
bool fGood = vchSecret.SetString(strSecret);
|
||||
|
||||
if (!fGood) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key");
|
||||
if (fWalletUnlockStakingOnly)
|
||||
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
|
||||
|
||||
CKey key;
|
||||
bool fCompressed;
|
||||
CSecret secret = vchSecret.GetSecret(fCompressed);
|
||||
key.SetSecret(secret, fCompressed);
|
||||
CKeyID vchAddress = key.GetPubKey().GetID();
|
||||
{
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
|
||||
pwalletMain->MarkDirty();
|
||||
pwalletMain->SetAddressBookName(vchAddress, strLabel);
|
||||
|
||||
if (!pwalletMain->AddKey(key))
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet");
|
||||
|
||||
pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
|
||||
pwalletMain->ReacceptWalletTransactions();
|
||||
}
|
||||
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
Value importwallet(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"importwallet <filename>\n"
|
||||
"Imports keys from a wallet dump file (see dumpwallet).");
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
ifstream file;
|
||||
file.open(params[0].get_str().c_str());
|
||||
if (!file.is_open())
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
|
||||
|
||||
int64_t nTimeBegin = pindexBest->nTime;
|
||||
|
||||
bool fGood = true;
|
||||
|
||||
while (file.good()) {
|
||||
std::string line;
|
||||
std::getline(file, line);
|
||||
if (line.empty() || line[0] == '#')
|
||||
continue;
|
||||
|
||||
auto vstr = SplitString(line, ' ');
|
||||
if (vstr.size() < 2)
|
||||
continue;
|
||||
CTrianglesSecret vchSecret;
|
||||
if (!vchSecret.SetString(vstr[0]))
|
||||
continue;
|
||||
|
||||
bool fCompressed;
|
||||
CKey key;
|
||||
CSecret secret = vchSecret.GetSecret(fCompressed);
|
||||
key.SetSecret(secret, fCompressed);
|
||||
CKeyID keyid = key.GetPubKey().GetID();
|
||||
|
||||
if (pwalletMain->HaveKey(keyid)) {
|
||||
printf("Skipping import of %s (key already present)\n", CTrianglesAddress(keyid).ToString().c_str());
|
||||
continue;
|
||||
}
|
||||
int64_t nTime = DecodeDumpTime(vstr[1]);
|
||||
std::string strLabel;
|
||||
bool fLabel = true;
|
||||
for (unsigned int nStr = 2; nStr < vstr.size(); nStr++) {
|
||||
if (vstr[nStr].starts_with("#"))
|
||||
break;
|
||||
if (vstr[nStr] == "change=1")
|
||||
fLabel = false;
|
||||
if (vstr[nStr] == "reserve=1")
|
||||
fLabel = false;
|
||||
if (vstr[nStr].starts_with("label=")) {
|
||||
strLabel = DecodeDumpString(vstr[nStr].substr(6));
|
||||
fLabel = true;
|
||||
}
|
||||
}
|
||||
printf("Importing %s...\n", CTrianglesAddress(keyid).ToString().c_str());
|
||||
if (!pwalletMain->AddKey(key)) {
|
||||
fGood = false;
|
||||
continue;
|
||||
}
|
||||
pwalletMain->mapKeyMetadata[keyid].nCreateTime = nTime;
|
||||
if (fLabel)
|
||||
pwalletMain->SetAddressBookName(keyid, strLabel);
|
||||
nTimeBegin = std::min(nTimeBegin, nTime);
|
||||
}
|
||||
file.close();
|
||||
|
||||
CBlockIndex *pindex = pindexBest;
|
||||
while (pindex && pindex->pprev && pindex->nTime > nTimeBegin - 7200)
|
||||
pindex = pindex->pprev;
|
||||
|
||||
if (!pwalletMain->nTimeFirstKey || nTimeBegin < pwalletMain->nTimeFirstKey)
|
||||
pwalletMain->nTimeFirstKey = nTimeBegin;
|
||||
|
||||
printf("Rescanning last %i blocks\n", pindexBest->nHeight - pindex->nHeight + 1);
|
||||
pwalletMain->ScanForWalletTransactions(pindex);
|
||||
pwalletMain->ReacceptWalletTransactions();
|
||||
pwalletMain->MarkDirty();
|
||||
|
||||
if (!fGood)
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding some keys to wallet");
|
||||
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
|
||||
Value dumpprivkey(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"dumpprivkey <Trianglesaddress>\n"
|
||||
"Reveals the private key corresponding to <Trianglesaddress>.");
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
string strAddress = params[0].get_str();
|
||||
CTrianglesAddress address;
|
||||
if (!address.SetString(strAddress))
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Triangles address");
|
||||
if (fWalletUnlockStakingOnly)
|
||||
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
|
||||
CKeyID keyID;
|
||||
if (!address.GetKeyID(keyID))
|
||||
throw JSONRPCError(RPC_TYPE_ERROR, "Address does not refer to a key");
|
||||
CSecret vchSecret;
|
||||
bool fCompressed;
|
||||
if (!pwalletMain->GetSecret(keyID, vchSecret, fCompressed))
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Private key for address " + strAddress + " is not known");
|
||||
return CTrianglesSecret(vchSecret, fCompressed).ToString();
|
||||
}
|
||||
|
||||
Value dumpwallet(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"dumpwallet <filename>\n"
|
||||
"Dumps all wallet keys in a human-readable format.");
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
ofstream file;
|
||||
file.open(params[0].get_str().c_str());
|
||||
if (!file.is_open())
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
|
||||
|
||||
std::map<CKeyID, int64_t> mapKeyBirth;
|
||||
|
||||
std::set<CKeyID> setKeyPool;
|
||||
|
||||
pwalletMain->GetKeyBirthTimes(mapKeyBirth);
|
||||
|
||||
pwalletMain->GetAllReserveKeys(setKeyPool);
|
||||
|
||||
// sort time/key pairs
|
||||
std::vector<std::pair<int64_t, CKeyID> > vKeyBirth;
|
||||
for (std::map<CKeyID, int64_t>::const_iterator it = mapKeyBirth.begin(); it != mapKeyBirth.end(); it++) {
|
||||
vKeyBirth.push_back({it->second, it->first});
|
||||
}
|
||||
mapKeyBirth.clear();
|
||||
std::sort(vKeyBirth.begin(), vKeyBirth.end());
|
||||
|
||||
// produce output
|
||||
file << strprintf("# Wallet dump created by Triangles %s (%s)\n", CLIENT_BUILD.c_str(), CLIENT_DATE.c_str());
|
||||
file << strprintf("# * Created on %s\n", EncodeDumpTime(GetTime()).c_str());
|
||||
file << strprintf("# * Best block at time of backup was %i (%s),\n", nBestHeight, hashBestChain.ToString().c_str());
|
||||
file << strprintf("# mined on %s\n", EncodeDumpTime(pindexBest->nTime).c_str());
|
||||
file << "\n";
|
||||
for (std::vector<std::pair<int64_t, CKeyID> >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) {
|
||||
const CKeyID &keyid = it->second;
|
||||
std::string strTime = EncodeDumpTime(it->first);
|
||||
std::string strAddr = CTrianglesAddress(keyid).ToString();
|
||||
bool IsCompressed;
|
||||
|
||||
CKey key;
|
||||
if (pwalletMain->GetKey(keyid, key)) {
|
||||
if (pwalletMain->mapAddressBook.count(keyid)) {
|
||||
CSecret secret = key.GetSecret(IsCompressed);
|
||||
file << strprintf("%s %s label=%s # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), EncodeDumpString(pwalletMain->mapAddressBook[keyid]).c_str(), strAddr.c_str());
|
||||
} else if (setKeyPool.count(keyid)) {
|
||||
CSecret secret = key.GetSecret(IsCompressed);
|
||||
file << strprintf("%s %s reserve=1 # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), strAddr.c_str());
|
||||
} else {
|
||||
CSecret secret = key.GetSecret(IsCompressed);
|
||||
file << strprintf("%s %s change=1 # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), strAddr.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
file << "\n";
|
||||
file << "# End of dump\n";
|
||||
file.close();
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#include "db.h"
|
||||
#include "walletdb.h"
|
||||
#include "net_bootstrap.h"
|
||||
#include "i2p/i2p_embedded.h"
|
||||
#include "tor/onion_v3.h"
|
||||
#include "tor/tor_embedded.h"
|
||||
|
||||
using namespace json_spirit;
|
||||
using namespace std;
|
||||
@@ -34,12 +37,34 @@ Value getnetworkinfo(const Array& params, bool fHelp)
|
||||
healthObj.push_back(Pair("lastblocktime", static_cast<int64_t>(health.lastBlockTime)));
|
||||
healthObj.push_back(Pair("networkmode", "tor_native"));
|
||||
|
||||
// Tor .onion address (wallet hidden service).
|
||||
std::string onionAddress = CTorV3Manager::GetInstance()->GetWalletOnionAddress();
|
||||
if (onionAddress.empty())
|
||||
onionAddress = CTorEmbedded::GetInstance()->GetOnionAddress();
|
||||
|
||||
// I2P embedded router state and .b32.i2p address.
|
||||
CI2PEmbedded* i2p = CI2PEmbedded::GetInstance();
|
||||
int nI2PPeers = 0;
|
||||
{
|
||||
LOCK(cs_vNodes);
|
||||
for (CNode* pnode : vNodes)
|
||||
if (pnode->addr.IsI2P())
|
||||
nI2PPeers++;
|
||||
}
|
||||
Object i2pObj;
|
||||
i2pObj.push_back(Pair("enabled", i2p->IsRunning()));
|
||||
i2pObj.push_back(Pair("active", i2p->IsRunning()));
|
||||
i2pObj.push_back(Pair("address", i2p->GetI2PAddress()));
|
||||
i2pObj.push_back(Pair("peers", nI2PPeers));
|
||||
|
||||
Object obj;
|
||||
obj.push_back(Pair("version", FormatFullVersion()));
|
||||
obj.push_back(Pair("protocolversion", (int)PROTOCOL_VERSION));
|
||||
obj.push_back(Pair("connections", (int)vNodes.size()));
|
||||
obj.push_back(Pair("proxy", (proxy.first.IsValid() ? proxy.first.ToStringIPPort() : string())));
|
||||
obj.push_back(Pair("ip", addrSeenByPeer.ToStringIP()));
|
||||
obj.push_back(Pair("toraddress", onionAddress));
|
||||
obj.push_back(Pair("i2p", i2pObj));
|
||||
obj.push_back(Pair("localservices", strprintf("%016"PRIx64, nLocalServices)));
|
||||
obj.push_back(Pair("testnet", fTestNet));
|
||||
obj.push_back(Pair("networkhealth", healthObj));
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#include "init.h"
|
||||
#include "base58.h"
|
||||
#include "smessage.h"
|
||||
#include "i2p/i2p_embedded.h"
|
||||
#include "tor/onion_v3.h"
|
||||
#include "tor/tor_embedded.h"
|
||||
|
||||
using namespace json_spirit;
|
||||
using namespace std;
|
||||
@@ -100,6 +103,13 @@ Value getinfo(const Array& params, bool fHelp)
|
||||
obj.push_back(Pair("proxy", (proxy.first.IsValid() ? proxy.first.ToStringIPPort() : string())));
|
||||
obj.push_back(Pair("ip", addrSeenByPeer.ToStringIP()));
|
||||
|
||||
// Anonymous network identities.
|
||||
std::string onionAddress = CTorV3Manager::GetInstance()->GetWalletOnionAddress();
|
||||
if (onionAddress.empty())
|
||||
onionAddress = CTorEmbedded::GetInstance()->GetOnionAddress();
|
||||
obj.push_back(Pair("toraddress", onionAddress));
|
||||
obj.push_back(Pair("i2paddress", CI2PEmbedded::GetInstance()->GetI2PAddress()));
|
||||
|
||||
diff.push_back(Pair("proof-of-work", GetDifficulty()));
|
||||
diff.push_back(Pair("proof-of-stake", GetDifficulty(GetLastBlockIndex(pindexBest, true))));
|
||||
obj.push_back(Pair("difficulty", diff));
|
||||
|
||||
@@ -311,6 +311,24 @@ bool CTorProcess::WriteTorrc()
|
||||
torrc << "AvoidDiskWrites 1\n";
|
||||
torrc << "Log notice stderr\n";
|
||||
|
||||
// Append user-supplied extra Tor configuration if present. This lets
|
||||
// operators on censored / DPI-filtered networks add Bridge lines,
|
||||
// ClientTransportPlugin (obfs4), or a Socks5Proxy/HTTPSProxy upstream so
|
||||
// Tor can reach the network when direct connections are blocked. The file
|
||||
// is never overwritten by the wallet; only the auto-generated torrc is.
|
||||
{
|
||||
fs::path extraPath = dataPath / "torrc.extra";
|
||||
if (fs::exists(extraPath)) {
|
||||
std::ifstream extra(extraPath.string().c_str());
|
||||
if (extra.is_open()) {
|
||||
torrc << "\n# ---- appended from torrc.extra (user-managed) ----\n";
|
||||
torrc << extra.rdbuf();
|
||||
torrc << "\n";
|
||||
printf("Tor: appended user configuration from %s\n", extraPath.string().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
torrc.close();
|
||||
|
||||
if (hiddenServiceEnabled) {
|
||||
|
||||
+1368
-1567
File diff suppressed because it is too large
Load Diff
+41
-40
@@ -1,40 +1,41 @@
|
||||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2012 The Bitcoin developers
|
||||
// Copyright (c) 2026 The Triangles developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file license.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef TRIANGLES_TXDB_H
|
||||
#define TRIANGLES_TXDB_H
|
||||
|
||||
#include "txdb-base.h"
|
||||
#include "txdb-leveldb.h"
|
||||
#include "txdb-rocksdb.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
// Factory: returns a chain-database handle whose concrete backend is chosen
|
||||
// by the -chaindb command-line argument:
|
||||
//
|
||||
// -chaindb=leveldb (default — pending Phase-4 retirement)
|
||||
// -chaindb=rocksdb
|
||||
//
|
||||
// Callers receive a CTxDBBase*, so the rest of the codebase stays
|
||||
// backend-agnostic. Mode strings ("r", "r+", "cr+") match the pre-existing
|
||||
// CTxDB constructor convention.
|
||||
std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode = "r+");
|
||||
|
||||
// True when the configured chain-DB backend is RocksDB.
|
||||
bool IsRocksDbChainBackend();
|
||||
|
||||
// On-disk directory of the chain DB for the configured backend, e.g.
|
||||
// <datadir>/txleveldb (LevelDB) or <datadir>/rocksdb (RocksDB).
|
||||
std::filesystem::path GetChainDataDir();
|
||||
|
||||
// Remove the chain DB directory for the configured backend. Callers that
|
||||
// need a fresh DB (-reindex, snapshot load) must invoke this BEFORE
|
||||
// MakeChainDB() opens the global handle for the first time.
|
||||
void WipeChainDataDir();
|
||||
|
||||
#endif // TRIANGLES_TXDB_H
|
||||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2012 The Bitcoin developers
|
||||
// Copyright (c) 2026 The Triangles developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file license.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef TRIANGLES_TXDB_H
|
||||
#define TRIANGLES_TXDB_H
|
||||
|
||||
#include "txdb-base.h"
|
||||
#include "txdb-leveldb.h"
|
||||
#include "txdb-rocksdb.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
// Factory: returns a chain-database handle whose concrete backend is chosen
|
||||
// by the -chaindb command-line argument:
|
||||
//
|
||||
// -chaindb=rocksdb (default)
|
||||
// -chaindb=leveldb (retained as migration source + fallback; pending
|
||||
// retirement after live-chain validation)
|
||||
//
|
||||
// Callers receive a CTxDBBase*, so the rest of the codebase stays
|
||||
// backend-agnostic. Mode strings ("r", "r+", "cr+") match the pre-existing
|
||||
// CTxDB constructor convention.
|
||||
std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode = "r+");
|
||||
|
||||
// True when the configured chain-DB backend is RocksDB.
|
||||
bool IsRocksDbChainBackend();
|
||||
|
||||
// On-disk directory of the chain DB for the configured backend, e.g.
|
||||
// <datadir>/txleveldb (LevelDB) or <datadir>/rocksdb (RocksDB).
|
||||
std::filesystem::path GetChainDataDir();
|
||||
|
||||
// Remove the chain DB directory for the configured backend. Callers that
|
||||
// need a fresh DB (-reindex, snapshot load) must invoke this BEFORE
|
||||
// MakeChainDB() opens the global handle for the first time.
|
||||
void WipeChainDataDir();
|
||||
|
||||
#endif // TRIANGLES_TXDB_H
|
||||
|
||||
+68
-23
@@ -41,18 +41,6 @@
|
||||
#include "version.h"
|
||||
#include "ui_interface.h"
|
||||
|
||||
// Work around clang compilation problem in Boost 1.46:
|
||||
// /usr/include/boost/program_options/detail/config_file.hpp:163:17: error: call to function 'to_internal' that is neither visible in the template definition nor found by argument-dependent lookup
|
||||
// See also: http://stackoverflow.com/questions/10020179/compilation-fail-in-boost-librairies-program-options
|
||||
// http://clang.debian.net/status.php?version=3.0&key=CANNOT_FIND_FUNCTION
|
||||
namespace boost {
|
||||
namespace program_options {
|
||||
std::string to_internal(const std::string&);
|
||||
}
|
||||
}
|
||||
|
||||
#include <boost/program_options/detail/config_file.hpp>
|
||||
#include <boost/program_options/parsers.hpp>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
@@ -1137,24 +1125,81 @@ std::filesystem::path GetConfigFile()
|
||||
void ReadConfigFile(map<string, string>& mapSettingsRet,
|
||||
map<string, vector<string> >& mapMultiSettingsRet)
|
||||
{
|
||||
// Modernization: replaced boost::program_options::detail::config_file_iterator
|
||||
// with a std::ifstream + getline parser. Supports the same syntax that the
|
||||
// triangle.conf files actually use:
|
||||
// - `key=value` or `key = value` (whitespace around = is ignored)
|
||||
// - `#` comments and blank lines are skipped
|
||||
// - Surrounding double quotes are stripped from values
|
||||
// - Same `InterpretNegativeSetting` semantics as the Boost version
|
||||
// - Command-line settings still take precedence (we don't overwrite
|
||||
// keys that are already in mapSettingsRet)
|
||||
//
|
||||
// Intentionally NOT supported (different from Boost):
|
||||
// - Backslash line continuations
|
||||
// - Escape sequences inside quoted values (\n, \t, etc.)
|
||||
// - Section headers ([section])
|
||||
// If any of those become needed, the actual conf syntax in
|
||||
// contrib/triangles.conf.example should be extended first.
|
||||
std::ifstream streamConfig(GetConfigFile());
|
||||
if (!streamConfig.good())
|
||||
return; // No triangles.conf file is OK
|
||||
|
||||
set<string> setOptions;
|
||||
setOptions.insert("*");
|
||||
|
||||
for (boost::program_options::detail::config_file_iterator it(streamConfig, setOptions), end; it != end; ++it)
|
||||
string strLine;
|
||||
while (std::getline(streamConfig, strLine))
|
||||
{
|
||||
// Don't overwrite existing settings so command line settings override triangles.conf
|
||||
string strKey = string("-") + it->string_key;
|
||||
if (mapSettingsRet.count(strKey) == 0)
|
||||
// Strip trailing CR (Windows line endings)
|
||||
if (!strLine.empty() && strLine.back() == '\r')
|
||||
strLine.pop_back();
|
||||
|
||||
// Trim leading whitespace
|
||||
size_t start = strLine.find_first_not_of(" \t");
|
||||
if (start == string::npos)
|
||||
continue; // blank line
|
||||
if (strLine[start] == '#')
|
||||
continue; // comment
|
||||
|
||||
// Find '=' separator
|
||||
size_t eq = strLine.find('=', start);
|
||||
if (eq == string::npos)
|
||||
continue; // malformed; skip silently
|
||||
|
||||
// Extract key, trim trailing whitespace
|
||||
string strKey = strLine.substr(start, eq - start);
|
||||
size_t keyEnd = strKey.find_last_not_of(" \t");
|
||||
if (keyEnd == string::npos)
|
||||
continue; // empty key
|
||||
strKey = strKey.substr(0, keyEnd + 1);
|
||||
|
||||
// Extract value, trim leading whitespace
|
||||
size_t valStart = eq + 1;
|
||||
valStart = strLine.find_first_not_of(" \t", valStart);
|
||||
if (valStart == string::npos)
|
||||
valStart = eq + 1; // empty value, no leading ws
|
||||
|
||||
string strValue = strLine.substr(valStart);
|
||||
|
||||
// Trim trailing whitespace from value
|
||||
size_t valEnd = strValue.find_last_not_of(" \t");
|
||||
if (valEnd != string::npos)
|
||||
strValue = strValue.substr(0, valEnd + 1);
|
||||
|
||||
// Strip surrounding double quotes if present
|
||||
if (strValue.size() >= 2 &&
|
||||
strValue.front() == '"' && strValue.back() == '"')
|
||||
{
|
||||
mapSettingsRet[strKey] = it->value[0];
|
||||
// interpret nofoo=1 as foo=0 (and nofoo=0 as foo=1) as long as foo not set)
|
||||
InterpretNegativeSetting(strKey, mapSettingsRet);
|
||||
strValue = strValue.substr(1, strValue.size() - 2);
|
||||
}
|
||||
mapMultiSettingsRet[strKey].push_back(it->value[0]);
|
||||
|
||||
// Don't overwrite existing settings so command line settings override triangles.conf
|
||||
string strSetting = string("-") + strKey;
|
||||
if (mapSettingsRet.count(strSetting) == 0)
|
||||
{
|
||||
mapSettingsRet[strSetting] = strValue;
|
||||
// interpret nofoo=1 as foo=0 (and nofoo=0 as foo=1) as long as foo not set
|
||||
InterpretNegativeSetting(strSetting, mapSettingsRet);
|
||||
}
|
||||
mapMultiSettingsRet[strSetting].push_back(strValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+96
-4
@@ -111,6 +111,23 @@ bool DumpSnapshot(const fs::path& destPath,
|
||||
if (blkSize > 0) numBlocks = (unsigned int)blkSize;
|
||||
}
|
||||
}
|
||||
// v3: collect setStakeSeen entries (prevoutStake, nStakeTime) from the
|
||||
// last N PoS blocks. Required so a snapshot-loaded node has the recent
|
||||
// stake-collision set restored without walking blocks at startup.
|
||||
static const unsigned int STAKE_SEEN_DEPTH = 5000; // 10x LoadBlockIndex default
|
||||
std::vector<std::pair<COutPoint, unsigned int> > vStakeSeen;
|
||||
{
|
||||
CBlockIndex* pindex = pindexBest;
|
||||
unsigned int nVisited = 0;
|
||||
while (pindex && nVisited < STAKE_SEEN_DEPTH) {
|
||||
if (pindex->IsProofOfStake()) {
|
||||
vStakeSeen.push_back(std::make_pair(pindex->prevoutStake, pindex->nStakeTime));
|
||||
}
|
||||
pindex = pindex->pprev;
|
||||
nVisited++;
|
||||
}
|
||||
}
|
||||
unsigned int numStakeSeen = (unsigned int)vStakeSeen.size();
|
||||
uint256 contentHash; // placeholder, filled after writing data
|
||||
|
||||
fwrite(&magic, sizeof(magic), 1, file);
|
||||
@@ -122,6 +139,7 @@ bool DumpSnapshot(const fs::path& destPath,
|
||||
fwrite(&numHeaders, sizeof(numHeaders), 1, file);
|
||||
fwrite(&numUtxos, sizeof(numUtxos), 1, file);
|
||||
fwrite(&numBlocks, sizeof(numBlocks), 1, file); // v2+
|
||||
fwrite(&numStakeSeen, sizeof(numStakeSeen), 1, file); // v3+
|
||||
long contentHashPos = ftell(file);
|
||||
fwrite(&contentHash, sizeof(contentHash), 1, file); // placeholder
|
||||
|
||||
@@ -195,14 +213,40 @@ bool DumpSnapshot(const fs::path& destPath,
|
||||
// Update actual count (in case it changed during iteration)
|
||||
if (nWritten != numUtxos) {
|
||||
numUtxos = nWritten;
|
||||
// Seek back and update numUtxos in header
|
||||
// Seek back and update numUtxos in header.
|
||||
// Header layout (v3):
|
||||
// magic(4) + version(4) + network(4) + height(4) + blockHash(32)
|
||||
// + moneySupply(8) + numHeaders(4) + numUtxos(4)
|
||||
// + numBlocks(4) + numStakeSeen(4) + contentHash(32)
|
||||
// contentHashPos is the offset of contentHash. numUtxos is at
|
||||
// contentHashPos - sizeof(contentHash) - sizeof(numStakeSeen)
|
||||
// - sizeof(numBlocks) - sizeof(numUtxos).
|
||||
long currentPos = ftell(file);
|
||||
fseek(file, contentHashPos - sizeof(numUtxos), SEEK_SET);
|
||||
fseek(file, contentHashPos - sizeof(uint256) - sizeof(numStakeSeen)
|
||||
- sizeof(numBlocks) - sizeof(numUtxos), SEEK_SET);
|
||||
fwrite(&numUtxos, sizeof(numUtxos), 1, file);
|
||||
fseek(file, currentPos, SEEK_SET);
|
||||
}
|
||||
}
|
||||
|
||||
// v3: After UTXOs, write the setStakeSeen entries collected from the last
|
||||
// N PoS blocks. Format: a length-prefixed flat array of
|
||||
// (COutPoint prevout, unsigned int nStakeTime) records.
|
||||
if (version >= 3) {
|
||||
printf("UtxoSnapshot: writing %d setStakeSeen entries...\n", numStakeSeen);
|
||||
for (unsigned int i = 0; i < vStakeSeen.size(); i++) {
|
||||
CDataStream ssEntry(SER_DISK, CLIENT_VERSION);
|
||||
ssEntry << vStakeSeen[i].first; // COutPoint (hash + index)
|
||||
ssEntry << vStakeSeen[i].second; // nStakeTime
|
||||
unsigned int entrySize = (unsigned int)ssEntry.size();
|
||||
std::string strEntry = ssEntry.str();
|
||||
fwrite(&entrySize, sizeof(entrySize), 1, file);
|
||||
fwrite(strEntry.data(), 1, entrySize, file);
|
||||
SHA256_Update(&sha256, &entrySize, sizeof(entrySize));
|
||||
SHA256_Update(&sha256, strEntry.data(), entrySize);
|
||||
}
|
||||
}
|
||||
|
||||
// v2: After UTXOs, append raw blk0001.dat content. Streams in chunks;
|
||||
// SHA256 covers the bytes. A snapshot-loaded node has full block data
|
||||
// ready in datadir/blk0001.dat — no separate bootstrap needed.
|
||||
@@ -290,7 +334,7 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
||||
int height;
|
||||
uint256 blockHash;
|
||||
int64_t moneySupply;
|
||||
unsigned int numHeaders = 0, numUtxos = 0, numBlocks = 0;
|
||||
unsigned int numHeaders = 0, numUtxos = 0, numBlocks = 0, numStakeSeen = 0;
|
||||
uint256 expectedContentHash;
|
||||
|
||||
if (fread(&magic, sizeof(magic), 1, file) != 1 ||
|
||||
@@ -305,7 +349,7 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
||||
strError = "Truncated snapshot header (common fields)";
|
||||
return false;
|
||||
}
|
||||
// v2+ has numBlocks between numUtxos and contentHash. v1 stops here.
|
||||
// v2+ has numBlocks between numUtxos and (numStakeSeen|contentHash).
|
||||
if (version >= 2) {
|
||||
if (fread(&numBlocks, sizeof(numBlocks), 1, file) != 1) {
|
||||
fclose(file);
|
||||
@@ -313,6 +357,14 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// v3+ has numStakeSeen before contentHash.
|
||||
if (version >= 3) {
|
||||
if (fread(&numStakeSeen, sizeof(numStakeSeen), 1, file) != 1) {
|
||||
fclose(file);
|
||||
strError = "Truncated snapshot header (numStakeSeen)";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (fread(&expectedContentHash, sizeof(expectedContentHash), 1, file) != 1) {
|
||||
fclose(file);
|
||||
strError = "Truncated snapshot header (contentHash)";
|
||||
@@ -522,6 +574,46 @@ bool LoadSnapshot(const fs::path& snapshotPath,
|
||||
success = false;
|
||||
}
|
||||
|
||||
// v3: After UTXOs (before the embedded blocks), read the setStakeSeen
|
||||
// entries collected from the last N PoS blocks of the source chain.
|
||||
// Required so a snapshot-loaded node has the recent stake-collision set
|
||||
// restored immediately, without having to walk blocks at startup. This
|
||||
// is what lets the anti-spam "too little proof-of-stake" check in
|
||||
// ProcessBlock function correctly right after a snapshot bootstrap.
|
||||
if (success && version >= 3 && numStakeSeen > 0) {
|
||||
printf("UtxoSnapshot: loading %d setStakeSeen entries...\n", numStakeSeen);
|
||||
// setStakeSeen is declared in main.cpp — we reference it via the
|
||||
// header declaration. Clear first so the snapshot's view is authoritative.
|
||||
setStakeSeen.clear();
|
||||
unsigned int nLoadedStakeSeen = 0;
|
||||
for (unsigned int i = 0; i < numStakeSeen; i++) {
|
||||
unsigned int entrySize;
|
||||
if (fread(&entrySize, sizeof(entrySize), 1, file) != 1 || entrySize > 1000) {
|
||||
success = false;
|
||||
strError = "Invalid setStakeSeen entry size at index " + std::to_string(i);
|
||||
break;
|
||||
}
|
||||
std::vector<char> buf(entrySize);
|
||||
if (fread(buf.data(), 1, entrySize, file) != entrySize) {
|
||||
success = false;
|
||||
strError = "Truncated setStakeSeen entry at index " + std::to_string(i);
|
||||
break;
|
||||
}
|
||||
SHA256_Update(&sha256, &entrySize, sizeof(entrySize));
|
||||
SHA256_Update(&sha256, buf.data(), entrySize);
|
||||
|
||||
CDataStream ssEntry(buf.data(), buf.data() + buf.size(), SER_DISK, CLIENT_VERSION);
|
||||
COutPoint prevout;
|
||||
unsigned int nStakeTime;
|
||||
ssEntry >> prevout;
|
||||
ssEntry >> nStakeTime;
|
||||
setStakeSeen.insert(std::make_pair(prevout, nStakeTime));
|
||||
nLoadedStakeSeen++;
|
||||
}
|
||||
if (success)
|
||||
printf("UtxoSnapshot: loaded %d setStakeSeen entries\n", nLoadedStakeSeen);
|
||||
}
|
||||
|
||||
// v2: After UTXOs, extract the raw blk0001.dat content. This makes the
|
||||
// loaded node fully self-contained — no separate bootstrap needed.
|
||||
if (success && version >= 2 && numBlocks > 0) {
|
||||
|
||||
+9
-1
@@ -11,7 +11,15 @@
|
||||
static const unsigned int UTXO_SNAPSHOT_MAGIC = 0x53585455; // "UTXS" little-endian
|
||||
|
||||
// UTXO snapshot format version
|
||||
static const unsigned int UTXO_SNAPSHOT_VERSION = 2;
|
||||
// v1: original (headers + UTXOs)
|
||||
// v2: + embeds raw blk0001.dat for full self-contained bootstrap
|
||||
// v3: + carries setStakeSeen entries (prevoutStake, nStakeTime) so a
|
||||
// snapshot-loaded node has the recent PoS stake-collision set restored
|
||||
// immediately, without needing to walk the last N blocks on startup.
|
||||
// Required for the anti-spam "too little proof-of-stake" check in
|
||||
// ProcessBlock to work correctly post-snapshot-bootstrap, since
|
||||
// LoadBlockIndex only walks 500 blocks back from pindexBest.
|
||||
static const unsigned int UTXO_SNAPSHOT_VERSION = 3;
|
||||
|
||||
// Number of block index entries to include in snapshot (covers difficulty,
|
||||
// median time, stake modifier, and reorg depth requirements)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
//
|
||||
// Backend-agnostic wallet storage seam.
|
||||
//
|
||||
// Historically CWalletDB derived directly from CDB (Berkeley DB). To allow the
|
||||
// wallet to be stored in SQLite instead, storage is abstracted behind two
|
||||
// interfaces modeled on Bitcoin Core's WalletDatabase / DatabaseBatch:
|
||||
//
|
||||
// WalletDatabase - owns the on-disk database (open/close/flush/backup/
|
||||
// rewrite) and hands out batches.
|
||||
// WalletBatch - a unit of work against the database: raw byte-level
|
||||
// Read/Write/Erase/Exists, a cursor for full scans, and an
|
||||
// optional atomic transaction.
|
||||
//
|
||||
// Only RAW BYTES cross this interface. All key/value (de)serialization stays in
|
||||
// CWalletDB via CDataStream with SER_DISK / CLIENT_VERSION, exactly as before,
|
||||
// so the on-disk record encoding is identical across backends. That byte
|
||||
// identity is what makes the Berkeley -> SQLite migration a verbatim key/value
|
||||
// copy.
|
||||
|
||||
#ifndef TRIANGLES_WALLETDB_BASE_H
|
||||
#define TRIANGLES_WALLETDB_BASE_H
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using KeyBytes = std::vector<unsigned char>;
|
||||
using ValueBytes = std::vector<unsigned char>;
|
||||
|
||||
// Result of advancing a cursor.
|
||||
enum class WalletCursorStatus { MORE, DONE, FAIL };
|
||||
|
||||
// Forward scan over every record in a database. Yields raw serialized
|
||||
// key/value bytes; the caller deserializes. Cursors do not observe uncommitted
|
||||
// writes in an open transaction (all wallet scan sites run outside txns).
|
||||
class WalletCursor
|
||||
{
|
||||
public:
|
||||
virtual ~WalletCursor() = default;
|
||||
virtual WalletCursorStatus Next(KeyBytes& key, ValueBytes& value) = 0;
|
||||
};
|
||||
|
||||
// A unit of work against a wallet database.
|
||||
class WalletBatch
|
||||
{
|
||||
public:
|
||||
virtual ~WalletBatch() = default;
|
||||
|
||||
// Byte-level accessors. WriteKey honors fOverwrite (false => fail if the
|
||||
// key already exists, matching Berkeley's DB_NOOVERWRITE). EraseKey returns
|
||||
// true when the key is gone afterwards (including "was not present").
|
||||
virtual bool ReadKey(const KeyBytes& key, ValueBytes& value) = 0;
|
||||
virtual bool WriteKey(const KeyBytes& key, const ValueBytes& value, bool fOverwrite = true) = 0;
|
||||
virtual bool EraseKey(const KeyBytes& key) = 0;
|
||||
virtual bool HasKey(const KeyBytes& key) = 0;
|
||||
|
||||
// Full-database scan.
|
||||
virtual std::unique_ptr<WalletCursor> GetNewCursor() = 0;
|
||||
|
||||
// Atomic transaction around a group of writes/erases. At most one may be
|
||||
// open per batch at a time.
|
||||
virtual bool TxnBegin() = 0;
|
||||
virtual bool TxnCommit() = 0;
|
||||
virtual bool TxnAbort() = 0;
|
||||
|
||||
virtual void Close() = 0;
|
||||
};
|
||||
|
||||
// An on-disk wallet database.
|
||||
class WalletDatabase
|
||||
{
|
||||
public:
|
||||
virtual ~WalletDatabase() = default;
|
||||
|
||||
// Hand out a batch. flush_on_close asks the backend to flush durable state
|
||||
// when the batch is destroyed (Berkeley parity for the common write path).
|
||||
virtual std::unique_ptr<WalletBatch> MakeBatch(bool flush_on_close = true) = 0;
|
||||
|
||||
// Rewrite the database compactly, optionally skipping records whose key
|
||||
// begins with pszSkip (used by the wallet to drop the unencrypted "key"
|
||||
// records after encryption). Berkeley implements this via CDB::Rewrite;
|
||||
// SQLite implements it via VACUUM (+ optional delete of skipped keys).
|
||||
virtual bool Rewrite(const char* pszSkip = nullptr) = 0;
|
||||
|
||||
// Copy the live database to a destination path (wallet backup).
|
||||
virtual bool Backup(const std::string& strDest) const = 0;
|
||||
|
||||
// Durability / lifecycle.
|
||||
virtual void Flush() = 0;
|
||||
virtual void Close() = 0;
|
||||
|
||||
// Integrity check before first use. Fills strError on failure.
|
||||
virtual bool Verify(std::string& strError) = 0;
|
||||
|
||||
// Human-readable identifier for logging (filename or path).
|
||||
virtual std::string Filename() const = 0;
|
||||
};
|
||||
|
||||
// Backend selector, parsed from -walletdb. SQLite is the default; Berkeley is
|
||||
// retained for one release as a fallback and as the migration source.
|
||||
enum class WalletDbKind { SQLite, Berkeley };
|
||||
|
||||
// Resolve the configured wallet backend from -walletdb (default: SQLite).
|
||||
WalletDbKind ResolveWalletDbKind();
|
||||
|
||||
// Open (creating if needed) the wallet database for the configured backend.
|
||||
// strFilename is the logical wallet name (e.g. "wallet.dat"); the SQLite
|
||||
// backend stores it as "<name>" under the data dir, Berkeley as before.
|
||||
std::unique_ptr<WalletDatabase> MakeWalletDatabase(const std::string& strFilename,
|
||||
std::string& strError);
|
||||
|
||||
#endif // TRIANGLES_WALLETDB_BASE_H
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
//
|
||||
// Typed, backend-agnostic wallet batch — the bridge between CWalletDB's typed
|
||||
// record calls and the raw byte-level WalletBatch interface (walletdb-base.h).
|
||||
//
|
||||
// It reproduces the exact serialization behavior of the old Berkeley CDB
|
||||
// (CDataStream with SER_DISK / CLIENT_VERSION), so the bytes written are
|
||||
// identical regardless of backend and CWalletDB's call sites need only change
|
||||
// their base class — the Read/Write/Erase/Exists template calls are unchanged.
|
||||
//
|
||||
// CWalletDB is intended to derive from CWalletBatchTyped (replacing `: public
|
||||
// CDB`). The Berkeley cursor methods CWalletDB used directly (GetAtCursor,
|
||||
// ReadAtCursor with DB_NEXT/DB_SET_RANGE) map onto StartCursor()/NextRecord()
|
||||
// here, which iterate the whole keyspace; range-seek call sites filter in the
|
||||
// loop, as the SQLite cursor does not support keyed range seeks.
|
||||
|
||||
#ifndef TRIANGLES_WALLETDB_BATCH_H
|
||||
#define TRIANGLES_WALLETDB_BATCH_H
|
||||
|
||||
#include "walletdb-base.h"
|
||||
#include "serialize.h" // CDataStream, SER_DISK
|
||||
#include "version.h" // CLIENT_VERSION
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
class CWalletBatchTyped
|
||||
{
|
||||
public:
|
||||
// Default-constructed handle is unusable until Open() runs. Subclasses
|
||||
// (CWalletDB) call Open() once they have opened a WalletDatabase.
|
||||
CWalletBatchTyped() = default;
|
||||
virtual ~CWalletBatchTyped() { Close(); }
|
||||
|
||||
// Open a fresh batch against the given database. Closes any previously
|
||||
// open batch+database. Returns false (and leaves the handle null) if the
|
||||
// database fails to produce a batch.
|
||||
bool Open(std::unique_ptr<WalletDatabase> db)
|
||||
{
|
||||
Close();
|
||||
if (!db)
|
||||
return false;
|
||||
m_database = std::move(db);
|
||||
m_batch = m_database->MakeBatch(/*flush_on_close=*/true);
|
||||
if (!m_batch) {
|
||||
m_database.reset();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Close()
|
||||
{
|
||||
m_batch.reset();
|
||||
m_database.reset();
|
||||
}
|
||||
bool IsNull() const { return m_batch == nullptr; }
|
||||
|
||||
// ── Transactions ─────────────────────────────────────────────────────────
|
||||
bool TxnBegin() { return m_batch && m_batch->TxnBegin(); }
|
||||
bool TxnCommit() { return m_batch && m_batch->TxnCommit(); }
|
||||
bool TxnAbort() { return m_batch && m_batch->TxnAbort(); }
|
||||
|
||||
protected:
|
||||
std::unique_ptr<WalletDatabase> m_database;
|
||||
std::unique_ptr<WalletBatch> m_batch;
|
||||
|
||||
// ── Typed accessors (serialize key/value, dispatch to the raw batch) ──────
|
||||
template <typename K, typename T>
|
||||
bool Read(const K& key, T& value)
|
||||
{
|
||||
if (!m_batch) return false;
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
ssKey.reserve(1000);
|
||||
ssKey << key;
|
||||
KeyBytes vKey(ssKey.begin(), ssKey.end());
|
||||
|
||||
ValueBytes vValue;
|
||||
if (!m_batch->ReadKey(vKey, vValue))
|
||||
return false;
|
||||
try {
|
||||
CDataStream ssValue(reinterpret_cast<const char*>(vValue.data()),
|
||||
reinterpret_cast<const char*>(vValue.data()) + vValue.size(),
|
||||
SER_DISK, CLIENT_VERSION);
|
||||
ssValue >> value;
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename K, typename T>
|
||||
bool Write(const K& key, const T& value, bool fOverwrite = true)
|
||||
{
|
||||
if (!m_batch) return false;
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
ssKey.reserve(1000);
|
||||
ssKey << key;
|
||||
KeyBytes vKey(ssKey.begin(), ssKey.end());
|
||||
|
||||
CDataStream ssValue(SER_DISK, CLIENT_VERSION);
|
||||
ssValue.reserve(10000);
|
||||
ssValue << value;
|
||||
ValueBytes vValue(ssValue.begin(), ssValue.end());
|
||||
|
||||
return m_batch->WriteKey(vKey, vValue, fOverwrite);
|
||||
}
|
||||
|
||||
template <typename K>
|
||||
bool Erase(const K& key)
|
||||
{
|
||||
if (!m_batch) return false;
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
ssKey.reserve(1000);
|
||||
ssKey << key;
|
||||
KeyBytes vKey(ssKey.begin(), ssKey.end());
|
||||
return m_batch->EraseKey(vKey);
|
||||
}
|
||||
|
||||
template <typename K>
|
||||
bool Exists(const K& key)
|
||||
{
|
||||
if (!m_batch) return false;
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
ssKey.reserve(1000);
|
||||
ssKey << key;
|
||||
KeyBytes vKey(ssKey.begin(), ssKey.end());
|
||||
return m_batch->HasKey(vKey);
|
||||
}
|
||||
|
||||
// ── Cursor ────────────────────────────────────────────────────────────────
|
||||
// Replaces CDB::GetCursor()/ReadAtCursor(). Open a cursor, then call
|
||||
// NextRecord() repeatedly: returns true and fills the streams while records
|
||||
// remain, false at end-of-data, and sets fError on failure.
|
||||
std::unique_ptr<WalletCursor> StartCursor()
|
||||
{
|
||||
if (!m_batch) return nullptr;
|
||||
return m_batch->GetNewCursor();
|
||||
}
|
||||
|
||||
bool NextRecord(WalletCursor& cursor, CDataStream& ssKey, CDataStream& ssValue, bool& fError)
|
||||
{
|
||||
fError = false;
|
||||
KeyBytes vKey;
|
||||
ValueBytes vValue;
|
||||
switch (cursor.Next(vKey, vValue)) {
|
||||
case WalletCursorStatus::MORE:
|
||||
ssKey.SetType(SER_DISK);
|
||||
ssKey.clear();
|
||||
ssKey.write(reinterpret_cast<const char*>(vKey.data()), vKey.size());
|
||||
ssValue.SetType(SER_DISK);
|
||||
ssValue.clear();
|
||||
ssValue.write(reinterpret_cast<const char*>(vValue.data()), vValue.size());
|
||||
return true;
|
||||
case WalletCursorStatus::DONE:
|
||||
return false;
|
||||
case WalletCursorStatus::FAIL:
|
||||
default:
|
||||
fError = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // TRIANGLES_WALLETDB_BATCH_H
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include "walletdb-base.h"
|
||||
#include "walletdb-sqlite.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
WalletDbKind ResolveWalletDbKind()
|
||||
{
|
||||
// SQLite is the default wallet backend. Berkeley DB is retained for one
|
||||
// release as a fallback (-walletdb=bdb) and as the migration source.
|
||||
std::string s = GetArg("-walletdb", std::string("sqlite"));
|
||||
for (auto& c : s) c = std::tolower(static_cast<unsigned char>(c));
|
||||
|
||||
if (s == "sqlite")
|
||||
return WalletDbKind::SQLite;
|
||||
if (s == "bdb" || s == "berkeley")
|
||||
return WalletDbKind::Berkeley;
|
||||
|
||||
throw std::runtime_error(
|
||||
"-walletdb=" + s + " is not a recognized wallet backend. "
|
||||
"Valid values: sqlite, bdb.");
|
||||
}
|
||||
|
||||
std::unique_ptr<WalletDatabase> MakeWalletDatabase(const std::string& strFilename,
|
||||
std::string& strError)
|
||||
{
|
||||
const fs::path path = GetDataDir() / strFilename;
|
||||
|
||||
switch (ResolveWalletDbKind()) {
|
||||
case WalletDbKind::SQLite: {
|
||||
auto db = std::make_unique<SQLiteDatabase>(path);
|
||||
if (!db->Open(strError))
|
||||
return nullptr;
|
||||
return db;
|
||||
}
|
||||
case WalletDbKind::Berkeley:
|
||||
// The Berkeley backend is still served by the legacy CWalletDB/CDB code
|
||||
// path. The thin BerkeleyDatabase adapter that plugs the existing
|
||||
// CDBEnv/CDB into this seam is added during CWalletDB integration; see
|
||||
// WALLET-SQLITE-MIGRATION.md. Until then, selecting -walletdb=bdb keeps
|
||||
// the original code path rather than routing through MakeWalletDatabase.
|
||||
strError = "Berkeley backend uses the legacy wallet path; not served by MakeWalletDatabase yet.";
|
||||
return nullptr;
|
||||
}
|
||||
return nullptr; // unreachable
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
//
|
||||
// Berkeley-only wallet recovery helpers. See walletdb-recover.h.
|
||||
|
||||
#include "walletdb-recover.h"
|
||||
#include "wallet.h"
|
||||
|
||||
#include <db_cxx.h>
|
||||
#include <boost/version.hpp>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
class CWalletScanState_BdbOnly {
|
||||
public:
|
||||
unsigned int nKeys;
|
||||
unsigned int nCKeys;
|
||||
unsigned int nKeyMeta;
|
||||
bool fIsEncrypted;
|
||||
bool fAnyUnordered;
|
||||
int nFileVersion;
|
||||
std::vector<uint256> vWalletUpgrade;
|
||||
|
||||
CWalletScanState_BdbOnly() {
|
||||
nKeys = nCKeys = nKeyMeta = 0;
|
||||
fIsEncrypted = false;
|
||||
fAnyUnordered = false;
|
||||
nFileVersion = 0;
|
||||
}
|
||||
};
|
||||
|
||||
static bool IsKeyType_BdbOnly(const std::string& strType)
|
||||
{
|
||||
return (strType == "key" || strType == "wkey" ||
|
||||
strType == "mkey" || strType == "ckey" ||
|
||||
strType == "hdmnemonic" || strType == "hdcmnemonic");
|
||||
}
|
||||
|
||||
// Same logic as walletdb.cpp::ReadKeyValue, but the only places it is called
|
||||
// here are Recover() (which scans records) and the resulting scan state. The
|
||||
// same logic — duplicated locally to avoid dragging in the typed batch seam
|
||||
// for a Berkeley-only escape hatch.
|
||||
static bool ReadKeyValue_BdbOnly(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
||||
CWalletScanState_BdbOnly& wss,
|
||||
std::string& strType, std::string& strErr)
|
||||
{
|
||||
try {
|
||||
ssKey >> strType;
|
||||
if (strType == "name") {
|
||||
std::string strAddress;
|
||||
ssKey >> strAddress;
|
||||
ssValue >> pwallet->mapAddressBook[CTrianglesAddress(strAddress).Get()];
|
||||
} else if (strType == "tx") {
|
||||
uint256 hash;
|
||||
ssKey >> hash;
|
||||
CWalletTx& wtx = pwallet->mapWallet[hash];
|
||||
ssValue >> wtx;
|
||||
if (wtx.CheckTransaction() && (wtx.GetHash() == hash))
|
||||
wtx.BindWallet(pwallet);
|
||||
else {
|
||||
pwallet->mapWallet.erase(hash);
|
||||
return false;
|
||||
}
|
||||
if (31404 <= wtx.fTimeReceivedIsTxTime && wtx.fTimeReceivedIsTxTime <= 31703) {
|
||||
wss.vWalletUpgrade.push_back(hash);
|
||||
}
|
||||
} else if (strType == "acentry") {
|
||||
std::string strAccount;
|
||||
ssKey >> strAccount;
|
||||
uint64_t nNumber;
|
||||
ssKey >> nNumber;
|
||||
// Note: we intentionally do NOT bump nAccountingEntryNumber here.
|
||||
// That counter is file-static in walletdb.cpp; the recovery path
|
||||
// does not need the high-water mark because the salvaged records
|
||||
// are not re-ordered or re-emitted as new entries.
|
||||
(void)nNumber;
|
||||
} else if (strType == "key" || strType == "wkey") {
|
||||
std::vector<unsigned char> vchPubKey;
|
||||
ssKey >> vchPubKey;
|
||||
CKey key;
|
||||
if (strType == "key") {
|
||||
wss.nKeys++;
|
||||
CPrivKey pkey;
|
||||
ssValue >> pkey;
|
||||
key.SetPubKey(vchPubKey);
|
||||
if (!key.SetPrivKey(pkey))
|
||||
{ strErr = "Recover: CPrivKey corrupt"; return false; }
|
||||
if (key.GetPubKey() != vchPubKey)
|
||||
{ strErr = "Recover: CPrivKey pubkey inconsistency"; return false; }
|
||||
if (!key.IsValid())
|
||||
{ strErr = "Recover: invalid CPrivKey"; return false; }
|
||||
} else {
|
||||
CWalletKey wkey;
|
||||
ssValue >> wkey;
|
||||
key.SetPubKey(vchPubKey);
|
||||
if (!key.SetPrivKey(wkey.vchPrivKey))
|
||||
{ strErr = "Recover: CPrivKey corrupt"; return false; }
|
||||
if (key.GetPubKey() != vchPubKey)
|
||||
{ strErr = "Recover: CWalletKey pubkey inconsistency"; return false; }
|
||||
if (!key.IsValid())
|
||||
{ strErr = "Recover: invalid CWalletKey"; return false; }
|
||||
}
|
||||
if (!pwallet->LoadKey(key))
|
||||
{ strErr = "Recover: LoadKey failed"; return false; }
|
||||
} else if (strType == "mkey") {
|
||||
unsigned int nID;
|
||||
ssKey >> nID;
|
||||
CMasterKey kMasterKey;
|
||||
ssValue >> kMasterKey;
|
||||
if (pwallet->mapMasterKeys.count(nID) != 0) {
|
||||
strErr = strprintf("Recover: duplicate CMasterKey id %u", nID);
|
||||
return false;
|
||||
}
|
||||
pwallet->mapMasterKeys[nID] = kMasterKey;
|
||||
if (pwallet->nMasterKeyMaxID < nID)
|
||||
pwallet->nMasterKeyMaxID = nID;
|
||||
} else if (strType == "ckey") {
|
||||
wss.nCKeys++;
|
||||
std::vector<unsigned char> vchPubKey;
|
||||
ssKey >> vchPubKey;
|
||||
std::vector<unsigned char> vchPrivKey;
|
||||
ssValue >> vchPrivKey;
|
||||
if (!pwallet->LoadCryptedKey(vchPubKey, vchPrivKey))
|
||||
{ strErr = "Recover: LoadCryptedKey failed"; return false; }
|
||||
wss.fIsEncrypted = true;
|
||||
} else if (strType == "keymeta") {
|
||||
CPubKey vchPubKey;
|
||||
ssKey >> vchPubKey;
|
||||
CKeyMetadata keyMeta;
|
||||
ssValue >> keyMeta;
|
||||
wss.nKeyMeta++;
|
||||
pwallet->LoadKeyMetadata(vchPubKey, keyMeta);
|
||||
if (!pwallet->nTimeFirstKey ||
|
||||
(keyMeta.nCreateTime < pwallet->nTimeFirstKey))
|
||||
pwallet->nTimeFirstKey = keyMeta.nCreateTime;
|
||||
} else if (strType == "defaultkey") {
|
||||
ssValue >> pwallet->vchDefaultKey;
|
||||
} else if (strType == "pool") {
|
||||
int64_t nIndex;
|
||||
ssKey >> nIndex;
|
||||
CKeyPool keypool;
|
||||
ssValue >> keypool;
|
||||
pwallet->setKeyPool.insert(nIndex);
|
||||
CKeyID keyid = keypool.vchPubKey.GetID();
|
||||
if (pwallet->mapKeyMetadata.count(keyid) == 0)
|
||||
pwallet->mapKeyMetadata[keyid] = CKeyMetadata(keypool.nTime);
|
||||
} else if (strType == "hdmnemonic") {
|
||||
std::string m;
|
||||
ssValue >> m;
|
||||
pwallet->LoadHDMnemonic(m);
|
||||
} else if (strType == "hdcmnemonic") {
|
||||
std::pair<uint256, std::vector<unsigned char>> cm;
|
||||
ssValue >> cm;
|
||||
pwallet->LoadCryptedHDMnemonic(cm.first, cm.second);
|
||||
} else if (strType == "hdchain") {
|
||||
int64_t n;
|
||||
ssValue >> n;
|
||||
pwallet->nHDChainIndex = n;
|
||||
} else if (strType == "version") {
|
||||
ssValue >> wss.nFileVersion;
|
||||
if (wss.nFileVersion == 10300)
|
||||
wss.nFileVersion = 300;
|
||||
} else if (strType == "cscript") {
|
||||
uint160 hash;
|
||||
ssKey >> hash;
|
||||
CScript script;
|
||||
ssValue >> script;
|
||||
if (!pwallet->LoadCScript(script))
|
||||
{ strErr = "Recover: LoadCScript failed"; return false; }
|
||||
} else if (strType == "orderposnext") {
|
||||
ssValue >> pwallet->nOrderPosNext;
|
||||
}
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BerkeleyRecoverWallet(CDBEnv& dbenv, std::string filename, bool fOnlyKeys)
|
||||
{
|
||||
int64_t now = GetTime();
|
||||
std::string newFilename = strprintf("wallet.%"PRId64".bak", now);
|
||||
|
||||
int result = dbenv.dbenv.dbrename(NULL, filename.c_str(), NULL,
|
||||
newFilename.c_str(), DB_AUTO_COMMIT);
|
||||
if (result == 0)
|
||||
printf("Renamed %s to %s\n", filename.c_str(), newFilename.c_str());
|
||||
else {
|
||||
printf("Failed to rename %s to %s\n", filename.c_str(), newFilename.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<CDBEnv::KeyValPair> salvagedData;
|
||||
bool allOK = dbenv.Salvage(newFilename, true, salvagedData);
|
||||
if (salvagedData.empty()) {
|
||||
printf("Salvage(aggressive) found no records in %s.\n", newFilename.c_str());
|
||||
return false;
|
||||
}
|
||||
printf("Salvage(aggressive) found %"PRIszu" records\n", salvagedData.size());
|
||||
|
||||
bool fSuccess = allOK;
|
||||
Db* pdbCopy = new Db(&dbenv.dbenv, 0);
|
||||
int ret = pdbCopy->open(NULL, filename.c_str(), "main", DB_BTREE, DB_CREATE, 0);
|
||||
if (ret > 0) {
|
||||
printf("Cannot create database file %s\n", filename.c_str());
|
||||
return false;
|
||||
}
|
||||
CWallet dummyWallet;
|
||||
CWalletScanState_BdbOnly wss;
|
||||
|
||||
DbTxn* ptxn = dbenv.TxnBegin();
|
||||
for (CDBEnv::KeyValPair& row : salvagedData) {
|
||||
if (fOnlyKeys) {
|
||||
CDataStream ssKey(row.first, SER_DISK, CLIENT_VERSION);
|
||||
CDataStream ssValue(row.second, SER_DISK, CLIENT_VERSION);
|
||||
std::string strType, strErr;
|
||||
bool fReadOK = ReadKeyValue_BdbOnly(&dummyWallet, ssKey, ssValue,
|
||||
wss, strType, strErr);
|
||||
if (!IsKeyType_BdbOnly(strType))
|
||||
continue;
|
||||
if (!fReadOK) {
|
||||
printf("WARNING: BerkeleyRecoverWallet skipping %s: %s\n",
|
||||
strType.c_str(), strErr.c_str());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Dbt datKey(&row.first[0], row.first.size());
|
||||
Dbt datValue(&row.second[0], row.second.size());
|
||||
int ret2 = pdbCopy->put(ptxn, &datKey, &datValue, DB_NOOVERWRITE);
|
||||
if (ret2 > 0)
|
||||
fSuccess = false;
|
||||
}
|
||||
ptxn->commit(0);
|
||||
pdbCopy->close(0);
|
||||
delete pdbCopy;
|
||||
|
||||
return fSuccess;
|
||||
}
|
||||
|
||||
bool BerkeleyZapWalletTx(const std::string& strWalletFile)
|
||||
{
|
||||
printf("BerkeleyZapWalletTx: erasing transaction records from %s\n",
|
||||
strWalletFile.c_str());
|
||||
|
||||
// Walk the Berkeley file directly. The CDB wrapper hides its members, but
|
||||
// the underlying Db* / Dbc* API is the same thing the wrapper does.
|
||||
DbEnv env(0u);
|
||||
env.set_error_stream(&std::cerr);
|
||||
u_int32_t envFlags = DB_CREATE | DB_INIT_MPOOL | DB_PRIVATE;
|
||||
if (env.open(GetDataDir().string().c_str(), envFlags, 0) != 0) {
|
||||
printf("BerkeleyZapWalletTx: cannot open Berkeley environment\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
{
|
||||
Db db(&env, 0);
|
||||
if (db.open(nullptr, strWalletFile.c_str(), "main", DB_BTREE, DB_RDONLY, 0) != 0) {
|
||||
printf("BerkeleyZapWalletTx: failed to open wallet database\n");
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
Dbc* pcursor = nullptr;
|
||||
if (db.cursor(nullptr, &pcursor, 0) != 0) {
|
||||
printf("BerkeleyZapWalletTx: failed to get cursor\n");
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint256> vTxHash;
|
||||
Dbt datKey, datValue;
|
||||
while (pcursor->get(&datKey, &datValue, DB_NEXT) == 0) {
|
||||
try {
|
||||
CDataStream ssKey(static_cast<const char*>(datKey.get_data()),
|
||||
static_cast<const char*>(datKey.get_data()) + datKey.get_size(),
|
||||
SER_DISK, CLIENT_VERSION);
|
||||
std::string strType;
|
||||
ssKey >> strType;
|
||||
if (strType == "tx") {
|
||||
uint256 hash;
|
||||
ssKey >> hash;
|
||||
vTxHash.push_back(hash);
|
||||
}
|
||||
} catch (...) {
|
||||
// Skip records we cannot decode — salvage logic is best-effort.
|
||||
}
|
||||
}
|
||||
pcursor->close();
|
||||
db.close(0);
|
||||
|
||||
// Second pass: re-open the file in r/w mode and erase the collected tx
|
||||
// records. Two separate connections keep the read pass free of the
|
||||
// BDB cursor lifetime rules.
|
||||
if (db.open(nullptr, strWalletFile.c_str(), "main", DB_BTREE, DB_CREATE, 0) != 0) {
|
||||
printf("BerkeleyZapWalletTx: failed to reopen wallet for erase\n");
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
int nErased = 0;
|
||||
for (const uint256& hash : vTxHash) {
|
||||
CDataStream ssKey(SER_DISK, CLIENT_VERSION);
|
||||
ssKey << std::make_pair(std::string("tx"), hash);
|
||||
Dbt datKey2(&ssKey[0], ssKey.size());
|
||||
int rc = db.del(nullptr, &datKey2, 0);
|
||||
if (rc == 0 || rc == DB_NOTFOUND)
|
||||
++nErased;
|
||||
}
|
||||
db.close(0);
|
||||
printf("BerkeleyZapWalletTx: erased %d of %d transaction records\n",
|
||||
nErased, (int)vTxHash.size());
|
||||
ok = true;
|
||||
}
|
||||
env.close(0);
|
||||
return ok;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
//
|
||||
// Berkeley-only wallet recovery helpers — moved out of CWalletDB so the
|
||||
// mainline wallet code path (SQLite via the typed batch seam) does not have
|
||||
// to include <db_cxx.h>.
|
||||
//
|
||||
// These functions operate directly on bitdb / CDB and are used only:
|
||||
// * during startup, before the wallet migration hook (on a possible BDB
|
||||
// wallet.dat), and
|
||||
// * on the .bdb.bak copy that migration leaves behind, for diagnostic /
|
||||
// manual recovery if migration ever needs investigation.
|
||||
//
|
||||
// They are intentionally NOT methods of CWalletDB — that class is on the
|
||||
// SQLite seam now and has no Berkeley state.
|
||||
|
||||
#ifndef TRIANGLES_WALLETDB_RECOVER_H
|
||||
#define TRIANGLES_WALLETDB_RECOVER_H
|
||||
|
||||
#include "db.h"
|
||||
#include <string>
|
||||
|
||||
// Aggressive salvage of a Berkeley wallet.dat file. Moves the file aside to
|
||||
// wallet.<timestamp>.bak, then walks the salvaged records and re-writes them
|
||||
// into a fresh Berkeley database at the original path.
|
||||
//
|
||||
// If fOnlyKeys is true, only key-type records are kept (used for recovery
|
||||
// when transaction history is corrupt). Returns true on success.
|
||||
bool BerkeleyRecoverWallet(CDBEnv& dbenv, std::string filename, bool fOnlyKeys);
|
||||
inline bool BerkeleyRecoverWallet(CDBEnv& dbenv, std::string filename)
|
||||
{
|
||||
return BerkeleyRecoverWallet(dbenv, filename, false);
|
||||
}
|
||||
|
||||
// Strip every "tx" record from a Berkeley wallet.dat, leaving keys and other
|
||||
// metadata intact. A rescan rebuilds the transaction list from the chain.
|
||||
// Used for `-zapwallettxes` on legacy (pre-migration) wallets.
|
||||
bool BerkeleyZapWalletTx(const std::string& strWalletFile);
|
||||
|
||||
#endif // TRIANGLES_WALLETDB_RECOVER_H
|
||||
@@ -0,0 +1,364 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include "walletdb-sqlite.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// ─── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
// Bind a byte buffer as a BLOB parameter (1-based index). SQLITE_TRANSIENT so
|
||||
// SQLite copies the bytes; the source vector need not outlive the step.
|
||||
static int BindBlob(sqlite3_stmt* stmt, int idx, const std::vector<unsigned char>& v)
|
||||
{
|
||||
// A zero-length blob still binds correctly with a non-null pointer.
|
||||
const void* p = v.empty() ? "" : static_cast<const void*>(v.data());
|
||||
return sqlite3_bind_blob(stmt, idx, p, static_cast<int>(v.size()), SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
static void ColumnBlob(sqlite3_stmt* stmt, int col, std::vector<unsigned char>& out)
|
||||
{
|
||||
const unsigned char* p = static_cast<const unsigned char*>(sqlite3_column_blob(stmt, col));
|
||||
int n = sqlite3_column_bytes(stmt, col);
|
||||
out.assign(p, p + (n > 0 ? n : 0));
|
||||
}
|
||||
|
||||
// ─── SQLiteDatabase ──────────────────────────────────────────────────────────
|
||||
|
||||
SQLiteDatabase::SQLiteDatabase(const fs::path& file_path)
|
||||
: m_file_path(file_path)
|
||||
{
|
||||
}
|
||||
|
||||
SQLiteDatabase::~SQLiteDatabase()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::ExecOrError(const char* sql, std::string& strError) const
|
||||
{
|
||||
char* errmsg = nullptr;
|
||||
int rc = sqlite3_exec(m_db, sql, nullptr, nullptr, &errmsg);
|
||||
if (rc != SQLITE_OK) {
|
||||
strError = strprintf("SQLite: '%s' failed: %s", sql, errmsg ? errmsg : sqlite3_errstr(rc));
|
||||
if (errmsg) sqlite3_free(errmsg);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::Open(std::string& strError)
|
||||
{
|
||||
if (m_db)
|
||||
return true;
|
||||
|
||||
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX;
|
||||
int rc = sqlite3_open_v2(m_file_path.string().c_str(), &m_db, flags, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
strError = strprintf("Failed to open SQLite wallet %s: %s",
|
||||
m_file_path.string().c_str(), sqlite3_errstr(rc));
|
||||
if (m_db) { sqlite3_close(m_db); m_db = nullptr; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Block (rather than fail) for up to 5s if another handle holds the lock.
|
||||
sqlite3_busy_timeout(m_db, 5000);
|
||||
|
||||
// Durability + integrity pragmas. FULL fsync on commit — a wallet must not
|
||||
// lose a freshly-written key on power loss.
|
||||
if (!ExecOrError("PRAGMA synchronous = FULL;", strError)) return false;
|
||||
if (!ExecOrError("PRAGMA foreign_keys = ON;", strError)) return false;
|
||||
// Fail loudly instead of silently truncating an over-long blob.
|
||||
if (!ExecOrError("PRAGMA cell_size_check = ON;", strError)) return false;
|
||||
|
||||
// Identify our schema via application_id / user_version. A brand-new file
|
||||
// reports 0/0; an existing file must match ours (refuse foreign DBs).
|
||||
int appId = 0, userVer = 0;
|
||||
{
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(m_db, "PRAGMA application_id;", -1, &st, nullptr) == SQLITE_OK &&
|
||||
sqlite3_step(st) == SQLITE_ROW)
|
||||
appId = sqlite3_column_int(st, 0);
|
||||
sqlite3_finalize(st);
|
||||
st = nullptr;
|
||||
if (sqlite3_prepare_v2(m_db, "PRAGMA user_version;", -1, &st, nullptr) == SQLITE_OK &&
|
||||
sqlite3_step(st) == SQLITE_ROW)
|
||||
userVer = sqlite3_column_int(st, 0);
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
|
||||
if (appId != 0 && appId != SQLITE_WALLET_APP_ID) {
|
||||
strError = strprintf("%s is not a Triangles SQLite wallet (application_id=0x%08x)",
|
||||
m_file_path.string().c_str(), appId);
|
||||
sqlite3_close(m_db);
|
||||
m_db = nullptr;
|
||||
return false;
|
||||
}
|
||||
if (userVer > SQLITE_WALLET_SCHEMA_VERSION) {
|
||||
strError = strprintf("%s was written by a newer wallet (schema v%d > v%d)",
|
||||
m_file_path.string().c_str(), userVer, SQLITE_WALLET_SCHEMA_VERSION);
|
||||
sqlite3_close(m_db);
|
||||
m_db = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create schema (idempotent) and stamp identity on fresh files.
|
||||
if (!ExecOrError("CREATE TABLE IF NOT EXISTS main "
|
||||
"(key BLOB PRIMARY KEY NOT NULL, value BLOB NOT NULL);", strError))
|
||||
return false;
|
||||
if (appId == 0) {
|
||||
std::string set = strprintf("PRAGMA application_id = %d;", SQLITE_WALLET_APP_ID);
|
||||
if (!ExecOrError(set.c_str(), strError)) return false;
|
||||
}
|
||||
{
|
||||
std::string set = strprintf("PRAGMA user_version = %d;", SQLITE_WALLET_SCHEMA_VERSION);
|
||||
if (!ExecOrError(set.c_str(), strError)) return false;
|
||||
}
|
||||
|
||||
printf("SQLite wallet opened: %s\n", m_file_path.string().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<WalletBatch> SQLiteDatabase::MakeBatch(bool /*flush_on_close*/)
|
||||
{
|
||||
return std::make_unique<SQLiteBatch>(*this);
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::Rewrite(const char* /*pszSkip*/)
|
||||
{
|
||||
// SQLite reclaims space and defragments via VACUUM. The wallet erases
|
||||
// superseded records (e.g. unencrypted keys after encryption) explicitly,
|
||||
// so the pszSkip filter that the Berkeley backend used is unnecessary here.
|
||||
if (!m_db)
|
||||
return false;
|
||||
std::string err;
|
||||
if (!ExecOrError("VACUUM;", err)) {
|
||||
printf("SQLiteDatabase::Rewrite VACUUM failed: %s\n", err.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::Backup(const std::string& strDest) const
|
||||
{
|
||||
if (!m_db)
|
||||
return false;
|
||||
|
||||
sqlite3* pDest = nullptr;
|
||||
if (sqlite3_open_v2(strDest.c_str(), &pDest,
|
||||
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr) != SQLITE_OK) {
|
||||
printf("SQLiteDatabase::Backup cannot open destination %s: %s\n",
|
||||
strDest.c_str(), pDest ? sqlite3_errmsg(pDest) : "?");
|
||||
if (pDest) sqlite3_close(pDest);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_backup* bk = sqlite3_backup_init(pDest, "main", m_db, "main");
|
||||
bool ok = false;
|
||||
if (bk) {
|
||||
sqlite3_backup_step(bk, -1); // copy entire DB in one shot
|
||||
int rc = sqlite3_backup_finish(bk);
|
||||
ok = (rc == SQLITE_OK);
|
||||
if (!ok)
|
||||
printf("SQLiteDatabase::Backup failed: %s\n", sqlite3_errstr(rc));
|
||||
} else {
|
||||
printf("SQLiteDatabase::Backup init failed: %s\n", sqlite3_errmsg(pDest));
|
||||
}
|
||||
sqlite3_close(pDest);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void SQLiteDatabase::Flush()
|
||||
{
|
||||
// No-op: with synchronous=FULL and rollback journaling, each committed
|
||||
// transaction is already durable. (If WAL is ever enabled, checkpoint here.)
|
||||
}
|
||||
|
||||
void SQLiteDatabase::Close()
|
||||
{
|
||||
if (m_db) {
|
||||
sqlite3_close(m_db);
|
||||
m_db = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::Verify(std::string& strError)
|
||||
{
|
||||
if (!m_db) {
|
||||
strError = "SQLite database not open";
|
||||
return false;
|
||||
}
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(m_db, "PRAGMA integrity_check;", -1, &st, nullptr) != SQLITE_OK) {
|
||||
strError = strprintf("integrity_check prepare failed: %s", sqlite3_errmsg(m_db));
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
const unsigned char* res = sqlite3_column_text(st, 0);
|
||||
ok = (res && std::strcmp(reinterpret_cast<const char*>(res), "ok") == 0);
|
||||
if (!ok)
|
||||
strError = strprintf("integrity_check: %s", res ? reinterpret_cast<const char*>(res) : "(null)");
|
||||
} else {
|
||||
strError = "integrity_check returned no rows";
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ─── SQLiteBatch ──────────────────────────────────────────────────────────────
|
||||
|
||||
SQLiteBatch::SQLiteBatch(SQLiteDatabase& database)
|
||||
: m_database(database)
|
||||
{
|
||||
PrepareStatements();
|
||||
}
|
||||
|
||||
bool SQLiteBatch::PrepareStatements()
|
||||
{
|
||||
sqlite3* db = m_database.Handle();
|
||||
if (!db)
|
||||
return false;
|
||||
|
||||
struct { sqlite3_stmt** out; const char* sql; } stmts[] = {
|
||||
{ &m_read_stmt, "SELECT value FROM main WHERE key = ?;" },
|
||||
{ &m_insert_stmt, "INSERT OR REPLACE INTO main (key, value) VALUES (?, ?);" },
|
||||
{ &m_overwrite_stmt, "INSERT INTO main (key, value) VALUES (?, ?);" },
|
||||
{ &m_delete_stmt, "DELETE FROM main WHERE key = ?;" },
|
||||
};
|
||||
for (auto& s : stmts) {
|
||||
if (*s.out) continue;
|
||||
if (sqlite3_prepare_v2(db, s.sql, -1, s.out, nullptr) != SQLITE_OK) {
|
||||
printf("SQLiteBatch: prepare failed for '%s': %s\n", s.sql, sqlite3_errmsg(db));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SQLiteBatch::Close()
|
||||
{
|
||||
sqlite3_stmt* all[] = { m_read_stmt, m_insert_stmt, m_overwrite_stmt, m_delete_stmt };
|
||||
for (auto* st : all)
|
||||
if (st) sqlite3_finalize(st);
|
||||
m_read_stmt = m_insert_stmt = m_overwrite_stmt = m_delete_stmt = nullptr;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::ReadKey(const KeyBytes& key, ValueBytes& value)
|
||||
{
|
||||
if (!m_read_stmt) return false;
|
||||
sqlite3_reset(m_read_stmt);
|
||||
sqlite3_clear_bindings(m_read_stmt);
|
||||
if (BindBlob(m_read_stmt, 1, key) != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
bool found = false;
|
||||
if (sqlite3_step(m_read_stmt) == SQLITE_ROW) {
|
||||
ColumnBlob(m_read_stmt, 0, value);
|
||||
found = true;
|
||||
}
|
||||
sqlite3_reset(m_read_stmt);
|
||||
return found;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::WriteKey(const KeyBytes& key, const ValueBytes& value, bool fOverwrite)
|
||||
{
|
||||
sqlite3_stmt* st = fOverwrite ? m_insert_stmt : m_overwrite_stmt;
|
||||
if (!st) return false;
|
||||
sqlite3_reset(st);
|
||||
sqlite3_clear_bindings(st);
|
||||
if (BindBlob(st, 1, key) != SQLITE_OK) return false;
|
||||
if (BindBlob(st, 2, value) != SQLITE_OK) return false;
|
||||
|
||||
int rc = sqlite3_step(st);
|
||||
sqlite3_reset(st);
|
||||
if (rc == SQLITE_DONE)
|
||||
return true;
|
||||
// Non-overwrite insert hitting an existing key => constraint violation,
|
||||
// which mirrors Berkeley's DB_NOOVERWRITE returning false (not an error).
|
||||
if (!fOverwrite && (rc == SQLITE_CONSTRAINT))
|
||||
return false;
|
||||
printf("SQLiteBatch::WriteKey step failed: %s\n", sqlite3_errstr(rc));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::EraseKey(const KeyBytes& key)
|
||||
{
|
||||
if (!m_delete_stmt) return false;
|
||||
sqlite3_reset(m_delete_stmt);
|
||||
sqlite3_clear_bindings(m_delete_stmt);
|
||||
if (BindBlob(m_delete_stmt, 1, key) != SQLITE_OK)
|
||||
return false;
|
||||
int rc = sqlite3_step(m_delete_stmt);
|
||||
sqlite3_reset(m_delete_stmt);
|
||||
// DONE whether or not a row matched — "key is gone" either way.
|
||||
return rc == SQLITE_DONE;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::HasKey(const KeyBytes& key)
|
||||
{
|
||||
if (!m_read_stmt) return false;
|
||||
sqlite3_reset(m_read_stmt);
|
||||
sqlite3_clear_bindings(m_read_stmt);
|
||||
if (BindBlob(m_read_stmt, 1, key) != SQLITE_OK)
|
||||
return false;
|
||||
bool present = (sqlite3_step(m_read_stmt) == SQLITE_ROW);
|
||||
sqlite3_reset(m_read_stmt);
|
||||
return present;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
class SQLiteCursor final : public WalletCursor
|
||||
{
|
||||
public:
|
||||
explicit SQLiteCursor(sqlite3_stmt* stmt) : m_stmt(stmt) {}
|
||||
~SQLiteCursor() override { if (m_stmt) sqlite3_finalize(m_stmt); }
|
||||
|
||||
WalletCursorStatus Next(KeyBytes& key, ValueBytes& value) override
|
||||
{
|
||||
if (!m_stmt) return WalletCursorStatus::FAIL;
|
||||
int rc = sqlite3_step(m_stmt);
|
||||
if (rc == SQLITE_DONE) return WalletCursorStatus::DONE;
|
||||
if (rc != SQLITE_ROW) return WalletCursorStatus::FAIL;
|
||||
ColumnBlob(m_stmt, 0, key);
|
||||
ColumnBlob(m_stmt, 1, value);
|
||||
return WalletCursorStatus::MORE;
|
||||
}
|
||||
|
||||
private:
|
||||
sqlite3_stmt* m_stmt;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<WalletCursor> SQLiteBatch::GetNewCursor()
|
||||
{
|
||||
sqlite3* db = m_database.Handle();
|
||||
if (!db) return nullptr;
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(db, "SELECT key, value FROM main;", -1, &st, nullptr) != SQLITE_OK) {
|
||||
printf("SQLiteBatch::GetNewCursor prepare failed: %s\n", sqlite3_errmsg(db));
|
||||
return nullptr;
|
||||
}
|
||||
return std::make_unique<SQLiteCursor>(st);
|
||||
}
|
||||
|
||||
bool SQLiteBatch::TxnBegin()
|
||||
{
|
||||
return sqlite3_exec(m_database.Handle(), "BEGIN TRANSACTION;", nullptr, nullptr, nullptr) == SQLITE_OK;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::TxnCommit()
|
||||
{
|
||||
return sqlite3_exec(m_database.Handle(), "COMMIT TRANSACTION;", nullptr, nullptr, nullptr) == SQLITE_OK;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::TxnAbort()
|
||||
{
|
||||
return sqlite3_exec(m_database.Handle(), "ROLLBACK TRANSACTION;", nullptr, nullptr, nullptr) == SQLITE_OK;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
//
|
||||
// SQLite backend for the wallet database. Stores every wallet record as a row
|
||||
// in a single table:
|
||||
//
|
||||
// CREATE TABLE main (key BLOB PRIMARY KEY NOT NULL, value BLOB NOT NULL);
|
||||
//
|
||||
// The key/value blobs are the exact serialized bytes CWalletDB already
|
||||
// produces (SER_DISK / CLIENT_VERSION), so a SQLite wallet is byte-for-byte
|
||||
// equivalent in content to the Berkeley wallet.dat it was migrated from.
|
||||
//
|
||||
// Modeled on Bitcoin Core's SQLiteDatabase / SQLiteBatch.
|
||||
|
||||
#ifndef TRIANGLES_WALLETDB_SQLITE_H
|
||||
#define TRIANGLES_WALLETDB_SQLITE_H
|
||||
|
||||
#include "walletdb-base.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
class SQLiteDatabase;
|
||||
|
||||
// A batch (and optional transaction) against a SQLiteDatabase. Holds prepared
|
||||
// statements bound to the shared connection owned by SQLiteDatabase.
|
||||
class SQLiteBatch final : public WalletBatch
|
||||
{
|
||||
public:
|
||||
explicit SQLiteBatch(SQLiteDatabase& database);
|
||||
~SQLiteBatch() override { Close(); }
|
||||
|
||||
bool ReadKey(const KeyBytes& key, ValueBytes& value) override;
|
||||
bool WriteKey(const KeyBytes& key, const ValueBytes& value, bool fOverwrite = true) override;
|
||||
bool EraseKey(const KeyBytes& key) override;
|
||||
bool HasKey(const KeyBytes& key) override;
|
||||
|
||||
std::unique_ptr<WalletCursor> GetNewCursor() override;
|
||||
|
||||
bool TxnBegin() override;
|
||||
bool TxnCommit() override;
|
||||
bool TxnAbort() override;
|
||||
|
||||
void Close() override;
|
||||
|
||||
private:
|
||||
SQLiteDatabase& m_database;
|
||||
|
||||
// Prepared statements (lazily compiled on first use, finalized on Close).
|
||||
sqlite3_stmt* m_read_stmt = nullptr;
|
||||
sqlite3_stmt* m_insert_stmt = nullptr; // INSERT OR REPLACE
|
||||
sqlite3_stmt* m_overwrite_stmt = nullptr; // INSERT (fail if exists)
|
||||
sqlite3_stmt* m_delete_stmt = nullptr;
|
||||
|
||||
bool PrepareStatements();
|
||||
};
|
||||
|
||||
// The on-disk SQLite wallet database. Owns the single sqlite3 connection that
|
||||
// all of its batches share (wallet access is serialized by the wallet's own
|
||||
// locks, matching the Berkeley backend's single-environment model).
|
||||
class SQLiteDatabase final : public WalletDatabase
|
||||
{
|
||||
public:
|
||||
// file_path: absolute path to the .dat file on disk.
|
||||
explicit SQLiteDatabase(const std::filesystem::path& file_path);
|
||||
~SQLiteDatabase() override;
|
||||
|
||||
// Open the connection, apply pragmas, and create the schema if absent.
|
||||
// Returns false (with strError set) on failure.
|
||||
bool Open(std::string& strError);
|
||||
|
||||
std::unique_ptr<WalletBatch> MakeBatch(bool flush_on_close = true) override;
|
||||
|
||||
bool Rewrite(const char* pszSkip = nullptr) override;
|
||||
bool Backup(const std::string& strDest) const override;
|
||||
void Flush() override;
|
||||
void Close() override;
|
||||
bool Verify(std::string& strError) override;
|
||||
std::string Filename() const override { return m_file_path.string(); }
|
||||
|
||||
sqlite3* Handle() const { return m_db; }
|
||||
|
||||
private:
|
||||
std::filesystem::path m_file_path;
|
||||
sqlite3* m_db = nullptr;
|
||||
|
||||
bool ExecOrError(const char* sql, std::string& strError) const;
|
||||
};
|
||||
|
||||
// Magic written into PRAGMA application_id so we can recognize our wallet files
|
||||
// and refuse to open foreign SQLite databases. ASCII "TRIw".
|
||||
static constexpr int SQLITE_WALLET_APP_ID = 0x54526977;
|
||||
// Schema version in PRAGMA user_version.
|
||||
static constexpr int SQLITE_WALLET_SCHEMA_VERSION = 1;
|
||||
|
||||
#endif // TRIANGLES_WALLETDB_SQLITE_H
|
||||
+241
-426
File diff suppressed because it is too large
Load Diff
+39
-31
@@ -5,12 +5,26 @@
|
||||
#ifndef TRIANGLES_WALLETDB_H
|
||||
#define TRIANGLES_WALLETDB_H
|
||||
|
||||
#include "db.h"
|
||||
#include "walletdb-batch.h" // CWalletBatchTyped (the typed batch seam)
|
||||
#include "base58.h"
|
||||
|
||||
class CKeyPool;
|
||||
class CAccount;
|
||||
class CAccountingEntry;
|
||||
class CBlockLocator; // forward decl — pulled in via db.h→main.h before
|
||||
class CPubKey;
|
||||
class CScript;
|
||||
class CMasterKey;
|
||||
class uint160;
|
||||
class uint256;
|
||||
class CWallet; // pulled in via db.h→main.h→wallet.h before
|
||||
class CWalletTx; // forward decl — walletdb.h used to pull this in
|
||||
// transitively via db.h; the seam removes that.
|
||||
|
||||
// Wallet-update counter used by the periodic flush thread (db.cpp defines it).
|
||||
// Touched on every wallet write; needed regardless of backend so the daemon's
|
||||
// auto-flush logic can detect changes to the wallet file.
|
||||
extern unsigned int nWalletDBUpdated;
|
||||
|
||||
/** Error statuses for the wallet database */
|
||||
enum DBErrors
|
||||
@@ -57,39 +71,20 @@ public:
|
||||
|
||||
|
||||
/** Access to the wallet database (wallet.dat) */
|
||||
class CWalletDB : public CDB
|
||||
class CWalletDB : public CWalletBatchTyped
|
||||
{
|
||||
public:
|
||||
CWalletDB(std::string strFilename, const char* pszMode="r+") : CDB(strFilename.c_str(), pszMode)
|
||||
{
|
||||
}
|
||||
/**
|
||||
* Open (or create) the wallet database via the configured backend
|
||||
* (-walletdb, default SQLite). The legacy pszMode argument is accepted
|
||||
* for source compatibility but currently ignored — SQLite is always
|
||||
* opened read/write with create-if-missing.
|
||||
*/
|
||||
CWalletDB(std::string strFilename, const char* pszMode="r+");
|
||||
private:
|
||||
CWalletDB(const CWalletDB&);
|
||||
void operator=(const CWalletDB&);
|
||||
public:
|
||||
Dbc* GetAtCursor()
|
||||
{
|
||||
return GetCursor();
|
||||
}
|
||||
|
||||
Dbc* GetTxnCursor()
|
||||
{
|
||||
if (!pdb)
|
||||
return NULL;
|
||||
|
||||
DbTxn* ptxnid = activeTxn; // call TxnBegin first
|
||||
|
||||
Dbc* pcursor = NULL;
|
||||
int ret = pdb->cursor(ptxnid, &pcursor, 0);
|
||||
if (ret != 0)
|
||||
return NULL;
|
||||
return pcursor;
|
||||
}
|
||||
|
||||
DbTxn* GetAtActiveTxn()
|
||||
{
|
||||
return activeTxn;
|
||||
}
|
||||
|
||||
bool WriteName(const std::string& strAddress, const std::string& strName);
|
||||
|
||||
@@ -225,6 +220,18 @@ public:
|
||||
return Write(std::string("minversion"), nVersion);
|
||||
}
|
||||
|
||||
// Mirrors the legacy CDB::WriteVersion / ReadVersion; explicitly retained
|
||||
// because LoadWallet() upgrades the on-disk version to CLIENT_VERSION.
|
||||
bool WriteVersion(int nVersion)
|
||||
{
|
||||
return Write(std::string("version"), nVersion);
|
||||
}
|
||||
bool ReadVersion(int& nVersion)
|
||||
{
|
||||
nVersion = 0;
|
||||
return Read(std::string("version"), nVersion);
|
||||
}
|
||||
|
||||
bool ReadAccount(const std::string& strAccount, CAccount& account);
|
||||
bool WriteAccount(const std::string& strAccount, const CAccount& account);
|
||||
private:
|
||||
@@ -236,9 +243,10 @@ public:
|
||||
|
||||
DBErrors ReorderTransactions(CWallet*);
|
||||
DBErrors LoadWallet(CWallet* pwallet);
|
||||
static bool Recover(CDBEnv& dbenv, std::string filename, bool fOnlyKeys);
|
||||
static bool Recover(CDBEnv& dbenv, std::string filename);
|
||||
static bool ZapWalletTx(const std::string& strWalletFile);
|
||||
// NOTE: Recover() / ZapWalletTx() are Berkeley-only escape hatches. They
|
||||
// live in walletdb-recover.{h,cpp} (which still depends on db.h / db_cxx.h).
|
||||
// After wallet migration to SQLite those helpers are invoked on the
|
||||
// .bdb.bak copy at startup, never on the live wallet.
|
||||
};
|
||||
|
||||
#endif // TRIANGLES_WALLETDB_H
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include "walletmigrate.h"
|
||||
#include "walletdb-sqlite.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include <db_cxx.h>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
bool IsSQLiteFile(const fs::path& path)
|
||||
{
|
||||
std::error_code ec;
|
||||
if (!fs::exists(path, ec) || fs::file_size(path, ec) < 16)
|
||||
return false;
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
char hdr[16] = {};
|
||||
in.read(hdr, sizeof(hdr));
|
||||
if (!in)
|
||||
return false;
|
||||
// SQLite database files always start with this exact 16-byte string,
|
||||
// including the trailing NUL. Berkeley DB files do not.
|
||||
static const char kMagic[16] = {'S','Q','L','i','t','e',' ','f','o','r','m','a','t',' ','3','\0'};
|
||||
return std::memcmp(hdr, kMagic, 16) == 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Count rows currently in the SQLite "main" table.
|
||||
bool SQLiteRowCount(SQLiteDatabase& db, int64_t& nOut, std::string& strError)
|
||||
{
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(db.Handle(), "SELECT COUNT(*) FROM main;", -1, &st, nullptr) != SQLITE_OK) {
|
||||
strError = strprintf("count prepare failed: %s", sqlite3_errmsg(db.Handle()));
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
nOut = sqlite3_column_int64(st, 0);
|
||||
ok = true;
|
||||
} else {
|
||||
strError = "count query returned no rows";
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool MaybeMigrateBerkeleyWalletToSQLite(const fs::path& walletPath, std::string& strError)
|
||||
{
|
||||
strError.clear();
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::exists(walletPath, ec))
|
||||
return true; // fresh install — the SQLite backend will create it
|
||||
if (IsSQLiteFile(walletPath))
|
||||
return true; // already migrated / already SQLite
|
||||
|
||||
const fs::path dir = walletPath.parent_path();
|
||||
const std::string file = walletPath.filename().string();
|
||||
const fs::path tmpPath = dir / (file + ".sqlite.tmp");
|
||||
const fs::path bakPath = dir / (file + ".bdb.bak");
|
||||
|
||||
printf("Wallet migration: converting Berkeley %s to SQLite...\n", walletPath.string().c_str());
|
||||
|
||||
fs::remove(tmpPath, ec); // clear any stale temp from a prior aborted run
|
||||
|
||||
int64_t nCopied = 0;
|
||||
|
||||
// ── Read side: a private, read-only Berkeley environment over the wallet
|
||||
// directory, then the "main" sub-database (matches CDB::CDB's open call). ──
|
||||
DbEnv env(0u);
|
||||
env.set_error_stream(&std::cerr);
|
||||
env.set_cachesize(0, 1 << 20, 1); // 1 MiB cache is plenty for sequential read
|
||||
u_int32_t envFlags = DB_CREATE | DB_INIT_MPOOL | DB_PRIVATE;
|
||||
if (env.open(dir.string().c_str(), envFlags, 0) != 0) {
|
||||
strError = "migration: cannot open Berkeley environment on wallet directory";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
{
|
||||
Db db(&env, 0);
|
||||
if (db.open(nullptr, file.c_str(), "main", DB_BTREE, DB_RDONLY, 0) != 0) {
|
||||
strError = "migration: cannot open Berkeley wallet (is it a valid wallet.dat?)";
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Write side: fresh SQLite database in the temp file. ──
|
||||
SQLiteDatabase sqlite(tmpPath);
|
||||
std::string sqlErr;
|
||||
if (!sqlite.Open(sqlErr)) {
|
||||
strError = "migration: cannot create SQLite wallet: " + sqlErr;
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto batch = sqlite.MakeBatch();
|
||||
if (!batch || !batch->TxnBegin()) {
|
||||
strError = "migration: cannot begin SQLite transaction";
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
Dbc* pcursor = nullptr;
|
||||
if (db.cursor(nullptr, &pcursor, 0) != 0) {
|
||||
strError = "migration: cannot open Berkeley cursor";
|
||||
batch->TxnAbort();
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
Dbt datKey, datValue; // BDB-owned buffers, valid until the next get()
|
||||
int ret;
|
||||
bool writeFailed = false;
|
||||
while ((ret = pcursor->get(&datKey, &datValue, DB_NEXT)) == 0) {
|
||||
const unsigned char* kp = static_cast<const unsigned char*>(datKey.get_data());
|
||||
const unsigned char* vp = static_cast<const unsigned char*>(datValue.get_data());
|
||||
KeyBytes key(kp, kp + datKey.get_size());
|
||||
ValueBytes val(vp, vp + datValue.get_size());
|
||||
if (!batch->WriteKey(key, val, /*fOverwrite=*/true)) {
|
||||
writeFailed = true;
|
||||
break;
|
||||
}
|
||||
++nCopied;
|
||||
}
|
||||
pcursor->close();
|
||||
|
||||
if (writeFailed || (ret != DB_NOTFOUND && ret != 0)) {
|
||||
strError = strprintf("migration: copy aborted after %lld records (bdb get=%d)",
|
||||
(long long)nCopied, ret);
|
||||
batch->TxnAbort();
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!batch->TxnCommit()) {
|
||||
strError = "migration: SQLite commit failed";
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Verify the destination row count matches what we copied. ──
|
||||
int64_t nDst = -1;
|
||||
if (!SQLiteRowCount(sqlite, nDst, strError)) {
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
if (nDst != nCopied) {
|
||||
strError = strprintf("migration: record count mismatch (copied=%lld sqlite=%lld)",
|
||||
(long long)nCopied, (long long)nDst);
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
batch.reset();
|
||||
sqlite.Close();
|
||||
db.close(0);
|
||||
ok = true;
|
||||
}
|
||||
env.close(0);
|
||||
|
||||
if (!ok) {
|
||||
fs::remove(tmpPath, ec);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Atomic-ish swap: back up the Berkeley original, then move SQLite in. ──
|
||||
fs::rename(walletPath, bakPath, ec);
|
||||
if (ec) {
|
||||
strError = strprintf("migration: cannot back up Berkeley wallet to %s: %s",
|
||||
bakPath.string().c_str(), ec.message().c_str());
|
||||
fs::remove(tmpPath, ec);
|
||||
return false;
|
||||
}
|
||||
fs::rename(tmpPath, walletPath, ec);
|
||||
if (ec) {
|
||||
// Roll the original back into place so the wallet is never left missing.
|
||||
std::error_code ec2;
|
||||
fs::rename(bakPath, walletPath, ec2);
|
||||
strError = strprintf("migration: cannot move SQLite wallet into place: %s",
|
||||
ec.message().c_str());
|
||||
fs::remove(tmpPath, ec2);
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("Wallet migration: complete. %lld records migrated to SQLite. "
|
||||
"Berkeley original preserved at %s\n",
|
||||
(long long)nCopied, bakPath.string().c_str());
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef TRIANGLES_WALLETMIGRATE_H
|
||||
#define TRIANGLES_WALLETMIGRATE_H
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
// Migrate a Berkeley DB wallet (wallet.dat) to a SQLite wallet of the same
|
||||
// name, IN PLACE and NON-DESTRUCTIVELY:
|
||||
//
|
||||
// 1. If walletPath does not exist, or is already a SQLite database, there is
|
||||
// nothing to do — returns true.
|
||||
// 2. Otherwise the Berkeley records are copied verbatim (raw key/value bytes)
|
||||
// into a fresh SQLite database written to a temporary file.
|
||||
// 3. The record count is verified to match.
|
||||
// 4. The original Berkeley file is renamed to "<name>.bdb.bak" (kept as a
|
||||
// fallback, never deleted), and the SQLite file is moved into place as
|
||||
// "<name>".
|
||||
//
|
||||
// On any failure the original Berkeley wallet is left exactly as it was and the
|
||||
// temporary SQLite file is removed; strError describes the problem.
|
||||
bool MaybeMigrateBerkeleyWalletToSQLite(const std::filesystem::path& walletPath,
|
||||
std::string& strError);
|
||||
|
||||
// True if the file begins with the SQLite format-3 magic header.
|
||||
bool IsSQLiteFile(const std::filesystem::path& path);
|
||||
|
||||
#endif // TRIANGLES_WALLETMIGRATE_H
|
||||
Reference in New Issue
Block a user