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:
+814
-722
File diff suppressed because it is too large
Load Diff
+109
-100
@@ -1,100 +1,109 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
# 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.
|
||||
# See .clang-format and .clang-tidy for the rule sets.
|
||||
|
||||
jobs:
|
||||
clang-format-diff:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Need merge-base with target branch to compute the diff.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install clang-format
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-format-15
|
||||
sudo ln -sf /usr/bin/clang-format-15 /usr/local/bin/clang-format
|
||||
|
||||
- name: Check format on changed lines
|
||||
run: |
|
||||
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD)
|
||||
echo "Comparing against merge-base: $BASE_SHA"
|
||||
|
||||
# git-clang-format prints a diff if any changed line violates style.
|
||||
# --diff exits non-zero when reformatting would change something.
|
||||
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
|
||||
echo "clang-format: clean"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
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 "$OUTPUT"
|
||||
exit 1
|
||||
|
||||
clang-tidy-diff:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies + clang-tidy
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential cmake ninja-build clang-tidy-15 \
|
||||
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
|
||||
librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev
|
||||
sudo ln -sf /usr/bin/clang-tidy-15 /usr/local/bin/clang-tidy
|
||||
|
||||
- name: Configure (export compile_commands.json)
|
||||
run: |
|
||||
cmake -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DBUILD_QT=OFF \
|
||||
-DBUILD_DAEMON=ON \
|
||||
-DBUILD_TESTS=ON \
|
||||
-DUSE_UPNP=OFF
|
||||
|
||||
- name: Generate build artifacts that headers depend on
|
||||
# build.h, qt UI headers, etc. — clang-tidy needs them to parse sources.
|
||||
run: cmake --build build --target generate_build_info
|
||||
|
||||
- name: Run clang-tidy on changed lines
|
||||
run: |
|
||||
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD)
|
||||
echo "Comparing against merge-base: $BASE_SHA"
|
||||
|
||||
# clang-tidy-diff.py ships with clang-tidy; runs tidy only on changed lines.
|
||||
DIFF_SCRIPT=$(dpkg -L clang-tidy-15 | grep clang-tidy-diff.py | head -1)
|
||||
if [ -z "$DIFF_SCRIPT" ]; then
|
||||
DIFF_SCRIPT=/usr/share/clang/clang-tidy-diff.py
|
||||
fi
|
||||
echo "Using: $DIFF_SCRIPT"
|
||||
|
||||
# -p1 strips the leading "a/"/"b/" from git diff paths.
|
||||
# -path=build points clang-tidy at compile_commands.json.
|
||||
# -iregex restricts to project sources (not vendored).
|
||||
git diff -U0 "$BASE_SHA" -- 'src/*.cpp' 'src/*.h' \
|
||||
':(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
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
# 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.
|
||||
# See .clang-format and .clang-tidy for the rule sets.
|
||||
|
||||
jobs:
|
||||
clang-format-diff:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# Need merge-base with target branch to compute the diff.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install clang-format
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang-format-15
|
||||
sudo ln -sf /usr/bin/clang-format-15 /usr/local/bin/clang-format
|
||||
|
||||
- name: Check format on changed lines
|
||||
run: |
|
||||
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD)
|
||||
echo "Comparing against merge-base: $BASE_SHA"
|
||||
|
||||
# git-clang-format prints a diff if any changed line violates style.
|
||||
# --diff exits non-zero when reformatting would change something.
|
||||
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
|
||||
echo "clang-format: clean"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
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 "$OUTPUT"
|
||||
exit 1
|
||||
|
||||
clang-tidy-diff:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies + clang-tidy
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential cmake ninja-build clang-tidy-15 \
|
||||
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
|
||||
libevent-dev libminiupnpc-dev zlib1g-dev \
|
||||
libsnappy-dev liblz4-dev libzstd-dev libsqlite3-dev
|
||||
sudo ln -sf /usr/bin/clang-tidy-15 /usr/local/bin/clang-tidy
|
||||
|
||||
- name: Build RocksDB from source
|
||||
# Ubuntu 22.04's librocksdb-dev is 6.11.4 which CMakeLists.txt now
|
||||
# refuses to configure against (need >= 7.4 for XXH3 per-block
|
||||
# checksum). Build 8.9.1 from source — same version DNS2 ships —
|
||||
# into /usr/local so CMake's find_library picks it up first.
|
||||
run: sudo bash scripts/ci/build-rocksdb.sh
|
||||
|
||||
- name: Configure (export compile_commands.json)
|
||||
run: |
|
||||
cmake -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DBUILD_QT=OFF \
|
||||
-DBUILD_DAEMON=ON \
|
||||
-DBUILD_TESTS=ON \
|
||||
-DUSE_UPNP=OFF
|
||||
|
||||
- name: Generate build artifacts that headers depend on
|
||||
# build.h, qt UI headers, etc. — clang-tidy needs them to parse sources.
|
||||
run: cmake --build build --target generate_build_info
|
||||
|
||||
- name: Run clang-tidy on changed lines
|
||||
run: |
|
||||
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD)
|
||||
echo "Comparing against merge-base: $BASE_SHA"
|
||||
|
||||
# clang-tidy-diff.py ships with clang-tidy; runs tidy only on changed lines.
|
||||
DIFF_SCRIPT=$(dpkg -L clang-tidy-15 | grep clang-tidy-diff.py | head -1)
|
||||
if [ -z "$DIFF_SCRIPT" ]; then
|
||||
DIFF_SCRIPT=/usr/share/clang/clang-tidy-diff.py
|
||||
fi
|
||||
echo "Using: $DIFF_SCRIPT"
|
||||
|
||||
# -p1 strips the leading "a/"/"b/" from git diff paths.
|
||||
# -path=build points clang-tidy at compile_commands.json.
|
||||
# -iregex restricts to project sources (not vendored).
|
||||
git diff -U0 "$BASE_SHA" -- 'src/*.cpp' 'src/*.h' \
|
||||
':(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
|
||||
|
||||
@@ -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.
|
||||
@@ -1,250 +1,272 @@
|
||||
# Cryptographic Triangles (TRI)
|
||||
|
||||
Triangles is a privacy-focused cryptocurrency featuring Proof-of-Stake consensus, Tor v3 onion routing, and built-in encrypted messaging. Originally launched in July 2014, the chain was revived in March 2026 after being frozen since December 2022.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Proof-of-Stake** - Energy-efficient block production with 33% annual staking rewards (coin-age based)
|
||||
- **Hash9 Algorithm** - Unique 13-step hash cascade (Fugue, Hamsi, Groestl, Blake, BMW, Skein, Keccak, Shavite, JH, Luffa, Cubehash, Echo, SIMD)
|
||||
- **Encrypted Messaging** - Send and receive encrypted messages directly through the wallet
|
||||
- **Tor v3 Integration** - Connect and transact over the Tor network with v3 onion hidden services
|
||||
- **120-second Block Time** - Fast confirmations with 2-minute target spacing
|
||||
|
||||
## Specifications
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Algorithm | Hash9 (PoW blocks 0-9000), PoS from block 9001 |
|
||||
| Block Time | ~120 seconds |
|
||||
| Max Supply | 2,222,222 TRI |
|
||||
| PoS Reward | 33% annual, coin-age based |
|
||||
| P2P Port | 24112 |
|
||||
| RPC Port | 19112 |
|
||||
| Protocol | 70205 |
|
||||
|
||||
## Network Status
|
||||
|
||||
The Triangles network operates exclusively over Tor for privacy:
|
||||
|
||||
**Tor v3 Seeds:**
|
||||
- `jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112`
|
||||
- `uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112`
|
||||
- `el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112`
|
||||
- `sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112`
|
||||
- `i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112`
|
||||
|
||||
**HTTP Seed List:**
|
||||
- `seeds.cryptographic-triangles.org/seeds.txt` - Dynamically updated list of active onion peers
|
||||
|
||||
## Building from Source
|
||||
|
||||
Triangles uses CMake. All platforms follow the same build pattern.
|
||||
|
||||
### Dependencies
|
||||
|
||||
| Dependency | Minimum Version |
|
||||
|------------|----------------|
|
||||
| CMake | 3.16+ |
|
||||
| C++ compiler | C++17 support |
|
||||
| OpenSSL | 3.x |
|
||||
| Boost | 1.90+ |
|
||||
| Berkeley DB | 5.3 (with C++ bindings) |
|
||||
| libevent | 2.x |
|
||||
| LevelDB | bundled |
|
||||
|
||||
### Linux (Ubuntu 24.04 / Debian 12+)
|
||||
|
||||
Install dependencies:
|
||||
```bash
|
||||
sudo apt-get install -y build-essential cmake ninja-build \
|
||||
libboost-all-dev libssl-dev libdb5.3++-dev libevent-dev \
|
||||
zlib1g-dev libminiupnpc-dev
|
||||
```
|
||||
|
||||
For the Qt wallet, also install:
|
||||
```bash
|
||||
sudo apt-get install -y qtbase5-dev qt5-qmake libqrencode-dev
|
||||
```
|
||||
|
||||
Build:
|
||||
```bash
|
||||
cmake -B build -G Ninja -DBUILD_QT=ON
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
### Linux (AlmaLinux 9 / RHEL 9)
|
||||
|
||||
Install dependencies:
|
||||
```bash
|
||||
sudo dnf install -y gcc-c++ cmake ninja-build boost-devel openssl-devel \
|
||||
libevent-devel zlib-devel miniupnpc-devel
|
||||
```
|
||||
|
||||
BDB 5.3 C++ bindings must be built from source on RHEL-based systems (the `libdb-devel` package does not include C++ headers). Download BDB 5.3.28 from Oracle and build with `--enable-cxx`.
|
||||
|
||||
Then build as above.
|
||||
|
||||
### Windows (MSYS2 MinGW64)
|
||||
|
||||
Open an MSYS2 MinGW64 shell and install:
|
||||
```bash
|
||||
pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja \
|
||||
mingw-w64-x86_64-boost mingw-w64-x86_64-openssl \
|
||||
mingw-w64-x86_64-db mingw-w64-x86_64-miniupnpc \
|
||||
mingw-w64-x86_64-qt5-base mingw-w64-x86_64-qrencode \
|
||||
mingw-w64-x86_64-libevent
|
||||
```
|
||||
|
||||
Build:
|
||||
```bash
|
||||
cmake -B build -G Ninja -DBUILD_QT=ON
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
### Build Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `BUILD_QT` | ON | Build the Qt GUI wallet |
|
||||
| `BUILD_DAEMON` | ON | Build the headless daemon |
|
||||
| `BUILD_TESTS` | OFF | Build unit tests |
|
||||
|
||||
## Running
|
||||
|
||||
### First Run
|
||||
```bash
|
||||
mkdir -p ~/.triangles
|
||||
cat > ~/.triangles/triangles.conf << 'EOF'
|
||||
port=24112
|
||||
rpcport=19112
|
||||
rpcuser=trianglesrpc
|
||||
rpcpassword=<generate-a-strong-password>
|
||||
rpcallowip=127.0.0.1
|
||||
staking=1
|
||||
txindex=1
|
||||
listen=1
|
||||
server=1
|
||||
daemon=1
|
||||
proxy=127.0.0.1:9050
|
||||
EOF
|
||||
|
||||
trianglesd
|
||||
```
|
||||
|
||||
The node will connect to seed nodes over Tor and sync the blockchain automatically.
|
||||
|
||||
### Existing Wallet Holders
|
||||
|
||||
If you have a `wallet.dat` from the original Triangles network:
|
||||
|
||||
1. Place your `wallet.dat` in `~/.triangles/` (Linux) or `%APPDATA%\triangles\` (Windows)
|
||||
2. Start the wallet - it will sync the blockchain and your balance will appear automatically
|
||||
3. No migration or special action is needed - all keys and balances are preserved
|
||||
|
||||
### Staking
|
||||
|
||||
To stake, your wallet must be:
|
||||
- Running with `staking=1` in the config
|
||||
- Connected to at least one peer
|
||||
- Containing coins with sufficient coin-age (mature inputs)
|
||||
|
||||
Check staking status:
|
||||
```bash
|
||||
trianglesd getstakinginfo
|
||||
```
|
||||
|
||||
### Encrypted Messaging
|
||||
|
||||
Send and receive encrypted messages between wallet addresses:
|
||||
|
||||
```bash
|
||||
# Enable messaging
|
||||
trianglesd smsgenable
|
||||
|
||||
# Send a message
|
||||
trianglesd smsgsend <your-address> <recipient-address> "Hello from Triangles!"
|
||||
|
||||
# Check inbox
|
||||
trianglesd smsginbox all
|
||||
|
||||
# Send anonymous message
|
||||
trianglesd smsgsendanon <recipient-address> "Anonymous message"
|
||||
```
|
||||
|
||||
Messages are encrypted end-to-end using AES and distributed through the peer network in time-bucketed batches.
|
||||
|
||||
### Tor Support
|
||||
|
||||
Triangles is designed as a Tor-only network. Install the Tor daemon and configure your proxy:
|
||||
```
|
||||
# triangles.conf
|
||||
proxy=127.0.0.1:9050
|
||||
```
|
||||
|
||||
To run your own hidden service, add to `/etc/tor/torrc`:
|
||||
```
|
||||
HiddenServiceDir /var/lib/tor/triangles/
|
||||
HiddenServiceVersion 3
|
||||
HiddenServicePort 24112 127.0.0.1:24112
|
||||
```
|
||||
|
||||
Then set `externalip=<your-onion-address>` in `triangles.conf`.
|
||||
|
||||
## RPC Commands
|
||||
|
||||
### General
|
||||
- `getinfo` - Node status, balance, block height, connections
|
||||
- `getpeerinfo` - Connected peer details
|
||||
- `getstakinginfo` - Staking status and weight
|
||||
|
||||
### Wallet
|
||||
- `getbalance` - Current balance
|
||||
- `listunspent` - Unspent transaction outputs
|
||||
- `sendtoaddress <addr> <amount>` - Send TRI
|
||||
- `getnewaddress` - Generate new receiving address
|
||||
|
||||
### Messaging
|
||||
- `smsgenable` / `smsgdisable` - Toggle secure messaging
|
||||
- `smsgsend <from> <to> <message>` - Send encrypted message
|
||||
- `smsgsendanon <to> <message>` - Send anonymous message
|
||||
- `smsginbox [all|unread|clear]` - View received messages
|
||||
- `smsgoutbox [all|clear]` - View sent messages
|
||||
- `smsglocalkeys` - List messaging-enabled addresses
|
||||
- `smsgscanchain` - Scan blockchain for public keys
|
||||
|
||||
## Chain History
|
||||
|
||||
- **July 16, 2014** - Genesis block
|
||||
- **Block 0-9000** - Proof-of-Work mining phase (Hash9)
|
||||
- **Block 9001+** - Proof-of-Stake only
|
||||
- **Block 17,651** - V5 hard fork (removed Tor v2, disabled checkpoint master key)
|
||||
- **December 8, 2022** - Chain frozen (all nodes offline)
|
||||
- **March 11, 2026** - Chain revived, staking resumed
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
main.cpp - Core blockchain logic, block/tx validation, message routing
|
||||
miner.cpp - Staking miner thread
|
||||
net.cpp - P2P networking
|
||||
init.cpp - Daemon initialization
|
||||
wallet.cpp - Wallet management
|
||||
smessage.cpp/h - Encrypted messaging system
|
||||
kernel.cpp - PoS kernel (stake validation)
|
||||
checkpoints.cpp - Hardcoded checkpoints
|
||||
net_bootstrap.h - DNS/IP seed configuration
|
||||
onionseed.h - Tor v3 onion seed addresses
|
||||
tor/
|
||||
onion_v3.cpp/h - Tor v3 hidden service management
|
||||
tor_crypto_compat.h - Ed25519/SHA3 crypto compatibility
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the MIT/X11 software license. See `COPYING` for details.
|
||||
|
||||
## Links
|
||||
|
||||
- Website: [cryptographic-triangles.org](https://cryptographic-triangles.org)
|
||||
- Explorer: [blocks.cryptographic-triangles.org](https://blocks.cryptographic-triangles.org)
|
||||
# Cryptographic Triangles (TRI)
|
||||
|
||||
Triangles is a privacy-focused cryptocurrency featuring Proof-of-Stake consensus, Tor v3 onion routing, and built-in encrypted messaging. Originally launched in July 2014, the chain was revived in March 2026 after being frozen since December 2022.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Proof-of-Stake** - Energy-efficient block production with 33% annual staking rewards (coin-age based)
|
||||
- **Hash9 Algorithm** - Unique 13-step hash cascade (Fugue, Hamsi, Groestl, Blake, BMW, Skein, Keccak, Shavite, JH, Luffa, Cubehash, Echo, SIMD)
|
||||
- **Encrypted Messaging** - Send and receive encrypted messages directly through the wallet
|
||||
- **Tor v3 Integration** - Connect and transact over the Tor network with v3 onion hidden services
|
||||
- **120-second Block Time** - Fast confirmations with 2-minute target spacing
|
||||
|
||||
## Specifications
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Algorithm | Hash9 (PoW blocks 0-9000), PoS from block 9001 |
|
||||
| Block Time | ~120 seconds |
|
||||
| Max Supply | 2,222,222 TRI |
|
||||
| PoS Reward | 33% annual, coin-age based |
|
||||
| P2P Port | 24112 |
|
||||
| RPC Port | 19112 |
|
||||
| Protocol | 70205 |
|
||||
|
||||
## Network Status
|
||||
|
||||
The Triangles network operates exclusively over Tor for privacy:
|
||||
|
||||
**Tor v3 Seeds:**
|
||||
- `jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112`
|
||||
- `uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112`
|
||||
- `el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112`
|
||||
- `sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112`
|
||||
- `i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112`
|
||||
|
||||
**HTTP Seed List:**
|
||||
- `seeds.cryptographic-triangles.org/seeds.txt` - Dynamically updated list of active onion peers
|
||||
|
||||
## Building from Source
|
||||
|
||||
Triangles uses CMake. All platforms follow the same build pattern.
|
||||
|
||||
### Dependencies
|
||||
|
||||
| Dependency | Minimum Version |
|
||||
|------------|----------------|
|
||||
| CMake | 3.16+ |
|
||||
| C++ compiler | C++17 support |
|
||||
| OpenSSL | 3.x |
|
||||
| Boost | 1.90+ |
|
||||
| SQLite | 3.x (default wallet database backend) |
|
||||
| Berkeley DB | 5.3 with C++ bindings (legacy wallet backend, used for migration) |
|
||||
| libevent | 2.x |
|
||||
| RocksDB | 7.4+ (default chain database backend) |
|
||||
| LevelDB | bundled (legacy chain DB backend, used for migration) |
|
||||
|
||||
### Linux (Ubuntu 24.04 / Debian 12+)
|
||||
|
||||
Install dependencies:
|
||||
```bash
|
||||
sudo apt-get install -y build-essential cmake ninja-build \
|
||||
libboost-all-dev libssl-dev libdb5.3++-dev libevent-dev \
|
||||
zlib1g-dev libminiupnpc-dev
|
||||
```
|
||||
|
||||
For the Qt wallet, also install:
|
||||
```bash
|
||||
sudo apt-get install -y qtbase5-dev qt5-qmake libqrencode-dev
|
||||
```
|
||||
|
||||
Build:
|
||||
```bash
|
||||
cmake -B build -G Ninja -DBUILD_QT=ON
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
### Linux (AlmaLinux 9 / RHEL 9)
|
||||
|
||||
Install dependencies:
|
||||
```bash
|
||||
sudo dnf install -y gcc-c++ cmake ninja-build boost-devel openssl-devel \
|
||||
libevent-devel zlib-devel miniupnpc-devel
|
||||
```
|
||||
|
||||
BDB 5.3 C++ bindings must be built from source on RHEL-based systems (the `libdb-devel` package does not include C++ headers). Download BDB 5.3.28 from Oracle and build with `--enable-cxx`.
|
||||
|
||||
Then build as above.
|
||||
|
||||
### Windows (MSYS2 MinGW64)
|
||||
|
||||
Open an MSYS2 MinGW64 shell and install:
|
||||
```bash
|
||||
pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja \
|
||||
mingw-w64-x86_64-boost mingw-w64-x86_64-openssl \
|
||||
mingw-w64-x86_64-db mingw-w64-x86_64-miniupnpc \
|
||||
mingw-w64-x86_64-qt5-base mingw-w64-x86_64-qrencode \
|
||||
mingw-w64-x86_64-libevent
|
||||
```
|
||||
|
||||
Build:
|
||||
```bash
|
||||
cmake -B build -G Ninja -DBUILD_QT=ON
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
### Build Options
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `BUILD_QT` | ON | Build the Qt GUI wallet |
|
||||
| `BUILD_DAEMON` | ON | Build the headless daemon |
|
||||
| `BUILD_TESTS` | OFF | Build unit tests |
|
||||
|
||||
## Running
|
||||
|
||||
### First Run
|
||||
```bash
|
||||
mkdir -p ~/.triangles
|
||||
cat > ~/.triangles/triangles.conf << 'EOF'
|
||||
port=24112
|
||||
rpcport=19112
|
||||
rpcuser=trianglesrpc
|
||||
rpcpassword=<generate-a-strong-password>
|
||||
rpcallowip=127.0.0.1
|
||||
staking=1
|
||||
txindex=1
|
||||
listen=1
|
||||
server=1
|
||||
daemon=1
|
||||
proxy=127.0.0.1:9050
|
||||
EOF
|
||||
|
||||
trianglesd
|
||||
```
|
||||
|
||||
The node will connect to seed nodes over Tor and sync the blockchain automatically.
|
||||
|
||||
### Chain Database (RocksDB)
|
||||
|
||||
The chain database (block index, transaction index, UTXO set, address index) uses **RocksDB by default**. RocksDB gives faster sync and lookups than the legacy LevelDB backend through parallel compaction, bloom filters, and a larger write buffer and block cache (tunable with `-dbcache=<MB>`).
|
||||
|
||||
If you are upgrading a node that already has a LevelDB chain database (`txleveldb/` in your data directory), it is migrated automatically on first launch: the chain state is copied into a new `rocksdb/` directory and verified (record count, UTXO count and value, best-chain hash, and DB format must all match) before use. The original `txleveldb/` directory is left untouched as a fallback and is never modified.
|
||||
|
||||
To select a backend explicitly:
|
||||
|
||||
```bash
|
||||
trianglesd -chaindb=rocksdb # default
|
||||
trianglesd -chaindb=leveldb # legacy backend (retained for fallback/migration)
|
||||
```
|
||||
|
||||
Migration can also be triggered or forced manually:
|
||||
|
||||
```bash
|
||||
trianglesd -migratechaindb # migrate txleveldb -> rocksdb if not already done
|
||||
trianglesd -migratechaindbforce # re-migrate, replacing any existing rocksdb/
|
||||
```
|
||||
|
||||
### Existing Wallet Holders
|
||||
|
||||
If you have a `wallet.dat` from the original Triangles network:
|
||||
|
||||
1. Place your `wallet.dat` in `~/.triangles/` (Linux) or `%APPDATA%\triangles\` (Windows)
|
||||
2. Start the wallet - it will sync the blockchain and your balance will appear automatically
|
||||
3. No migration or special action is needed - all keys and balances are preserved
|
||||
|
||||
### Staking
|
||||
|
||||
To stake, your wallet must be:
|
||||
- Running with `staking=1` in the config
|
||||
- Connected to at least one peer
|
||||
- Containing coins with sufficient coin-age (mature inputs)
|
||||
|
||||
Check staking status:
|
||||
```bash
|
||||
trianglesd getstakinginfo
|
||||
```
|
||||
|
||||
### Encrypted Messaging
|
||||
|
||||
Send and receive encrypted messages between wallet addresses:
|
||||
|
||||
```bash
|
||||
# Enable messaging
|
||||
trianglesd smsgenable
|
||||
|
||||
# Send a message
|
||||
trianglesd smsgsend <your-address> <recipient-address> "Hello from Triangles!"
|
||||
|
||||
# Check inbox
|
||||
trianglesd smsginbox all
|
||||
|
||||
# Send anonymous message
|
||||
trianglesd smsgsendanon <recipient-address> "Anonymous message"
|
||||
```
|
||||
|
||||
Messages are encrypted end-to-end using AES and distributed through the peer network in time-bucketed batches.
|
||||
|
||||
### Tor Support
|
||||
|
||||
Triangles is designed as a Tor-only network. Install the Tor daemon and configure your proxy:
|
||||
```
|
||||
# triangles.conf
|
||||
proxy=127.0.0.1:9050
|
||||
```
|
||||
|
||||
To run your own hidden service, add to `/etc/tor/torrc`:
|
||||
```
|
||||
HiddenServiceDir /var/lib/tor/triangles/
|
||||
HiddenServiceVersion 3
|
||||
HiddenServicePort 24112 127.0.0.1:24112
|
||||
```
|
||||
|
||||
Then set `externalip=<your-onion-address>` in `triangles.conf`.
|
||||
|
||||
## RPC Commands
|
||||
|
||||
### General
|
||||
- `getinfo` - Node status, balance, block height, connections
|
||||
- `getpeerinfo` - Connected peer details
|
||||
- `getstakinginfo` - Staking status and weight
|
||||
|
||||
### Wallet
|
||||
- `getbalance` - Current balance
|
||||
- `listunspent` - Unspent transaction outputs
|
||||
- `sendtoaddress <addr> <amount>` - Send TRI
|
||||
- `getnewaddress` - Generate new receiving address
|
||||
|
||||
### Messaging
|
||||
- `smsgenable` / `smsgdisable` - Toggle secure messaging
|
||||
- `smsgsend <from> <to> <message>` - Send encrypted message
|
||||
- `smsgsendanon <to> <message>` - Send anonymous message
|
||||
- `smsginbox [all|unread|clear]` - View received messages
|
||||
- `smsgoutbox [all|clear]` - View sent messages
|
||||
- `smsglocalkeys` - List messaging-enabled addresses
|
||||
- `smsgscanchain` - Scan blockchain for public keys
|
||||
|
||||
## Chain History
|
||||
|
||||
- **July 16, 2014** - Genesis block
|
||||
- **Block 0-9000** - Proof-of-Work mining phase (Hash9)
|
||||
- **Block 9001+** - Proof-of-Stake only
|
||||
- **Block 17,651** - V5 hard fork (removed Tor v2, disabled checkpoint master key)
|
||||
- **December 8, 2022** - Chain frozen (all nodes offline)
|
||||
- **March 11, 2026** - Chain revived, staking resumed
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
main.cpp - Core blockchain logic, block/tx validation, message routing
|
||||
miner.cpp - Staking miner thread
|
||||
net.cpp - P2P networking
|
||||
init.cpp - Daemon initialization
|
||||
wallet.cpp - Wallet management
|
||||
smessage.cpp/h - Encrypted messaging system
|
||||
kernel.cpp - PoS kernel (stake validation)
|
||||
checkpoints.cpp - Hardcoded checkpoints
|
||||
net_bootstrap.h - DNS/IP seed configuration
|
||||
onionseed.h - Tor v3 onion seed addresses
|
||||
tor/
|
||||
onion_v3.cpp/h - Tor v3 hidden service management
|
||||
tor_crypto_compat.h - Ed25519/SHA3 crypto compatibility
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the MIT/X11 software license. See `COPYING` for details.
|
||||
|
||||
## Links
|
||||
|
||||
- Website: [cryptographic-triangles.org](https://cryptographic-triangles.org)
|
||||
- Explorer: [blocks.cryptographic-triangles.org](https://blocks.cryptographic-triangles.org)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# RocksDB as the default chain database backend
|
||||
|
||||
This change finishes the RocksDB chain-database backend, makes it the default,
|
||||
and provides a transparent migration path off LevelDB. **No consensus rules
|
||||
change** — only how the block index / tx index / UTXO set / address index are
|
||||
stored on disk. On-disk key bytes remain identical across both backends, which
|
||||
is what the migration and the dual-backend equivalence tests rely on.
|
||||
|
||||
## What changed
|
||||
|
||||
### 1. Fixed the column-family iteration bug (the real "unfinished" blocker)
|
||||
|
||||
The RocksDB backend routed keys into per-prefix **column families**
|
||||
(`blockindex`, `txindex`, `utxo`, `addrindex`) on write, but the read path —
|
||||
both `CRocksTxDB::NewIterator()` and `CRocksTxDB::LoadBlockIndex()` — only ever
|
||||
iterated the **default** column family. With column families enabled:
|
||||
|
||||
- `LoadBlockIndex()` loaded **zero** blocks (block-index records were in a
|
||||
non-default CF the loader never scanned),
|
||||
- UTXO snapshot dumps and address-index range scans saw nothing, and
|
||||
- the migration verifier `CollectStats()` reported a record-count mismatch.
|
||||
|
||||
This is why `-chaindb=rocksdb` "compiled clean but was never runtime-valid."
|
||||
|
||||
**Fix:** column-family partitioning is disabled. `GetCF()` now always returns
|
||||
the default CF, so writes, point reads, `Exists`, `Erase`, and full-keyspace
|
||||
iteration are mutually consistent — and byte-identical to the single-keyspace
|
||||
LevelDB backend. New databases are created single-CF; pre-existing experimental
|
||||
multi-CF databases are still opened (for compatibility) but should be
|
||||
re-migrated or reindexed. RocksDB still delivers its performance win from
|
||||
parallel compaction, bloom filters, large write buffer, and block cache — the
|
||||
CF split was a premature optimization, not the source of the speedup.
|
||||
|
||||
Re-introducing column families is a tracked follow-up that first requires
|
||||
CF-aware iterators (a multiplexed merge across CFs) in `NewIterator()` /
|
||||
`LoadBlockIndex()`.
|
||||
|
||||
### 2. Automatic LevelDB -> RocksDB migration on startup
|
||||
|
||||
`init.cpp` now runs the migration automatically when RocksDB is the active
|
||||
backend and the only chain DB present is a legacy `txleveldb/` (no `rocksdb/`
|
||||
yet). `MaybeMigrateLevelDbToRocksDb()` is a no-op when there is nothing to
|
||||
migrate, so it is safe on every launch. The LevelDB source is never modified;
|
||||
it remains a fallback.
|
||||
|
||||
### 3. RocksDB is now the default backend
|
||||
|
||||
`-chaindb` defaults to `rocksdb` (was `leveldb`). LevelDB stays selectable with
|
||||
`-chaindb=leveldb` and is retained as migration source + fallback. Full removal
|
||||
of LevelDB is deferred to a later phase, after live-chain validation.
|
||||
|
||||
### 4. Fixed `NeedsBootstrap()` to recognize the RocksDB directory
|
||||
|
||||
`Bootstrap::NeedsBootstrap()` checked for `txleveldb/` but not `rocksdb/`. With
|
||||
RocksDB as default, a fully-synced rocksdb-only node would have been treated as
|
||||
"fresh" and could have triggered a bootstrap download over a healthy chain on
|
||||
every restart. It now treats a `rocksdb/` directory as an existing chain DB.
|
||||
|
||||
## Files changed
|
||||
|
||||
- `src/txdb-rocksdb.cpp` — disable CF routing; single-CF open; remove dead CF tables
|
||||
- `src/txdb-rocksdb.h` — update CF member docs
|
||||
- `src/txdb-factory.cpp` — default backend `leveldb` -> `rocksdb`
|
||||
- `src/txdb.h` — update factory doc comment
|
||||
- `src/init.cpp` — auto-migrate on startup when RocksDB active + legacy LevelDB present
|
||||
- `src/bootstrap.cpp` — `NeedsBootstrap()` recognizes `rocksdb/`
|
||||
- `src/test/chaindb_runtime_tests.cpp` — update default-backend expectations
|
||||
- `README.md` — document RocksDB default + migration
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cmake -B build -G Ninja -DBUILD_QT=ON -DBUILD_TESTS=ON
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
RocksDB is required (`librocksdb-dev` >= 7.4 on Debian/Ubuntu,
|
||||
`mingw-w64-x86_64-rocksdb` on MSYS2, `rocksdb` on Homebrew).
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
# RocksDB wrapper runtime smoke tests (the class the daemon uses at runtime)
|
||||
./build/bin/test_chaindb_runtime
|
||||
|
||||
# LevelDB/RocksDB byte-for-byte migration equivalence
|
||||
./build/bin/test_chaindb_equivalence
|
||||
|
||||
# Full unit suite
|
||||
./build/bin/test_triangles
|
||||
```
|
||||
|
||||
Expected after this change:
|
||||
- `get_chain_data_dir_default_is_rocksdb` passes (default resolves to rocksdb).
|
||||
- `iterator_walks_every_key_in_sorted_order` passes (the `"banana"` key, which
|
||||
previously routed to a non-default CF the iterator never read, now lives in
|
||||
the default CF and is iterated).
|
||||
- Migration verification (`CollectStats` / `StatsMatch`) passes end-to-end.
|
||||
|
||||
## Live-chain validation checklist (V6 task T010)
|
||||
|
||||
This is the step that cannot be done without real chain data and must be run on
|
||||
a node before release:
|
||||
|
||||
1. **Migrate a real chain.** On a node with an existing `txleveldb/`, launch the
|
||||
new binary (default backend). Confirm the log shows
|
||||
`ChainDB: RocksDB backend active with a legacy LevelDB present; migrating
|
||||
automatically.` followed by `ChainDB migration: verified N records ... best=<hash>`.
|
||||
2. **Verify block index loads.** Confirm `LoadBlockIndex()` reports the correct
|
||||
`height=` and `hashBestChain=` (matching the prior LevelDB tip), not 0.
|
||||
3. **Compare RPC output.** `getinfo`, `getblockcount`, `getbestblockhash`, and a
|
||||
spot-check of `gettxout` / address-index queries must match a LevelDB run of
|
||||
the same datadir (`-chaindb=leveldb`).
|
||||
4. **Restart twice.** Confirm no spurious bootstrap download fires and the tip is
|
||||
stable across restarts.
|
||||
5. **Sync new blocks.** Let the node accept and stake new blocks; confirm UTXO
|
||||
set and money supply stay consistent.
|
||||
6. **Benchmark.** Use `contrib/bench/bench-chaindb.sh --backends=rocksdb` vs
|
||||
`leveldb` to confirm the speedup on this hardware.
|
||||
|
||||
## Rollback
|
||||
|
||||
Set `-chaindb=leveldb` in `triangles.conf` (or on the command line). The
|
||||
original `txleveldb/` is untouched by migration, so reverting is immediate.
|
||||
|
||||
## Remaining follow-ups
|
||||
|
||||
- CF-aware iteration, then re-enable column-family partitioning for independent
|
||||
compaction/caching.
|
||||
- Retire LevelDB entirely (remove `txdb-leveldb.*`, drop the `-chaindb=leveldb`
|
||||
option and the bundled LevelDB dependency) once RocksDB is validated in
|
||||
production for at least one release cycle.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Wallet storage: Berkeley DB → SQLite
|
||||
|
||||
Goal: retire Berkeley DB as the wallet store and make **SQLite the default**
|
||||
wallet backend, with a transparent, non-destructive migration of existing
|
||||
`wallet.dat` files. This removes the single ugliest build dependency (BDB 5.3
|
||||
with C++ bindings, hand-built on RHEL/MSYS2) and gives the wallet a modern,
|
||||
maintainable, single-file store — the kind exchanges expect.
|
||||
|
||||
No consensus or wire behavior changes. The on-disk *record encoding* is
|
||||
unchanged: keys and values are the exact `SER_DISK / CLIENT_VERSION` bytes
|
||||
`CWalletDB` already produces, just stored as `(key BLOB, value BLOB)` rows in
|
||||
SQLite instead of Berkeley B-tree entries. That byte-for-byte identity is what
|
||||
makes migration a verbatim copy.
|
||||
|
||||
## Delivered in this pass
|
||||
|
||||
New, self-contained modules (do not disturb the working Berkeley path):
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/walletdb-base.h` | Backend-agnostic seam: `WalletDatabase`, `WalletBatch` (raw byte Read/Write/Erase/Has + cursor + txn), `WalletCursor`; `ResolveWalletDbKind()` / `MakeWalletDatabase()` declarations. |
|
||||
| `src/walletdb-sqlite.h/.cpp` | `SQLiteDatabase` / `SQLiteBatch` — single `main(key BLOB PRIMARY KEY, value BLOB)` table, `synchronous=FULL`, prepared statements, transactions, cursor, online-backup, `integrity_check`. App-id/user-version stamping to reject foreign DBs. |
|
||||
| `src/walletmigrate.h/.cpp` | `MaybeMigrateBerkeleyWalletToSQLite()` — detects a Berkeley `wallet.dat`, copies every record verbatim into a temp SQLite file, verifies the row count, backs up the original to `wallet.dat.bdb.bak`, then swaps SQLite into place. Idempotent and non-destructive. |
|
||||
| `src/walletdb-factory.cpp` | `ResolveWalletDbKind()` (default **sqlite**, `-walletdb=bdb` fallback) and `MakeWalletDatabase()` (SQLite implemented). |
|
||||
| `src/walletdb-batch.h` | `CWalletBatchTyped` — typed Read/Write/Erase/Exists + cursor over `WalletBatch`, byte-identical to the old `CDB` templates. The drop-in base for `CWalletDB`. |
|
||||
|
||||
Build wiring:
|
||||
- `find_package(SQLite3 REQUIRED)` in the top-level `CMakeLists.txt`.
|
||||
- `SQLite::SQLite3` linked into `triangles_common`; the new sources added to `CORE_SOURCES`.
|
||||
|
||||
## Remaining integration (compile-in-the-loop)
|
||||
|
||||
The new modules are complete but `CWalletDB` is not yet routed through the seam
|
||||
— it still inherits Berkeley `CDB`. This is the mechanical-but-careful step that
|
||||
needs a compiler in the loop. **It must be done and landed as one unit** (it
|
||||
touches `walletdb.h`, `walletdb.cpp`, `wallet.cpp`, `db.cpp`, and `init.cpp`):
|
||||
re-basing ~800 lines of funds-critical code is exactly the kind of change that
|
||||
should be compiled and run against a real `wallet.dat` rather than committed
|
||||
blind.
|
||||
|
||||
1. **Typed wrappers over the batch — DONE.** `src/walletdb-batch.h`
|
||||
(`CWalletBatchTyped`) provides `Read/Write/Erase/Exists` + cursor over a
|
||||
`WalletBatch`, byte-identical to `CDB`'s templates. `CWalletDB` derives from
|
||||
it instead of `CDB`.
|
||||
2. **Re-base `CWalletDB`.** Hold a `std::unique_ptr<WalletDatabase>` +
|
||||
`WalletBatch` obtained from `MakeWalletDatabase("wallet.dat", err)` instead of
|
||||
deriving from `CDB`. Route `TxnBegin/Commit/Abort` to the batch.
|
||||
3. **Cursors.** Replace `GetAtCursor` / `GetTxnCursor` / `ReadAtCursor`
|
||||
(Berkeley `Dbc*`, `DB_NEXT`) in `walletdb.cpp` (`LoadWallet`,
|
||||
`ReorderTransactions`) with `WalletBatch::GetNewCursor()` + `WalletCursor::Next()`.
|
||||
4. **Berkeley-specific call sites.**
|
||||
- `BackupWallet()` / `AutoBackupWallet()` → `WalletDatabase::Backup()`.
|
||||
- `CDB::Rewrite()` (used by `CWallet::EncryptWallet`) → `WalletDatabase::Rewrite()`
|
||||
(VACUUM). Unencrypted-key cleanup already happens via explicit `Erase`.
|
||||
- `bitdb.Flush()` / env shutdown in `init.cpp` → `WalletDatabase::Flush()/Close()`
|
||||
(no-op for SQLite).
|
||||
5. **Berkeley behind the same seam (optional but recommended).** Add a thin
|
||||
`BerkeleyDatabase`/`BerkeleyBatch` adapter wrapping the existing `CDBEnv`/`CDB`
|
||||
so `-walletdb=bdb` routes through `MakeWalletDatabase` too, instead of the
|
||||
legacy path. Keeps one code path for one release, then delete BDB entirely.
|
||||
6. **Run the migration on startup.** In `init.cpp`, before the wallet is loaded
|
||||
and when the backend is SQLite, call
|
||||
`MaybeMigrateBerkeleyWalletToSQLite(GetDataDir()/strWalletFileName, err)`.
|
||||
|
||||
## Gating
|
||||
|
||||
```
|
||||
trianglesd # SQLite (default)
|
||||
trianglesd -walletdb=bdb # Berkeley fallback (retained for one release)
|
||||
```
|
||||
|
||||
## Validation checklist (must pass before release)
|
||||
|
||||
Cannot be verified without a build + a real wallet. Run on a node:
|
||||
|
||||
1. **Build** with `-DBUILD_TESTS=ON`; confirm SQLite is found and linked.
|
||||
2. **Fresh wallet**: start with no wallet → a SQLite `wallet.dat` is created;
|
||||
`getnewaddress`, `getinfo` work; restart preserves keys/balance.
|
||||
3. **Migration**: copy a real Berkeley `wallet.dat` into the datadir, start the
|
||||
node. Confirm: `wallet.dat.bdb.bak` is created, `wallet.dat` is now SQLite
|
||||
(`sqlite3 wallet.dat "PRAGMA integrity_check;"` → `ok`), and
|
||||
`listaddressgroupings` / `getbalance` / `dumpwallet` match a `-walletdb=bdb`
|
||||
run against the `.bdb.bak` original.
|
||||
4. **Key parity**: `dumpwallet` before (bdb) and after (sqlite); diff must be
|
||||
empty (same keys, labels, metadata, HD seed).
|
||||
5. **Encryption**: `encryptwallet`, restart, `walletpassphrase`, sign/spend.
|
||||
6. **Backup/restore**: `backupwallet`, restore into a fresh datadir, verify
|
||||
balance and spend.
|
||||
7. **Send/receive + staking** over a few blocks; confirm new keys/txns persist
|
||||
across restart.
|
||||
8. **Crash safety**: kill -9 mid-write; restart; `integrity_check` ok, no loss.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Add `test_wallet_sqlite` unit tests (round-trip, migration parity, cursor).
|
||||
- Once SQLite is validated for a release, remove `-walletdb=bdb`, delete
|
||||
`db.cpp`/`walletdb`'s Berkeley code, and drop the `BerkeleyDB` CMake
|
||||
dependency — completing the retirement.
|
||||
+685
-475
File diff suppressed because it is too large
Load Diff
+1110
-808
File diff suppressed because it is too large
Load Diff
+1934
-1564
File diff suppressed because it is too large
Load Diff
+89
-82
@@ -1,32 +1,27 @@
|
||||
// Copyright (c) 2009-2012 The Bitcoin developers
|
||||
// Copyright (c) 2026 The Triangles developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <boost/version.hpp>
|
||||
#if defined(WIN32) && BOOST_VERSION == 104900
|
||||
#define BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME
|
||||
#define BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME
|
||||
#endif
|
||||
//
|
||||
// Single-instance "triangles:" URI handoff. When the wallet is launched with a
|
||||
// URI argument and an instance is already running, the URI is relayed to the
|
||||
// running instance over a local socket; otherwise this instance becomes the
|
||||
// listener. Reworked from Boost.Interprocess message queues onto Qt's
|
||||
// QLocalServer/QLocalSocket (QtNetwork) — no Boost dependency.
|
||||
|
||||
#include "qtipcserver.h"
|
||||
#include "guiconstants.h"
|
||||
#include "ui_interface.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
||||
#include <boost/interprocess/ipc/message_queue.hpp>
|
||||
#include <boost/version.hpp>
|
||||
|
||||
#if defined(WIN32) && (!defined(BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME) || !defined(BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME) || BOOST_VERSION < 104900)
|
||||
#warning Compiling without BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME and BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME uncommented in boost/interprocess/detail/tmp_dir_helpers.hpp or using a boost version before 1.49 may have unintended results see svn.boost.org/trac/boost/ticket/5392
|
||||
#endif
|
||||
|
||||
using namespace boost;
|
||||
using namespace boost::interprocess;
|
||||
using namespace boost::posix_time;
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QLocalServer>
|
||||
#include <QLocalSocket>
|
||||
#include <QString>
|
||||
|
||||
#if defined MAC_OSX || defined __FreeBSD__
|
||||
// URI handling not implemented on OSX yet
|
||||
@@ -36,33 +31,47 @@ void ipcInit(int argc, char *argv[]) { }
|
||||
|
||||
#else
|
||||
|
||||
// Local-socket server name. QLocalServer maps this to a named pipe on Windows
|
||||
// and a filesystem socket on Unix.
|
||||
static const QString IPC_SERVER_NAME = QStringLiteral(TRIANGLESURI_QUEUE_NAME);
|
||||
|
||||
static void ipcThread2(void* pArg);
|
||||
|
||||
static bool IsTrianglesURI(const char* arg)
|
||||
{
|
||||
// Case-insensitive match of the "Triangles:" scheme prefix.
|
||||
return std::equal(std::begin("Triangles:"), std::end("Triangles:") - 1, arg,
|
||||
[](char a, char b) {
|
||||
return std::tolower(static_cast<unsigned char>(a)) ==
|
||||
std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
}
|
||||
|
||||
static bool ipcScanCmd(int argc, char *argv[], bool fRelay)
|
||||
{
|
||||
// Check for URI in argv
|
||||
// Check for URI in argv and relay it to a running instance, if any.
|
||||
bool fSent = false;
|
||||
for (int i = 1; i < argc; i++)
|
||||
{
|
||||
if (std::equal(std::begin("Triangles:"), std::end("Triangles:") - 1, argv[i], [](char a, char b) { return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b)); }))
|
||||
if (!IsTrianglesURI(argv[i]))
|
||||
continue;
|
||||
|
||||
const char *strURI = argv[i];
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(IPC_SERVER_NAME);
|
||||
if (socket.waitForConnected(1000))
|
||||
{
|
||||
const char *strURI = argv[i];
|
||||
try {
|
||||
boost::interprocess::message_queue mq(boost::interprocess::open_only, TRIANGLESURI_QUEUE_NAME);
|
||||
if (mq.try_send(strURI, strlen(strURI), 0))
|
||||
fSent = true;
|
||||
else if (fRelay)
|
||||
break;
|
||||
}
|
||||
catch (boost::interprocess::interprocess_exception &ex) {
|
||||
// don't log the "file not found" exception, because that's normal for
|
||||
// the first start of the first instance
|
||||
if (ex.get_error_code() != boost::interprocess::not_found_error || !fRelay)
|
||||
{
|
||||
printf("main() - boost interprocess exception #%d: %s\n", ex.get_error_code(), ex.what());
|
||||
break;
|
||||
}
|
||||
}
|
||||
socket.write(strURI, static_cast<qint64>(strlen(strURI)));
|
||||
socket.flush();
|
||||
socket.waitForBytesWritten(1000);
|
||||
socket.disconnectFromServer();
|
||||
fSent = true;
|
||||
}
|
||||
else if (fRelay)
|
||||
{
|
||||
// No running instance accepted the URI; this process should become
|
||||
// the listener instead of relaying.
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fSent;
|
||||
@@ -78,7 +87,7 @@ static void ipcThread(void* pArg)
|
||||
{
|
||||
// Make this thread recognisable as the GUI-IPC thread
|
||||
RenameThread("Triangles-gui-ipc");
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
ipcThread2(pArg);
|
||||
@@ -95,69 +104,67 @@ static void ipcThread2(void* pArg)
|
||||
{
|
||||
printf("ipcThread started\n");
|
||||
|
||||
message_queue* mq = (message_queue*)pArg;
|
||||
char buffer[MAX_URI_LENGTH + 1] = "";
|
||||
size_t nSize = 0;
|
||||
unsigned int nPriority = 0;
|
||||
QLocalServer* server = static_cast<QLocalServer*>(pArg);
|
||||
|
||||
// Poll for inbound connections without requiring a Qt event loop:
|
||||
// waitForNewConnection(timeout) pumps the socket internally.
|
||||
while (true)
|
||||
{
|
||||
ptime d = boost::posix_time::microsec_clock::universal_time() + millisec(100);
|
||||
if (mq->timed_receive(&buffer, sizeof(buffer), nSize, nPriority, d))
|
||||
if (server->waitForNewConnection(100))
|
||||
{
|
||||
uiInterface.ThreadSafeHandleURI(std::string(buffer, nSize));
|
||||
MilliSleep(1000);
|
||||
QLocalSocket* client = server->nextPendingConnection();
|
||||
if (client)
|
||||
{
|
||||
if (client->waitForReadyRead(1000))
|
||||
{
|
||||
QByteArray data = client->readAll();
|
||||
if (data.size() > MAX_URI_LENGTH)
|
||||
data.truncate(MAX_URI_LENGTH);
|
||||
uiInterface.ThreadSafeHandleURI(std::string(data.constData(), data.size()));
|
||||
MilliSleep(1000);
|
||||
}
|
||||
client->disconnectFromServer();
|
||||
delete client;
|
||||
}
|
||||
}
|
||||
|
||||
if (fShutdown)
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove message queue
|
||||
message_queue::remove(TRIANGLESURI_QUEUE_NAME);
|
||||
// Cleanup allocated memory
|
||||
delete mq;
|
||||
server->close();
|
||||
delete server;
|
||||
}
|
||||
|
||||
void ipcInit(int argc, char *argv[])
|
||||
{
|
||||
message_queue* mq = NULL;
|
||||
char buffer[MAX_URI_LENGTH + 1] = "";
|
||||
size_t nSize = 0;
|
||||
unsigned int nPriority = 0;
|
||||
// Clear any stale socket/pipe left by a previous crashed instance, then
|
||||
// listen. If listen() fails, another instance already owns the name — in
|
||||
// that case relay our own URI args (below) and don't start a server.
|
||||
QLocalServer::removeServer(IPC_SERVER_NAME);
|
||||
|
||||
try {
|
||||
mq = new message_queue(open_or_create, TRIANGLESURI_QUEUE_NAME, 2, MAX_URI_LENGTH);
|
||||
|
||||
// Make sure we don't lose any Triangles: URIs
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
ptime d = boost::posix_time::microsec_clock::universal_time() + millisec(1);
|
||||
if (mq->timed_receive(&buffer, sizeof(buffer), nSize, nPriority, d))
|
||||
{
|
||||
uiInterface.ThreadSafeHandleURI(std::string(buffer, nSize));
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// Make sure only one Triangles instance is listening
|
||||
message_queue::remove(TRIANGLESURI_QUEUE_NAME);
|
||||
delete mq;
|
||||
|
||||
mq = new message_queue(open_or_create, TRIANGLESURI_QUEUE_NAME, 2, MAX_URI_LENGTH);
|
||||
}
|
||||
catch (interprocess_exception &ex) {
|
||||
printf("ipcInit() - boost interprocess exception #%d: %s\n", ex.get_error_code(), ex.what());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NewThread(ipcThread, mq))
|
||||
QLocalServer* server = new QLocalServer();
|
||||
server->setSocketOptions(QLocalServer::UserAccessOption); // owner-only access
|
||||
if (!server->listen(IPC_SERVER_NAME))
|
||||
{
|
||||
delete mq;
|
||||
printf("ipcInit() - QLocalServer listen failed: %s\n",
|
||||
server->errorString().toUtf8().constData());
|
||||
delete server;
|
||||
// Still try to relay any URI passed on our command line to whoever is
|
||||
// listening.
|
||||
ipcScanCmd(argc, argv, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NewThread(ipcThread, server))
|
||||
{
|
||||
server->close();
|
||||
delete server;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle a URI passed on our own command line (relayed to the server we
|
||||
// just started).
|
||||
ipcScanCmd(argc, argv, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
//
|
||||
// Raw-socket transport for the JSON-RPC / REST HTTP server, replacing the
|
||||
// previous Boost.Asio implementation. Provides:
|
||||
//
|
||||
// - CSocketIOStream : a std::iostream backed by a connected SOCKET, so the
|
||||
// existing HTTP/JSON/SSE/REST code (which reads and writes std::iostream)
|
||||
// is unchanged.
|
||||
// - ConnectRPCSocket() : client-side connect (used by CallRPC).
|
||||
// - BindRPCSockets() : create listening sockets for the RPC server.
|
||||
// - SockaddrToString() : numeric host string for a peer address.
|
||||
//
|
||||
// TLS for the RPC port is intentionally not supported here (it was a rarely
|
||||
// used Boost.Asio::ssl feature). For remote access, front the RPC port with a
|
||||
// TLS terminator (stunnel / nginx) or reach it over SSH / Tor — the same
|
||||
// guidance Bitcoin Core adopted when it moved its RPC server off Boost.Asio.
|
||||
|
||||
#ifndef TRIANGLES_RPC_HTTPSOCKET_H
|
||||
#define TRIANGLES_RPC_HTTPSOCKET_H
|
||||
|
||||
#include "compat.h" // SOCKET, closesocket, INVALID_SOCKET, MSG_NOSIGNAL
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <streambuf>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef WIN32
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#endif
|
||||
|
||||
// ── std::streambuf over a connected socket ──────────────────────────────────
|
||||
class CSocketStreamBuf : public std::streambuf
|
||||
{
|
||||
public:
|
||||
explicit CSocketStreamBuf(SOCKET s) : m_socket(s)
|
||||
{
|
||||
setg(m_in, m_in, m_in); // empty get area to start
|
||||
}
|
||||
|
||||
protected:
|
||||
// Refill the get area with one recv().
|
||||
int_type underflow() override
|
||||
{
|
||||
if (gptr() < egptr())
|
||||
return traits_type::to_int_type(*gptr());
|
||||
int n = ::recv(m_socket, m_in, static_cast<int>(sizeof(m_in)), 0);
|
||||
if (n <= 0)
|
||||
return traits_type::eof(); // peer closed or error
|
||||
setg(m_in, m_in, m_in + n);
|
||||
return traits_type::to_int_type(*gptr());
|
||||
}
|
||||
|
||||
// Bulk write (operator<< on strings lands here).
|
||||
std::streamsize xsputn(const char* s, std::streamsize n) override
|
||||
{
|
||||
return SendAll(s, n) ? n : 0;
|
||||
}
|
||||
|
||||
int_type overflow(int_type ch) override
|
||||
{
|
||||
if (traits_type::eq_int_type(ch, traits_type::eof()))
|
||||
return traits_type::not_eof(ch);
|
||||
char c = static_cast<char>(ch);
|
||||
return SendAll(&c, 1) ? ch : traits_type::eof();
|
||||
}
|
||||
|
||||
int sync() override { return 0; } // sends are immediate; nothing buffered
|
||||
|
||||
private:
|
||||
bool SendAll(const char* s, std::streamsize n)
|
||||
{
|
||||
std::streamsize sent = 0;
|
||||
while (sent < n) {
|
||||
int r = ::send(m_socket, s + sent, static_cast<int>(n - sent), MSG_NOSIGNAL);
|
||||
if (r <= 0)
|
||||
return false;
|
||||
sent += r;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
SOCKET m_socket;
|
||||
char m_in[8192];
|
||||
};
|
||||
|
||||
// std::iostream that owns a CSocketStreamBuf bound to a socket. The socket
|
||||
// itself is owned by the caller (AcceptedConnection / CallRPC), not closed here.
|
||||
class CSocketIOStream : public std::iostream
|
||||
{
|
||||
public:
|
||||
explicit CSocketIOStream(SOCKET s) : std::iostream(nullptr), m_buf(s)
|
||||
{
|
||||
rdbuf(&m_buf);
|
||||
}
|
||||
|
||||
private:
|
||||
CSocketStreamBuf m_buf;
|
||||
};
|
||||
|
||||
// Numeric (no DNS) host string for a peer sockaddr, e.g. "127.0.0.1" or "::1".
|
||||
inline std::string SockaddrToString(const struct sockaddr* sa, socklen_t salen)
|
||||
{
|
||||
char host[NI_MAXHOST] = {0};
|
||||
if (::getnameinfo(sa, salen, host, sizeof(host), nullptr, 0, NI_NUMERICHOST) != 0)
|
||||
return "unknown";
|
||||
return std::string(host);
|
||||
}
|
||||
|
||||
// Client connect to host:port. Returns INVALID_SOCKET on failure.
|
||||
inline SOCKET ConnectRPCSocket(const std::string& host, int port)
|
||||
{
|
||||
struct addrinfo hints;
|
||||
std::memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
struct addrinfo* res = nullptr;
|
||||
const std::string portStr = std::to_string(port);
|
||||
if (::getaddrinfo(host.c_str(), portStr.c_str(), &hints, &res) != 0)
|
||||
return INVALID_SOCKET;
|
||||
|
||||
SOCKET hSocket = INVALID_SOCKET;
|
||||
for (struct addrinfo* rp = res; rp != nullptr; rp = rp->ai_next) {
|
||||
hSocket = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
if (hSocket == INVALID_SOCKET)
|
||||
continue;
|
||||
if (::connect(hSocket, rp->ai_addr, static_cast<int>(rp->ai_addrlen)) == 0)
|
||||
break;
|
||||
closesocket(hSocket);
|
||||
hSocket = INVALID_SOCKET;
|
||||
}
|
||||
::freeaddrinfo(res);
|
||||
return hSocket;
|
||||
}
|
||||
|
||||
// Create listening sockets for the RPC server. When loopbackOnly is true the
|
||||
// server binds the loopback interface(s) only; otherwise it binds the wildcard
|
||||
// address(es). IPv4 and IPv6 are bound on separate sockets (IPV6_V6ONLY) so the
|
||||
// two never conflict. Returns the bound, listening sockets; empty + strError on
|
||||
// total failure (partial success — e.g. only IPv4 — is returned as success).
|
||||
inline std::vector<SOCKET> BindRPCSockets(int port, bool loopbackOnly, std::string& strError)
|
||||
{
|
||||
std::vector<SOCKET> vListen;
|
||||
|
||||
struct addrinfo hints;
|
||||
std::memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_flags = AI_PASSIVE; // wildcard when node == nullptr
|
||||
|
||||
struct addrinfo* res = nullptr;
|
||||
const std::string portStr = std::to_string(port);
|
||||
// "localhost" resolves to the loopback addresses (127.0.0.1 and ::1);
|
||||
// nullptr + AI_PASSIVE yields the wildcard addresses.
|
||||
const char* node = loopbackOnly ? "localhost" : nullptr;
|
||||
int gai = ::getaddrinfo(node, portStr.c_str(), &hints, &res);
|
||||
if (gai != 0) {
|
||||
strError = std::string("RPC bind: getaddrinfo failed: ") + gai_strerror(gai);
|
||||
return vListen;
|
||||
}
|
||||
|
||||
for (struct addrinfo* rp = res; rp != nullptr; rp = rp->ai_next) {
|
||||
if (rp->ai_family != AF_INET && rp->ai_family != AF_INET6)
|
||||
continue;
|
||||
SOCKET s = ::socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
if (s == INVALID_SOCKET)
|
||||
continue;
|
||||
|
||||
int one = 1;
|
||||
::setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
|
||||
reinterpret_cast<const char*>(&one), sizeof(one));
|
||||
if (rp->ai_family == AF_INET6) {
|
||||
// Keep IPv6 sockets v6-only so a separate IPv4 socket can also bind.
|
||||
::setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY,
|
||||
reinterpret_cast<const char*>(&one), sizeof(one));
|
||||
}
|
||||
|
||||
if (::bind(s, rp->ai_addr, static_cast<int>(rp->ai_addrlen)) != 0 ||
|
||||
::listen(s, SOMAXCONN) != 0) {
|
||||
closesocket(s);
|
||||
continue;
|
||||
}
|
||||
vListen.push_back(s);
|
||||
}
|
||||
::freeaddrinfo(res);
|
||||
|
||||
if (vListen.empty())
|
||||
strError = "RPC bind: could not bind any address (port in use?)";
|
||||
return vListen;
|
||||
}
|
||||
|
||||
#endif // TRIANGLES_RPC_HTTPSOCKET_H
|
||||
+315
-319
@@ -1,319 +1,315 @@
|
||||
// Copyright (c) 2009-2012 Bitcoin Developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
|
||||
#include "init.h" // for pwalletMain
|
||||
#include "trianglesrpc.h"
|
||||
#include "ui_interface.h"
|
||||
#include "base58.h"
|
||||
|
||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
||||
|
||||
#define printf OutputDebugStringF
|
||||
|
||||
using namespace json_spirit;
|
||||
using namespace std;
|
||||
|
||||
void EnsureWalletIsUnlocked();
|
||||
|
||||
namespace bt = boost::posix_time;
|
||||
|
||||
// Extended DecodeDumpTime implementation, see this page for details:
|
||||
// http://stackoverflow.com/questions/3786201/parsing-of-date-time-from-string-boost
|
||||
const std::locale formats[] = {
|
||||
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%dT%H:%M:%SZ")),
|
||||
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%d %H:%M:%S")),
|
||||
std::locale(std::locale::classic(),new bt::time_input_facet("%Y/%m/%d %H:%M:%S")),
|
||||
std::locale(std::locale::classic(),new bt::time_input_facet("%d.%m.%Y %H:%M:%S")),
|
||||
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%d"))
|
||||
};
|
||||
|
||||
const size_t formats_n = sizeof(formats)/sizeof(formats[0]);
|
||||
|
||||
std::time_t pt_to_time_t(const bt::ptime& pt)
|
||||
{
|
||||
bt::ptime timet_start(boost::gregorian::date(1970,1,1));
|
||||
bt::time_duration diff = pt - timet_start;
|
||||
return diff.ticks()/bt::time_duration::rep_type::ticks_per_second;
|
||||
}
|
||||
|
||||
int64_t DecodeDumpTime(const std::string& s)
|
||||
{
|
||||
bt::ptime pt;
|
||||
|
||||
for(size_t i=0; i<formats_n; ++i)
|
||||
{
|
||||
std::istringstream is(s);
|
||||
is.imbue(formats[i]);
|
||||
is >> pt;
|
||||
if(pt != bt::ptime()) break;
|
||||
}
|
||||
|
||||
return pt_to_time_t(pt);
|
||||
}
|
||||
|
||||
std::string static EncodeDumpTime(int64_t nTime) {
|
||||
return DateTimeStrFormat("%Y-%m-%dT%H:%M:%SZ", nTime);
|
||||
}
|
||||
|
||||
std::string static EncodeDumpString(const std::string &str) {
|
||||
std::stringstream ret;
|
||||
for (unsigned char c : str) {
|
||||
if (c <= 32 || c >= 128 || c == '%') {
|
||||
ret << '%' << HexStr(&c, &c + 1);
|
||||
} else {
|
||||
ret << c;
|
||||
}
|
||||
}
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
std::string DecodeDumpString(const std::string &str) {
|
||||
std::stringstream ret;
|
||||
for (unsigned int pos = 0; pos < str.length(); pos++) {
|
||||
unsigned char c = str[pos];
|
||||
if (c == '%' && pos+2 < str.length()) {
|
||||
c = (((str[pos+1]>>6)*9+((str[pos+1]-'0')&15)) << 4) |
|
||||
((str[pos+2]>>6)*9+((str[pos+2]-'0')&15));
|
||||
pos += 2;
|
||||
}
|
||||
ret << c;
|
||||
}
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
class CTxDump
|
||||
{
|
||||
public:
|
||||
CBlockIndex *pindex;
|
||||
int64_t nValue;
|
||||
bool fSpent;
|
||||
CWalletTx* ptx;
|
||||
int nOut;
|
||||
CTxDump(CWalletTx* ptx = nullptr, int nOut = -1)
|
||||
{
|
||||
pindex = nullptr;
|
||||
nValue = 0;
|
||||
fSpent = false;
|
||||
this->ptx = ptx;
|
||||
this->nOut = nOut;
|
||||
}
|
||||
};
|
||||
|
||||
Value importprivkey(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() < 1 || params.size() > 2)
|
||||
throw runtime_error(
|
||||
"importprivkey <Trianglesprivkey> [label]\n"
|
||||
"Adds a private key (as returned by dumpprivkey) to your wallet.");
|
||||
|
||||
string strSecret = params[0].get_str();
|
||||
string strLabel = "";
|
||||
if (params.size() > 1)
|
||||
strLabel = params[1].get_str();
|
||||
CTrianglesSecret vchSecret;
|
||||
bool fGood = vchSecret.SetString(strSecret);
|
||||
|
||||
if (!fGood) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key");
|
||||
if (fWalletUnlockStakingOnly)
|
||||
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
|
||||
|
||||
CKey key;
|
||||
bool fCompressed;
|
||||
CSecret secret = vchSecret.GetSecret(fCompressed);
|
||||
key.SetSecret(secret, fCompressed);
|
||||
CKeyID vchAddress = key.GetPubKey().GetID();
|
||||
{
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
|
||||
pwalletMain->MarkDirty();
|
||||
pwalletMain->SetAddressBookName(vchAddress, strLabel);
|
||||
|
||||
if (!pwalletMain->AddKey(key))
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet");
|
||||
|
||||
pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
|
||||
pwalletMain->ReacceptWalletTransactions();
|
||||
}
|
||||
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
Value importwallet(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"importwallet <filename>\n"
|
||||
"Imports keys from a wallet dump file (see dumpwallet).");
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
ifstream file;
|
||||
file.open(params[0].get_str().c_str());
|
||||
if (!file.is_open())
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
|
||||
|
||||
int64_t nTimeBegin = pindexBest->nTime;
|
||||
|
||||
bool fGood = true;
|
||||
|
||||
while (file.good()) {
|
||||
std::string line;
|
||||
std::getline(file, line);
|
||||
if (line.empty() || line[0] == '#')
|
||||
continue;
|
||||
|
||||
auto vstr = SplitString(line, ' ');
|
||||
if (vstr.size() < 2)
|
||||
continue;
|
||||
CTrianglesSecret vchSecret;
|
||||
if (!vchSecret.SetString(vstr[0]))
|
||||
continue;
|
||||
|
||||
bool fCompressed;
|
||||
CKey key;
|
||||
CSecret secret = vchSecret.GetSecret(fCompressed);
|
||||
key.SetSecret(secret, fCompressed);
|
||||
CKeyID keyid = key.GetPubKey().GetID();
|
||||
|
||||
if (pwalletMain->HaveKey(keyid)) {
|
||||
printf("Skipping import of %s (key already present)\n", CTrianglesAddress(keyid).ToString().c_str());
|
||||
continue;
|
||||
}
|
||||
int64_t nTime = DecodeDumpTime(vstr[1]);
|
||||
std::string strLabel;
|
||||
bool fLabel = true;
|
||||
for (unsigned int nStr = 2; nStr < vstr.size(); nStr++) {
|
||||
if (vstr[nStr].starts_with("#"))
|
||||
break;
|
||||
if (vstr[nStr] == "change=1")
|
||||
fLabel = false;
|
||||
if (vstr[nStr] == "reserve=1")
|
||||
fLabel = false;
|
||||
if (vstr[nStr].starts_with("label=")) {
|
||||
strLabel = DecodeDumpString(vstr[nStr].substr(6));
|
||||
fLabel = true;
|
||||
}
|
||||
}
|
||||
printf("Importing %s...\n", CTrianglesAddress(keyid).ToString().c_str());
|
||||
if (!pwalletMain->AddKey(key)) {
|
||||
fGood = false;
|
||||
continue;
|
||||
}
|
||||
pwalletMain->mapKeyMetadata[keyid].nCreateTime = nTime;
|
||||
if (fLabel)
|
||||
pwalletMain->SetAddressBookName(keyid, strLabel);
|
||||
nTimeBegin = std::min(nTimeBegin, nTime);
|
||||
}
|
||||
file.close();
|
||||
|
||||
CBlockIndex *pindex = pindexBest;
|
||||
while (pindex && pindex->pprev && pindex->nTime > nTimeBegin - 7200)
|
||||
pindex = pindex->pprev;
|
||||
|
||||
if (!pwalletMain->nTimeFirstKey || nTimeBegin < pwalletMain->nTimeFirstKey)
|
||||
pwalletMain->nTimeFirstKey = nTimeBegin;
|
||||
|
||||
printf("Rescanning last %i blocks\n", pindexBest->nHeight - pindex->nHeight + 1);
|
||||
pwalletMain->ScanForWalletTransactions(pindex);
|
||||
pwalletMain->ReacceptWalletTransactions();
|
||||
pwalletMain->MarkDirty();
|
||||
|
||||
if (!fGood)
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding some keys to wallet");
|
||||
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
|
||||
Value dumpprivkey(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"dumpprivkey <Trianglesaddress>\n"
|
||||
"Reveals the private key corresponding to <Trianglesaddress>.");
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
string strAddress = params[0].get_str();
|
||||
CTrianglesAddress address;
|
||||
if (!address.SetString(strAddress))
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Triangles address");
|
||||
if (fWalletUnlockStakingOnly)
|
||||
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
|
||||
CKeyID keyID;
|
||||
if (!address.GetKeyID(keyID))
|
||||
throw JSONRPCError(RPC_TYPE_ERROR, "Address does not refer to a key");
|
||||
CSecret vchSecret;
|
||||
bool fCompressed;
|
||||
if (!pwalletMain->GetSecret(keyID, vchSecret, fCompressed))
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Private key for address " + strAddress + " is not known");
|
||||
return CTrianglesSecret(vchSecret, fCompressed).ToString();
|
||||
}
|
||||
|
||||
Value dumpwallet(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"dumpwallet <filename>\n"
|
||||
"Dumps all wallet keys in a human-readable format.");
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
ofstream file;
|
||||
file.open(params[0].get_str().c_str());
|
||||
if (!file.is_open())
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
|
||||
|
||||
std::map<CKeyID, int64_t> mapKeyBirth;
|
||||
|
||||
std::set<CKeyID> setKeyPool;
|
||||
|
||||
pwalletMain->GetKeyBirthTimes(mapKeyBirth);
|
||||
|
||||
pwalletMain->GetAllReserveKeys(setKeyPool);
|
||||
|
||||
// sort time/key pairs
|
||||
std::vector<std::pair<int64_t, CKeyID> > vKeyBirth;
|
||||
for (std::map<CKeyID, int64_t>::const_iterator it = mapKeyBirth.begin(); it != mapKeyBirth.end(); it++) {
|
||||
vKeyBirth.push_back({it->second, it->first});
|
||||
}
|
||||
mapKeyBirth.clear();
|
||||
std::sort(vKeyBirth.begin(), vKeyBirth.end());
|
||||
|
||||
// produce output
|
||||
file << strprintf("# Wallet dump created by Triangles %s (%s)\n", CLIENT_BUILD.c_str(), CLIENT_DATE.c_str());
|
||||
file << strprintf("# * Created on %s\n", EncodeDumpTime(GetTime()).c_str());
|
||||
file << strprintf("# * Best block at time of backup was %i (%s),\n", nBestHeight, hashBestChain.ToString().c_str());
|
||||
file << strprintf("# mined on %s\n", EncodeDumpTime(pindexBest->nTime).c_str());
|
||||
file << "\n";
|
||||
for (std::vector<std::pair<int64_t, CKeyID> >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) {
|
||||
const CKeyID &keyid = it->second;
|
||||
std::string strTime = EncodeDumpTime(it->first);
|
||||
std::string strAddr = CTrianglesAddress(keyid).ToString();
|
||||
bool IsCompressed;
|
||||
|
||||
CKey key;
|
||||
if (pwalletMain->GetKey(keyid, key)) {
|
||||
if (pwalletMain->mapAddressBook.count(keyid)) {
|
||||
CSecret secret = key.GetSecret(IsCompressed);
|
||||
file << strprintf("%s %s label=%s # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), EncodeDumpString(pwalletMain->mapAddressBook[keyid]).c_str(), strAddr.c_str());
|
||||
} else if (setKeyPool.count(keyid)) {
|
||||
CSecret secret = key.GetSecret(IsCompressed);
|
||||
file << strprintf("%s %s reserve=1 # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), strAddr.c_str());
|
||||
} else {
|
||||
CSecret secret = key.GetSecret(IsCompressed);
|
||||
file << strprintf("%s %s change=1 # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), strAddr.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
file << "\n";
|
||||
file << "# End of dump\n";
|
||||
file.close();
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
|
||||
// Copyright (c) 2009-2012 Bitcoin Developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <ctime>
|
||||
|
||||
#include "init.h" // for pwalletMain
|
||||
#include "trianglesrpc.h"
|
||||
#include "ui_interface.h"
|
||||
#include "base58.h"
|
||||
|
||||
#define printf OutputDebugStringF
|
||||
|
||||
using namespace json_spirit;
|
||||
using namespace std;
|
||||
|
||||
void EnsureWalletIsUnlocked();
|
||||
|
||||
// Accepted timestamp formats, tried in order. Replaces the boost::posix_time
|
||||
// parser; std::get_time is portable (C++11) and parses against each format.
|
||||
static const char* const dumptime_formats[] = {
|
||||
"%Y-%m-%dT%H:%M:%SZ",
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y/%m/%d %H:%M:%S",
|
||||
"%d.%m.%Y %H:%M:%S",
|
||||
"%Y-%m-%d",
|
||||
};
|
||||
|
||||
int64_t DecodeDumpTime(const std::string& s)
|
||||
{
|
||||
for (const char* fmt : dumptime_formats)
|
||||
{
|
||||
std::tm tm = {};
|
||||
std::istringstream is(s);
|
||||
is >> std::get_time(&tm, fmt);
|
||||
if (is.fail())
|
||||
continue;
|
||||
// Interpret the parsed broken-down time as UTC.
|
||||
#ifdef WIN32
|
||||
std::time_t t = _mkgmtime(&tm);
|
||||
#else
|
||||
std::time_t t = timegm(&tm);
|
||||
#endif
|
||||
if (t != static_cast<std::time_t>(-1))
|
||||
return static_cast<int64_t>(t);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string static EncodeDumpTime(int64_t nTime) {
|
||||
return DateTimeStrFormat("%Y-%m-%dT%H:%M:%SZ", nTime);
|
||||
}
|
||||
|
||||
std::string static EncodeDumpString(const std::string &str) {
|
||||
std::stringstream ret;
|
||||
for (unsigned char c : str) {
|
||||
if (c <= 32 || c >= 128 || c == '%') {
|
||||
ret << '%' << HexStr(&c, &c + 1);
|
||||
} else {
|
||||
ret << c;
|
||||
}
|
||||
}
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
std::string DecodeDumpString(const std::string &str) {
|
||||
std::stringstream ret;
|
||||
for (unsigned int pos = 0; pos < str.length(); pos++) {
|
||||
unsigned char c = str[pos];
|
||||
if (c == '%' && pos+2 < str.length()) {
|
||||
c = (((str[pos+1]>>6)*9+((str[pos+1]-'0')&15)) << 4) |
|
||||
((str[pos+2]>>6)*9+((str[pos+2]-'0')&15));
|
||||
pos += 2;
|
||||
}
|
||||
ret << c;
|
||||
}
|
||||
return ret.str();
|
||||
}
|
||||
|
||||
class CTxDump
|
||||
{
|
||||
public:
|
||||
CBlockIndex *pindex;
|
||||
int64_t nValue;
|
||||
bool fSpent;
|
||||
CWalletTx* ptx;
|
||||
int nOut;
|
||||
CTxDump(CWalletTx* ptx = nullptr, int nOut = -1)
|
||||
{
|
||||
pindex = nullptr;
|
||||
nValue = 0;
|
||||
fSpent = false;
|
||||
this->ptx = ptx;
|
||||
this->nOut = nOut;
|
||||
}
|
||||
};
|
||||
|
||||
Value importprivkey(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() < 1 || params.size() > 2)
|
||||
throw runtime_error(
|
||||
"importprivkey <Trianglesprivkey> [label]\n"
|
||||
"Adds a private key (as returned by dumpprivkey) to your wallet.");
|
||||
|
||||
string strSecret = params[0].get_str();
|
||||
string strLabel = "";
|
||||
if (params.size() > 1)
|
||||
strLabel = params[1].get_str();
|
||||
CTrianglesSecret vchSecret;
|
||||
bool fGood = vchSecret.SetString(strSecret);
|
||||
|
||||
if (!fGood) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key");
|
||||
if (fWalletUnlockStakingOnly)
|
||||
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
|
||||
|
||||
CKey key;
|
||||
bool fCompressed;
|
||||
CSecret secret = vchSecret.GetSecret(fCompressed);
|
||||
key.SetSecret(secret, fCompressed);
|
||||
CKeyID vchAddress = key.GetPubKey().GetID();
|
||||
{
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
|
||||
pwalletMain->MarkDirty();
|
||||
pwalletMain->SetAddressBookName(vchAddress, strLabel);
|
||||
|
||||
if (!pwalletMain->AddKey(key))
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet");
|
||||
|
||||
pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
|
||||
pwalletMain->ReacceptWalletTransactions();
|
||||
}
|
||||
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
Value importwallet(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"importwallet <filename>\n"
|
||||
"Imports keys from a wallet dump file (see dumpwallet).");
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
ifstream file;
|
||||
file.open(params[0].get_str().c_str());
|
||||
if (!file.is_open())
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
|
||||
|
||||
int64_t nTimeBegin = pindexBest->nTime;
|
||||
|
||||
bool fGood = true;
|
||||
|
||||
while (file.good()) {
|
||||
std::string line;
|
||||
std::getline(file, line);
|
||||
if (line.empty() || line[0] == '#')
|
||||
continue;
|
||||
|
||||
auto vstr = SplitString(line, ' ');
|
||||
if (vstr.size() < 2)
|
||||
continue;
|
||||
CTrianglesSecret vchSecret;
|
||||
if (!vchSecret.SetString(vstr[0]))
|
||||
continue;
|
||||
|
||||
bool fCompressed;
|
||||
CKey key;
|
||||
CSecret secret = vchSecret.GetSecret(fCompressed);
|
||||
key.SetSecret(secret, fCompressed);
|
||||
CKeyID keyid = key.GetPubKey().GetID();
|
||||
|
||||
if (pwalletMain->HaveKey(keyid)) {
|
||||
printf("Skipping import of %s (key already present)\n", CTrianglesAddress(keyid).ToString().c_str());
|
||||
continue;
|
||||
}
|
||||
int64_t nTime = DecodeDumpTime(vstr[1]);
|
||||
std::string strLabel;
|
||||
bool fLabel = true;
|
||||
for (unsigned int nStr = 2; nStr < vstr.size(); nStr++) {
|
||||
if (vstr[nStr].starts_with("#"))
|
||||
break;
|
||||
if (vstr[nStr] == "change=1")
|
||||
fLabel = false;
|
||||
if (vstr[nStr] == "reserve=1")
|
||||
fLabel = false;
|
||||
if (vstr[nStr].starts_with("label=")) {
|
||||
strLabel = DecodeDumpString(vstr[nStr].substr(6));
|
||||
fLabel = true;
|
||||
}
|
||||
}
|
||||
printf("Importing %s...\n", CTrianglesAddress(keyid).ToString().c_str());
|
||||
if (!pwalletMain->AddKey(key)) {
|
||||
fGood = false;
|
||||
continue;
|
||||
}
|
||||
pwalletMain->mapKeyMetadata[keyid].nCreateTime = nTime;
|
||||
if (fLabel)
|
||||
pwalletMain->SetAddressBookName(keyid, strLabel);
|
||||
nTimeBegin = std::min(nTimeBegin, nTime);
|
||||
}
|
||||
file.close();
|
||||
|
||||
CBlockIndex *pindex = pindexBest;
|
||||
while (pindex && pindex->pprev && pindex->nTime > nTimeBegin - 7200)
|
||||
pindex = pindex->pprev;
|
||||
|
||||
if (!pwalletMain->nTimeFirstKey || nTimeBegin < pwalletMain->nTimeFirstKey)
|
||||
pwalletMain->nTimeFirstKey = nTimeBegin;
|
||||
|
||||
printf("Rescanning last %i blocks\n", pindexBest->nHeight - pindex->nHeight + 1);
|
||||
pwalletMain->ScanForWalletTransactions(pindex);
|
||||
pwalletMain->ReacceptWalletTransactions();
|
||||
pwalletMain->MarkDirty();
|
||||
|
||||
if (!fGood)
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding some keys to wallet");
|
||||
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
|
||||
Value dumpprivkey(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"dumpprivkey <Trianglesaddress>\n"
|
||||
"Reveals the private key corresponding to <Trianglesaddress>.");
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
string strAddress = params[0].get_str();
|
||||
CTrianglesAddress address;
|
||||
if (!address.SetString(strAddress))
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Triangles address");
|
||||
if (fWalletUnlockStakingOnly)
|
||||
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
|
||||
CKeyID keyID;
|
||||
if (!address.GetKeyID(keyID))
|
||||
throw JSONRPCError(RPC_TYPE_ERROR, "Address does not refer to a key");
|
||||
CSecret vchSecret;
|
||||
bool fCompressed;
|
||||
if (!pwalletMain->GetSecret(keyID, vchSecret, fCompressed))
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Private key for address " + strAddress + " is not known");
|
||||
return CTrianglesSecret(vchSecret, fCompressed).ToString();
|
||||
}
|
||||
|
||||
Value dumpwallet(const Array& params, bool fHelp)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"dumpwallet <filename>\n"
|
||||
"Dumps all wallet keys in a human-readable format.");
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
ofstream file;
|
||||
file.open(params[0].get_str().c_str());
|
||||
if (!file.is_open())
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
|
||||
|
||||
std::map<CKeyID, int64_t> mapKeyBirth;
|
||||
|
||||
std::set<CKeyID> setKeyPool;
|
||||
|
||||
pwalletMain->GetKeyBirthTimes(mapKeyBirth);
|
||||
|
||||
pwalletMain->GetAllReserveKeys(setKeyPool);
|
||||
|
||||
// sort time/key pairs
|
||||
std::vector<std::pair<int64_t, CKeyID> > vKeyBirth;
|
||||
for (std::map<CKeyID, int64_t>::const_iterator it = mapKeyBirth.begin(); it != mapKeyBirth.end(); it++) {
|
||||
vKeyBirth.push_back({it->second, it->first});
|
||||
}
|
||||
mapKeyBirth.clear();
|
||||
std::sort(vKeyBirth.begin(), vKeyBirth.end());
|
||||
|
||||
// produce output
|
||||
file << strprintf("# Wallet dump created by Triangles %s (%s)\n", CLIENT_BUILD.c_str(), CLIENT_DATE.c_str());
|
||||
file << strprintf("# * Created on %s\n", EncodeDumpTime(GetTime()).c_str());
|
||||
file << strprintf("# * Best block at time of backup was %i (%s),\n", nBestHeight, hashBestChain.ToString().c_str());
|
||||
file << strprintf("# mined on %s\n", EncodeDumpTime(pindexBest->nTime).c_str());
|
||||
file << "\n";
|
||||
for (std::vector<std::pair<int64_t, CKeyID> >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) {
|
||||
const CKeyID &keyid = it->second;
|
||||
std::string strTime = EncodeDumpTime(it->first);
|
||||
std::string strAddr = CTrianglesAddress(keyid).ToString();
|
||||
bool IsCompressed;
|
||||
|
||||
CKey key;
|
||||
if (pwalletMain->GetKey(keyid, key)) {
|
||||
if (pwalletMain->mapAddressBook.count(keyid)) {
|
||||
CSecret secret = key.GetSecret(IsCompressed);
|
||||
file << strprintf("%s %s label=%s # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), EncodeDumpString(pwalletMain->mapAddressBook[keyid]).c_str(), strAddr.c_str());
|
||||
} else if (setKeyPool.count(keyid)) {
|
||||
CSecret secret = key.GetSecret(IsCompressed);
|
||||
file << strprintf("%s %s reserve=1 # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), strAddr.c_str());
|
||||
} else {
|
||||
CSecret secret = key.GetSecret(IsCompressed);
|
||||
file << strprintf("%s %s change=1 # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), strAddr.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
file << "\n";
|
||||
file << "# End of dump\n";
|
||||
file.close();
|
||||
return Value::null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+75
-73
@@ -1,73 +1,75 @@
|
||||
// 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 "txdb.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
// Pick the backend once per process. -chaindb is a startup flag; switching at
|
||||
// runtime would require reopening every CTxDB instance, which the codebase
|
||||
// doesn't currently support. We cache the resolved choice so subsequent
|
||||
// MakeChainDB calls don't re-parse the argument.
|
||||
enum class ChainDbKind { LevelDB, RocksDB };
|
||||
|
||||
ChainDbKind ResolveChainDbKind()
|
||||
{
|
||||
static const ChainDbKind kKind = []() {
|
||||
std::string s = GetArg("-chaindb", std::string("leveldb"));
|
||||
for (auto& c : s) c = std::tolower(static_cast<unsigned char>(c));
|
||||
|
||||
if (s == "leveldb")
|
||||
return ChainDbKind::LevelDB;
|
||||
if (s == "rocksdb")
|
||||
return ChainDbKind::RocksDB;
|
||||
|
||||
throw std::runtime_error(
|
||||
"-chaindb=" + s + " is not a recognized backend. "
|
||||
"Valid values: leveldb, rocksdb.");
|
||||
}();
|
||||
return kKind;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode)
|
||||
{
|
||||
switch (ResolveChainDbKind()) {
|
||||
case ChainDbKind::LevelDB:
|
||||
return std::unique_ptr<CTxDBBase>(new CTxDB(pszMode));
|
||||
case ChainDbKind::RocksDB:
|
||||
return std::unique_ptr<CTxDBBase>(new CRocksTxDB(pszMode));
|
||||
}
|
||||
// Unreachable — ResolveChainDbKind throws on bad input.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsRocksDbChainBackend()
|
||||
{
|
||||
return ResolveChainDbKind() == ChainDbKind::RocksDB;
|
||||
}
|
||||
|
||||
std::filesystem::path GetChainDataDir()
|
||||
{
|
||||
switch (ResolveChainDbKind()) {
|
||||
case ChainDbKind::LevelDB: return GetDataDir() / "txleveldb";
|
||||
case ChainDbKind::RocksDB: return GetDataDir() / "rocksdb";
|
||||
}
|
||||
return GetDataDir() / "txleveldb"; // unreachable
|
||||
}
|
||||
|
||||
void WipeChainDataDir()
|
||||
{
|
||||
fs::path p = GetChainDataDir();
|
||||
if (fs::exists(p))
|
||||
fs::remove_all(p);
|
||||
}
|
||||
// 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 "txdb.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
// Pick the backend on every call. The daemon sets -chaindb once at startup
|
||||
// and never changes it, so the per-call cost (a GetArg + tolower loop on a
|
||||
// short string) is negligible compared to the cost of opening the chain DB.
|
||||
// The earlier static-cache version broke test_chaindb_runtime, which
|
||||
// legitimately toggles -chaindb across test cases to exercise both backends
|
||||
// in the same process. Caching would freeze the first-seen choice.
|
||||
enum class ChainDbKind { LevelDB, RocksDB };
|
||||
|
||||
ChainDbKind ResolveChainDbKind()
|
||||
{
|
||||
// RocksDB is the default backend. LevelDB remains selectable with
|
||||
// -chaindb=leveldb and is retained as the migration source and fallback;
|
||||
// its removal is deferred to a later phase after live-chain validation.
|
||||
std::string s = GetArg("-chaindb", std::string("rocksdb"));
|
||||
for (auto& c : s) c = std::tolower(static_cast<unsigned char>(c));
|
||||
|
||||
if (s == "leveldb")
|
||||
return ChainDbKind::LevelDB;
|
||||
if (s == "rocksdb")
|
||||
return ChainDbKind::RocksDB;
|
||||
|
||||
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)
|
||||
{
|
||||
switch (ResolveChainDbKind()) {
|
||||
case ChainDbKind::LevelDB:
|
||||
return std::unique_ptr<CTxDBBase>(new CTxDB(pszMode));
|
||||
case ChainDbKind::RocksDB:
|
||||
return std::unique_ptr<CTxDBBase>(new CRocksTxDB(pszMode));
|
||||
}
|
||||
// Unreachable — ResolveChainDbKind throws on bad input.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsRocksDbChainBackend()
|
||||
{
|
||||
return ResolveChainDbKind() == ChainDbKind::RocksDB;
|
||||
}
|
||||
|
||||
std::filesystem::path GetChainDataDir()
|
||||
{
|
||||
switch (ResolveChainDbKind()) {
|
||||
case ChainDbKind::LevelDB: return GetDataDir() / "txleveldb";
|
||||
case ChainDbKind::RocksDB: return GetDataDir() / "rocksdb";
|
||||
}
|
||||
return GetDataDir() / "txleveldb"; // unreachable
|
||||
}
|
||||
|
||||
void WipeChainDataDir()
|
||||
{
|
||||
fs::path p = GetChainDataDir();
|
||||
if (fs::exists(p))
|
||||
fs::remove_all(p);
|
||||
}
|
||||
|
||||
+696
-673
File diff suppressed because it is too large
Load Diff
+875
-710
File diff suppressed because it is too large
Load Diff
+101
-74
@@ -1,74 +1,101 @@
|
||||
// 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_TXDB_ROCKSDB_H
|
||||
#define TRIANGLES_TXDB_ROCKSDB_H
|
||||
|
||||
#include "txdb-base.h"
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <rocksdb/db.h>
|
||||
#include <rocksdb/options.h>
|
||||
#include <rocksdb/write_batch.h>
|
||||
|
||||
// RocksDB backend for the chain database.
|
||||
//
|
||||
// Mirrors CTxDB (LevelDB) for byte-level compatibility. CTxDBBase owns all
|
||||
// key serialization, so keys produced by this backend are bit-identical to
|
||||
// the LevelDB backend. That property is what lets the M1.4 dual-backend
|
||||
// parity harness verify equivalence.
|
||||
//
|
||||
// Data lives under <datadir>/rocksdb/, separate from <datadir>/txleveldb/,
|
||||
// so both backends can coexist for migration and side-by-side testing.
|
||||
class CRocksTxDB final : public CTxDBBase
|
||||
{
|
||||
public:
|
||||
CRocksTxDB(const char* pszMode = "r+");
|
||||
~CRocksTxDB() override;
|
||||
|
||||
void Close() override;
|
||||
|
||||
bool TxnBegin() override;
|
||||
bool TxnCommit() override;
|
||||
bool TxnAbort() override;
|
||||
|
||||
bool LoadBlockIndex() override;
|
||||
|
||||
// Write a raw serialized key/value pair, bypassing the typed Write<>()
|
||||
// overloads. Intended for the chaindb migration utility, which carries
|
||||
// bytes directly across from a CTxDB (LevelDB) iterator. Honors the
|
||||
// active write batch if one is open.
|
||||
bool WriteRawRecordForMigration(const std::string& key, const std::string& value)
|
||||
{
|
||||
return WriteRaw(key, value);
|
||||
}
|
||||
|
||||
std::unique_ptr<CTxDBIteratorBase> NewIterator() const override;
|
||||
|
||||
protected:
|
||||
bool ReadRaw(const std::string& key, std::string& value) const override;
|
||||
bool WriteRaw(const std::string& key, const std::string& value) override;
|
||||
bool EraseRaw(const std::string& key) override;
|
||||
bool ExistsRaw(const std::string& key) const override;
|
||||
|
||||
private:
|
||||
rocksdb::DB* pdb; // Points to the global instance.
|
||||
rocksdb::WriteBatch* activeBatch; // When non-NULL, writes/deletes go here.
|
||||
rocksdb::Options options;
|
||||
int nVersion;
|
||||
|
||||
// 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::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
|
||||
// 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_TXDB_ROCKSDB_H
|
||||
#define TRIANGLES_TXDB_ROCKSDB_H
|
||||
|
||||
#include "txdb-base.h"
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <rocksdb/db.h>
|
||||
#include <rocksdb/options.h>
|
||||
#include <rocksdb/write_batch.h>
|
||||
#include <rocksdb/utilities/db_ttl.h>
|
||||
|
||||
// RocksDB backend for the chain database.
|
||||
//
|
||||
// Mirrors CTxDB (LevelDB) for byte-level compatibility. CTxDBBase owns all
|
||||
// key serialization, so keys produced by this backend are bit-identical to
|
||||
// the LevelDB backend. That property is what lets the M1.4 dual-backend
|
||||
// parity harness verify equivalence.
|
||||
//
|
||||
// Data lives under <datadir>/rocksdb/, separate from <datadir>/txleveldb/,
|
||||
// so both backends can coexist for migration and side-by-side testing.
|
||||
class CRocksTxDB final : public CTxDBBase
|
||||
{
|
||||
public:
|
||||
CRocksTxDB(const char* pszMode = "r+");
|
||||
~CRocksTxDB() override;
|
||||
|
||||
void Close() override;
|
||||
|
||||
bool TxnBegin() override;
|
||||
bool TxnCommit() override;
|
||||
bool TxnAbort() override;
|
||||
|
||||
bool LoadBlockIndex() override;
|
||||
|
||||
// Write a raw serialized key/value pair, bypassing the typed Write<>()
|
||||
// overloads. Intended for the chaindb migration utility, which carries
|
||||
// bytes directly across from a CTxDB (LevelDB) iterator. Honors the
|
||||
// active write batch if one is open.
|
||||
bool WriteRawRecordForMigration(const std::string& key, const std::string& value)
|
||||
{
|
||||
return WriteRaw(key, value);
|
||||
}
|
||||
|
||||
std::unique_ptr<CTxDBIteratorBase> NewIterator() const override;
|
||||
|
||||
// ─── Test-only friend accessor ──────────────────────────────────────────
|
||||
// test_chaindb_runtime exercises the protected raw methods (ReadRaw /
|
||||
// WriteRaw / EraseRaw / ExistsRaw) directly to verify the wrapper layer
|
||||
// that the daemon uses at runtime when launched with -chaindb=rocksdb.
|
||||
// We don't widen the public API just for the test — instead the test
|
||||
// declares a ChainDbRuntimeTestAccessor struct that this class befriends,
|
||||
// giving it the same access the class itself has. White-box test pattern,
|
||||
// zero impact on production callers.
|
||||
friend struct ChainDbRuntimeTestAccessor;
|
||||
|
||||
protected:
|
||||
bool ReadRaw(const std::string& key, std::string& value) const override;
|
||||
bool WriteRaw(const std::string& key, const std::string& value) override;
|
||||
bool EraseRaw(const std::string& key) override;
|
||||
bool ExistsRaw(const std::string& key) const override;
|
||||
|
||||
private:
|
||||
rocksdb::DB* pdb; // Points to the global instance.
|
||||
rocksdb::WriteBatch* activeBatch; // When non-NULL, writes/deletes go here.
|
||||
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
@@ -1,40 +1,41 @@
|
||||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2012 The Bitcoin developers
|
||||
// Copyright (c) 2026 The Triangles developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file license.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef TRIANGLES_TXDB_H
|
||||
#define TRIANGLES_TXDB_H
|
||||
|
||||
#include "txdb-base.h"
|
||||
#include "txdb-leveldb.h"
|
||||
#include "txdb-rocksdb.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
// Factory: returns a chain-database handle whose concrete backend is chosen
|
||||
// by the -chaindb command-line argument:
|
||||
//
|
||||
// -chaindb=leveldb (default — pending Phase-4 retirement)
|
||||
// -chaindb=rocksdb
|
||||
//
|
||||
// Callers receive a CTxDBBase*, so the rest of the codebase stays
|
||||
// backend-agnostic. Mode strings ("r", "r+", "cr+") match the pre-existing
|
||||
// CTxDB constructor convention.
|
||||
std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode = "r+");
|
||||
|
||||
// True when the configured chain-DB backend is RocksDB.
|
||||
bool IsRocksDbChainBackend();
|
||||
|
||||
// On-disk directory of the chain DB for the configured backend, e.g.
|
||||
// <datadir>/txleveldb (LevelDB) or <datadir>/rocksdb (RocksDB).
|
||||
std::filesystem::path GetChainDataDir();
|
||||
|
||||
// Remove the chain DB directory for the configured backend. Callers that
|
||||
// need a fresh DB (-reindex, snapshot load) must invoke this BEFORE
|
||||
// MakeChainDB() opens the global handle for the first time.
|
||||
void WipeChainDataDir();
|
||||
|
||||
#endif // TRIANGLES_TXDB_H
|
||||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||
// Copyright (c) 2009-2012 The Bitcoin developers
|
||||
// Copyright (c) 2026 The Triangles developers
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file license.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef TRIANGLES_TXDB_H
|
||||
#define TRIANGLES_TXDB_H
|
||||
|
||||
#include "txdb-base.h"
|
||||
#include "txdb-leveldb.h"
|
||||
#include "txdb-rocksdb.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
// Factory: returns a chain-database handle whose concrete backend is chosen
|
||||
// by the -chaindb command-line argument:
|
||||
//
|
||||
// -chaindb=rocksdb (default)
|
||||
// -chaindb=leveldb (retained as migration source + fallback; pending
|
||||
// retirement after live-chain validation)
|
||||
//
|
||||
// Callers receive a CTxDBBase*, so the rest of the codebase stays
|
||||
// backend-agnostic. Mode strings ("r", "r+", "cr+") match the pre-existing
|
||||
// CTxDB constructor convention.
|
||||
std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode = "r+");
|
||||
|
||||
// True when the configured chain-DB backend is RocksDB.
|
||||
bool IsRocksDbChainBackend();
|
||||
|
||||
// On-disk directory of the chain DB for the configured backend, e.g.
|
||||
// <datadir>/txleveldb (LevelDB) or <datadir>/rocksdb (RocksDB).
|
||||
std::filesystem::path GetChainDataDir();
|
||||
|
||||
// Remove the chain DB directory for the configured backend. Callers that
|
||||
// need a fresh DB (-reindex, snapshot load) must invoke this BEFORE
|
||||
// MakeChainDB() opens the global handle for the first time.
|
||||
void WipeChainDataDir();
|
||||
|
||||
#endif // TRIANGLES_TXDB_H
|
||||
|
||||
+1422
-1394
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
//
|
||||
// Backend-agnostic wallet storage seam.
|
||||
//
|
||||
// Historically CWalletDB derived directly from CDB (Berkeley DB). To allow the
|
||||
// wallet to be stored in SQLite instead, storage is abstracted behind two
|
||||
// interfaces modeled on Bitcoin Core's WalletDatabase / DatabaseBatch:
|
||||
//
|
||||
// WalletDatabase - owns the on-disk database (open/close/flush/backup/
|
||||
// rewrite) and hands out batches.
|
||||
// WalletBatch - a unit of work against the database: raw byte-level
|
||||
// Read/Write/Erase/Exists, a cursor for full scans, and an
|
||||
// optional atomic transaction.
|
||||
//
|
||||
// Only RAW BYTES cross this interface. All key/value (de)serialization stays in
|
||||
// CWalletDB via CDataStream with SER_DISK / CLIENT_VERSION, exactly as before,
|
||||
// so the on-disk record encoding is identical across backends. That byte
|
||||
// identity is what makes the Berkeley -> SQLite migration a verbatim key/value
|
||||
// copy.
|
||||
|
||||
#ifndef TRIANGLES_WALLETDB_BASE_H
|
||||
#define TRIANGLES_WALLETDB_BASE_H
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using KeyBytes = std::vector<unsigned char>;
|
||||
using ValueBytes = std::vector<unsigned char>;
|
||||
|
||||
// Result of advancing a cursor.
|
||||
enum class WalletCursorStatus { MORE, DONE, FAIL };
|
||||
|
||||
// Forward scan over every record in a database. Yields raw serialized
|
||||
// key/value bytes; the caller deserializes. Cursors do not observe uncommitted
|
||||
// writes in an open transaction (all wallet scan sites run outside txns).
|
||||
class WalletCursor
|
||||
{
|
||||
public:
|
||||
virtual ~WalletCursor() = default;
|
||||
virtual WalletCursorStatus Next(KeyBytes& key, ValueBytes& value) = 0;
|
||||
};
|
||||
|
||||
// A unit of work against a wallet database.
|
||||
class WalletBatch
|
||||
{
|
||||
public:
|
||||
virtual ~WalletBatch() = default;
|
||||
|
||||
// Byte-level accessors. WriteKey honors fOverwrite (false => fail if the
|
||||
// key already exists, matching Berkeley's DB_NOOVERWRITE). EraseKey returns
|
||||
// true when the key is gone afterwards (including "was not present").
|
||||
virtual bool ReadKey(const KeyBytes& key, ValueBytes& value) = 0;
|
||||
virtual bool WriteKey(const KeyBytes& key, const ValueBytes& value, bool fOverwrite = true) = 0;
|
||||
virtual bool EraseKey(const KeyBytes& key) = 0;
|
||||
virtual bool HasKey(const KeyBytes& key) = 0;
|
||||
|
||||
// Full-database scan.
|
||||
virtual std::unique_ptr<WalletCursor> GetNewCursor() = 0;
|
||||
|
||||
// Atomic transaction around a group of writes/erases. At most one may be
|
||||
// open per batch at a time.
|
||||
virtual bool TxnBegin() = 0;
|
||||
virtual bool TxnCommit() = 0;
|
||||
virtual bool TxnAbort() = 0;
|
||||
|
||||
virtual void Close() = 0;
|
||||
};
|
||||
|
||||
// An on-disk wallet database.
|
||||
class WalletDatabase
|
||||
{
|
||||
public:
|
||||
virtual ~WalletDatabase() = default;
|
||||
|
||||
// Hand out a batch. flush_on_close asks the backend to flush durable state
|
||||
// when the batch is destroyed (Berkeley parity for the common write path).
|
||||
virtual std::unique_ptr<WalletBatch> MakeBatch(bool flush_on_close = true) = 0;
|
||||
|
||||
// Rewrite the database compactly, optionally skipping records whose key
|
||||
// begins with pszSkip (used by the wallet to drop the unencrypted "key"
|
||||
// records after encryption). Berkeley implements this via CDB::Rewrite;
|
||||
// SQLite implements it via VACUUM (+ optional delete of skipped keys).
|
||||
virtual bool Rewrite(const char* pszSkip = nullptr) = 0;
|
||||
|
||||
// Copy the live database to a destination path (wallet backup).
|
||||
virtual bool Backup(const std::string& strDest) const = 0;
|
||||
|
||||
// Durability / lifecycle.
|
||||
virtual void Flush() = 0;
|
||||
virtual void Close() = 0;
|
||||
|
||||
// Integrity check before first use. Fills strError on failure.
|
||||
virtual bool Verify(std::string& strError) = 0;
|
||||
|
||||
// Human-readable identifier for logging (filename or path).
|
||||
virtual std::string Filename() const = 0;
|
||||
};
|
||||
|
||||
// Backend selector, parsed from -walletdb. SQLite is the default; Berkeley is
|
||||
// retained for one release as a fallback and as the migration source.
|
||||
enum class WalletDbKind { SQLite, Berkeley };
|
||||
|
||||
// Resolve the configured wallet backend from -walletdb (default: SQLite).
|
||||
WalletDbKind ResolveWalletDbKind();
|
||||
|
||||
// Open (creating if needed) the wallet database for the configured backend.
|
||||
// strFilename is the logical wallet name (e.g. "wallet.dat"); the SQLite
|
||||
// backend stores it as "<name>" under the data dir, Berkeley as before.
|
||||
std::unique_ptr<WalletDatabase> MakeWalletDatabase(const std::string& strFilename,
|
||||
std::string& strError);
|
||||
|
||||
#endif // TRIANGLES_WALLETDB_BASE_H
|
||||
@@ -0,0 +1,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
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include "walletdb-base.h"
|
||||
#include "walletdb-sqlite.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
WalletDbKind ResolveWalletDbKind()
|
||||
{
|
||||
// SQLite is the default wallet backend. Berkeley DB is retained for one
|
||||
// release as a fallback (-walletdb=bdb) and as the migration source.
|
||||
std::string s = GetArg("-walletdb", std::string("sqlite"));
|
||||
for (auto& c : s) c = std::tolower(static_cast<unsigned char>(c));
|
||||
|
||||
if (s == "sqlite")
|
||||
return WalletDbKind::SQLite;
|
||||
if (s == "bdb" || s == "berkeley")
|
||||
return WalletDbKind::Berkeley;
|
||||
|
||||
throw std::runtime_error(
|
||||
"-walletdb=" + s + " is not a recognized wallet backend. "
|
||||
"Valid values: sqlite, bdb.");
|
||||
}
|
||||
|
||||
std::unique_ptr<WalletDatabase> MakeWalletDatabase(const std::string& strFilename,
|
||||
std::string& strError)
|
||||
{
|
||||
const fs::path path = GetDataDir() / strFilename;
|
||||
|
||||
switch (ResolveWalletDbKind()) {
|
||||
case WalletDbKind::SQLite: {
|
||||
auto db = std::make_unique<SQLiteDatabase>(path);
|
||||
if (!db->Open(strError))
|
||||
return nullptr;
|
||||
return db;
|
||||
}
|
||||
case WalletDbKind::Berkeley:
|
||||
// The Berkeley backend is still served by the legacy CWalletDB/CDB code
|
||||
// path. The thin BerkeleyDatabase adapter that plugs the existing
|
||||
// CDBEnv/CDB into this seam is added during CWalletDB integration; see
|
||||
// WALLET-SQLITE-MIGRATION.md. Until then, selecting -walletdb=bdb keeps
|
||||
// the original code path rather than routing through MakeWalletDatabase.
|
||||
strError = "Berkeley backend uses the legacy wallet path; not served by MakeWalletDatabase yet.";
|
||||
return nullptr;
|
||||
}
|
||||
return nullptr; // unreachable
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include "walletdb-sqlite.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// ─── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
// Bind a byte buffer as a BLOB parameter (1-based index). SQLITE_TRANSIENT so
|
||||
// SQLite copies the bytes; the source vector need not outlive the step.
|
||||
static int BindBlob(sqlite3_stmt* stmt, int idx, const std::vector<unsigned char>& v)
|
||||
{
|
||||
// A zero-length blob still binds correctly with a non-null pointer.
|
||||
const void* p = v.empty() ? "" : static_cast<const void*>(v.data());
|
||||
return sqlite3_bind_blob(stmt, idx, p, static_cast<int>(v.size()), SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
static void ColumnBlob(sqlite3_stmt* stmt, int col, std::vector<unsigned char>& out)
|
||||
{
|
||||
const unsigned char* p = static_cast<const unsigned char*>(sqlite3_column_blob(stmt, col));
|
||||
int n = sqlite3_column_bytes(stmt, col);
|
||||
out.assign(p, p + (n > 0 ? n : 0));
|
||||
}
|
||||
|
||||
// ─── SQLiteDatabase ──────────────────────────────────────────────────────────
|
||||
|
||||
SQLiteDatabase::SQLiteDatabase(const fs::path& file_path)
|
||||
: m_file_path(file_path)
|
||||
{
|
||||
}
|
||||
|
||||
SQLiteDatabase::~SQLiteDatabase()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::ExecOrError(const char* sql, std::string& strError) const
|
||||
{
|
||||
char* errmsg = nullptr;
|
||||
int rc = sqlite3_exec(m_db, sql, nullptr, nullptr, &errmsg);
|
||||
if (rc != SQLITE_OK) {
|
||||
strError = strprintf("SQLite: '%s' failed: %s", sql, errmsg ? errmsg : sqlite3_errstr(rc));
|
||||
if (errmsg) sqlite3_free(errmsg);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::Open(std::string& strError)
|
||||
{
|
||||
if (m_db)
|
||||
return true;
|
||||
|
||||
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX;
|
||||
int rc = sqlite3_open_v2(m_file_path.string().c_str(), &m_db, flags, nullptr);
|
||||
if (rc != SQLITE_OK) {
|
||||
strError = strprintf("Failed to open SQLite wallet %s: %s",
|
||||
m_file_path.string().c_str(), sqlite3_errstr(rc));
|
||||
if (m_db) { sqlite3_close(m_db); m_db = nullptr; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Block (rather than fail) for up to 5s if another handle holds the lock.
|
||||
sqlite3_busy_timeout(m_db, 5000);
|
||||
|
||||
// Durability + integrity pragmas. FULL fsync on commit — a wallet must not
|
||||
// lose a freshly-written key on power loss.
|
||||
if (!ExecOrError("PRAGMA synchronous = FULL;", strError)) return false;
|
||||
if (!ExecOrError("PRAGMA foreign_keys = ON;", strError)) return false;
|
||||
// Fail loudly instead of silently truncating an over-long blob.
|
||||
if (!ExecOrError("PRAGMA cell_size_check = ON;", strError)) return false;
|
||||
|
||||
// Identify our schema via application_id / user_version. A brand-new file
|
||||
// reports 0/0; an existing file must match ours (refuse foreign DBs).
|
||||
int appId = 0, userVer = 0;
|
||||
{
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(m_db, "PRAGMA application_id;", -1, &st, nullptr) == SQLITE_OK &&
|
||||
sqlite3_step(st) == SQLITE_ROW)
|
||||
appId = sqlite3_column_int(st, 0);
|
||||
sqlite3_finalize(st);
|
||||
st = nullptr;
|
||||
if (sqlite3_prepare_v2(m_db, "PRAGMA user_version;", -1, &st, nullptr) == SQLITE_OK &&
|
||||
sqlite3_step(st) == SQLITE_ROW)
|
||||
userVer = sqlite3_column_int(st, 0);
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
|
||||
if (appId != 0 && appId != SQLITE_WALLET_APP_ID) {
|
||||
strError = strprintf("%s is not a Triangles SQLite wallet (application_id=0x%08x)",
|
||||
m_file_path.string().c_str(), appId);
|
||||
sqlite3_close(m_db);
|
||||
m_db = nullptr;
|
||||
return false;
|
||||
}
|
||||
if (userVer > SQLITE_WALLET_SCHEMA_VERSION) {
|
||||
strError = strprintf("%s was written by a newer wallet (schema v%d > v%d)",
|
||||
m_file_path.string().c_str(), userVer, SQLITE_WALLET_SCHEMA_VERSION);
|
||||
sqlite3_close(m_db);
|
||||
m_db = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create schema (idempotent) and stamp identity on fresh files.
|
||||
if (!ExecOrError("CREATE TABLE IF NOT EXISTS main "
|
||||
"(key BLOB PRIMARY KEY NOT NULL, value BLOB NOT NULL);", strError))
|
||||
return false;
|
||||
if (appId == 0) {
|
||||
std::string set = strprintf("PRAGMA application_id = %d;", SQLITE_WALLET_APP_ID);
|
||||
if (!ExecOrError(set.c_str(), strError)) return false;
|
||||
}
|
||||
{
|
||||
std::string set = strprintf("PRAGMA user_version = %d;", SQLITE_WALLET_SCHEMA_VERSION);
|
||||
if (!ExecOrError(set.c_str(), strError)) return false;
|
||||
}
|
||||
|
||||
printf("SQLite wallet opened: %s\n", m_file_path.string().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<WalletBatch> SQLiteDatabase::MakeBatch(bool /*flush_on_close*/)
|
||||
{
|
||||
return std::make_unique<SQLiteBatch>(*this);
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::Rewrite(const char* /*pszSkip*/)
|
||||
{
|
||||
// SQLite reclaims space and defragments via VACUUM. The wallet erases
|
||||
// superseded records (e.g. unencrypted keys after encryption) explicitly,
|
||||
// so the pszSkip filter that the Berkeley backend used is unnecessary here.
|
||||
if (!m_db)
|
||||
return false;
|
||||
std::string err;
|
||||
if (!ExecOrError("VACUUM;", err)) {
|
||||
printf("SQLiteDatabase::Rewrite VACUUM failed: %s\n", err.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::Backup(const std::string& strDest) const
|
||||
{
|
||||
if (!m_db)
|
||||
return false;
|
||||
|
||||
sqlite3* pDest = nullptr;
|
||||
if (sqlite3_open_v2(strDest.c_str(), &pDest,
|
||||
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr) != SQLITE_OK) {
|
||||
printf("SQLiteDatabase::Backup cannot open destination %s: %s\n",
|
||||
strDest.c_str(), pDest ? sqlite3_errmsg(pDest) : "?");
|
||||
if (pDest) sqlite3_close(pDest);
|
||||
return false;
|
||||
}
|
||||
|
||||
sqlite3_backup* bk = sqlite3_backup_init(pDest, "main", m_db, "main");
|
||||
bool ok = false;
|
||||
if (bk) {
|
||||
sqlite3_backup_step(bk, -1); // copy entire DB in one shot
|
||||
int rc = sqlite3_backup_finish(bk);
|
||||
ok = (rc == SQLITE_OK);
|
||||
if (!ok)
|
||||
printf("SQLiteDatabase::Backup failed: %s\n", sqlite3_errstr(rc));
|
||||
} else {
|
||||
printf("SQLiteDatabase::Backup init failed: %s\n", sqlite3_errmsg(pDest));
|
||||
}
|
||||
sqlite3_close(pDest);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void SQLiteDatabase::Flush()
|
||||
{
|
||||
// No-op: with synchronous=FULL and rollback journaling, each committed
|
||||
// transaction is already durable. (If WAL is ever enabled, checkpoint here.)
|
||||
}
|
||||
|
||||
void SQLiteDatabase::Close()
|
||||
{
|
||||
if (m_db) {
|
||||
sqlite3_close(m_db);
|
||||
m_db = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool SQLiteDatabase::Verify(std::string& strError)
|
||||
{
|
||||
if (!m_db) {
|
||||
strError = "SQLite database not open";
|
||||
return false;
|
||||
}
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(m_db, "PRAGMA integrity_check;", -1, &st, nullptr) != SQLITE_OK) {
|
||||
strError = strprintf("integrity_check prepare failed: %s", sqlite3_errmsg(m_db));
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
const unsigned char* res = sqlite3_column_text(st, 0);
|
||||
ok = (res && std::strcmp(reinterpret_cast<const char*>(res), "ok") == 0);
|
||||
if (!ok)
|
||||
strError = strprintf("integrity_check: %s", res ? reinterpret_cast<const char*>(res) : "(null)");
|
||||
} else {
|
||||
strError = "integrity_check returned no rows";
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ─── SQLiteBatch ──────────────────────────────────────────────────────────────
|
||||
|
||||
SQLiteBatch::SQLiteBatch(SQLiteDatabase& database)
|
||||
: m_database(database)
|
||||
{
|
||||
PrepareStatements();
|
||||
}
|
||||
|
||||
bool SQLiteBatch::PrepareStatements()
|
||||
{
|
||||
sqlite3* db = m_database.Handle();
|
||||
if (!db)
|
||||
return false;
|
||||
|
||||
struct { sqlite3_stmt** out; const char* sql; } stmts[] = {
|
||||
{ &m_read_stmt, "SELECT value FROM main WHERE key = ?;" },
|
||||
{ &m_insert_stmt, "INSERT OR REPLACE INTO main (key, value) VALUES (?, ?);" },
|
||||
{ &m_overwrite_stmt, "INSERT INTO main (key, value) VALUES (?, ?);" },
|
||||
{ &m_delete_stmt, "DELETE FROM main WHERE key = ?;" },
|
||||
};
|
||||
for (auto& s : stmts) {
|
||||
if (*s.out) continue;
|
||||
if (sqlite3_prepare_v2(db, s.sql, -1, s.out, nullptr) != SQLITE_OK) {
|
||||
printf("SQLiteBatch: prepare failed for '%s': %s\n", s.sql, sqlite3_errmsg(db));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SQLiteBatch::Close()
|
||||
{
|
||||
sqlite3_stmt* all[] = { m_read_stmt, m_insert_stmt, m_overwrite_stmt, m_delete_stmt };
|
||||
for (auto* st : all)
|
||||
if (st) sqlite3_finalize(st);
|
||||
m_read_stmt = m_insert_stmt = m_overwrite_stmt = m_delete_stmt = nullptr;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::ReadKey(const KeyBytes& key, ValueBytes& value)
|
||||
{
|
||||
if (!m_read_stmt) return false;
|
||||
sqlite3_reset(m_read_stmt);
|
||||
sqlite3_clear_bindings(m_read_stmt);
|
||||
if (BindBlob(m_read_stmt, 1, key) != SQLITE_OK)
|
||||
return false;
|
||||
|
||||
bool found = false;
|
||||
if (sqlite3_step(m_read_stmt) == SQLITE_ROW) {
|
||||
ColumnBlob(m_read_stmt, 0, value);
|
||||
found = true;
|
||||
}
|
||||
sqlite3_reset(m_read_stmt);
|
||||
return found;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::WriteKey(const KeyBytes& key, const ValueBytes& value, bool fOverwrite)
|
||||
{
|
||||
sqlite3_stmt* st = fOverwrite ? m_insert_stmt : m_overwrite_stmt;
|
||||
if (!st) return false;
|
||||
sqlite3_reset(st);
|
||||
sqlite3_clear_bindings(st);
|
||||
if (BindBlob(st, 1, key) != SQLITE_OK) return false;
|
||||
if (BindBlob(st, 2, value) != SQLITE_OK) return false;
|
||||
|
||||
int rc = sqlite3_step(st);
|
||||
sqlite3_reset(st);
|
||||
if (rc == SQLITE_DONE)
|
||||
return true;
|
||||
// Non-overwrite insert hitting an existing key => constraint violation,
|
||||
// which mirrors Berkeley's DB_NOOVERWRITE returning false (not an error).
|
||||
if (!fOverwrite && (rc == SQLITE_CONSTRAINT))
|
||||
return false;
|
||||
printf("SQLiteBatch::WriteKey step failed: %s\n", sqlite3_errstr(rc));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::EraseKey(const KeyBytes& key)
|
||||
{
|
||||
if (!m_delete_stmt) return false;
|
||||
sqlite3_reset(m_delete_stmt);
|
||||
sqlite3_clear_bindings(m_delete_stmt);
|
||||
if (BindBlob(m_delete_stmt, 1, key) != SQLITE_OK)
|
||||
return false;
|
||||
int rc = sqlite3_step(m_delete_stmt);
|
||||
sqlite3_reset(m_delete_stmt);
|
||||
// DONE whether or not a row matched — "key is gone" either way.
|
||||
return rc == SQLITE_DONE;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::HasKey(const KeyBytes& key)
|
||||
{
|
||||
if (!m_read_stmt) return false;
|
||||
sqlite3_reset(m_read_stmt);
|
||||
sqlite3_clear_bindings(m_read_stmt);
|
||||
if (BindBlob(m_read_stmt, 1, key) != SQLITE_OK)
|
||||
return false;
|
||||
bool present = (sqlite3_step(m_read_stmt) == SQLITE_ROW);
|
||||
sqlite3_reset(m_read_stmt);
|
||||
return present;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
class SQLiteCursor final : public WalletCursor
|
||||
{
|
||||
public:
|
||||
explicit SQLiteCursor(sqlite3_stmt* stmt) : m_stmt(stmt) {}
|
||||
~SQLiteCursor() override { if (m_stmt) sqlite3_finalize(m_stmt); }
|
||||
|
||||
WalletCursorStatus Next(KeyBytes& key, ValueBytes& value) override
|
||||
{
|
||||
if (!m_stmt) return WalletCursorStatus::FAIL;
|
||||
int rc = sqlite3_step(m_stmt);
|
||||
if (rc == SQLITE_DONE) return WalletCursorStatus::DONE;
|
||||
if (rc != SQLITE_ROW) return WalletCursorStatus::FAIL;
|
||||
ColumnBlob(m_stmt, 0, key);
|
||||
ColumnBlob(m_stmt, 1, value);
|
||||
return WalletCursorStatus::MORE;
|
||||
}
|
||||
|
||||
private:
|
||||
sqlite3_stmt* m_stmt;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<WalletCursor> SQLiteBatch::GetNewCursor()
|
||||
{
|
||||
sqlite3* db = m_database.Handle();
|
||||
if (!db) return nullptr;
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(db, "SELECT key, value FROM main;", -1, &st, nullptr) != SQLITE_OK) {
|
||||
printf("SQLiteBatch::GetNewCursor prepare failed: %s\n", sqlite3_errmsg(db));
|
||||
return nullptr;
|
||||
}
|
||||
return std::make_unique<SQLiteCursor>(st);
|
||||
}
|
||||
|
||||
bool SQLiteBatch::TxnBegin()
|
||||
{
|
||||
return sqlite3_exec(m_database.Handle(), "BEGIN TRANSACTION;", nullptr, nullptr, nullptr) == SQLITE_OK;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::TxnCommit()
|
||||
{
|
||||
return sqlite3_exec(m_database.Handle(), "COMMIT TRANSACTION;", nullptr, nullptr, nullptr) == SQLITE_OK;
|
||||
}
|
||||
|
||||
bool SQLiteBatch::TxnAbort()
|
||||
{
|
||||
return sqlite3_exec(m_database.Handle(), "ROLLBACK TRANSACTION;", nullptr, nullptr, nullptr) == SQLITE_OK;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
//
|
||||
// SQLite backend for the wallet database. Stores every wallet record as a row
|
||||
// in a single table:
|
||||
//
|
||||
// CREATE TABLE main (key BLOB PRIMARY KEY NOT NULL, value BLOB NOT NULL);
|
||||
//
|
||||
// The key/value blobs are the exact serialized bytes CWalletDB already
|
||||
// produces (SER_DISK / CLIENT_VERSION), so a SQLite wallet is byte-for-byte
|
||||
// equivalent in content to the Berkeley wallet.dat it was migrated from.
|
||||
//
|
||||
// Modeled on Bitcoin Core's SQLiteDatabase / SQLiteBatch.
|
||||
|
||||
#ifndef TRIANGLES_WALLETDB_SQLITE_H
|
||||
#define TRIANGLES_WALLETDB_SQLITE_H
|
||||
|
||||
#include "walletdb-base.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
class SQLiteDatabase;
|
||||
|
||||
// A batch (and optional transaction) against a SQLiteDatabase. Holds prepared
|
||||
// statements bound to the shared connection owned by SQLiteDatabase.
|
||||
class SQLiteBatch final : public WalletBatch
|
||||
{
|
||||
public:
|
||||
explicit SQLiteBatch(SQLiteDatabase& database);
|
||||
~SQLiteBatch() override { Close(); }
|
||||
|
||||
bool ReadKey(const KeyBytes& key, ValueBytes& value) override;
|
||||
bool WriteKey(const KeyBytes& key, const ValueBytes& value, bool fOverwrite = true) override;
|
||||
bool EraseKey(const KeyBytes& key) override;
|
||||
bool HasKey(const KeyBytes& key) override;
|
||||
|
||||
std::unique_ptr<WalletCursor> GetNewCursor() override;
|
||||
|
||||
bool TxnBegin() override;
|
||||
bool TxnCommit() override;
|
||||
bool TxnAbort() override;
|
||||
|
||||
void Close() override;
|
||||
|
||||
private:
|
||||
SQLiteDatabase& m_database;
|
||||
|
||||
// Prepared statements (lazily compiled on first use, finalized on Close).
|
||||
sqlite3_stmt* m_read_stmt = nullptr;
|
||||
sqlite3_stmt* m_insert_stmt = nullptr; // INSERT OR REPLACE
|
||||
sqlite3_stmt* m_overwrite_stmt = nullptr; // INSERT (fail if exists)
|
||||
sqlite3_stmt* m_delete_stmt = nullptr;
|
||||
|
||||
bool PrepareStatements();
|
||||
};
|
||||
|
||||
// The on-disk SQLite wallet database. Owns the single sqlite3 connection that
|
||||
// all of its batches share (wallet access is serialized by the wallet's own
|
||||
// locks, matching the Berkeley backend's single-environment model).
|
||||
class SQLiteDatabase final : public WalletDatabase
|
||||
{
|
||||
public:
|
||||
// file_path: absolute path to the .dat file on disk.
|
||||
explicit SQLiteDatabase(const std::filesystem::path& file_path);
|
||||
~SQLiteDatabase() override;
|
||||
|
||||
// Open the connection, apply pragmas, and create the schema if absent.
|
||||
// Returns false (with strError set) on failure.
|
||||
bool Open(std::string& strError);
|
||||
|
||||
std::unique_ptr<WalletBatch> MakeBatch(bool flush_on_close = true) override;
|
||||
|
||||
bool Rewrite(const char* pszSkip = nullptr) override;
|
||||
bool Backup(const std::string& strDest) const override;
|
||||
void Flush() override;
|
||||
void Close() override;
|
||||
bool Verify(std::string& strError) override;
|
||||
std::string Filename() const override { return m_file_path.string(); }
|
||||
|
||||
sqlite3* Handle() const { return m_db; }
|
||||
|
||||
private:
|
||||
std::filesystem::path m_file_path;
|
||||
sqlite3* m_db = nullptr;
|
||||
|
||||
bool ExecOrError(const char* sql, std::string& strError) const;
|
||||
};
|
||||
|
||||
// Magic written into PRAGMA application_id so we can recognize our wallet files
|
||||
// and refuse to open foreign SQLite databases. ASCII "TRIw".
|
||||
static constexpr int SQLITE_WALLET_APP_ID = 0x54526977;
|
||||
// Schema version in PRAGMA user_version.
|
||||
static constexpr int SQLITE_WALLET_SCHEMA_VERSION = 1;
|
||||
|
||||
#endif // TRIANGLES_WALLETDB_SQLITE_H
|
||||
+832
-818
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,207 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include "walletmigrate.h"
|
||||
#include "walletdb-sqlite.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include <db_cxx.h>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
bool IsSQLiteFile(const fs::path& path)
|
||||
{
|
||||
std::error_code ec;
|
||||
if (!fs::exists(path, ec) || fs::file_size(path, ec) < 16)
|
||||
return false;
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
char hdr[16] = {};
|
||||
in.read(hdr, sizeof(hdr));
|
||||
if (!in)
|
||||
return false;
|
||||
// SQLite database files always start with this exact 16-byte string,
|
||||
// including the trailing NUL. Berkeley DB files do not.
|
||||
static const char kMagic[16] = {'S','Q','L','i','t','e',' ','f','o','r','m','a','t',' ','3','\0'};
|
||||
return std::memcmp(hdr, kMagic, 16) == 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Count rows currently in the SQLite "main" table.
|
||||
bool SQLiteRowCount(SQLiteDatabase& db, int64_t& nOut, std::string& strError)
|
||||
{
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(db.Handle(), "SELECT COUNT(*) FROM main;", -1, &st, nullptr) != SQLITE_OK) {
|
||||
strError = strprintf("count prepare failed: %s", sqlite3_errmsg(db.Handle()));
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
nOut = sqlite3_column_int64(st, 0);
|
||||
ok = true;
|
||||
} else {
|
||||
strError = "count query returned no rows";
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool MaybeMigrateBerkeleyWalletToSQLite(const fs::path& walletPath, std::string& strError)
|
||||
{
|
||||
strError.clear();
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::exists(walletPath, ec))
|
||||
return true; // fresh install — the SQLite backend will create it
|
||||
if (IsSQLiteFile(walletPath))
|
||||
return true; // already migrated / already SQLite
|
||||
|
||||
const fs::path dir = walletPath.parent_path();
|
||||
const std::string file = walletPath.filename().string();
|
||||
const fs::path tmpPath = dir / (file + ".sqlite.tmp");
|
||||
const fs::path bakPath = dir / (file + ".bdb.bak");
|
||||
|
||||
printf("Wallet migration: converting Berkeley %s to SQLite...\n", walletPath.string().c_str());
|
||||
|
||||
fs::remove(tmpPath, ec); // clear any stale temp from a prior aborted run
|
||||
|
||||
int64_t nCopied = 0;
|
||||
|
||||
// ── Read side: a private, read-only Berkeley environment over the wallet
|
||||
// directory, then the "main" sub-database (matches CDB::CDB's open call). ──
|
||||
DbEnv env(0u);
|
||||
env.set_error_stream(&std::cerr);
|
||||
env.set_cachesize(0, 1 << 20, 1); // 1 MiB cache is plenty for sequential read
|
||||
u_int32_t envFlags = DB_CREATE | DB_INIT_MPOOL | DB_PRIVATE;
|
||||
if (env.open(dir.string().c_str(), envFlags, 0) != 0) {
|
||||
strError = "migration: cannot open Berkeley environment on wallet directory";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
{
|
||||
Db db(&env, 0);
|
||||
if (db.open(nullptr, file.c_str(), "main", DB_BTREE, DB_RDONLY, 0) != 0) {
|
||||
strError = "migration: cannot open Berkeley wallet (is it a valid wallet.dat?)";
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Write side: fresh SQLite database in the temp file. ──
|
||||
SQLiteDatabase sqlite(tmpPath);
|
||||
std::string sqlErr;
|
||||
if (!sqlite.Open(sqlErr)) {
|
||||
strError = "migration: cannot create SQLite wallet: " + sqlErr;
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto batch = sqlite.MakeBatch();
|
||||
if (!batch || !batch->TxnBegin()) {
|
||||
strError = "migration: cannot begin SQLite transaction";
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
Dbc* pcursor = nullptr;
|
||||
if (db.cursor(nullptr, &pcursor, 0) != 0) {
|
||||
strError = "migration: cannot open Berkeley cursor";
|
||||
batch->TxnAbort();
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
Dbt datKey, datValue; // BDB-owned buffers, valid until the next get()
|
||||
int ret;
|
||||
bool writeFailed = false;
|
||||
while ((ret = pcursor->get(&datKey, &datValue, DB_NEXT)) == 0) {
|
||||
const unsigned char* kp = static_cast<const unsigned char*>(datKey.get_data());
|
||||
const unsigned char* vp = static_cast<const unsigned char*>(datValue.get_data());
|
||||
KeyBytes key(kp, kp + datKey.get_size());
|
||||
ValueBytes val(vp, vp + datValue.get_size());
|
||||
if (!batch->WriteKey(key, val, /*fOverwrite=*/true)) {
|
||||
writeFailed = true;
|
||||
break;
|
||||
}
|
||||
++nCopied;
|
||||
}
|
||||
pcursor->close();
|
||||
|
||||
if (writeFailed || (ret != DB_NOTFOUND && ret != 0)) {
|
||||
strError = strprintf("migration: copy aborted after %lld records (bdb get=%d)",
|
||||
(long long)nCopied, ret);
|
||||
batch->TxnAbort();
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!batch->TxnCommit()) {
|
||||
strError = "migration: SQLite commit failed";
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Verify the destination row count matches what we copied. ──
|
||||
int64_t nDst = -1;
|
||||
if (!SQLiteRowCount(sqlite, nDst, strError)) {
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
if (nDst != nCopied) {
|
||||
strError = strprintf("migration: record count mismatch (copied=%lld sqlite=%lld)",
|
||||
(long long)nCopied, (long long)nDst);
|
||||
db.close(0);
|
||||
env.close(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
batch.reset();
|
||||
sqlite.Close();
|
||||
db.close(0);
|
||||
ok = true;
|
||||
}
|
||||
env.close(0);
|
||||
|
||||
if (!ok) {
|
||||
fs::remove(tmpPath, ec);
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Atomic-ish swap: back up the Berkeley original, then move SQLite in. ──
|
||||
fs::rename(walletPath, bakPath, ec);
|
||||
if (ec) {
|
||||
strError = strprintf("migration: cannot back up Berkeley wallet to %s: %s",
|
||||
bakPath.string().c_str(), ec.message().c_str());
|
||||
fs::remove(tmpPath, ec);
|
||||
return false;
|
||||
}
|
||||
fs::rename(tmpPath, walletPath, ec);
|
||||
if (ec) {
|
||||
// Roll the original back into place so the wallet is never left missing.
|
||||
std::error_code ec2;
|
||||
fs::rename(bakPath, walletPath, ec2);
|
||||
strError = strprintf("migration: cannot move SQLite wallet into place: %s",
|
||||
ec.message().c_str());
|
||||
fs::remove(tmpPath, ec2);
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("Wallet migration: complete. %lld records migrated to SQLite. "
|
||||
"Berkeley original preserved at %s\n",
|
||||
(long long)nCopied, bakPath.string().c_str());
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2026 The Triangles developers.
|
||||
// Distributed under the MIT/X11 software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef TRIANGLES_WALLETMIGRATE_H
|
||||
#define TRIANGLES_WALLETMIGRATE_H
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
// Migrate a Berkeley DB wallet (wallet.dat) to a SQLite wallet of the same
|
||||
// name, IN PLACE and NON-DESTRUCTIVELY:
|
||||
//
|
||||
// 1. If walletPath does not exist, or is already a SQLite database, there is
|
||||
// nothing to do — returns true.
|
||||
// 2. Otherwise the Berkeley records are copied verbatim (raw key/value bytes)
|
||||
// into a fresh SQLite database written to a temporary file.
|
||||
// 3. The record count is verified to match.
|
||||
// 4. The original Berkeley file is renamed to "<name>.bdb.bak" (kept as a
|
||||
// fallback, never deleted), and the SQLite file is moved into place as
|
||||
// "<name>".
|
||||
//
|
||||
// On any failure the original Berkeley wallet is left exactly as it was and the
|
||||
// temporary SQLite file is removed; strError describes the problem.
|
||||
bool MaybeMigrateBerkeleyWalletToSQLite(const std::filesystem::path& walletPath,
|
||||
std::string& strError);
|
||||
|
||||
// True if the file begins with the SQLite format-3 magic header.
|
||||
bool IsSQLiteFile(const std::filesystem::path& path);
|
||||
|
||||
#endif // TRIANGLES_WALLETMIGRATE_H
|
||||
Reference in New Issue
Block a user