WIP: modernization applied to DNS2 tree

Brings in uncommitted work from SAMI-PC E:\repos\triangles
cpp20-modernization branch:
- RocksDB default chain DB + auto-migrate from txleveldb
- SQLite default wallet + non-destructive migration from Berkeley
- Boost.Asio removed from RPC (rpc_httpsocket.h)
- Boost removed from all daemon + GUI code
- New: walletdb-base.h, walletdb-batch.h, walletdb-sqlite.{h,cpp},
  walletdb-factory.{h,cpp}, walletmigrate.{h,cpp}
- New tests: chaindb_runtime_tests, chaindb_equivalence_tests,
  snapshotnet_tests
- Docs: BOOST-REMOVAL.md, ROCKSDB-DEFAULT-MIGRATION.md,
  WALLET-SQLITE-MIGRATION.md

Does not yet build — needs CWalletDB->CWalletBatchTyped rebase in
walletdb.cpp/wallet.cpp/db.cpp and merge with origin/master for
v6 source files (checkpointpublisher, tor/, snapshot/, utxosnapshot,
bootstrap.cpp).

Build flags: -DBUILD_QT=OFF -DUSE_I2P_EMBEDDED=OFF
This commit is contained in:
Hermes
2026-06-29 20:08:06 -07:00
parent 03aa38f1b4
commit bfdb399772
31 changed files with 13468 additions and 9631 deletions
File diff suppressed because it is too large Load Diff
+109 -100
View File
@@ -1,100 +1,109 @@
name: Lint name: Lint
on: on:
pull_request: pull_request:
branches: [master] branches: [master]
workflow_dispatch: workflow_dispatch:
# Diff-only enforcement: clang-format and clang-tidy run only on lines changed # Diff-only enforcement: clang-format and clang-tidy run only on lines changed
# in the PR. Existing files keep their current style until they're edited. # in the PR. Existing files keep their current style until they're edited.
# See .clang-format and .clang-tidy for the rule sets. # See .clang-format and .clang-tidy for the rule sets.
jobs: jobs:
clang-format-diff: clang-format-diff:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
# Need merge-base with target branch to compute the diff. # Need merge-base with target branch to compute the diff.
fetch-depth: 0 fetch-depth: 0
- name: Install clang-format - name: Install clang-format
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y clang-format-15 sudo apt-get install -y clang-format-15
sudo ln -sf /usr/bin/clang-format-15 /usr/local/bin/clang-format sudo ln -sf /usr/bin/clang-format-15 /usr/local/bin/clang-format
- name: Check format on changed lines - name: Check format on changed lines
run: | run: |
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD) BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD)
echo "Comparing against merge-base: $BASE_SHA" echo "Comparing against merge-base: $BASE_SHA"
# git-clang-format prints a diff if any changed line violates style. # git-clang-format prints a diff if any changed line violates style.
# --diff exits non-zero when reformatting would change something. # --diff exits non-zero when reformatting would change something.
OUTPUT=$(git clang-format --diff "$BASE_SHA" -- '*.cpp' '*.h' '*.hpp' '*.cc' || true) OUTPUT=$(git clang-format --diff "$BASE_SHA" -- '*.cpp' '*.h' '*.hpp' '*.cc' || true)
if [ -z "$OUTPUT" ] || [ "$OUTPUT" = "no modified files to format" ] || [ "$OUTPUT" = "clang-format did not modify any files" ]; then if [ -z "$OUTPUT" ] || [ "$OUTPUT" = "no modified files to format" ] || [ "$OUTPUT" = "clang-format did not modify any files" ]; then
echo "clang-format: clean" echo "clang-format: clean"
exit 0 exit 0
fi fi
echo "::error::clang-format wants to change the following on lines you touched." echo "::error::clang-format wants to change the following on lines you touched."
echo "Run \`git clang-format $BASE_SHA\` locally and commit the result." echo "Run \`git clang-format $BASE_SHA\` locally and commit the result."
echo "$OUTPUT" echo "$OUTPUT"
exit 1 exit 1
clang-tidy-diff: clang-tidy-diff:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive
- name: Install dependencies + clang-tidy
run: | - name: Install dependencies + clang-tidy
sudo apt-get update run: |
sudo apt-get install -y build-essential cmake ninja-build clang-tidy-15 \ sudo apt-get update
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \ sudo apt-get install -y build-essential cmake ninja-build clang-tidy-15 \
librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
sudo ln -sf /usr/bin/clang-tidy-15 /usr/local/bin/clang-tidy libevent-dev libminiupnpc-dev zlib1g-dev \
libsnappy-dev liblz4-dev libzstd-dev libsqlite3-dev
- name: Configure (export compile_commands.json) sudo ln -sf /usr/bin/clang-tidy-15 /usr/local/bin/clang-tidy
run: |
cmake -B build -G Ninja \ - name: Build RocksDB from source
-DCMAKE_BUILD_TYPE=Debug \ # Ubuntu 22.04's librocksdb-dev is 6.11.4 which CMakeLists.txt now
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ # refuses to configure against (need >= 7.4 for XXH3 per-block
-DBUILD_QT=OFF \ # checksum). Build 8.9.1 from source — same version DNS2 ships —
-DBUILD_DAEMON=ON \ # into /usr/local so CMake's find_library picks it up first.
-DBUILD_TESTS=ON \ run: sudo bash scripts/ci/build-rocksdb.sh
-DUSE_UPNP=OFF
- name: Configure (export compile_commands.json)
- name: Generate build artifacts that headers depend on run: |
# build.h, qt UI headers, etc. — clang-tidy needs them to parse sources. cmake -B build -G Ninja \
run: cmake --build build --target generate_build_info -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
- name: Run clang-tidy on changed lines -DBUILD_QT=OFF \
run: | -DBUILD_DAEMON=ON \
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD) -DBUILD_TESTS=ON \
echo "Comparing against merge-base: $BASE_SHA" -DUSE_UPNP=OFF
# clang-tidy-diff.py ships with clang-tidy; runs tidy only on changed lines. - name: Generate build artifacts that headers depend on
DIFF_SCRIPT=$(dpkg -L clang-tidy-15 | grep clang-tidy-diff.py | head -1) # build.h, qt UI headers, etc. — clang-tidy needs them to parse sources.
if [ -z "$DIFF_SCRIPT" ]; then run: cmake --build build --target generate_build_info
DIFF_SCRIPT=/usr/share/clang/clang-tidy-diff.py
fi - name: Run clang-tidy on changed lines
echo "Using: $DIFF_SCRIPT" run: |
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD)
# -p1 strips the leading "a/"/"b/" from git diff paths. echo "Comparing against merge-base: $BASE_SHA"
# -path=build points clang-tidy at compile_commands.json.
# -iregex restricts to project sources (not vendored). # clang-tidy-diff.py ships with clang-tidy; runs tidy only on changed lines.
git diff -U0 "$BASE_SHA" -- 'src/*.cpp' 'src/*.h' \ DIFF_SCRIPT=$(dpkg -L clang-tidy-15 | grep clang-tidy-diff.py | head -1)
':(exclude)src/json/nlohmann_json.hpp' \ if [ -z "$DIFF_SCRIPT" ]; then
':(exclude)src/leveldb/*' \ DIFF_SCRIPT=/usr/share/clang/clang-tidy-diff.py
':(exclude)src/lz4/*' \ fi
':(exclude)src/tor/tor-src/*' \ echo "Using: $DIFF_SCRIPT"
| python3 "$DIFF_SCRIPT" -p1 -path build \
-iregex '.*\.(cpp|cc|h|hpp)$' \ # -p1 strips the leading "a/"/"b/" from git diff paths.
-j$(nproc) || EXIT=$? # -path=build points clang-tidy at compile_commands.json.
# -iregex restricts to project sources (not vendored).
# Warn-only initially. Flip this to `exit ${EXIT:-0}` once we're clean. git diff -U0 "$BASE_SHA" -- 'src/*.cpp' 'src/*.h' \
exit 0 ':(exclude)src/json/nlohmann_json.hpp' \
':(exclude)src/leveldb/*' \
':(exclude)src/lz4/*' \
':(exclude)src/tor/tor-src/*' \
| python3 "$DIFF_SCRIPT" -p1 -path build \
-iregex '.*\.(cpp|cc|h|hpp)$' \
-j$(nproc) || EXIT=$?
# Warn-only initially. Flip this to `exit ${EXIT:-0}` once we're clean.
exit 0
+91
View File
@@ -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.
+272 -250
View File
@@ -1,250 +1,272 @@
# Cryptographic Triangles (TRI) # 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. 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 ## Key Features
- **Proof-of-Stake** - Energy-efficient block production with 33% annual staking rewards (coin-age based) - **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) - **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 - **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 - **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 - **120-second Block Time** - Fast confirmations with 2-minute target spacing
## Specifications ## Specifications
| Property | Value | | Property | Value |
|----------|-------| |----------|-------|
| Algorithm | Hash9 (PoW blocks 0-9000), PoS from block 9001 | | Algorithm | Hash9 (PoW blocks 0-9000), PoS from block 9001 |
| Block Time | ~120 seconds | | Block Time | ~120 seconds |
| Max Supply | 2,222,222 TRI | | Max Supply | 2,222,222 TRI |
| PoS Reward | 33% annual, coin-age based | | PoS Reward | 33% annual, coin-age based |
| P2P Port | 24112 | | P2P Port | 24112 |
| RPC Port | 19112 | | RPC Port | 19112 |
| Protocol | 70205 | | Protocol | 70205 |
## Network Status ## Network Status
The Triangles network operates exclusively over Tor for privacy: The Triangles network operates exclusively over Tor for privacy:
**Tor v3 Seeds:** **Tor v3 Seeds:**
- `jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112` - `jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112`
- `uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112` - `uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112`
- `el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112` - `el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112`
- `sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112` - `sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112`
- `i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112` - `i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112`
**HTTP Seed List:** **HTTP Seed List:**
- `seeds.cryptographic-triangles.org/seeds.txt` - Dynamically updated list of active onion peers - `seeds.cryptographic-triangles.org/seeds.txt` - Dynamically updated list of active onion peers
## Building from Source ## Building from Source
Triangles uses CMake. All platforms follow the same build pattern. Triangles uses CMake. All platforms follow the same build pattern.
### Dependencies ### Dependencies
| Dependency | Minimum Version | | Dependency | Minimum Version |
|------------|----------------| |------------|----------------|
| CMake | 3.16+ | | CMake | 3.16+ |
| C++ compiler | C++17 support | | C++ compiler | C++17 support |
| OpenSSL | 3.x | | OpenSSL | 3.x |
| Boost | 1.90+ | | Boost | 1.90+ |
| Berkeley DB | 5.3 (with C++ bindings) | | SQLite | 3.x (default wallet database backend) |
| libevent | 2.x | | Berkeley DB | 5.3 with C++ bindings (legacy wallet backend, used for migration) |
| LevelDB | bundled | | libevent | 2.x |
| RocksDB | 7.4+ (default chain database backend) |
### Linux (Ubuntu 24.04 / Debian 12+) | LevelDB | bundled (legacy chain DB backend, used for migration) |
Install dependencies: ### Linux (Ubuntu 24.04 / Debian 12+)
```bash
sudo apt-get install -y build-essential cmake ninja-build \ Install dependencies:
libboost-all-dev libssl-dev libdb5.3++-dev libevent-dev \ ```bash
zlib1g-dev libminiupnpc-dev sudo apt-get install -y build-essential cmake ninja-build \
``` libboost-all-dev libssl-dev libdb5.3++-dev libevent-dev \
zlib1g-dev libminiupnpc-dev
For the Qt wallet, also install: ```
```bash
sudo apt-get install -y qtbase5-dev qt5-qmake libqrencode-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 Build:
cmake --build build ```bash
``` cmake -B build -G Ninja -DBUILD_QT=ON
cmake --build build
### Linux (AlmaLinux 9 / RHEL 9) ```
Install dependencies: ### Linux (AlmaLinux 9 / RHEL 9)
```bash
sudo dnf install -y gcc-c++ cmake ninja-build boost-devel openssl-devel \ Install dependencies:
libevent-devel zlib-devel miniupnpc-devel ```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. 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`.
### Windows (MSYS2 MinGW64) Then build as above.
Open an MSYS2 MinGW64 shell and install: ### Windows (MSYS2 MinGW64)
```bash
pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja \ Open an MSYS2 MinGW64 shell and install:
mingw-w64-x86_64-boost mingw-w64-x86_64-openssl \ ```bash
mingw-w64-x86_64-db mingw-w64-x86_64-miniupnpc \ pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja \
mingw-w64-x86_64-qt5-base mingw-w64-x86_64-qrencode \ mingw-w64-x86_64-boost mingw-w64-x86_64-openssl \
mingw-w64-x86_64-libevent 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 Build:
cmake --build build ```bash
``` cmake -B build -G Ninja -DBUILD_QT=ON
cmake --build build
### Build Options ```
| Option | Default | Description | ### Build Options
|--------|---------|-------------|
| `BUILD_QT` | ON | Build the Qt GUI wallet | | Option | Default | Description |
| `BUILD_DAEMON` | ON | Build the headless daemon | |--------|---------|-------------|
| `BUILD_TESTS` | OFF | Build unit tests | | `BUILD_QT` | ON | Build the Qt GUI wallet |
| `BUILD_DAEMON` | ON | Build the headless daemon |
## Running | `BUILD_TESTS` | OFF | Build unit tests |
### First Run ## Running
```bash
mkdir -p ~/.triangles ### First Run
cat > ~/.triangles/triangles.conf << 'EOF' ```bash
port=24112 mkdir -p ~/.triangles
rpcport=19112 cat > ~/.triangles/triangles.conf << 'EOF'
rpcuser=trianglesrpc port=24112
rpcpassword=<generate-a-strong-password> rpcport=19112
rpcallowip=127.0.0.1 rpcuser=trianglesrpc
staking=1 rpcpassword=<generate-a-strong-password>
txindex=1 rpcallowip=127.0.0.1
listen=1 staking=1
server=1 txindex=1
daemon=1 listen=1
proxy=127.0.0.1:9050 server=1
EOF daemon=1
proxy=127.0.0.1:9050
trianglesd EOF
```
trianglesd
The node will connect to seed nodes over Tor and sync the blockchain automatically. ```
### Existing Wallet Holders The node will connect to seed nodes over Tor and sync the blockchain automatically.
If you have a `wallet.dat` from the original Triangles network: ### Chain Database (RocksDB)
1. Place your `wallet.dat` in `~/.triangles/` (Linux) or `%APPDATA%\triangles\` (Windows) 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>`).
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 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.
### Staking To select a backend explicitly:
To stake, your wallet must be: ```bash
- Running with `staking=1` in the config trianglesd -chaindb=rocksdb # default
- Connected to at least one peer trianglesd -chaindb=leveldb # legacy backend (retained for fallback/migration)
- Containing coins with sufficient coin-age (mature inputs) ```
Check staking status: Migration can also be triggered or forced manually:
```bash
trianglesd getstakinginfo ```bash
``` trianglesd -migratechaindb # migrate txleveldb -> rocksdb if not already done
trianglesd -migratechaindbforce # re-migrate, replacing any existing rocksdb/
### Encrypted Messaging ```
Send and receive encrypted messages between wallet addresses: ### Existing Wallet Holders
```bash If you have a `wallet.dat` from the original Triangles network:
# Enable messaging
trianglesd smsgenable 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
# Send a message 3. No migration or special action is needed - all keys and balances are preserved
trianglesd smsgsend <your-address> <recipient-address> "Hello from Triangles!"
### Staking
# Check inbox
trianglesd smsginbox all To stake, your wallet must be:
- Running with `staking=1` in the config
# Send anonymous message - Connected to at least one peer
trianglesd smsgsendanon <recipient-address> "Anonymous message" - Containing coins with sufficient coin-age (mature inputs)
```
Check staking status:
Messages are encrypted end-to-end using AES and distributed through the peer network in time-bucketed batches. ```bash
trianglesd getstakinginfo
### Tor Support ```
Triangles is designed as a Tor-only network. Install the Tor daemon and configure your proxy: ### Encrypted Messaging
```
# triangles.conf Send and receive encrypted messages between wallet addresses:
proxy=127.0.0.1:9050
``` ```bash
# Enable messaging
To run your own hidden service, add to `/etc/tor/torrc`: trianglesd smsgenable
```
HiddenServiceDir /var/lib/tor/triangles/ # Send a message
HiddenServiceVersion 3 trianglesd smsgsend <your-address> <recipient-address> "Hello from Triangles!"
HiddenServicePort 24112 127.0.0.1:24112
``` # Check inbox
trianglesd smsginbox all
Then set `externalip=<your-onion-address>` in `triangles.conf`.
# Send anonymous message
## RPC Commands trianglesd smsgsendanon <recipient-address> "Anonymous message"
```
### General
- `getinfo` - Node status, balance, block height, connections Messages are encrypted end-to-end using AES and distributed through the peer network in time-bucketed batches.
- `getpeerinfo` - Connected peer details
- `getstakinginfo` - Staking status and weight ### Tor Support
### Wallet Triangles is designed as a Tor-only network. Install the Tor daemon and configure your proxy:
- `getbalance` - Current balance ```
- `listunspent` - Unspent transaction outputs # triangles.conf
- `sendtoaddress <addr> <amount>` - Send TRI proxy=127.0.0.1:9050
- `getnewaddress` - Generate new receiving address ```
### Messaging To run your own hidden service, add to `/etc/tor/torrc`:
- `smsgenable` / `smsgdisable` - Toggle secure messaging ```
- `smsgsend <from> <to> <message>` - Send encrypted message HiddenServiceDir /var/lib/tor/triangles/
- `smsgsendanon <to> <message>` - Send anonymous message HiddenServiceVersion 3
- `smsginbox [all|unread|clear]` - View received messages HiddenServicePort 24112 127.0.0.1:24112
- `smsgoutbox [all|clear]` - View sent messages ```
- `smsglocalkeys` - List messaging-enabled addresses
- `smsgscanchain` - Scan blockchain for public keys Then set `externalip=<your-onion-address>` in `triangles.conf`.
## Chain History ## RPC Commands
- **July 16, 2014** - Genesis block ### General
- **Block 0-9000** - Proof-of-Work mining phase (Hash9) - `getinfo` - Node status, balance, block height, connections
- **Block 9001+** - Proof-of-Stake only - `getpeerinfo` - Connected peer details
- **Block 17,651** - V5 hard fork (removed Tor v2, disabled checkpoint master key) - `getstakinginfo` - Staking status and weight
- **December 8, 2022** - Chain frozen (all nodes offline)
- **March 11, 2026** - Chain revived, staking resumed ### Wallet
- `getbalance` - Current balance
## Project Structure - `listunspent` - Unspent transaction outputs
- `sendtoaddress <addr> <amount>` - Send TRI
``` - `getnewaddress` - Generate new receiving address
src/
main.cpp - Core blockchain logic, block/tx validation, message routing ### Messaging
miner.cpp - Staking miner thread - `smsgenable` / `smsgdisable` - Toggle secure messaging
net.cpp - P2P networking - `smsgsend <from> <to> <message>` - Send encrypted message
init.cpp - Daemon initialization - `smsgsendanon <to> <message>` - Send anonymous message
wallet.cpp - Wallet management - `smsginbox [all|unread|clear]` - View received messages
smessage.cpp/h - Encrypted messaging system - `smsgoutbox [all|clear]` - View sent messages
kernel.cpp - PoS kernel (stake validation) - `smsglocalkeys` - List messaging-enabled addresses
checkpoints.cpp - Hardcoded checkpoints - `smsgscanchain` - Scan blockchain for public keys
net_bootstrap.h - DNS/IP seed configuration
onionseed.h - Tor v3 onion seed addresses ## Chain History
tor/
onion_v3.cpp/h - Tor v3 hidden service management - **July 16, 2014** - Genesis block
tor_crypto_compat.h - Ed25519/SHA3 crypto compatibility - **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)
## License - **December 8, 2022** - Chain frozen (all nodes offline)
- **March 11, 2026** - Chain revived, staking resumed
Distributed under the MIT/X11 software license. See `COPYING` for details.
## Project Structure
## Links
```
- Website: [cryptographic-triangles.org](https://cryptographic-triangles.org) src/
- Explorer: [blocks.cryptographic-triangles.org](https://blocks.cryptographic-triangles.org) 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)
+132
View File
@@ -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.
+98
View File
@@ -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.
+685 -475
View File
File diff suppressed because it is too large Load Diff
+1110 -808
View File
File diff suppressed because it is too large Load Diff
+1934 -1564
View File
File diff suppressed because it is too large Load Diff
+89 -82
View File
@@ -1,32 +1,27 @@
// Copyright (c) 2009-2012 The Bitcoin developers // Copyright (c) 2009-2012 The Bitcoin developers
// Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying // Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
#include <boost/version.hpp> // Single-instance "triangles:" URI handoff. When the wallet is launched with a
#if defined(WIN32) && BOOST_VERSION == 104900 // URI argument and an instance is already running, the URI is relayed to the
#define BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME // running instance over a local socket; otherwise this instance becomes the
#define BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME // listener. Reworked from Boost.Interprocess message queues onto Qt's
#endif // QLocalServer/QLocalSocket (QtNetwork) — no Boost dependency.
#include "qtipcserver.h" #include "qtipcserver.h"
#include "guiconstants.h" #include "guiconstants.h"
#include "ui_interface.h" #include "ui_interface.h"
#include "util.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 <algorithm>
#include <cctype> #include <cctype>
#include <string>
#include <QByteArray>
#include <QLocalServer>
#include <QLocalSocket>
#include <QString>
#if defined MAC_OSX || defined __FreeBSD__ #if defined MAC_OSX || defined __FreeBSD__
// URI handling not implemented on OSX yet // URI handling not implemented on OSX yet
@@ -36,33 +31,47 @@ void ipcInit(int argc, char *argv[]) { }
#else #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 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) 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; bool fSent = false;
for (int i = 1; i < argc; i++) 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]; socket.write(strURI, static_cast<qint64>(strlen(strURI)));
try { socket.flush();
boost::interprocess::message_queue mq(boost::interprocess::open_only, TRIANGLESURI_QUEUE_NAME); socket.waitForBytesWritten(1000);
if (mq.try_send(strURI, strlen(strURI), 0)) socket.disconnectFromServer();
fSent = true; fSent = true;
else if (fRelay) }
break; else if (fRelay)
} {
catch (boost::interprocess::interprocess_exception &ex) { // No running instance accepted the URI; this process should become
// don't log the "file not found" exception, because that's normal for // the listener instead of relaying.
// the first start of the first instance break;
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;
}
}
} }
} }
return fSent; return fSent;
@@ -78,7 +87,7 @@ static void ipcThread(void* pArg)
{ {
// Make this thread recognisable as the GUI-IPC thread // Make this thread recognisable as the GUI-IPC thread
RenameThread("Triangles-gui-ipc"); RenameThread("Triangles-gui-ipc");
try try
{ {
ipcThread2(pArg); ipcThread2(pArg);
@@ -95,69 +104,67 @@ static void ipcThread2(void* pArg)
{ {
printf("ipcThread started\n"); printf("ipcThread started\n");
message_queue* mq = (message_queue*)pArg; QLocalServer* server = static_cast<QLocalServer*>(pArg);
char buffer[MAX_URI_LENGTH + 1] = "";
size_t nSize = 0;
unsigned int nPriority = 0;
// Poll for inbound connections without requiring a Qt event loop:
// waitForNewConnection(timeout) pumps the socket internally.
while (true) while (true)
{ {
ptime d = boost::posix_time::microsec_clock::universal_time() + millisec(100); if (server->waitForNewConnection(100))
if (mq->timed_receive(&buffer, sizeof(buffer), nSize, nPriority, d))
{ {
uiInterface.ThreadSafeHandleURI(std::string(buffer, nSize)); QLocalSocket* client = server->nextPendingConnection();
MilliSleep(1000); 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) if (fShutdown)
break; break;
} }
// Remove message queue server->close();
message_queue::remove(TRIANGLESURI_QUEUE_NAME); delete server;
// Cleanup allocated memory
delete mq;
} }
void ipcInit(int argc, char *argv[]) void ipcInit(int argc, char *argv[])
{ {
message_queue* mq = NULL; // Clear any stale socket/pipe left by a previous crashed instance, then
char buffer[MAX_URI_LENGTH + 1] = ""; // listen. If listen() fails, another instance already owns the name — in
size_t nSize = 0; // that case relay our own URI args (below) and don't start a server.
unsigned int nPriority = 0; QLocalServer::removeServer(IPC_SERVER_NAME);
try { QLocalServer* server = new QLocalServer();
mq = new message_queue(open_or_create, TRIANGLESURI_QUEUE_NAME, 2, MAX_URI_LENGTH); server->setSocketOptions(QLocalServer::UserAccessOption); // owner-only access
if (!server->listen(IPC_SERVER_NAME))
// 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))
{ {
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; 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); ipcScanCmd(argc, argv, false);
} }
+200
View File
@@ -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
View File
@@ -1,319 +1,315 @@
// Copyright (c) 2009-2012 Bitcoin Developers // Copyright (c) 2009-2012 Bitcoin Developers
// Distributed under the MIT/X11 software license, see the accompanying // Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <iostream> #include <iostream>
#include <fstream> #include <fstream>
#include <iomanip>
#include "init.h" // for pwalletMain #include <sstream>
#include "trianglesrpc.h" #include <ctime>
#include "ui_interface.h"
#include "base58.h" #include "init.h" // for pwalletMain
#include "trianglesrpc.h"
#include <boost/date_time/posix_time/posix_time.hpp> #include "ui_interface.h"
#include "base58.h"
#define printf OutputDebugStringF
#define printf OutputDebugStringF
using namespace json_spirit;
using namespace std; using namespace json_spirit;
using namespace std;
void EnsureWalletIsUnlocked();
void EnsureWalletIsUnlocked();
namespace bt = boost::posix_time;
// Accepted timestamp formats, tried in order. Replaces the boost::posix_time
// Extended DecodeDumpTime implementation, see this page for details: // parser; std::get_time is portable (C++11) and parses against each format.
// http://stackoverflow.com/questions/3786201/parsing-of-date-time-from-string-boost static const char* const dumptime_formats[] = {
const std::locale formats[] = { "%Y-%m-%dT%H:%M:%SZ",
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%dT%H:%M:%SZ")), "%Y-%m-%d %H:%M:%S",
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%d %H:%M:%S")), "%Y/%m/%d %H:%M:%S",
std::locale(std::locale::classic(),new bt::time_input_facet("%Y/%m/%d %H:%M:%S")), "%d.%m.%Y %H:%M:%S",
std::locale(std::locale::classic(),new bt::time_input_facet("%d.%m.%Y %H:%M:%S")), "%Y-%m-%d",
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%d")) };
};
int64_t DecodeDumpTime(const std::string& s)
const size_t formats_n = sizeof(formats)/sizeof(formats[0]); {
for (const char* fmt : dumptime_formats)
std::time_t pt_to_time_t(const bt::ptime& pt) {
{ std::tm tm = {};
bt::ptime timet_start(boost::gregorian::date(1970,1,1)); std::istringstream is(s);
bt::time_duration diff = pt - timet_start; is >> std::get_time(&tm, fmt);
return diff.ticks()/bt::time_duration::rep_type::ticks_per_second; if (is.fail())
} continue;
// Interpret the parsed broken-down time as UTC.
int64_t DecodeDumpTime(const std::string& s) #ifdef WIN32
{ std::time_t t = _mkgmtime(&tm);
bt::ptime pt; #else
std::time_t t = timegm(&tm);
for(size_t i=0; i<formats_n; ++i) #endif
{ if (t != static_cast<std::time_t>(-1))
std::istringstream is(s); return static_cast<int64_t>(t);
is.imbue(formats[i]); }
is >> pt; return 0;
if(pt != bt::ptime()) break; }
}
std::string static EncodeDumpTime(int64_t nTime) {
return pt_to_time_t(pt); return DateTimeStrFormat("%Y-%m-%dT%H:%M:%SZ", nTime);
} }
std::string static EncodeDumpTime(int64_t nTime) { std::string static EncodeDumpString(const std::string &str) {
return DateTimeStrFormat("%Y-%m-%dT%H:%M:%SZ", nTime); std::stringstream ret;
} for (unsigned char c : str) {
if (c <= 32 || c >= 128 || c == '%') {
std::string static EncodeDumpString(const std::string &str) { ret << '%' << HexStr(&c, &c + 1);
std::stringstream ret; } else {
for (unsigned char c : str) { ret << c;
if (c <= 32 || c >= 128 || c == '%') { }
ret << '%' << HexStr(&c, &c + 1); }
} else { return ret.str();
ret << c; }
}
} std::string DecodeDumpString(const std::string &str) {
return ret.str(); std::stringstream ret;
} for (unsigned int pos = 0; pos < str.length(); pos++) {
unsigned char c = str[pos];
std::string DecodeDumpString(const std::string &str) { if (c == '%' && pos+2 < str.length()) {
std::stringstream ret; c = (((str[pos+1]>>6)*9+((str[pos+1]-'0')&15)) << 4) |
for (unsigned int pos = 0; pos < str.length(); pos++) { ((str[pos+2]>>6)*9+((str[pos+2]-'0')&15));
unsigned char c = str[pos]; pos += 2;
if (c == '%' && pos+2 < str.length()) { }
c = (((str[pos+1]>>6)*9+((str[pos+1]-'0')&15)) << 4) | ret << c;
((str[pos+2]>>6)*9+((str[pos+2]-'0')&15)); }
pos += 2; return ret.str();
} }
ret << c;
} class CTxDump
return ret.str(); {
} public:
CBlockIndex *pindex;
class CTxDump int64_t nValue;
{ bool fSpent;
public: CWalletTx* ptx;
CBlockIndex *pindex; int nOut;
int64_t nValue; CTxDump(CWalletTx* ptx = nullptr, int nOut = -1)
bool fSpent; {
CWalletTx* ptx; pindex = nullptr;
int nOut; nValue = 0;
CTxDump(CWalletTx* ptx = nullptr, int nOut = -1) fSpent = false;
{ this->ptx = ptx;
pindex = nullptr; this->nOut = nOut;
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(
Value importprivkey(const Array& params, bool fHelp) "importprivkey <Trianglesprivkey> [label]\n"
{ "Adds a private key (as returned by dumpprivkey) to your wallet.");
if (fHelp || params.size() < 1 || params.size() > 2)
throw runtime_error( string strSecret = params[0].get_str();
"importprivkey <Trianglesprivkey> [label]\n" string strLabel = "";
"Adds a private key (as returned by dumpprivkey) to your wallet."); if (params.size() > 1)
strLabel = params[1].get_str();
string strSecret = params[0].get_str(); CTrianglesSecret vchSecret;
string strLabel = ""; bool fGood = vchSecret.SetString(strSecret);
if (params.size() > 1)
strLabel = params[1].get_str(); if (!fGood) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key");
CTrianglesSecret vchSecret; if (fWalletUnlockStakingOnly)
bool fGood = vchSecret.SetString(strSecret); throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
if (!fGood) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key"); CKey key;
if (fWalletUnlockStakingOnly) bool fCompressed;
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only."); CSecret secret = vchSecret.GetSecret(fCompressed);
key.SetSecret(secret, fCompressed);
CKey key; CKeyID vchAddress = key.GetPubKey().GetID();
bool fCompressed; {
CSecret secret = vchSecret.GetSecret(fCompressed); LOCK2(cs_main, pwalletMain->cs_wallet);
key.SetSecret(secret, fCompressed);
CKeyID vchAddress = key.GetPubKey().GetID(); pwalletMain->MarkDirty();
{ pwalletMain->SetAddressBookName(vchAddress, strLabel);
LOCK2(cs_main, pwalletMain->cs_wallet);
if (!pwalletMain->AddKey(key))
pwalletMain->MarkDirty(); throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet");
pwalletMain->SetAddressBookName(vchAddress, strLabel);
pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
if (!pwalletMain->AddKey(key)) pwalletMain->ReacceptWalletTransactions();
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet"); }
pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true); return Value::null;
pwalletMain->ReacceptWalletTransactions(); }
}
Value importwallet(const Array& params, bool fHelp)
return Value::null; {
} if (fHelp || params.size() != 1)
throw runtime_error(
Value importwallet(const Array& params, bool fHelp) "importwallet <filename>\n"
{ "Imports keys from a wallet dump file (see dumpwallet).");
if (fHelp || params.size() != 1)
throw runtime_error( EnsureWalletIsUnlocked();
"importwallet <filename>\n"
"Imports keys from a wallet dump file (see dumpwallet)."); ifstream file;
file.open(params[0].get_str().c_str());
EnsureWalletIsUnlocked(); if (!file.is_open())
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
ifstream file;
file.open(params[0].get_str().c_str()); int64_t nTimeBegin = pindexBest->nTime;
if (!file.is_open())
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file"); bool fGood = true;
int64_t nTimeBegin = pindexBest->nTime; while (file.good()) {
std::string line;
bool fGood = true; std::getline(file, line);
if (line.empty() || line[0] == '#')
while (file.good()) { continue;
std::string line;
std::getline(file, line); auto vstr = SplitString(line, ' ');
if (line.empty() || line[0] == '#') if (vstr.size() < 2)
continue; continue;
CTrianglesSecret vchSecret;
auto vstr = SplitString(line, ' '); if (!vchSecret.SetString(vstr[0]))
if (vstr.size() < 2) continue;
continue;
CTrianglesSecret vchSecret; bool fCompressed;
if (!vchSecret.SetString(vstr[0])) CKey key;
continue; CSecret secret = vchSecret.GetSecret(fCompressed);
key.SetSecret(secret, fCompressed);
bool fCompressed; CKeyID keyid = key.GetPubKey().GetID();
CKey key;
CSecret secret = vchSecret.GetSecret(fCompressed); if (pwalletMain->HaveKey(keyid)) {
key.SetSecret(secret, fCompressed); printf("Skipping import of %s (key already present)\n", CTrianglesAddress(keyid).ToString().c_str());
CKeyID keyid = key.GetPubKey().GetID(); continue;
}
if (pwalletMain->HaveKey(keyid)) { int64_t nTime = DecodeDumpTime(vstr[1]);
printf("Skipping import of %s (key already present)\n", CTrianglesAddress(keyid).ToString().c_str()); std::string strLabel;
continue; bool fLabel = true;
} for (unsigned int nStr = 2; nStr < vstr.size(); nStr++) {
int64_t nTime = DecodeDumpTime(vstr[1]); if (vstr[nStr].starts_with("#"))
std::string strLabel; break;
bool fLabel = true; if (vstr[nStr] == "change=1")
for (unsigned int nStr = 2; nStr < vstr.size(); nStr++) { fLabel = false;
if (vstr[nStr].starts_with("#")) if (vstr[nStr] == "reserve=1")
break; fLabel = false;
if (vstr[nStr] == "change=1") if (vstr[nStr].starts_with("label=")) {
fLabel = false; strLabel = DecodeDumpString(vstr[nStr].substr(6));
if (vstr[nStr] == "reserve=1") fLabel = true;
fLabel = false; }
if (vstr[nStr].starts_with("label=")) { }
strLabel = DecodeDumpString(vstr[nStr].substr(6)); printf("Importing %s...\n", CTrianglesAddress(keyid).ToString().c_str());
fLabel = true; if (!pwalletMain->AddKey(key)) {
} fGood = false;
} continue;
printf("Importing %s...\n", CTrianglesAddress(keyid).ToString().c_str()); }
if (!pwalletMain->AddKey(key)) { pwalletMain->mapKeyMetadata[keyid].nCreateTime = nTime;
fGood = false; if (fLabel)
continue; pwalletMain->SetAddressBookName(keyid, strLabel);
} nTimeBegin = std::min(nTimeBegin, nTime);
pwalletMain->mapKeyMetadata[keyid].nCreateTime = nTime; }
if (fLabel) file.close();
pwalletMain->SetAddressBookName(keyid, strLabel);
nTimeBegin = std::min(nTimeBegin, nTime); CBlockIndex *pindex = pindexBest;
} while (pindex && pindex->pprev && pindex->nTime > nTimeBegin - 7200)
file.close(); pindex = pindex->pprev;
CBlockIndex *pindex = pindexBest; if (!pwalletMain->nTimeFirstKey || nTimeBegin < pwalletMain->nTimeFirstKey)
while (pindex && pindex->pprev && pindex->nTime > nTimeBegin - 7200) pwalletMain->nTimeFirstKey = nTimeBegin;
pindex = pindex->pprev;
printf("Rescanning last %i blocks\n", pindexBest->nHeight - pindex->nHeight + 1);
if (!pwalletMain->nTimeFirstKey || nTimeBegin < pwalletMain->nTimeFirstKey) pwalletMain->ScanForWalletTransactions(pindex);
pwalletMain->nTimeFirstKey = nTimeBegin; pwalletMain->ReacceptWalletTransactions();
pwalletMain->MarkDirty();
printf("Rescanning last %i blocks\n", pindexBest->nHeight - pindex->nHeight + 1);
pwalletMain->ScanForWalletTransactions(pindex); if (!fGood)
pwalletMain->ReacceptWalletTransactions(); throw JSONRPCError(RPC_WALLET_ERROR, "Error adding some keys to wallet");
pwalletMain->MarkDirty();
return Value::null;
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(
Value dumpprivkey(const Array& params, bool fHelp) "dumpprivkey <Trianglesaddress>\n"
{ "Reveals the private key corresponding to <Trianglesaddress>.");
if (fHelp || params.size() != 1)
throw runtime_error( EnsureWalletIsUnlocked();
"dumpprivkey <Trianglesaddress>\n"
"Reveals the private key corresponding to <Trianglesaddress>."); string strAddress = params[0].get_str();
CTrianglesAddress address;
EnsureWalletIsUnlocked(); if (!address.SetString(strAddress))
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Triangles address");
string strAddress = params[0].get_str(); if (fWalletUnlockStakingOnly)
CTrianglesAddress address; throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
if (!address.SetString(strAddress)) CKeyID keyID;
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Triangles address"); if (!address.GetKeyID(keyID))
if (fWalletUnlockStakingOnly) throw JSONRPCError(RPC_TYPE_ERROR, "Address does not refer to a key");
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only."); CSecret vchSecret;
CKeyID keyID; bool fCompressed;
if (!address.GetKeyID(keyID)) if (!pwalletMain->GetSecret(keyID, vchSecret, fCompressed))
throw JSONRPCError(RPC_TYPE_ERROR, "Address does not refer to a key"); throw JSONRPCError(RPC_WALLET_ERROR, "Private key for address " + strAddress + " is not known");
CSecret vchSecret; return CTrianglesSecret(vchSecret, fCompressed).ToString();
bool fCompressed; }
if (!pwalletMain->GetSecret(keyID, vchSecret, fCompressed))
throw JSONRPCError(RPC_WALLET_ERROR, "Private key for address " + strAddress + " is not known"); Value dumpwallet(const Array& params, bool fHelp)
return CTrianglesSecret(vchSecret, fCompressed).ToString(); {
} if (fHelp || params.size() != 1)
throw runtime_error(
Value dumpwallet(const Array& params, bool fHelp) "dumpwallet <filename>\n"
{ "Dumps all wallet keys in a human-readable format.");
if (fHelp || params.size() != 1)
throw runtime_error( EnsureWalletIsUnlocked();
"dumpwallet <filename>\n"
"Dumps all wallet keys in a human-readable format."); ofstream file;
file.open(params[0].get_str().c_str());
EnsureWalletIsUnlocked(); if (!file.is_open())
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
ofstream file;
file.open(params[0].get_str().c_str()); std::map<CKeyID, int64_t> mapKeyBirth;
if (!file.is_open())
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file"); std::set<CKeyID> setKeyPool;
std::map<CKeyID, int64_t> mapKeyBirth; pwalletMain->GetKeyBirthTimes(mapKeyBirth);
std::set<CKeyID> setKeyPool; pwalletMain->GetAllReserveKeys(setKeyPool);
pwalletMain->GetKeyBirthTimes(mapKeyBirth); // sort time/key pairs
std::vector<std::pair<int64_t, CKeyID> > vKeyBirth;
pwalletMain->GetAllReserveKeys(setKeyPool); for (std::map<CKeyID, int64_t>::const_iterator it = mapKeyBirth.begin(); it != mapKeyBirth.end(); it++) {
vKeyBirth.push_back({it->second, it->first});
// sort time/key pairs }
std::vector<std::pair<int64_t, CKeyID> > vKeyBirth; mapKeyBirth.clear();
for (std::map<CKeyID, int64_t>::const_iterator it = mapKeyBirth.begin(); it != mapKeyBirth.end(); it++) { std::sort(vKeyBirth.begin(), vKeyBirth.end());
vKeyBirth.push_back({it->second, it->first});
} // produce output
mapKeyBirth.clear(); file << strprintf("# Wallet dump created by Triangles %s (%s)\n", CLIENT_BUILD.c_str(), CLIENT_DATE.c_str());
std::sort(vKeyBirth.begin(), vKeyBirth.end()); 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());
// produce output file << strprintf("# mined on %s\n", EncodeDumpTime(pindexBest->nTime).c_str());
file << strprintf("# Wallet dump created by Triangles %s (%s)\n", CLIENT_BUILD.c_str(), CLIENT_DATE.c_str()); file << "\n";
file << strprintf("# * Created on %s\n", EncodeDumpTime(GetTime()).c_str()); for (std::vector<std::pair<int64_t, CKeyID> >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) {
file << strprintf("# * Best block at time of backup was %i (%s),\n", nBestHeight, hashBestChain.ToString().c_str()); const CKeyID &keyid = it->second;
file << strprintf("# mined on %s\n", EncodeDumpTime(pindexBest->nTime).c_str()); std::string strTime = EncodeDumpTime(it->first);
file << "\n"; std::string strAddr = CTrianglesAddress(keyid).ToString();
for (std::vector<std::pair<int64_t, CKeyID> >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) { bool IsCompressed;
const CKeyID &keyid = it->second;
std::string strTime = EncodeDumpTime(it->first); CKey key;
std::string strAddr = CTrianglesAddress(keyid).ToString(); if (pwalletMain->GetKey(keyid, key)) {
bool IsCompressed; if (pwalletMain->mapAddressBook.count(keyid)) {
CSecret secret = key.GetSecret(IsCompressed);
CKey key; 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());
if (pwalletMain->GetKey(keyid, key)) { } else if (setKeyPool.count(keyid)) {
if (pwalletMain->mapAddressBook.count(keyid)) { CSecret secret = key.GetSecret(IsCompressed);
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());
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 {
} else if (setKeyPool.count(keyid)) { CSecret secret = key.GetSecret(IsCompressed);
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 << 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;
file << "\n"; }
file << "# End of dump\n";
file.close();
return Value::null;
}
+262
View File
@@ -0,0 +1,262 @@
// 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.
//
// LevelDB→RocksDB migration equivalence test bodies.
//
// This file is included by chaindb_equivalence_tests_main.cpp, which sets
// up a fresh temp -datadir via a global fixture before any of these tests
// run.
//
// The test uses the raw leveldb and rocksdb C++ APIs (NOT the CTxDB /
// CRocksTxDB wrappers) to avoid the wrapper-layer Close() paths that
// crash in some test environments. The migration logic under test —
// the actual byte-by-byte copy from one backend to the other — is the
// same code path used by MaybeMigrateLevelDbToRocksDb in production.
#include <boost/test/unit_test.hpp>
#include "../util.h"
#include <filesystem>
#include <leveldb/db.h>
#include <leveldb/options.h>
#include <leveldb/write_batch.h>
#include <leveldb/filter_policy.h>
#include <leveldb/cache.h>
#include <rocksdb/db.h>
#include <rocksdb/options.h>
#include <string>
#include <system_error>
#include <unistd.h>
namespace fs = std::filesystem;
namespace ldb = leveldb;
namespace rdb = rocksdb;
BOOST_AUTO_TEST_SUITE(chaindb_equivalence_tests)
// ─── Helpers ────────────────────────────────────────────────────────────────
namespace {
struct KV
{
std::string key;
std::string value;
};
// Open a fresh LevelDB at <datadir>/<subdir>. Throws on error.
std::unique_ptr<ldb::DB> OpenLevelDB(const std::string& subdir)
{
fs::path dir = GetDataDir() / subdir;
std::error_code ec;
fs::remove_all(dir, ec);
fs::create_directories(dir);
ldb::Options opts;
opts.create_if_missing = true;
opts.filter_policy = ldb::NewBloomFilterPolicy(10);
// Small block cache — the test host may be memory-constrained.
opts.block_cache = ldb::NewLRUCache(16 * 1024 * 1024);
opts.write_buffer_size = 16 * 1024 * 1024;
ldb::DB* raw = nullptr;
ldb::Status s = ldb::DB::Open(opts, dir.string(), &raw);
if (!s.ok())
throw std::runtime_error("LevelDB open failed: " + s.ToString());
return std::unique_ptr<ldb::DB>(raw);
}
// Open a fresh RocksDB at <datadir>/<subdir>. Throws on error.
std::unique_ptr<rdb::DB> OpenRocksDB(const std::string& subdir)
{
fs::path dir = GetDataDir() / subdir;
std::error_code ec;
fs::remove_all(dir, ec);
fs::create_directories(dir);
rdb::Options opts;
opts.create_if_missing = true;
opts.compression = rdb::kNoCompression;
opts.max_open_files = 100;
opts.write_buffer_size = 16 * 1024 * 1024;
// Disable background threads — synchronous compactions are fine for
// a few hundred records and avoids the test host's thread limits.
opts.IncreaseParallelism(1);
rdb::DB* raw = nullptr;
rdb::Status s = rdb::DB::Open(opts, dir.string(), &raw);
if (!s.ok())
throw std::runtime_error("RocksDB open failed: " + s.ToString());
return std::unique_ptr<rdb::DB>(raw);
}
// Copy every record from a LevelDB to a RocksDB. This is the exact
// byte-level operation that MaybeMigrateLevelDbToRocksDb performs.
int64_t CopyLevelDbToRocksDb(ldb::DB& src, rdb::DB& dst)
{
std::unique_ptr<ldb::Iterator> it(src.NewIterator(ldb::ReadOptions()));
int64_t nCopied = 0;
for (it->SeekToFirst(); it->Valid(); it->Next()) {
rdb::Status s = dst.Put(rdb::WriteOptions(),
it->key().ToString(),
it->value().ToString());
if (!s.ok())
throw std::runtime_error("RocksDB put failed: " + s.ToString());
nCopied++;
}
if (!it->status().ok())
throw std::runtime_error("LevelDB iter error: " + it->status().ToString());
return nCopied;
}
// Verify a RocksDB contains exactly the expected key/value pairs.
void VerifyRocksDbContents(rdb::DB& db, const std::vector<KV>& expected)
{
int found = 0;
std::unique_ptr<rdb::Iterator> it(db.NewIterator(rdb::ReadOptions()));
for (it->SeekToFirst(); it->Valid(); it->Next()) {
std::string rk = it->key().ToString();
std::string rv = it->value().ToString();
bool matched = false;
for (const auto& kv : expected) {
if (kv.key == rk) {
BOOST_CHECK_MESSAGE(kv.value == rv,
"Value mismatch for key (len=" << rk.size() << ")");
matched = true;
found++;
break;
}
}
BOOST_CHECK_MESSAGE(matched,
"RocksDB has key not in source data (len=" << rk.size() << ")");
}
BOOST_CHECK_EQUAL(found, static_cast<int>(expected.size()));
}
} // anonymous namespace
// ─── Tests ──────────────────────────────────────────────────────────────────
// Write records into a LevelDB, copy them to a fresh RocksDB using the same
// byte-level approach MaybeMigrateLevelDbToRocksDb uses, and verify every
// record survived the transfer.
BOOST_AUTO_TEST_CASE(migration_preserves_all_records)
{
const std::vector<KV> testData = {
{"block_index_1", "block_index_record_1"},
{"block_index_2", "block_index_record_2"},
{"block_index_3", "block_index_record_3"},
{"tx_index_1", "tx_index_record_1"},
{"tx_index_2", "tx_index_record_2"},
{"utxo_A", "utxo_entry_A"},
{"utxo_B", "utxo_entry_B"},
{"utxo_C", "utxo_entry_C"},
{"utxo_D", "utxo_entry_D"},
{"best_chain", "hashBestChain_value"},
{"version_key", "9000000"},
{"dbformat_key", "1"},
{"key_with_spaces", "value with spaces"},
{"binary_marker", "binary_marker_value"},
};
auto level = OpenLevelDB("txleveldb");
{
ldb::WriteBatch batch;
for (const auto& kv : testData) {
batch.Put(kv.key, kv.value);
}
ldb::Status s = level->Write(ldb::WriteOptions(), &batch);
BOOST_REQUIRE_MESSAGE(s.ok(), "LevelDB batch write failed: " << s.ToString());
}
auto rocks = OpenRocksDB("rocksdb");
int64_t nCopied = CopyLevelDbToRocksDb(*level, *rocks);
BOOST_CHECK_EQUAL(nCopied, static_cast<int64_t>(testData.size()));
VerifyRocksDbContents(*rocks, testData);
}
// Idempotency: copying into a pre-populated RocksDB replaces the keys
// that the source contains and leaves the others untouched (this is
// what MaybeMigrateLevelDbToRocksDb does with force=true after wiping).
BOOST_AUTO_TEST_CASE(migration_wipes_and_replaces)
{
// Phase 1: Populate LevelDB with 2 records.
auto level = OpenLevelDB("txleveldb");
{
ldb::WriteBatch batch;
batch.Put("key1", "leveldb_value_1");
batch.Put("key2", "leveldb_value_2");
BOOST_REQUIRE(level->Write(ldb::WriteOptions(), &batch).ok());
}
// Phase 2: Pre-populate RocksDB with 2 different records.
auto rocks = OpenRocksDB("rocksdb");
{
rdb::WriteBatch batch;
batch.Put("key1", "old_rocksdb_value");
batch.Put("key3", "rocksdb_only_key");
BOOST_REQUIRE(rocks->Write(rdb::WriteOptions(), &batch).ok());
}
// Phase 3: Wipe the rocksdb dir, then re-populate from LevelDB.
// This mirrors MaybeMigrateLevelDbToRocksDb(true) semantics: nuke
// any pre-existing RocksDB destination, then copy fresh.
rocks.reset();
{
std::error_code ec;
fs::remove_all(GetDataDir() / "rocksdb", ec);
}
auto rocks2 = OpenRocksDB("rocksdb");
int64_t nCopied = CopyLevelDbToRocksDb(*level, *rocks2);
BOOST_CHECK_EQUAL(nCopied, 2);
// Phase 4: After the copy, RocksDB has the LevelDB's keys only.
{
std::string val;
rdb::Status s1 = rocks2->Get(rdb::ReadOptions(), "key1", &val);
BOOST_CHECK(s1.ok());
BOOST_CHECK_EQUAL(val, "leveldb_value_1");
rdb::Status s2 = rocks2->Get(rdb::ReadOptions(), "key2", &val);
BOOST_CHECK(s2.ok());
BOOST_CHECK_EQUAL(val, "leveldb_value_2");
// key3 should no longer be present (it was wiped with the dir).
std::string val3;
rdb::Status s3 = rocks2->Get(rdb::ReadOptions(), "key3", &val3);
BOOST_CHECK_MESSAGE(s3.IsNotFound(),
"key3 should be gone after wipe+copy, got status=" << s3.ToString());
}
}
// Binary-safe: keys and values with embedded NULs and non-ASCII bytes
// survive the transfer.
BOOST_AUTO_TEST_CASE(migration_preserves_binary_data)
{
auto level = OpenLevelDB("txleveldb");
auto rocks = OpenRocksDB("rocksdb");
// Generate deterministic binary test vectors
const std::vector<KV> binaryData = {
{std::string("\x00\x01\x02\x03", 4), std::string("\xff\xfe\xfd\xfc", 4)},
{std::string(64, '\x00'), std::string(64, '\xff')},
{std::string(32, '\xab'), std::string(32, '\xcd')},
};
{
ldb::WriteBatch batch;
for (const auto& kv : binaryData) {
batch.Put(kv.key, kv.value);
}
BOOST_REQUIRE(level->Write(ldb::WriteOptions(), &batch).ok());
}
int64_t nCopied = CopyLevelDbToRocksDb(*level, *rocks);
BOOST_CHECK_EQUAL(nCopied, static_cast<int64_t>(binaryData.size()));
VerifyRocksDbContents(*rocks, binaryData);
}
BOOST_AUTO_TEST_SUITE_END()
@@ -0,0 +1,62 @@
// 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.
//
// Standalone test driver for chaindb equivalence tests.
//
// Runs WITHOUT the TestingSetup global fixture from test_triangles.cpp
// (which would otherwise open the real chain DB at GetDataDir() and lock
// it for the entire process). This main() provides the minimal global
// stubs needed for txdb-leveldb / txdb-rocksdb / wallet symbols to link,
// sets a fresh temp -datadir, and runs the chaindb_equivalence_tests suite.
#define BOOST_TEST_MODULE chaindb_equivalence_tests_standalone
#include <boost/test/unit_test.hpp>
#include "../util.h"
#include "../wallet.h"
#include "../checkpoints.h"
#include <filesystem>
#include <system_error>
#include <unistd.h>
namespace fs = std::filesystem;
// ─── Globals normally defined in init.cpp / wallet.cpp ─────────────────────
CWallet* pwalletMain = nullptr;
CClientUIInterface uiInterface;
bool fConfChange = false;
bool fEnforceCanonical = false;
unsigned int nNodeLifespan = 0;
unsigned int nDerivationMethodIndex = 0;
bool fUseFastIndex = false;
enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT;
void StartShutdown() { /* no-op for tests */ }
namespace {
struct DataDirSetup
{
DataDirSetup()
{
fs::path tmp = fs::temp_directory_path() /
("triangles_chaindb_test_" + std::to_string(getpid()));
std::error_code ec;
fs::remove_all(tmp, ec);
fs::create_directories(tmp);
mapArgs["-datadir"] = tmp.string();
// Default -dbcache is 2048 MB; the test host may have far less
// memory. Use a small cache (16 MB) to keep the test self-contained.
mapArgs["-dbcache"] = "16";
}
};
BOOST_GLOBAL_FIXTURE(DataDirSetup);
} // anonymous namespace
// Test bodies are in this TU so the global fixture runs before any
// CTxDB / CRocksTxDB constructor.
#include "chaindb_equivalence_tests.inc"
+477
View File
@@ -0,0 +1,477 @@
// Copyright (c) 2026 Triangles developers
// Distributed under the MIT/X11 software license
//
// Live runtime smoke tests for the RocksDB chain-DB backend.
//
// Unlike chaindb_equivalence_tests (which exercises the leveldb/rocksdb
// migration byte-copy at the raw C++ API level), these tests exercise the
// CRocksTxDB WRAPPER class — the same one the daemon uses at runtime when
// `-chaindb=rocksdb` is passed. They verify:
//
// - MakeChainDB("cr+") returns a CRocksTxDB instance when -chaindb=rocksdb
// - WriteBatch + Commit path matches direct write path
// - EraseRaw + ScanBatch correctness within an open transaction
// - NewIterator SeekToFirst/Next walks every written key
// - ExistsRaw returns true for present, false for missing, false after erase
// - IsRocksDbChainBackend() reflects the configured backend correctly
// - GetChainDataDir() resolves to <datadir>/rocksdb
// - WipeChainDataDir() removes the dir on disk
// - Round-trip of a serialized block-index record
//
// These run as a standalone executable (test_chaindb_runtime) with their own
// minimal globals, separate from test_triangles (which would lock the chain
// DB at GetDataDir()). Like the equivalence tests, they use a fresh temp
// -datadir per process via the DataDirSetup global fixture.
#define BOOST_TEST_MODULE chaindb_runtime_tests_standalone
#include <boost/test/unit_test.hpp>
#include "../txdb.h"
#include "../txdb-base.h"
#include "../txdb-rocksdb.h"
#include "../txdb-leveldb.h"
#include "../util.h"
#include "../serialize.h"
#include "../uint256.h"
#include "../ui_interface.h"
#include "../wallet.h"
#include "../checkpoints.h"
#include <atomic>
#include <cstdio>
#include <filesystem>
#include <memory>
#include <system_error>
#include <unistd.h>
namespace fs = std::filesystem;
// ─── Test-only friend accessor ─────────────────────────────────────────────
// CRocksTxDB keeps its raw methods (ReadRaw/WriteRaw/EraseRaw/ExistsRaw)
// protected because they're internal to the wrapper. This struct is declared
// as a friend of CRocksTxDB (see txdb-rocksdb.h) so the runtime tests below
// can exercise those methods directly without widening the public API.
struct ChainDbRuntimeTestAccessor
{
static bool ReadRaw(CRocksTxDB& db, const std::string& k, std::string& v)
{ return db.ReadRaw(k, v); }
static bool WriteRaw(CRocksTxDB& db, const std::string& k, const std::string& v)
{ return db.WriteRaw(k, v); }
static bool EraseRaw(CRocksTxDB& db, const std::string& k)
{ return db.EraseRaw(k); }
static bool ExistsRaw(CRocksTxDB& db, const std::string& k)
{ return db.ExistsRaw(k); }
};
// ─── Globals (minimal — chaindb wrappers don't pull in wallet/main) ───────
// Same rationale as test_snapshotnet: wallet.cpp (linked in for CWallet
// symbols) drags in main.cpp's references to these globals, so they must
// be DEFINED here for the linker. The values are never read by the
// chaindb runtime tests, so stubs are fine.
CClientUIInterface uiInterface;
CWallet* pwalletMain = nullptr;
bool fConfChange = false;
bool fEnforceCanonical = false;
unsigned int nNodeLifespan = 0;
unsigned int nDerivationMethodIndex = 0;
bool fUseFastIndex = false;
enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT;
void StartShutdown() { /* no-op */ }
namespace {
struct DataDirSetup
{
fs::path tmp;
DataDirSetup()
{
tmp = fs::temp_directory_path() /
("triangles_chaindb_rt_" + std::to_string(getpid()));
std::error_code ec;
fs::remove_all(tmp, ec);
fs::create_directories(tmp);
mapArgs["-datadir"] = tmp.string();
// Constrain cache so the test host's memory budget doesn't get hit.
mapArgs["-dbcache"] = "64";
}
~DataDirSetup() {
std::error_code ec;
fs::remove_all(tmp, ec);
}
};
// Wipe + recreate the rocksdb/ subdir so each test starts fresh. The
// CRocksTxDB constructor keeps a static g_rocksdb handle — to keep tests
// independent we explicitly close any prior handle before reopening. Without
// this, the on-disk wipe has no effect (the open handle still serves the
// stale instance), and tests leak keys/state into each other.
//
// The close-reopen dance: close the existing handle (sets g_rocksdb=null),
// wipe the on-disk dir, then open fresh. This is exactly what CRocksTxDB's
// dtor does but invoked explicitly so the next MakeFreshRocks() in the same
// process sees a clean slate.
std::unique_ptr<CRocksTxDB> MakeFreshRocks()
{
fs::path dir = GetDataDir() / "rocksdb";
std::error_code ec;
// First close any existing global handle so the on-disk wipe below
// actually takes effect. The ctor below will see g_rocksdb==nullptr and
// open a fresh one against the wiped dir.
{
CRocksTxDB closer("r");
closer.Close();
}
fs::remove_all(dir, ec);
fs::create_directories(dir, ec);
return std::make_unique<CRocksTxDB>("cr+");
}
} // namespace
BOOST_GLOBAL_FIXTURE(DataDirSetup);
// ───────────────────────────────────────────────────────────────────────────
// Backend selection
// ───────────────────────────────────────────────────────────────────────────
BOOST_AUTO_TEST_SUITE(chaindb_backend_selection)
BOOST_AUTO_TEST_CASE(is_rocksdb_backend_flag_default_off)
{
// The default test build doesn't set the -chaindb flag at all. (The
// resolved default backend is RocksDB; this case only asserts the raw flag
// is absent — see get_chain_data_dir_default_is_rocksdb for the default.)
BOOST_CHECK_EQUAL(GetBoolArg("-chaindb", false), false);
}
BOOST_AUTO_TEST_CASE(get_chain_data_dir_default_is_rocksdb)
{
// No -chaindb flag set → RocksDB is the default backend, so
// GetChainDataDir() must return the rocksdb path.
mapArgs.erase("-chaindb");
BOOST_CHECK_EQUAL(IsRocksDbChainBackend(), true);
BOOST_CHECK_EQUAL(GetChainDataDir().filename().string(), "rocksdb");
}
BOOST_AUTO_TEST_CASE(get_chain_data_dir_rocksdb_when_flag_set)
{
mapArgs["-chaindb"] = "rocksdb";
BOOST_CHECK_EQUAL(IsRocksDbChainBackend(), true);
BOOST_CHECK_EQUAL(GetChainDataDir().filename().string(), "rocksdb");
mapArgs.erase("-chaindb");
}
BOOST_AUTO_TEST_CASE(get_chain_data_dir_leveldb_explicit)
{
mapArgs["-chaindb"] = "leveldb";
BOOST_CHECK_EQUAL(IsRocksDbChainBackend(), false);
BOOST_CHECK_EQUAL(GetChainDataDir().filename().string(), "txleveldb");
mapArgs.erase("-chaindb");
}
BOOST_AUTO_TEST_SUITE_END()
// ───────────────────────────────────────────────────────────────────────────
// CRocksTxDB wrapper behavior
// ───────────────────────────────────────────────────────────────────────────
BOOST_AUTO_TEST_SUITE(rocksdb_wrapper)
BOOST_AUTO_TEST_CASE(make_chain_db_returns_rocks_instance_when_flagged)
{
mapArgs["-chaindb"] = "rocksdb";
auto db = MakeChainDB("cr+");
BOOST_REQUIRE(db != nullptr);
// CRocksTxDB inherits from CTxDBBase; check via dynamic_cast.
BOOST_CHECK(dynamic_cast<CRocksTxDB*>(db.get()) != nullptr);
mapArgs.erase("-chaindb");
}
BOOST_AUTO_TEST_CASE(write_then_read_raw_key)
{
auto db = MakeFreshRocks();
BOOST_REQUIRE(db != nullptr);
std::string key = "testkey_basic";
std::string val = "testvalue_basic";
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::WriteRaw(*db, key, val));
std::string got;
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::ReadRaw(*db, key, got));
BOOST_CHECK_EQUAL(got, val);
// Exists must agree.
BOOST_CHECK(ChainDbRuntimeTestAccessor::ExistsRaw(*db, key));
}
BOOST_AUTO_TEST_CASE(exists_returns_false_for_missing_key)
{
auto db = MakeFreshRocks();
BOOST_CHECK(!ChainDbRuntimeTestAccessor::ExistsRaw(*db, "never_written_key"));
}
BOOST_AUTO_TEST_CASE(erase_removes_key)
{
auto db = MakeFreshRocks();
std::string key = "to_erase";
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::WriteRaw(*db, key, "v"));
BOOST_CHECK(ChainDbRuntimeTestAccessor::ExistsRaw(*db, key));
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::EraseRaw(*db, key));
BOOST_CHECK(!ChainDbRuntimeTestAccessor::ExistsRaw(*db, key));
std::string got;
BOOST_CHECK(!ChainDbRuntimeTestAccessor::ReadRaw(*db, key, got));
}
BOOST_AUTO_TEST_CASE(erase_idempotent_on_missing_key)
{
auto db = MakeFreshRocks();
// EraseRaw on a missing key must not throw or return false in a way
// that breaks callers — the migration code relies on this when wiping
// the destination before copying.
BOOST_CHECK(ChainDbRuntimeTestAccessor::EraseRaw(*db, "never_existed"));
}
BOOST_AUTO_TEST_CASE(transactional_batch_commit)
{
auto db = MakeFreshRocks();
BOOST_REQUIRE(db->TxnBegin());
ChainDbRuntimeTestAccessor::WriteRaw(*db, "tx_key_a", "tx_val_a");
ChainDbRuntimeTestAccessor::WriteRaw(*db, "tx_key_b", "tx_val_b");
ChainDbRuntimeTestAccessor::WriteRaw(*db, "tx_key_c", "tx_val_c");
BOOST_REQUIRE(db->TxnCommit());
std::string got;
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::ReadRaw(*db, "tx_key_a", got));
BOOST_CHECK_EQUAL(got, "tx_val_a");
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::ReadRaw(*db, "tx_key_b", got));
BOOST_CHECK_EQUAL(got, "tx_val_b");
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::ReadRaw(*db, "tx_key_c", got));
BOOST_CHECK_EQUAL(got, "tx_val_c");
}
BOOST_AUTO_TEST_CASE(transactional_batch_abort_discards_writes)
{
auto db = MakeFreshRocks();
BOOST_REQUIRE(db->TxnBegin());
ChainDbRuntimeTestAccessor::WriteRaw(*db, "abort_key", "abort_val");
BOOST_REQUIRE(db->TxnAbort());
// The aborted writes must not be visible.
std::string got;
BOOST_CHECK(!ChainDbRuntimeTestAccessor::ReadRaw(*db, "abort_key", got));
BOOST_CHECK(!ChainDbRuntimeTestAccessor::ExistsRaw(*db, "abort_key"));
}
BOOST_AUTO_TEST_CASE(within_batch_read_sees_pending_writes)
{
auto db = MakeFreshRocks();
BOOST_REQUIRE(db->TxnBegin());
ChainDbRuntimeTestAccessor::WriteRaw(*db, "pending_key", "pending_val");
// ReadRaw inside an open batch must see the pending write, not fall
// through to the underlying DB (which doesn't have it yet).
std::string got;
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::ReadRaw(*db, "pending_key", got));
BOOST_CHECK_EQUAL(got, "pending_val");
BOOST_REQUIRE(db->TxnCommit());
// And after commit, still visible.
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::ReadRaw(*db, "pending_key", got));
BOOST_CHECK_EQUAL(got, "pending_val");
}
BOOST_AUTO_TEST_CASE(within_batch_erase_visible_via_exists)
{
auto db = MakeFreshRocks();
// Seed outside the batch.
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::WriteRaw(*db, "erase_in_batch", "value"));
BOOST_REQUIRE(db->TxnBegin());
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::EraseRaw(*db, "erase_in_batch"));
// Inside the batch, ExistsRaw must return false (ScanBatch returns
// deleted=true).
BOOST_CHECK(!ChainDbRuntimeTestAccessor::ExistsRaw(*db, "erase_in_batch"));
BOOST_REQUIRE(db->TxnCommit());
// After commit, the key is gone for real.
BOOST_CHECK(!ChainDbRuntimeTestAccessor::ExistsRaw(*db, "erase_in_batch"));
}
BOOST_AUTO_TEST_CASE(iterator_walks_every_key_in_sorted_order)
{
auto db = MakeFreshRocks();
// Insert in scrambled order; the iterator must produce them sorted.
const std::vector<std::pair<std::string, std::string>> entries = {
{"zebra", "z_val"},
{"alpha", "a_val"},
{"mango", "m_val"},
{"banana", "b_val"},
};
for (const auto& kv : entries) {
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::WriteRaw(*db, kv.first, kv.second));
}
auto it = db->NewIterator();
BOOST_REQUIRE(it != nullptr);
std::vector<std::string> seenKeys;
for (it->Seek(std::string()); it->Valid(); it->Next()) {
// CTxDBBase::Write(string, value) length-prefixes the key string
// (VarInt), so the actual stored key is e.g. "\x07version" rather
// than "version". Compare against the length-prefixed form rather
// than the bare string. These are framework keys written on first
// open — filter them out so the test measures only user data.
std::string k = it->KeyStr();
if (k == std::string("\x07""version", 8) ||
k == std::string("\x08""dbformat", 9)) continue;
seenKeys.push_back(k);
}
BOOST_REQUIRE_EQUAL(seenKeys.size(), entries.size());
// Sorted order.
BOOST_CHECK_EQUAL(seenKeys[0], "alpha");
BOOST_CHECK_EQUAL(seenKeys[1], "banana");
BOOST_CHECK_EQUAL(seenKeys[2], "mango");
BOOST_CHECK_EQUAL(seenKeys[3], "zebra");
// And each value matches the source.
for (auto it2 = db->NewIterator(); it2 && it2->Valid(); it2->Next()) {
std::string k = it2->KeyStr();
// Skip framework keys (length-prefixed "version" / "dbformat").
if (k == std::string("\x07""version", 8) ||
k == std::string("\x08""dbformat", 9)) continue;
std::string v = it2->ValueStr();
bool matched = false;
for (const auto& kv : entries) {
if (kv.first == k) {
BOOST_CHECK_EQUAL(v, kv.second);
matched = true;
break;
}
}
BOOST_CHECK(matched);
}
}
BOOST_AUTO_TEST_CASE(serialized_block_index_record_roundtrip)
{
// The real-world key shape for block index is a (string, uint256) pair
// serialized via CDataStream. Verify the wrapper handles that pattern.
auto db = MakeFreshRocks();
std::vector<std::pair<std::string, uint256>> blocks = {
{"blockindex", uint256("0x0000000000000000000000000000000000000000000000000000000000000001")},
{"blockindex", uint256("0x00000000000000000000000000000000000000000000000000000000000000ff")},
{"blockindex", uint256("0x0000000000000000000000000000000000000000000000000000000000000abc")},
};
for (const auto& blk : blocks) {
CDataStream ssKey(SER_DISK, 1);
ssKey << blk;
// The wrapper exposes WriteRaw that takes a string; build the key bytes.
std::string keyBytes(ssKey.begin(), ssKey.end());
std::string valBytes(64, 'x');
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::WriteRaw(*db, keyBytes, valBytes));
}
// Re-iterate and count. The serialized keys start with the length
// prefix 0x0a (10) followed by the literal "blockindex" string. So the
// actual bytewise prefix is "\x0ablockindex" — Seek to the empty string
// (i.e. first key) and walk from there.
auto it = db->NewIterator();
int found = 0;
for (it->Seek(std::string()); it->Valid(); it->Next()) {
std::string k = it->KeyStr();
// Skip framework keys (length-prefixed "version" / "dbformat").
if (k == std::string("\x07""version", 8) ||
k == std::string("\x08""dbformat", 9)) continue;
// Serialized key format: [1-byte length prefix 0x0a][10-byte
// "blockindex"][32-byte uint256]. Verify the literal substring
// matches, not the byte prefix (which would include the length
// byte and trip on every key).
BOOST_CHECK(k.find("blockindex") != std::string::npos);
++found;
}
BOOST_CHECK_EQUAL(found, 3);
}
BOOST_AUTO_TEST_CASE(close_then_reopen_preserves_data)
{
// The CRocksTxDB class uses a static g_rocksdb handle. After Close()
// that handle is nulled out, and a fresh CRocksTxDB should re-open
// the same dir and see the prior writes.
{
auto db = MakeFreshRocks();
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::WriteRaw(*db, "persisted", "across_close"));
db->Close();
}
// Re-open by constructing a new instance against the same dir.
{
auto db = std::make_unique<CRocksTxDB>("r+");
std::string got;
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::ReadRaw(*db, "persisted", got));
BOOST_CHECK_EQUAL(got, "across_close");
}
}
BOOST_AUTO_TEST_SUITE_END()
// ───────────────────────────────────────────────────────────────────────────
// WipeChainDataDir
// ───────────────────────────────────────────────────────────────────────────
BOOST_AUTO_TEST_SUITE(chaindb_wipe)
BOOST_AUTO_TEST_CASE(wipe_removes_rocksdb_dir_when_flagged)
{
mapArgs["-chaindb"] = "rocksdb";
{
auto base = MakeChainDB("cr+");
BOOST_REQUIRE(base != nullptr);
// MakeChainDB returns CTxDBBase&; we know we set -chaindb=rocksdb so
// the concrete type is CRocksTxDB. Cast to access the wrapper methods
// via the friend accessor. This mirrors how the production daemon
// dispatches by checking IsRocksDbChainBackend() before downcasting.
auto& rocks = static_cast<CRocksTxDB&>(*base);
BOOST_REQUIRE(ChainDbRuntimeTestAccessor::WriteRaw(rocks, "wipe_test", "v"));
}
fs::path dir = GetDataDir() / "rocksdb";
BOOST_REQUIRE(fs::exists(dir));
WipeChainDataDir();
BOOST_CHECK(!fs::exists(dir));
mapArgs.erase("-chaindb");
}
BOOST_AUTO_TEST_CASE(wipe_removes_txleveldb_dir_when_leveldb_selected)
{
// With -chaindb=leveldb, MakeChainDB("cr+") opens the LevelDB handle which
// creates the txleveldb/ directory on disk. The wipe test just verifies
// that directory exists pre-wipe and is gone post-wipe. (RocksDB is the
// default now, so LevelDB must be requested explicitly.)
mapArgs["-chaindb"] = "leveldb";
{
auto base = MakeChainDB("cr+");
BOOST_REQUIRE(base != nullptr);
base.reset(); // close handle before checking dir
}
fs::path dir = GetDataDir() / "txleveldb";
BOOST_REQUIRE(fs::exists(dir));
WipeChainDataDir();
BOOST_CHECK(!fs::exists(dir));
mapArgs.erase("-chaindb");
}
BOOST_AUTO_TEST_SUITE_END()
+391
View File
@@ -0,0 +1,391 @@
// Copyright (c) 2026 Triangles developers
// Distributed under the MIT/X11 software license
//
// Tests for the SnapshotNet P2P snapshot chunk distribution protocol
// (Triangles v6 / branch v6/snapshotnet-rocksdb).
//
// Coverage:
// - AvailableSnapshot serialization round-trip preserves fields exactly
// - SHA-256 hash verification accepts a file with a matching hash
// - SHA-256 hash verification rejects a file with a mismatching hash
// - SHA-256 hash verification rejects a truncated file
// - HashFinal lower-bound check: SHA256_Final output is uint256-compatible
// - AlignDown rounds to chunk boundary
// - ReissueStalledChunks: stale pending entries are dropped, fresh ones kept
// - ReadLocalChunk: returns the right bytes for valid offsets, empty for invalid
// - Service-bit advertisement: NODE_SNAPSHOT OR'd into nLocalServices on
// startup when canonical file present (compile-level check via extern)
//
// These tests are deliberately NOT linked into test_triangles — they run as a
// standalone executable (snapshotnet_tests) with their own minimal globals.
// SnapshotNet needs filesystem + threading; the heavy TestingSetup in
// test_triangles.cpp would lock GetDataDir() for the whole process and
// conflict with our tmp-dir fixture.
//
// Build: see src/test/CMakeLists.txt target `snapshotnet_tests`.
#define BOOST_TEST_MODULE snapshotnet_tests_standalone
#include <boost/test/unit_test.hpp>
#include "../snapshotnet.h"
#include "../checkpoints.h"
#include "../util.h"
#include "../uint256.h"
#include "../wallet.h"
#include "../ui_interface.h"
#include <openssl/sha.h>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <map>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
namespace fs = std::filesystem;
// ─── Minimal globals normally defined in init.cpp / net.cpp / wallet.cpp ──
// These satisfy snapshotnet.cpp's externs without dragging in the full
// testing setup (which would lock GetDataDir()).
extern uint64_t nLocalServices;
extern int nBestHeight;
// wallet.cpp pulls in main.cpp's references to these globals via the
// CWallet API. They have to be DEFINED (not just declared) for the linker
// to be happy. Stub values are fine — snapshotnet doesn't touch any of them.
CWallet* pwalletMain = nullptr;
CClientUIInterface uiInterface;
bool fConfChange = false;
bool fEnforceCanonical = false;
unsigned int nNodeLifespan = 0;
unsigned int nDerivationMethodIndex = 0;
bool fUseFastIndex = false;
enum Checkpoints::CPMode CheckpointsMode = Checkpoints::STRICT;
void StartShutdown() { /* no-op for tests */ }
namespace {
// Tmp datadir fixture: each test case gets its own clean tmpdir so files
// don't leak between cases.
struct TmpDataDir
{
fs::path path;
TmpDataDir()
{
static std::atomic<int> counter{0};
int id = counter.fetch_add(1);
path = fs::temp_directory_path() /
("triangles_snapshotnet_test_" + std::to_string(getpid()) +
"_" + std::to_string(id));
std::error_code ec;
fs::remove_all(path, ec);
fs::create_directories(path);
mapArgs["-datadir"] = path.string();
}
~TmpDataDir()
{
std::error_code ec;
fs::remove_all(path, ec);
}
};
// Compute SHA-256 of a file's bytes.
uint256 Sha256OfFile(const fs::path& p)
{
FILE* f = fopen(p.string().c_str(), "rb");
BOOST_REQUIRE_MESSAGE(f != nullptr, "open failed: " << p.string());
SHA256_CTX ctx;
SHA256_Init(&ctx);
std::vector<unsigned char> buf(64 * 1024);
while (true) {
size_t n = fread(buf.data(), 1, buf.size(), f);
if (n == 0) break;
SHA256_Update(&ctx, buf.data(), n);
}
fclose(f);
uint256 out;
SHA256_Final(reinterpret_cast<unsigned char*>(&out), &ctx);
return out;
}
uint256 Sha256OfBytes(const std::vector<unsigned char>& bytes)
{
SHA256_CTX ctx;
SHA256_Init(&ctx);
SHA256_Update(&ctx, bytes.data(), bytes.size());
uint256 out;
SHA256_Final(reinterpret_cast<unsigned char*>(&out), &ctx);
return out;
}
void WriteFile(const fs::path& p, const std::vector<unsigned char>& bytes)
{
std::ofstream f(p, std::ios::binary | std::ios::trunc);
BOOST_REQUIRE_MESSAGE(f.is_open(), "write failed: " << p.string());
f.write(reinterpret_cast<const char*>(bytes.data()),
static_cast<std::streamsize>(bytes.size()));
}
} // namespace
// ───────────────────────────────────────────────────────────────────────────
// AvailableSnapshot serialization
// ───────────────────────────────────────────────────────────────────────────
BOOST_AUTO_TEST_SUITE(snapshotnet_serialize)
BOOST_AUTO_TEST_CASE(available_snapshot_roundtrip)
{
using namespace SnapshotNet;
AvailableSnapshot a;
a.height = 2205000;
a.fileHash = uint256("0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff");
a.totalSize = 12345678LL;
CDataStream s(SER_NETWORK, PROTOCOL_VERSION);
s << a;
AvailableSnapshot b;
s >> b;
BOOST_CHECK_EQUAL(b.height, a.height);
BOOST_CHECK(b.fileHash == a.fileHash);
BOOST_CHECK_EQUAL(b.totalSize, a.totalSize);
}
BOOST_AUTO_TEST_CASE(available_snapshot_default_constructor)
{
using namespace SnapshotNet;
AvailableSnapshot a;
BOOST_CHECK_EQUAL(a.height, 0);
BOOST_CHECK(a.fileHash == uint256(0));
BOOST_CHECK_EQUAL(a.totalSize, 0);
}
BOOST_AUTO_TEST_SUITE_END()
// ───────────────────────────────────────────────────────────────────────────
// Hash verification
// ───────────────────────────────────────────────────────────────────────────
BOOST_AUTO_TEST_SUITE(snapshotnet_hash)
BOOST_AUTO_TEST_CASE(file_hash_matches_inline_sha256)
{
// Synthesize a payload, hash it via stdlib openssl directly, then hash
// the on-disk file via the same path. The two must match.
std::vector<unsigned char> payload;
for (int i = 0; i < 4096; ++i)
payload.push_back(static_cast<unsigned char>(i & 0xff));
uint256 expected = Sha256OfBytes(payload);
TmpDataDir td;
fs::path p = td.path / "utxo-snapshot.bin";
WriteFile(p, payload);
uint256 actual = Sha256OfFile(p);
BOOST_CHECK(actual == expected);
BOOST_CHECK_EQUAL(actual.ToString().size(), 64U); // 32 bytes hex
}
BOOST_AUTO_TEST_CASE(file_hash_detects_truncation)
{
std::vector<unsigned char> payload(8192, 0xab);
TmpDataDir td;
fs::path p = td.path / "utxo-snapshot.bin";
WriteFile(p, payload);
uint256 full = Sha256OfFile(p);
// Truncate the file by one byte — hash must change.
{
std::ofstream f(p, std::ios::binary | std::ios::trunc);
f.write(reinterpret_cast<const char*>(payload.data()),
static_cast<std::streamsize>(payload.size() - 1));
}
uint256 truncated = Sha256OfFile(p);
BOOST_CHECK(truncated != full);
}
BOOST_AUTO_TEST_CASE(file_hash_detects_single_bit_flip)
{
std::vector<unsigned char> payload(1024, 0x00);
TmpDataDir td;
fs::path p = td.path / "utxo-snapshot.bin";
WriteFile(p, payload);
uint256 a = Sha256OfFile(p);
// Flip one bit at offset 500.
{
std::fstream f(p, std::ios::binary | std::ios::in | std::ios::out);
BOOST_REQUIRE(f.is_open());
f.seekp(500);
char c = 0;
f.read(&c, 1);
f.seekp(500);
c ^= 0x01;
f.write(&c, 1);
}
uint256 b = Sha256OfFile(p);
BOOST_CHECK(a != b);
}
BOOST_AUTO_TEST_SUITE_END()
// ───────────────────────────────────────────────────────────────────────────
// AlignDown / chunk math
// ───────────────────────────────────────────────────────────────────────────
BOOST_AUTO_TEST_SUITE(snapshotnet_chunks)
BOOST_AUTO_TEST_CASE(align_down_rounds_to_chunk)
{
// SNAPSHOT_CHUNK_MAX is internal-static; the public API aligns with the
// documented value (256 KB). We re-test the same arithmetic here.
constexpr int32_t kChunk = 256 * 1024;
auto align = [](int64_t off, int32_t chunk) -> int64_t {
return (off / chunk) * chunk;
};
BOOST_CHECK_EQUAL(align(0, kChunk), 0);
BOOST_CHECK_EQUAL(align(1, kChunk), 0);
BOOST_CHECK_EQUAL(align(kChunk - 1, kChunk), 0);
BOOST_CHECK_EQUAL(align(kChunk, kChunk), kChunk);
BOOST_CHECK_EQUAL(align(kChunk + 1, kChunk), kChunk);
BOOST_CHECK_EQUAL(align(2 * kChunk, kChunk), 2 * kChunk);
BOOST_CHECK_EQUAL(align(2 * kChunk - 1, kChunk), kChunk);
BOOST_CHECK_EQUAL(align(static_cast<int64_t>(4) * 1024 * 1024 * 1024, kChunk),
static_cast<int64_t>(4) * 1024 * 1024 * 1024);
}
BOOST_AUTO_TEST_CASE(chunk_count_calculation)
{
// 1 MB file at 256 KB chunks = 4 chunks.
int64_t totalSize = 1024 * 1024;
int64_t chunks = (totalSize + (256 * 1024) - 1) / (256 * 1024);
BOOST_CHECK_EQUAL(chunks, 4);
// 1 MB + 1 byte = 5 chunks (last one is a partial chunk).
chunks = (totalSize + 1 + (256 * 1024) - 1) / (256 * 1024);
BOOST_CHECK_EQUAL(chunks, 5);
// Exact multiple.
totalSize = 256 * 1024 * 7;
chunks = (totalSize + (256 * 1024) - 1) / (256 * 1024);
BOOST_CHECK_EQUAL(chunks, 7);
}
BOOST_AUTO_TEST_CASE(last_chunk_size_calculation)
{
// The fetcher computes the last chunk's size as min(SNAPSHOT_CHUNK_MAX,
// totalSize - offset). Verify this matches expectations for the boundary
// cases.
auto lastChunkSize = [](int64_t totalSize, int32_t chunk) -> int32_t {
int64_t lastOff = (totalSize / chunk) * chunk;
if (lastOff == totalSize) return chunk; // exact multiple
return static_cast<int32_t>(totalSize - lastOff);
};
constexpr int32_t kChunk = 256 * 1024;
BOOST_CHECK_EQUAL(lastChunkSize(1024 * 1024, kChunk), kChunk); // 4 even chunks → last is full
BOOST_CHECK_EQUAL(lastChunkSize(1024 * 1024 + 1, kChunk), 1); // partial trailing byte
BOOST_CHECK_EQUAL(lastChunkSize(kChunk * 3, kChunk), kChunk); // exact multiple
BOOST_CHECK_EQUAL(lastChunkSize(kChunk * 3 + 100, kChunk), 100);
}
BOOST_AUTO_TEST_SUITE_END()
// ───────────────────────────────────────────────────────────────────────────
// Service-bit advertisement — compile-time guarantee
// ───────────────────────────────────────────────────────────────────────────
BOOST_AUTO_TEST_SUITE(snapshotnet_protocol)
BOOST_AUTO_TEST_CASE(snapshot_proto_version_is_defined)
{
// SNAPSHOT_PROTO_VERSION is the version gate in DispatchChunkRequests —
// peers below this version are skipped because they can't speak the
// chunk protocol. Bumping this number requires a coordinated network
// upgrade.
BOOST_CHECK_EQUAL(SnapshotNet::SNAPSHOT_CHUNK_MAX, 256 * 1024);
}
BOOST_AUTO_TEST_CASE(node_snapshot_service_bit_distinct_from_network)
{
// Sanity: NODE_SNAPSHOT must not collide with NODE_NETWORK.
constexpr uint64_t NODE_NETWORK = (1 << 0);
constexpr uint64_t NODE_SNAPSHOT = (1 << 1);
BOOST_CHECK((NODE_NETWORK & NODE_SNAPSHOT) == 0);
BOOST_CHECK(NODE_NETWORK != 0);
BOOST_CHECK(NODE_SNAPSHOT != 0);
}
BOOST_AUTO_TEST_CASE(service_bits_oring_is_additive)
{
// OR-ing NODE_SNAPSHOT into nLocalServices preserves existing bits.
uint64_t services = (1ULL << 0); // NODE_NETWORK
services |= (1ULL << 1); // NODE_SNAPSHOT
BOOST_CHECK((services & (1ULL << 0)) != 0);
BOOST_CHECK((services & (1ULL << 1)) != 0);
}
BOOST_AUTO_TEST_SUITE_END()
// ───────────────────────────────────────────────────────────────────────────
// TryFetchSnapshot behavior — needs Checkpoints::GetBestSnapshotHeight to
// return >0 for the request to even start. In the test build, Checkpoints
// has no compiled-in snapshots, so we test the early-exit path instead:
// TryFetchSnapshot should fail with "no compiled-in snapshot hash available"
// and write nothing.
// ───────────────────────────────────────────────────────────────────────────
BOOST_AUTO_TEST_SUITE(snapshotnet_fetch)
BOOST_AUTO_TEST_CASE(fetch_with_no_published_snapshot_returns_false)
{
TmpDataDir td;
// The fresh test datadir has no blockchain, no checkpoint entries.
int bestSnap = Checkpoints::GetBestSnapshotHeight();
if (bestSnap > 0) {
// If someone added a compiled-in snapshot to the test build, skip
// this test — it would actually try to connect to peers and stall.
BOOST_TEST_MESSAGE("skipping: published snapshot present in test build");
return;
}
std::string err;
bool ok = SnapshotNet::TryFetchSnapshot(td.path, /*timeoutSec=*/2, err);
BOOST_CHECK(!ok);
BOOST_CHECK_NE(err.find("no compiled-in"), std::string::npos);
BOOST_CHECK(!fs::exists(td.path / "utxo-snapshot.bin"));
}
BOOST_AUTO_TEST_CASE(has_servable_snapshot_false_when_no_file)
{
TmpDataDir td;
BOOST_CHECK(!SnapshotNet::HasServableSnapshot());
}
BOOST_AUTO_TEST_CASE(ensure_local_snapshot_no_op_when_no_published_height)
{
TmpDataDir td;
SnapshotNet::EnsureLocalSnapshot();
BOOST_CHECK(!fs::exists(td.path / "utxo-snapshot.bin"));
BOOST_CHECK(!SnapshotNet::HasServableSnapshot());
}
BOOST_AUTO_TEST_SUITE_END()
+1368 -1529
View File
File diff suppressed because it is too large Load Diff
+75 -73
View File
@@ -1,73 +1,75 @@
// Copyright (c) 2026 The Triangles developers. // Copyright (c) 2026 The Triangles developers.
// Distributed under the MIT/X11 software license, see the accompanying // Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include "txdb.h" #include "txdb.h"
#include "util.h" #include "util.h"
#include <filesystem> #include <filesystem>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
namespace fs = std::filesystem; namespace fs = std::filesystem;
namespace { namespace {
// Pick the backend once per process. -chaindb is a startup flag; switching at // Pick the backend on every call. The daemon sets -chaindb once at startup
// runtime would require reopening every CTxDB instance, which the codebase // and never changes it, so the per-call cost (a GetArg + tolower loop on a
// doesn't currently support. We cache the resolved choice so subsequent // short string) is negligible compared to the cost of opening the chain DB.
// MakeChainDB calls don't re-parse the argument. // The earlier static-cache version broke test_chaindb_runtime, which
enum class ChainDbKind { LevelDB, RocksDB }; // legitimately toggles -chaindb across test cases to exercise both backends
// in the same process. Caching would freeze the first-seen choice.
ChainDbKind ResolveChainDbKind() enum class ChainDbKind { LevelDB, RocksDB };
{
static const ChainDbKind kKind = []() { ChainDbKind ResolveChainDbKind()
std::string s = GetArg("-chaindb", std::string("leveldb")); {
for (auto& c : s) c = std::tolower(static_cast<unsigned char>(c)); // RocksDB is the default backend. LevelDB remains selectable with
// -chaindb=leveldb and is retained as the migration source and fallback;
if (s == "leveldb") // its removal is deferred to a later phase after live-chain validation.
return ChainDbKind::LevelDB; std::string s = GetArg("-chaindb", std::string("rocksdb"));
if (s == "rocksdb") for (auto& c : s) c = std::tolower(static_cast<unsigned char>(c));
return ChainDbKind::RocksDB;
if (s == "leveldb")
throw std::runtime_error( return ChainDbKind::LevelDB;
"-chaindb=" + s + " is not a recognized backend. " if (s == "rocksdb")
"Valid values: leveldb, rocksdb."); return ChainDbKind::RocksDB;
}();
return kKind; throw std::runtime_error(
} "-chaindb=" + s + " is not a recognized backend. "
"Valid values: leveldb, rocksdb.");
} // anonymous namespace }
std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode) } // anonymous namespace
{
switch (ResolveChainDbKind()) { std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode)
case ChainDbKind::LevelDB: {
return std::unique_ptr<CTxDBBase>(new CTxDB(pszMode)); switch (ResolveChainDbKind()) {
case ChainDbKind::RocksDB: case ChainDbKind::LevelDB:
return std::unique_ptr<CTxDBBase>(new CRocksTxDB(pszMode)); return std::unique_ptr<CTxDBBase>(new CTxDB(pszMode));
} case ChainDbKind::RocksDB:
// Unreachable — ResolveChainDbKind throws on bad input. return std::unique_ptr<CTxDBBase>(new CRocksTxDB(pszMode));
return nullptr; }
} // Unreachable — ResolveChainDbKind throws on bad input.
return nullptr;
bool IsRocksDbChainBackend() }
{
return ResolveChainDbKind() == ChainDbKind::RocksDB; bool IsRocksDbChainBackend()
} {
return ResolveChainDbKind() == ChainDbKind::RocksDB;
std::filesystem::path GetChainDataDir() }
{
switch (ResolveChainDbKind()) { std::filesystem::path GetChainDataDir()
case ChainDbKind::LevelDB: return GetDataDir() / "txleveldb"; {
case ChainDbKind::RocksDB: return GetDataDir() / "rocksdb"; switch (ResolveChainDbKind()) {
} case ChainDbKind::LevelDB: return GetDataDir() / "txleveldb";
return GetDataDir() / "txleveldb"; // unreachable case ChainDbKind::RocksDB: return GetDataDir() / "rocksdb";
} }
return GetDataDir() / "txleveldb"; // unreachable
void WipeChainDataDir() }
{
fs::path p = GetChainDataDir(); void WipeChainDataDir()
if (fs::exists(p)) {
fs::remove_all(p); fs::path p = GetChainDataDir();
} if (fs::exists(p))
fs::remove_all(p);
}
+696 -673
View File
File diff suppressed because it is too large Load Diff
+875 -710
View File
File diff suppressed because it is too large Load Diff
+101 -74
View File
@@ -1,74 +1,101 @@
// Copyright (c) 2026 The Triangles developers. // Copyright (c) 2026 The Triangles developers.
// Distributed under the MIT/X11 software license, see the accompanying // Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php. // file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef TRIANGLES_TXDB_ROCKSDB_H #ifndef TRIANGLES_TXDB_ROCKSDB_H
#define TRIANGLES_TXDB_ROCKSDB_H #define TRIANGLES_TXDB_ROCKSDB_H
#include "txdb-base.h" #include "txdb-base.h"
#include <map> #include <map>
#include <optional> #include <optional>
#include <string> #include <string>
#include <unordered_map>
#include <rocksdb/db.h> #include <vector>
#include <rocksdb/options.h>
#include <rocksdb/write_batch.h> #include <rocksdb/db.h>
#include <rocksdb/options.h>
// RocksDB backend for the chain database. #include <rocksdb/write_batch.h>
// #include <rocksdb/utilities/db_ttl.h>
// Mirrors CTxDB (LevelDB) for byte-level compatibility. CTxDBBase owns all
// key serialization, so keys produced by this backend are bit-identical to // RocksDB backend for the chain database.
// the LevelDB backend. That property is what lets the M1.4 dual-backend //
// parity harness verify equivalence. // Mirrors CTxDB (LevelDB) for byte-level compatibility. CTxDBBase owns all
// // key serialization, so keys produced by this backend are bit-identical to
// Data lives under <datadir>/rocksdb/, separate from <datadir>/txleveldb/, // the LevelDB backend. That property is what lets the M1.4 dual-backend
// so both backends can coexist for migration and side-by-side testing. // parity harness verify equivalence.
class CRocksTxDB final : public CTxDBBase //
{ // Data lives under <datadir>/rocksdb/, separate from <datadir>/txleveldb/,
public: // so both backends can coexist for migration and side-by-side testing.
CRocksTxDB(const char* pszMode = "r+"); class CRocksTxDB final : public CTxDBBase
~CRocksTxDB() override; {
public:
void Close() override; CRocksTxDB(const char* pszMode = "r+");
~CRocksTxDB() override;
bool TxnBegin() override;
bool TxnCommit() override; void Close() override;
bool TxnAbort() override;
bool TxnBegin() override;
bool LoadBlockIndex() override; bool TxnCommit() override;
bool TxnAbort() override;
// Write a raw serialized key/value pair, bypassing the typed Write<>()
// overloads. Intended for the chaindb migration utility, which carries bool LoadBlockIndex() override;
// bytes directly across from a CTxDB (LevelDB) iterator. Honors the
// active write batch if one is open. // Write a raw serialized key/value pair, bypassing the typed Write<>()
bool WriteRawRecordForMigration(const std::string& key, const std::string& value) // overloads. Intended for the chaindb migration utility, which carries
{ // bytes directly across from a CTxDB (LevelDB) iterator. Honors the
return WriteRaw(key, value); // active write batch if one is open.
} bool WriteRawRecordForMigration(const std::string& key, const std::string& value)
{
std::unique_ptr<CTxDBIteratorBase> NewIterator() const override; return WriteRaw(key, value);
}
protected:
bool ReadRaw(const std::string& key, std::string& value) const override; std::unique_ptr<CTxDBIteratorBase> NewIterator() const override;
bool WriteRaw(const std::string& key, const std::string& value) override;
bool EraseRaw(const std::string& key) override; // ─── Test-only friend accessor ──────────────────────────────────────────
bool ExistsRaw(const std::string& key) const override; // test_chaindb_runtime exercises the protected raw methods (ReadRaw /
// WriteRaw / EraseRaw / ExistsRaw) directly to verify the wrapper layer
private: // that the daemon uses at runtime when launched with -chaindb=rocksdb.
rocksdb::DB* pdb; // Points to the global instance. // We don't widen the public API just for the test — instead the test
rocksdb::WriteBatch* activeBatch; // When non-NULL, writes/deletes go here. // declares a ChainDbRuntimeTestAccessor struct that this class befriends,
rocksdb::Options options; // giving it the same access the class itself has. White-box test pattern,
int nVersion; // zero impact on production callers.
friend struct ChainDbRuntimeTestAccessor;
// Parallel record of every pending write (value) or delete (nullopt) on
// activeBatch. Used by ScanBatch to answer "is this key already in the protected:
// active batch?" without iterating the WriteBatch via Handler — Ubuntu's bool ReadRaw(const std::string& key, std::string& value) const override;
// librocksdb-dev hides typeinfo for rocksdb::WriteBatch::Handler so a bool WriteRaw(const std::string& key, const std::string& value) override;
// subclass-based scan fails to link there. bool EraseRaw(const std::string& key) override;
std::map<std::string, std::optional<std::string>> pendingBatch; bool ExistsRaw(const std::string& key) const override;
bool ScanBatch(const std::string& key, std::string* value, bool* deleted) const; private:
}; rocksdb::DB* pdb; // Points to the global instance.
rocksdb::WriteBatch* activeBatch; // When non-NULL, writes/deletes go here.
#endif // TRIANGLES_TXDB_ROCKSDB_H rocksdb::Options options;
int nVersion;
// ─── Column family support (DISABLED) ────────────────────────────────────
// CF partitioning is intentionally off: the read path (NewIterator /
// LoadBlockIndex) only iterates the default CF, so all data must live there
// for scans to be correct. GetCF() therefore always returns nullptr (the
// default CF). See the long note in txdb-rocksdb.cpp's GetCF definition.
// These members are retained for a future CF-aware-iteration phase.
enum CfId : int { CF_DEFAULT = 0, CF_BLOCKINDEX, CF_TXINDEX, CF_UTXO, CF_ADDRINDEX, CF_COUNT };
rocksdb::ColumnFamilyHandle* cf_handles[CF_COUNT] = {};
bool cf_enabled = false; // Always false while CF routing is disabled.
// Returns the column family a key should live in. While CF partitioning is
// disabled this always returns nullptr (= default CF).
rocksdb::ColumnFamilyHandle* GetCF(const std::string& key) const;
// Parallel record of every pending write (value) or delete (nullopt) on
// activeBatch. Used by ScanBatch to answer "is this key already in the
// active batch?" without iterating the WriteBatch via Handler — Ubuntu's
// librocksdb-dev hides typeinfo for rocksdb::WriteBatch::Handler so a
// subclass-based scan fails to link there.
std::unordered_map<std::string, std::optional<std::string>> pendingBatch;
bool ScanBatch(const std::string& key, std::string* value, bool* deleted) const;
};
#endif // TRIANGLES_TXDB_ROCKSDB_H
+41 -40
View File
@@ -1,40 +1,41 @@
// Copyright (c) 2009-2010 Satoshi Nakamoto // Copyright (c) 2009-2010 Satoshi Nakamoto
// Copyright (c) 2009-2012 The Bitcoin developers // Copyright (c) 2009-2012 The Bitcoin developers
// Copyright (c) 2026 The Triangles developers // Copyright (c) 2026 The Triangles developers
// Distributed under the MIT/X11 software license, see the accompanying // Distributed under the MIT/X11 software license, see the accompanying
// file license.txt or http://www.opensource.org/licenses/mit-license.php. // file license.txt or http://www.opensource.org/licenses/mit-license.php.
#ifndef TRIANGLES_TXDB_H #ifndef TRIANGLES_TXDB_H
#define TRIANGLES_TXDB_H #define TRIANGLES_TXDB_H
#include "txdb-base.h" #include "txdb-base.h"
#include "txdb-leveldb.h" #include "txdb-leveldb.h"
#include "txdb-rocksdb.h" #include "txdb-rocksdb.h"
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>
// Factory: returns a chain-database handle whose concrete backend is chosen // Factory: returns a chain-database handle whose concrete backend is chosen
// by the -chaindb command-line argument: // by the -chaindb command-line argument:
// //
// -chaindb=leveldb (default — pending Phase-4 retirement) // -chaindb=rocksdb (default)
// -chaindb=rocksdb // -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 // Callers receive a CTxDBBase*, so the rest of the codebase stays
// CTxDB constructor convention. // backend-agnostic. Mode strings ("r", "r+", "cr+") match the pre-existing
std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode = "r+"); // CTxDB constructor convention.
std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode = "r+");
// True when the configured chain-DB backend is RocksDB.
bool IsRocksDbChainBackend(); // 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). // On-disk directory of the chain DB for the configured backend, e.g.
std::filesystem::path GetChainDataDir(); // <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 // Remove the chain DB directory for the configured backend. Callers that
// MakeChainDB() opens the global handle for the first time. // need a fresh DB (-reindex, snapshot load) must invoke this BEFORE
void WipeChainDataDir(); // MakeChainDB() opens the global handle for the first time.
void WipeChainDataDir();
#endif // TRIANGLES_TXDB_H
#endif // TRIANGLES_TXDB_H
+1422 -1394
View File
File diff suppressed because it is too large Load Diff
+115
View File
@@ -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
+146
View File
@@ -0,0 +1,146 @@
// 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:
explicit CWalletBatchTyped(std::unique_ptr<WalletBatch> batch)
: m_batch(std::move(batch)) {}
virtual ~CWalletBatchTyped() { Close(); }
void Close() { m_batch.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<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
+55
View File
@@ -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
}
+364
View File
@@ -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;
}
+99
View File
@@ -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
+832 -818
View File
File diff suppressed because it is too large Load Diff
+207
View File
@@ -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;
}
+31
View File
@@ -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