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
|
name: Lint
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
# Diff-only enforcement: clang-format and clang-tidy run only on lines changed
|
# Diff-only enforcement: clang-format and clang-tidy run only on lines changed
|
||||||
# in the PR. Existing files keep their current style until they're edited.
|
# in the PR. Existing files keep their current style until they're edited.
|
||||||
# See .clang-format and .clang-tidy for the rule sets.
|
# See .clang-format and .clang-tidy for the rule sets.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
clang-format-diff:
|
clang-format-diff:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# Need merge-base with target branch to compute the diff.
|
# Need merge-base with target branch to compute the diff.
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install clang-format
|
- name: Install clang-format
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y clang-format-15
|
sudo apt-get install -y clang-format-15
|
||||||
sudo ln -sf /usr/bin/clang-format-15 /usr/local/bin/clang-format
|
sudo ln -sf /usr/bin/clang-format-15 /usr/local/bin/clang-format
|
||||||
|
|
||||||
- name: Check format on changed lines
|
- name: Check format on changed lines
|
||||||
run: |
|
run: |
|
||||||
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD)
|
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD)
|
||||||
echo "Comparing against merge-base: $BASE_SHA"
|
echo "Comparing against merge-base: $BASE_SHA"
|
||||||
|
|
||||||
# git-clang-format prints a diff if any changed line violates style.
|
# git-clang-format prints a diff if any changed line violates style.
|
||||||
# --diff exits non-zero when reformatting would change something.
|
# --diff exits non-zero when reformatting would change something.
|
||||||
OUTPUT=$(git clang-format --diff "$BASE_SHA" -- '*.cpp' '*.h' '*.hpp' '*.cc' || true)
|
OUTPUT=$(git clang-format --diff "$BASE_SHA" -- '*.cpp' '*.h' '*.hpp' '*.cc' || true)
|
||||||
|
|
||||||
if [ -z "$OUTPUT" ] || [ "$OUTPUT" = "no modified files to format" ] || [ "$OUTPUT" = "clang-format did not modify any files" ]; then
|
if [ -z "$OUTPUT" ] || [ "$OUTPUT" = "no modified files to format" ] || [ "$OUTPUT" = "clang-format did not modify any files" ]; then
|
||||||
echo "clang-format: clean"
|
echo "clang-format: clean"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "::error::clang-format wants to change the following on lines you touched."
|
echo "::error::clang-format wants to change the following on lines you touched."
|
||||||
echo "Run \`git clang-format $BASE_SHA\` locally and commit the result."
|
echo "Run \`git clang-format $BASE_SHA\` locally and commit the result."
|
||||||
echo "$OUTPUT"
|
echo "$OUTPUT"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
clang-tidy-diff:
|
clang-tidy-diff:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
- name: Install dependencies + clang-tidy
|
|
||||||
run: |
|
- name: Install dependencies + clang-tidy
|
||||||
sudo apt-get update
|
run: |
|
||||||
sudo apt-get install -y build-essential cmake ninja-build clang-tidy-15 \
|
sudo apt-get update
|
||||||
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
|
sudo apt-get install -y build-essential cmake ninja-build clang-tidy-15 \
|
||||||
librocksdb-dev libevent-dev libminiupnpc-dev zlib1g-dev
|
libboost-all-dev libssl-dev libdb++-dev libleveldb-dev \
|
||||||
sudo ln -sf /usr/bin/clang-tidy-15 /usr/local/bin/clang-tidy
|
libevent-dev libminiupnpc-dev zlib1g-dev \
|
||||||
|
libsnappy-dev liblz4-dev libzstd-dev libsqlite3-dev
|
||||||
- name: Configure (export compile_commands.json)
|
sudo ln -sf /usr/bin/clang-tidy-15 /usr/local/bin/clang-tidy
|
||||||
run: |
|
|
||||||
cmake -B build -G Ninja \
|
- name: Build RocksDB from source
|
||||||
-DCMAKE_BUILD_TYPE=Debug \
|
# Ubuntu 22.04's librocksdb-dev is 6.11.4 which CMakeLists.txt now
|
||||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
# refuses to configure against (need >= 7.4 for XXH3 per-block
|
||||||
-DBUILD_QT=OFF \
|
# checksum). Build 8.9.1 from source — same version DNS2 ships —
|
||||||
-DBUILD_DAEMON=ON \
|
# into /usr/local so CMake's find_library picks it up first.
|
||||||
-DBUILD_TESTS=ON \
|
run: sudo bash scripts/ci/build-rocksdb.sh
|
||||||
-DUSE_UPNP=OFF
|
|
||||||
|
- name: Configure (export compile_commands.json)
|
||||||
- name: Generate build artifacts that headers depend on
|
run: |
|
||||||
# build.h, qt UI headers, etc. — clang-tidy needs them to parse sources.
|
cmake -B build -G Ninja \
|
||||||
run: cmake --build build --target generate_build_info
|
-DCMAKE_BUILD_TYPE=Debug \
|
||||||
|
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||||
- name: Run clang-tidy on changed lines
|
-DBUILD_QT=OFF \
|
||||||
run: |
|
-DBUILD_DAEMON=ON \
|
||||||
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD)
|
-DBUILD_TESTS=ON \
|
||||||
echo "Comparing against merge-base: $BASE_SHA"
|
-DUSE_UPNP=OFF
|
||||||
|
|
||||||
# clang-tidy-diff.py ships with clang-tidy; runs tidy only on changed lines.
|
- name: Generate build artifacts that headers depend on
|
||||||
DIFF_SCRIPT=$(dpkg -L clang-tidy-15 | grep clang-tidy-diff.py | head -1)
|
# build.h, qt UI headers, etc. — clang-tidy needs them to parse sources.
|
||||||
if [ -z "$DIFF_SCRIPT" ]; then
|
run: cmake --build build --target generate_build_info
|
||||||
DIFF_SCRIPT=/usr/share/clang/clang-tidy-diff.py
|
|
||||||
fi
|
- name: Run clang-tidy on changed lines
|
||||||
echo "Using: $DIFF_SCRIPT"
|
run: |
|
||||||
|
BASE_SHA=$(git merge-base origin/${{ github.base_ref }} HEAD)
|
||||||
# -p1 strips the leading "a/"/"b/" from git diff paths.
|
echo "Comparing against merge-base: $BASE_SHA"
|
||||||
# -path=build points clang-tidy at compile_commands.json.
|
|
||||||
# -iregex restricts to project sources (not vendored).
|
# clang-tidy-diff.py ships with clang-tidy; runs tidy only on changed lines.
|
||||||
git diff -U0 "$BASE_SHA" -- 'src/*.cpp' 'src/*.h' \
|
DIFF_SCRIPT=$(dpkg -L clang-tidy-15 | grep clang-tidy-diff.py | head -1)
|
||||||
':(exclude)src/json/nlohmann_json.hpp' \
|
if [ -z "$DIFF_SCRIPT" ]; then
|
||||||
':(exclude)src/leveldb/*' \
|
DIFF_SCRIPT=/usr/share/clang/clang-tidy-diff.py
|
||||||
':(exclude)src/lz4/*' \
|
fi
|
||||||
':(exclude)src/tor/tor-src/*' \
|
echo "Using: $DIFF_SCRIPT"
|
||||||
| python3 "$DIFF_SCRIPT" -p1 -path build \
|
|
||||||
-iregex '.*\.(cpp|cc|h|hpp)$' \
|
# -p1 strips the leading "a/"/"b/" from git diff paths.
|
||||||
-j$(nproc) || EXIT=$?
|
# -path=build points clang-tidy at compile_commands.json.
|
||||||
|
# -iregex restricts to project sources (not vendored).
|
||||||
# Warn-only initially. Flip this to `exit ${EXIT:-0}` once we're clean.
|
git diff -U0 "$BASE_SHA" -- 'src/*.cpp' 'src/*.h' \
|
||||||
exit 0
|
':(exclude)src/json/nlohmann_json.hpp' \
|
||||||
|
':(exclude)src/leveldb/*' \
|
||||||
|
':(exclude)src/lz4/*' \
|
||||||
|
':(exclude)src/tor/tor-src/*' \
|
||||||
|
| python3 "$DIFF_SCRIPT" -p1 -path build \
|
||||||
|
-iregex '.*\.(cpp|cc|h|hpp)$' \
|
||||||
|
-j$(nproc) || EXIT=$?
|
||||||
|
|
||||||
|
# Warn-only initially. Flip this to `exit ${EXIT:-0}` once we're clean.
|
||||||
|
exit 0
|
||||||
|
|||||||
@@ -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)
|
# Cryptographic Triangles (TRI)
|
||||||
|
|
||||||
Triangles is a privacy-focused cryptocurrency featuring Proof-of-Stake consensus, Tor v3 onion routing, and built-in encrypted messaging. Originally launched in July 2014, the chain was revived in March 2026 after being frozen since December 2022.
|
Triangles is a privacy-focused cryptocurrency featuring Proof-of-Stake consensus, Tor v3 onion routing, and built-in encrypted messaging. Originally launched in July 2014, the chain was revived in March 2026 after being frozen since December 2022.
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- **Proof-of-Stake** - Energy-efficient block production with 33% annual staking rewards (coin-age based)
|
- **Proof-of-Stake** - Energy-efficient block production with 33% annual staking rewards (coin-age based)
|
||||||
- **Hash9 Algorithm** - Unique 13-step hash cascade (Fugue, Hamsi, Groestl, Blake, BMW, Skein, Keccak, Shavite, JH, Luffa, Cubehash, Echo, SIMD)
|
- **Hash9 Algorithm** - Unique 13-step hash cascade (Fugue, Hamsi, Groestl, Blake, BMW, Skein, Keccak, Shavite, JH, Luffa, Cubehash, Echo, SIMD)
|
||||||
- **Encrypted Messaging** - Send and receive encrypted messages directly through the wallet
|
- **Encrypted Messaging** - Send and receive encrypted messages directly through the wallet
|
||||||
- **Tor v3 Integration** - Connect and transact over the Tor network with v3 onion hidden services
|
- **Tor v3 Integration** - Connect and transact over the Tor network with v3 onion hidden services
|
||||||
- **120-second Block Time** - Fast confirmations with 2-minute target spacing
|
- **120-second Block Time** - Fast confirmations with 2-minute target spacing
|
||||||
|
|
||||||
## Specifications
|
## Specifications
|
||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| Algorithm | Hash9 (PoW blocks 0-9000), PoS from block 9001 |
|
| Algorithm | Hash9 (PoW blocks 0-9000), PoS from block 9001 |
|
||||||
| Block Time | ~120 seconds |
|
| Block Time | ~120 seconds |
|
||||||
| Max Supply | 2,222,222 TRI |
|
| Max Supply | 2,222,222 TRI |
|
||||||
| PoS Reward | 33% annual, coin-age based |
|
| PoS Reward | 33% annual, coin-age based |
|
||||||
| P2P Port | 24112 |
|
| P2P Port | 24112 |
|
||||||
| RPC Port | 19112 |
|
| RPC Port | 19112 |
|
||||||
| Protocol | 70205 |
|
| Protocol | 70205 |
|
||||||
|
|
||||||
## Network Status
|
## Network Status
|
||||||
|
|
||||||
The Triangles network operates exclusively over Tor for privacy:
|
The Triangles network operates exclusively over Tor for privacy:
|
||||||
|
|
||||||
**Tor v3 Seeds:**
|
**Tor v3 Seeds:**
|
||||||
- `jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112`
|
- `jbpfhe7zw3qm67wy3j2ayysp3mnrjobopthnko3b3sgahqtecblwqmid.onion:24112`
|
||||||
- `uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112`
|
- `uddaxjbo3lh2zskg7w6gwln4ty5cel7q4c5jbx7fdtv6zf2j47gdlyad.onion:24112`
|
||||||
- `el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112`
|
- `el5sirhhleecuctpeeprelzubpqmoqivvra3rzlwbjttinxa4fq3wnid.onion:24112`
|
||||||
- `sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112`
|
- `sj5dhybnlp3v4y5niyc5unrnd6s43lyx5ibup7rolyosjbi2u2hsbvyd.onion:24112`
|
||||||
- `i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112`
|
- `i3kr5meha7se4ns3wss3h7v46m6uksfzv4wrohdqxpj6n35wyo2bvlid.onion:24112`
|
||||||
|
|
||||||
**HTTP Seed List:**
|
**HTTP Seed List:**
|
||||||
- `seeds.cryptographic-triangles.org/seeds.txt` - Dynamically updated list of active onion peers
|
- `seeds.cryptographic-triangles.org/seeds.txt` - Dynamically updated list of active onion peers
|
||||||
|
|
||||||
## Building from Source
|
## Building from Source
|
||||||
|
|
||||||
Triangles uses CMake. All platforms follow the same build pattern.
|
Triangles uses CMake. All platforms follow the same build pattern.
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
| Dependency | Minimum Version |
|
| Dependency | Minimum Version |
|
||||||
|------------|----------------|
|
|------------|----------------|
|
||||||
| CMake | 3.16+ |
|
| CMake | 3.16+ |
|
||||||
| C++ compiler | C++17 support |
|
| C++ compiler | C++17 support |
|
||||||
| OpenSSL | 3.x |
|
| OpenSSL | 3.x |
|
||||||
| Boost | 1.90+ |
|
| Boost | 1.90+ |
|
||||||
| Berkeley DB | 5.3 (with C++ bindings) |
|
| SQLite | 3.x (default wallet database backend) |
|
||||||
| libevent | 2.x |
|
| Berkeley DB | 5.3 with C++ bindings (legacy wallet backend, used for migration) |
|
||||||
| LevelDB | bundled |
|
| libevent | 2.x |
|
||||||
|
| RocksDB | 7.4+ (default chain database backend) |
|
||||||
### Linux (Ubuntu 24.04 / Debian 12+)
|
| LevelDB | bundled (legacy chain DB backend, used for migration) |
|
||||||
|
|
||||||
Install dependencies:
|
### Linux (Ubuntu 24.04 / Debian 12+)
|
||||||
```bash
|
|
||||||
sudo apt-get install -y build-essential cmake ninja-build \
|
Install dependencies:
|
||||||
libboost-all-dev libssl-dev libdb5.3++-dev libevent-dev \
|
```bash
|
||||||
zlib1g-dev libminiupnpc-dev
|
sudo apt-get install -y build-essential cmake ninja-build \
|
||||||
```
|
libboost-all-dev libssl-dev libdb5.3++-dev libevent-dev \
|
||||||
|
zlib1g-dev libminiupnpc-dev
|
||||||
For the Qt wallet, also install:
|
```
|
||||||
```bash
|
|
||||||
sudo apt-get install -y qtbase5-dev qt5-qmake libqrencode-dev
|
For the Qt wallet, also install:
|
||||||
```
|
```bash
|
||||||
|
sudo apt-get install -y qtbase5-dev qt5-qmake libqrencode-dev
|
||||||
Build:
|
```
|
||||||
```bash
|
|
||||||
cmake -B build -G Ninja -DBUILD_QT=ON
|
Build:
|
||||||
cmake --build build
|
```bash
|
||||||
```
|
cmake -B build -G Ninja -DBUILD_QT=ON
|
||||||
|
cmake --build build
|
||||||
### Linux (AlmaLinux 9 / RHEL 9)
|
```
|
||||||
|
|
||||||
Install dependencies:
|
### Linux (AlmaLinux 9 / RHEL 9)
|
||||||
```bash
|
|
||||||
sudo dnf install -y gcc-c++ cmake ninja-build boost-devel openssl-devel \
|
Install dependencies:
|
||||||
libevent-devel zlib-devel miniupnpc-devel
|
```bash
|
||||||
```
|
sudo dnf install -y gcc-c++ cmake ninja-build boost-devel openssl-devel \
|
||||||
|
libevent-devel zlib-devel miniupnpc-devel
|
||||||
BDB 5.3 C++ bindings must be built from source on RHEL-based systems (the `libdb-devel` package does not include C++ headers). Download BDB 5.3.28 from Oracle and build with `--enable-cxx`.
|
```
|
||||||
|
|
||||||
Then build as above.
|
BDB 5.3 C++ bindings must be built from source on RHEL-based systems (the `libdb-devel` package does not include C++ headers). Download BDB 5.3.28 from Oracle and build with `--enable-cxx`.
|
||||||
|
|
||||||
### Windows (MSYS2 MinGW64)
|
Then build as above.
|
||||||
|
|
||||||
Open an MSYS2 MinGW64 shell and install:
|
### Windows (MSYS2 MinGW64)
|
||||||
```bash
|
|
||||||
pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja \
|
Open an MSYS2 MinGW64 shell and install:
|
||||||
mingw-w64-x86_64-boost mingw-w64-x86_64-openssl \
|
```bash
|
||||||
mingw-w64-x86_64-db mingw-w64-x86_64-miniupnpc \
|
pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja \
|
||||||
mingw-w64-x86_64-qt5-base mingw-w64-x86_64-qrencode \
|
mingw-w64-x86_64-boost mingw-w64-x86_64-openssl \
|
||||||
mingw-w64-x86_64-libevent
|
mingw-w64-x86_64-db mingw-w64-x86_64-miniupnpc \
|
||||||
```
|
mingw-w64-x86_64-qt5-base mingw-w64-x86_64-qrencode \
|
||||||
|
mingw-w64-x86_64-libevent
|
||||||
Build:
|
```
|
||||||
```bash
|
|
||||||
cmake -B build -G Ninja -DBUILD_QT=ON
|
Build:
|
||||||
cmake --build build
|
```bash
|
||||||
```
|
cmake -B build -G Ninja -DBUILD_QT=ON
|
||||||
|
cmake --build build
|
||||||
### Build Options
|
```
|
||||||
|
|
||||||
| Option | Default | Description |
|
### Build Options
|
||||||
|--------|---------|-------------|
|
|
||||||
| `BUILD_QT` | ON | Build the Qt GUI wallet |
|
| Option | Default | Description |
|
||||||
| `BUILD_DAEMON` | ON | Build the headless daemon |
|
|--------|---------|-------------|
|
||||||
| `BUILD_TESTS` | OFF | Build unit tests |
|
| `BUILD_QT` | ON | Build the Qt GUI wallet |
|
||||||
|
| `BUILD_DAEMON` | ON | Build the headless daemon |
|
||||||
## Running
|
| `BUILD_TESTS` | OFF | Build unit tests |
|
||||||
|
|
||||||
### First Run
|
## Running
|
||||||
```bash
|
|
||||||
mkdir -p ~/.triangles
|
### First Run
|
||||||
cat > ~/.triangles/triangles.conf << 'EOF'
|
```bash
|
||||||
port=24112
|
mkdir -p ~/.triangles
|
||||||
rpcport=19112
|
cat > ~/.triangles/triangles.conf << 'EOF'
|
||||||
rpcuser=trianglesrpc
|
port=24112
|
||||||
rpcpassword=<generate-a-strong-password>
|
rpcport=19112
|
||||||
rpcallowip=127.0.0.1
|
rpcuser=trianglesrpc
|
||||||
staking=1
|
rpcpassword=<generate-a-strong-password>
|
||||||
txindex=1
|
rpcallowip=127.0.0.1
|
||||||
listen=1
|
staking=1
|
||||||
server=1
|
txindex=1
|
||||||
daemon=1
|
listen=1
|
||||||
proxy=127.0.0.1:9050
|
server=1
|
||||||
EOF
|
daemon=1
|
||||||
|
proxy=127.0.0.1:9050
|
||||||
trianglesd
|
EOF
|
||||||
```
|
|
||||||
|
trianglesd
|
||||||
The node will connect to seed nodes over Tor and sync the blockchain automatically.
|
```
|
||||||
|
|
||||||
### Existing Wallet Holders
|
The node will connect to seed nodes over Tor and sync the blockchain automatically.
|
||||||
|
|
||||||
If you have a `wallet.dat` from the original Triangles network:
|
### Chain Database (RocksDB)
|
||||||
|
|
||||||
1. Place your `wallet.dat` in `~/.triangles/` (Linux) or `%APPDATA%\triangles\` (Windows)
|
The chain database (block index, transaction index, UTXO set, address index) uses **RocksDB by default**. RocksDB gives faster sync and lookups than the legacy LevelDB backend through parallel compaction, bloom filters, and a larger write buffer and block cache (tunable with `-dbcache=<MB>`).
|
||||||
2. Start the wallet - it will sync the blockchain and your balance will appear automatically
|
|
||||||
3. No migration or special action is needed - all keys and balances are preserved
|
If you are upgrading a node that already has a LevelDB chain database (`txleveldb/` in your data directory), it is migrated automatically on first launch: the chain state is copied into a new `rocksdb/` directory and verified (record count, UTXO count and value, best-chain hash, and DB format must all match) before use. The original `txleveldb/` directory is left untouched as a fallback and is never modified.
|
||||||
|
|
||||||
### Staking
|
To select a backend explicitly:
|
||||||
|
|
||||||
To stake, your wallet must be:
|
```bash
|
||||||
- Running with `staking=1` in the config
|
trianglesd -chaindb=rocksdb # default
|
||||||
- Connected to at least one peer
|
trianglesd -chaindb=leveldb # legacy backend (retained for fallback/migration)
|
||||||
- Containing coins with sufficient coin-age (mature inputs)
|
```
|
||||||
|
|
||||||
Check staking status:
|
Migration can also be triggered or forced manually:
|
||||||
```bash
|
|
||||||
trianglesd getstakinginfo
|
```bash
|
||||||
```
|
trianglesd -migratechaindb # migrate txleveldb -> rocksdb if not already done
|
||||||
|
trianglesd -migratechaindbforce # re-migrate, replacing any existing rocksdb/
|
||||||
### Encrypted Messaging
|
```
|
||||||
|
|
||||||
Send and receive encrypted messages between wallet addresses:
|
### Existing Wallet Holders
|
||||||
|
|
||||||
```bash
|
If you have a `wallet.dat` from the original Triangles network:
|
||||||
# Enable messaging
|
|
||||||
trianglesd smsgenable
|
1. Place your `wallet.dat` in `~/.triangles/` (Linux) or `%APPDATA%\triangles\` (Windows)
|
||||||
|
2. Start the wallet - it will sync the blockchain and your balance will appear automatically
|
||||||
# Send a message
|
3. No migration or special action is needed - all keys and balances are preserved
|
||||||
trianglesd smsgsend <your-address> <recipient-address> "Hello from Triangles!"
|
|
||||||
|
### Staking
|
||||||
# Check inbox
|
|
||||||
trianglesd smsginbox all
|
To stake, your wallet must be:
|
||||||
|
- Running with `staking=1` in the config
|
||||||
# Send anonymous message
|
- Connected to at least one peer
|
||||||
trianglesd smsgsendanon <recipient-address> "Anonymous message"
|
- Containing coins with sufficient coin-age (mature inputs)
|
||||||
```
|
|
||||||
|
Check staking status:
|
||||||
Messages are encrypted end-to-end using AES and distributed through the peer network in time-bucketed batches.
|
```bash
|
||||||
|
trianglesd getstakinginfo
|
||||||
### Tor Support
|
```
|
||||||
|
|
||||||
Triangles is designed as a Tor-only network. Install the Tor daemon and configure your proxy:
|
### Encrypted Messaging
|
||||||
```
|
|
||||||
# triangles.conf
|
Send and receive encrypted messages between wallet addresses:
|
||||||
proxy=127.0.0.1:9050
|
|
||||||
```
|
```bash
|
||||||
|
# Enable messaging
|
||||||
To run your own hidden service, add to `/etc/tor/torrc`:
|
trianglesd smsgenable
|
||||||
```
|
|
||||||
HiddenServiceDir /var/lib/tor/triangles/
|
# Send a message
|
||||||
HiddenServiceVersion 3
|
trianglesd smsgsend <your-address> <recipient-address> "Hello from Triangles!"
|
||||||
HiddenServicePort 24112 127.0.0.1:24112
|
|
||||||
```
|
# Check inbox
|
||||||
|
trianglesd smsginbox all
|
||||||
Then set `externalip=<your-onion-address>` in `triangles.conf`.
|
|
||||||
|
# Send anonymous message
|
||||||
## RPC Commands
|
trianglesd smsgsendanon <recipient-address> "Anonymous message"
|
||||||
|
```
|
||||||
### General
|
|
||||||
- `getinfo` - Node status, balance, block height, connections
|
Messages are encrypted end-to-end using AES and distributed through the peer network in time-bucketed batches.
|
||||||
- `getpeerinfo` - Connected peer details
|
|
||||||
- `getstakinginfo` - Staking status and weight
|
### Tor Support
|
||||||
|
|
||||||
### Wallet
|
Triangles is designed as a Tor-only network. Install the Tor daemon and configure your proxy:
|
||||||
- `getbalance` - Current balance
|
```
|
||||||
- `listunspent` - Unspent transaction outputs
|
# triangles.conf
|
||||||
- `sendtoaddress <addr> <amount>` - Send TRI
|
proxy=127.0.0.1:9050
|
||||||
- `getnewaddress` - Generate new receiving address
|
```
|
||||||
|
|
||||||
### Messaging
|
To run your own hidden service, add to `/etc/tor/torrc`:
|
||||||
- `smsgenable` / `smsgdisable` - Toggle secure messaging
|
```
|
||||||
- `smsgsend <from> <to> <message>` - Send encrypted message
|
HiddenServiceDir /var/lib/tor/triangles/
|
||||||
- `smsgsendanon <to> <message>` - Send anonymous message
|
HiddenServiceVersion 3
|
||||||
- `smsginbox [all|unread|clear]` - View received messages
|
HiddenServicePort 24112 127.0.0.1:24112
|
||||||
- `smsgoutbox [all|clear]` - View sent messages
|
```
|
||||||
- `smsglocalkeys` - List messaging-enabled addresses
|
|
||||||
- `smsgscanchain` - Scan blockchain for public keys
|
Then set `externalip=<your-onion-address>` in `triangles.conf`.
|
||||||
|
|
||||||
## Chain History
|
## RPC Commands
|
||||||
|
|
||||||
- **July 16, 2014** - Genesis block
|
### General
|
||||||
- **Block 0-9000** - Proof-of-Work mining phase (Hash9)
|
- `getinfo` - Node status, balance, block height, connections
|
||||||
- **Block 9001+** - Proof-of-Stake only
|
- `getpeerinfo` - Connected peer details
|
||||||
- **Block 17,651** - V5 hard fork (removed Tor v2, disabled checkpoint master key)
|
- `getstakinginfo` - Staking status and weight
|
||||||
- **December 8, 2022** - Chain frozen (all nodes offline)
|
|
||||||
- **March 11, 2026** - Chain revived, staking resumed
|
### Wallet
|
||||||
|
- `getbalance` - Current balance
|
||||||
## Project Structure
|
- `listunspent` - Unspent transaction outputs
|
||||||
|
- `sendtoaddress <addr> <amount>` - Send TRI
|
||||||
```
|
- `getnewaddress` - Generate new receiving address
|
||||||
src/
|
|
||||||
main.cpp - Core blockchain logic, block/tx validation, message routing
|
### Messaging
|
||||||
miner.cpp - Staking miner thread
|
- `smsgenable` / `smsgdisable` - Toggle secure messaging
|
||||||
net.cpp - P2P networking
|
- `smsgsend <from> <to> <message>` - Send encrypted message
|
||||||
init.cpp - Daemon initialization
|
- `smsgsendanon <to> <message>` - Send anonymous message
|
||||||
wallet.cpp - Wallet management
|
- `smsginbox [all|unread|clear]` - View received messages
|
||||||
smessage.cpp/h - Encrypted messaging system
|
- `smsgoutbox [all|clear]` - View sent messages
|
||||||
kernel.cpp - PoS kernel (stake validation)
|
- `smsglocalkeys` - List messaging-enabled addresses
|
||||||
checkpoints.cpp - Hardcoded checkpoints
|
- `smsgscanchain` - Scan blockchain for public keys
|
||||||
net_bootstrap.h - DNS/IP seed configuration
|
|
||||||
onionseed.h - Tor v3 onion seed addresses
|
## Chain History
|
||||||
tor/
|
|
||||||
onion_v3.cpp/h - Tor v3 hidden service management
|
- **July 16, 2014** - Genesis block
|
||||||
tor_crypto_compat.h - Ed25519/SHA3 crypto compatibility
|
- **Block 0-9000** - Proof-of-Work mining phase (Hash9)
|
||||||
```
|
- **Block 9001+** - Proof-of-Stake only
|
||||||
|
- **Block 17,651** - V5 hard fork (removed Tor v2, disabled checkpoint master key)
|
||||||
## License
|
- **December 8, 2022** - Chain frozen (all nodes offline)
|
||||||
|
- **March 11, 2026** - Chain revived, staking resumed
|
||||||
Distributed under the MIT/X11 software license. See `COPYING` for details.
|
|
||||||
|
## Project Structure
|
||||||
## Links
|
|
||||||
|
```
|
||||||
- Website: [cryptographic-triangles.org](https://cryptographic-triangles.org)
|
src/
|
||||||
- Explorer: [blocks.cryptographic-triangles.org](https://blocks.cryptographic-triangles.org)
|
main.cpp - Core blockchain logic, block/tx validation, message routing
|
||||||
|
miner.cpp - Staking miner thread
|
||||||
|
net.cpp - P2P networking
|
||||||
|
init.cpp - Daemon initialization
|
||||||
|
wallet.cpp - Wallet management
|
||||||
|
smessage.cpp/h - Encrypted messaging system
|
||||||
|
kernel.cpp - PoS kernel (stake validation)
|
||||||
|
checkpoints.cpp - Hardcoded checkpoints
|
||||||
|
net_bootstrap.h - DNS/IP seed configuration
|
||||||
|
onionseed.h - Tor v3 onion seed addresses
|
||||||
|
tor/
|
||||||
|
onion_v3.cpp/h - Tor v3 hidden service management
|
||||||
|
tor_crypto_compat.h - Ed25519/SHA3 crypto compatibility
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Distributed under the MIT/X11 software license. See `COPYING` for details.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Website: [cryptographic-triangles.org](https://cryptographic-triangles.org)
|
||||||
|
- Explorer: [blocks.cryptographic-triangles.org](https://blocks.cryptographic-triangles.org)
|
||||||
|
|||||||
@@ -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) 2009-2012 The Bitcoin developers
|
||||||
|
// Copyright (c) 2026 The Triangles developers
|
||||||
// Distributed under the MIT/X11 software license, see the accompanying
|
// Distributed under the MIT/X11 software license, see the accompanying
|
||||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
//
|
||||||
#include <boost/version.hpp>
|
// Single-instance "triangles:" URI handoff. When the wallet is launched with a
|
||||||
#if defined(WIN32) && BOOST_VERSION == 104900
|
// URI argument and an instance is already running, the URI is relayed to the
|
||||||
#define BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME
|
// running instance over a local socket; otherwise this instance becomes the
|
||||||
#define BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME
|
// listener. Reworked from Boost.Interprocess message queues onto Qt's
|
||||||
#endif
|
// QLocalServer/QLocalSocket (QtNetwork) — no Boost dependency.
|
||||||
|
|
||||||
#include "qtipcserver.h"
|
#include "qtipcserver.h"
|
||||||
#include "guiconstants.h"
|
#include "guiconstants.h"
|
||||||
#include "ui_interface.h"
|
#include "ui_interface.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
|
||||||
#include <boost/interprocess/ipc/message_queue.hpp>
|
|
||||||
#include <boost/version.hpp>
|
|
||||||
|
|
||||||
#if defined(WIN32) && (!defined(BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME) || !defined(BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME) || BOOST_VERSION < 104900)
|
|
||||||
#warning Compiling without BOOST_INTERPROCESS_HAS_WINDOWS_KERNEL_BOOTTIME and BOOST_INTERPROCESS_HAS_KERNEL_BOOTTIME uncommented in boost/interprocess/detail/tmp_dir_helpers.hpp or using a boost version before 1.49 may have unintended results see svn.boost.org/trac/boost/ticket/5392
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using namespace boost;
|
|
||||||
using namespace boost::interprocess;
|
|
||||||
using namespace boost::posix_time;
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QLocalServer>
|
||||||
|
#include <QLocalSocket>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
#if defined MAC_OSX || defined __FreeBSD__
|
#if defined MAC_OSX || defined __FreeBSD__
|
||||||
// URI handling not implemented on OSX yet
|
// URI handling not implemented on OSX yet
|
||||||
@@ -36,33 +31,47 @@ void ipcInit(int argc, char *argv[]) { }
|
|||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
|
// Local-socket server name. QLocalServer maps this to a named pipe on Windows
|
||||||
|
// and a filesystem socket on Unix.
|
||||||
|
static const QString IPC_SERVER_NAME = QStringLiteral(TRIANGLESURI_QUEUE_NAME);
|
||||||
|
|
||||||
static void ipcThread2(void* pArg);
|
static void ipcThread2(void* pArg);
|
||||||
|
|
||||||
|
static bool IsTrianglesURI(const char* arg)
|
||||||
|
{
|
||||||
|
// Case-insensitive match of the "Triangles:" scheme prefix.
|
||||||
|
return std::equal(std::begin("Triangles:"), std::end("Triangles:") - 1, arg,
|
||||||
|
[](char a, char b) {
|
||||||
|
return std::tolower(static_cast<unsigned char>(a)) ==
|
||||||
|
std::tolower(static_cast<unsigned char>(b));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static bool ipcScanCmd(int argc, char *argv[], bool fRelay)
|
static bool ipcScanCmd(int argc, char *argv[], bool fRelay)
|
||||||
{
|
{
|
||||||
// Check for URI in argv
|
// Check for URI in argv and relay it to a running instance, if any.
|
||||||
bool fSent = false;
|
bool fSent = false;
|
||||||
for (int i = 1; i < argc; i++)
|
for (int i = 1; i < argc; i++)
|
||||||
{
|
{
|
||||||
if (std::equal(std::begin("Triangles:"), std::end("Triangles:") - 1, argv[i], [](char a, char b) { return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b)); }))
|
if (!IsTrianglesURI(argv[i]))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const char *strURI = argv[i];
|
||||||
|
QLocalSocket socket;
|
||||||
|
socket.connectToServer(IPC_SERVER_NAME);
|
||||||
|
if (socket.waitForConnected(1000))
|
||||||
{
|
{
|
||||||
const char *strURI = argv[i];
|
socket.write(strURI, static_cast<qint64>(strlen(strURI)));
|
||||||
try {
|
socket.flush();
|
||||||
boost::interprocess::message_queue mq(boost::interprocess::open_only, TRIANGLESURI_QUEUE_NAME);
|
socket.waitForBytesWritten(1000);
|
||||||
if (mq.try_send(strURI, strlen(strURI), 0))
|
socket.disconnectFromServer();
|
||||||
fSent = true;
|
fSent = true;
|
||||||
else if (fRelay)
|
}
|
||||||
break;
|
else if (fRelay)
|
||||||
}
|
{
|
||||||
catch (boost::interprocess::interprocess_exception &ex) {
|
// No running instance accepted the URI; this process should become
|
||||||
// don't log the "file not found" exception, because that's normal for
|
// the listener instead of relaying.
|
||||||
// the first start of the first instance
|
break;
|
||||||
if (ex.get_error_code() != boost::interprocess::not_found_error || !fRelay)
|
|
||||||
{
|
|
||||||
printf("main() - boost interprocess exception #%d: %s\n", ex.get_error_code(), ex.what());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fSent;
|
return fSent;
|
||||||
@@ -78,7 +87,7 @@ static void ipcThread(void* pArg)
|
|||||||
{
|
{
|
||||||
// Make this thread recognisable as the GUI-IPC thread
|
// Make this thread recognisable as the GUI-IPC thread
|
||||||
RenameThread("Triangles-gui-ipc");
|
RenameThread("Triangles-gui-ipc");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ipcThread2(pArg);
|
ipcThread2(pArg);
|
||||||
@@ -95,69 +104,67 @@ static void ipcThread2(void* pArg)
|
|||||||
{
|
{
|
||||||
printf("ipcThread started\n");
|
printf("ipcThread started\n");
|
||||||
|
|
||||||
message_queue* mq = (message_queue*)pArg;
|
QLocalServer* server = static_cast<QLocalServer*>(pArg);
|
||||||
char buffer[MAX_URI_LENGTH + 1] = "";
|
|
||||||
size_t nSize = 0;
|
|
||||||
unsigned int nPriority = 0;
|
|
||||||
|
|
||||||
|
// Poll for inbound connections without requiring a Qt event loop:
|
||||||
|
// waitForNewConnection(timeout) pumps the socket internally.
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
ptime d = boost::posix_time::microsec_clock::universal_time() + millisec(100);
|
if (server->waitForNewConnection(100))
|
||||||
if (mq->timed_receive(&buffer, sizeof(buffer), nSize, nPriority, d))
|
|
||||||
{
|
{
|
||||||
uiInterface.ThreadSafeHandleURI(std::string(buffer, nSize));
|
QLocalSocket* client = server->nextPendingConnection();
|
||||||
MilliSleep(1000);
|
if (client)
|
||||||
|
{
|
||||||
|
if (client->waitForReadyRead(1000))
|
||||||
|
{
|
||||||
|
QByteArray data = client->readAll();
|
||||||
|
if (data.size() > MAX_URI_LENGTH)
|
||||||
|
data.truncate(MAX_URI_LENGTH);
|
||||||
|
uiInterface.ThreadSafeHandleURI(std::string(data.constData(), data.size()));
|
||||||
|
MilliSleep(1000);
|
||||||
|
}
|
||||||
|
client->disconnectFromServer();
|
||||||
|
delete client;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fShutdown)
|
if (fShutdown)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove message queue
|
server->close();
|
||||||
message_queue::remove(TRIANGLESURI_QUEUE_NAME);
|
delete server;
|
||||||
// Cleanup allocated memory
|
|
||||||
delete mq;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ipcInit(int argc, char *argv[])
|
void ipcInit(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
message_queue* mq = NULL;
|
// Clear any stale socket/pipe left by a previous crashed instance, then
|
||||||
char buffer[MAX_URI_LENGTH + 1] = "";
|
// listen. If listen() fails, another instance already owns the name — in
|
||||||
size_t nSize = 0;
|
// that case relay our own URI args (below) and don't start a server.
|
||||||
unsigned int nPriority = 0;
|
QLocalServer::removeServer(IPC_SERVER_NAME);
|
||||||
|
|
||||||
try {
|
QLocalServer* server = new QLocalServer();
|
||||||
mq = new message_queue(open_or_create, TRIANGLESURI_QUEUE_NAME, 2, MAX_URI_LENGTH);
|
server->setSocketOptions(QLocalServer::UserAccessOption); // owner-only access
|
||||||
|
if (!server->listen(IPC_SERVER_NAME))
|
||||||
// Make sure we don't lose any Triangles: URIs
|
|
||||||
for (int i = 0; i < 2; i++)
|
|
||||||
{
|
|
||||||
ptime d = boost::posix_time::microsec_clock::universal_time() + millisec(1);
|
|
||||||
if (mq->timed_receive(&buffer, sizeof(buffer), nSize, nPriority, d))
|
|
||||||
{
|
|
||||||
uiInterface.ThreadSafeHandleURI(std::string(buffer, nSize));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure only one Triangles instance is listening
|
|
||||||
message_queue::remove(TRIANGLESURI_QUEUE_NAME);
|
|
||||||
delete mq;
|
|
||||||
|
|
||||||
mq = new message_queue(open_or_create, TRIANGLESURI_QUEUE_NAME, 2, MAX_URI_LENGTH);
|
|
||||||
}
|
|
||||||
catch (interprocess_exception &ex) {
|
|
||||||
printf("ipcInit() - boost interprocess exception #%d: %s\n", ex.get_error_code(), ex.what());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!NewThread(ipcThread, mq))
|
|
||||||
{
|
{
|
||||||
delete mq;
|
printf("ipcInit() - QLocalServer listen failed: %s\n",
|
||||||
|
server->errorString().toUtf8().constData());
|
||||||
|
delete server;
|
||||||
|
// Still try to relay any URI passed on our command line to whoever is
|
||||||
|
// listening.
|
||||||
|
ipcScanCmd(argc, argv, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!NewThread(ipcThread, server))
|
||||||
|
{
|
||||||
|
server->close();
|
||||||
|
delete server;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a URI passed on our own command line (relayed to the server we
|
||||||
|
// just started).
|
||||||
ipcScanCmd(argc, argv, false);
|
ipcScanCmd(argc, argv, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// Copyright (c) 2009-2012 Bitcoin Developers
|
||||||
// Distributed under the MIT/X11 software license, see the accompanying
|
// Distributed under the MIT/X11 software license, see the accompanying
|
||||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
#include "init.h" // for pwalletMain
|
#include <sstream>
|
||||||
#include "trianglesrpc.h"
|
#include <ctime>
|
||||||
#include "ui_interface.h"
|
|
||||||
#include "base58.h"
|
#include "init.h" // for pwalletMain
|
||||||
|
#include "trianglesrpc.h"
|
||||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
#include "ui_interface.h"
|
||||||
|
#include "base58.h"
|
||||||
#define printf OutputDebugStringF
|
|
||||||
|
#define printf OutputDebugStringF
|
||||||
using namespace json_spirit;
|
|
||||||
using namespace std;
|
using namespace json_spirit;
|
||||||
|
using namespace std;
|
||||||
void EnsureWalletIsUnlocked();
|
|
||||||
|
void EnsureWalletIsUnlocked();
|
||||||
namespace bt = boost::posix_time;
|
|
||||||
|
// Accepted timestamp formats, tried in order. Replaces the boost::posix_time
|
||||||
// Extended DecodeDumpTime implementation, see this page for details:
|
// parser; std::get_time is portable (C++11) and parses against each format.
|
||||||
// http://stackoverflow.com/questions/3786201/parsing-of-date-time-from-string-boost
|
static const char* const dumptime_formats[] = {
|
||||||
const std::locale formats[] = {
|
"%Y-%m-%dT%H:%M:%SZ",
|
||||||
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%dT%H:%M:%SZ")),
|
"%Y-%m-%d %H:%M:%S",
|
||||||
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%d %H:%M:%S")),
|
"%Y/%m/%d %H:%M:%S",
|
||||||
std::locale(std::locale::classic(),new bt::time_input_facet("%Y/%m/%d %H:%M:%S")),
|
"%d.%m.%Y %H:%M:%S",
|
||||||
std::locale(std::locale::classic(),new bt::time_input_facet("%d.%m.%Y %H:%M:%S")),
|
"%Y-%m-%d",
|
||||||
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%d"))
|
};
|
||||||
};
|
|
||||||
|
int64_t DecodeDumpTime(const std::string& s)
|
||||||
const size_t formats_n = sizeof(formats)/sizeof(formats[0]);
|
{
|
||||||
|
for (const char* fmt : dumptime_formats)
|
||||||
std::time_t pt_to_time_t(const bt::ptime& pt)
|
{
|
||||||
{
|
std::tm tm = {};
|
||||||
bt::ptime timet_start(boost::gregorian::date(1970,1,1));
|
std::istringstream is(s);
|
||||||
bt::time_duration diff = pt - timet_start;
|
is >> std::get_time(&tm, fmt);
|
||||||
return diff.ticks()/bt::time_duration::rep_type::ticks_per_second;
|
if (is.fail())
|
||||||
}
|
continue;
|
||||||
|
// Interpret the parsed broken-down time as UTC.
|
||||||
int64_t DecodeDumpTime(const std::string& s)
|
#ifdef WIN32
|
||||||
{
|
std::time_t t = _mkgmtime(&tm);
|
||||||
bt::ptime pt;
|
#else
|
||||||
|
std::time_t t = timegm(&tm);
|
||||||
for(size_t i=0; i<formats_n; ++i)
|
#endif
|
||||||
{
|
if (t != static_cast<std::time_t>(-1))
|
||||||
std::istringstream is(s);
|
return static_cast<int64_t>(t);
|
||||||
is.imbue(formats[i]);
|
}
|
||||||
is >> pt;
|
return 0;
|
||||||
if(pt != bt::ptime()) break;
|
}
|
||||||
}
|
|
||||||
|
std::string static EncodeDumpTime(int64_t nTime) {
|
||||||
return pt_to_time_t(pt);
|
return DateTimeStrFormat("%Y-%m-%dT%H:%M:%SZ", nTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string static EncodeDumpTime(int64_t nTime) {
|
std::string static EncodeDumpString(const std::string &str) {
|
||||||
return DateTimeStrFormat("%Y-%m-%dT%H:%M:%SZ", nTime);
|
std::stringstream ret;
|
||||||
}
|
for (unsigned char c : str) {
|
||||||
|
if (c <= 32 || c >= 128 || c == '%') {
|
||||||
std::string static EncodeDumpString(const std::string &str) {
|
ret << '%' << HexStr(&c, &c + 1);
|
||||||
std::stringstream ret;
|
} else {
|
||||||
for (unsigned char c : str) {
|
ret << c;
|
||||||
if (c <= 32 || c >= 128 || c == '%') {
|
}
|
||||||
ret << '%' << HexStr(&c, &c + 1);
|
}
|
||||||
} else {
|
return ret.str();
|
||||||
ret << c;
|
}
|
||||||
}
|
|
||||||
}
|
std::string DecodeDumpString(const std::string &str) {
|
||||||
return ret.str();
|
std::stringstream ret;
|
||||||
}
|
for (unsigned int pos = 0; pos < str.length(); pos++) {
|
||||||
|
unsigned char c = str[pos];
|
||||||
std::string DecodeDumpString(const std::string &str) {
|
if (c == '%' && pos+2 < str.length()) {
|
||||||
std::stringstream ret;
|
c = (((str[pos+1]>>6)*9+((str[pos+1]-'0')&15)) << 4) |
|
||||||
for (unsigned int pos = 0; pos < str.length(); pos++) {
|
((str[pos+2]>>6)*9+((str[pos+2]-'0')&15));
|
||||||
unsigned char c = str[pos];
|
pos += 2;
|
||||||
if (c == '%' && pos+2 < str.length()) {
|
}
|
||||||
c = (((str[pos+1]>>6)*9+((str[pos+1]-'0')&15)) << 4) |
|
ret << c;
|
||||||
((str[pos+2]>>6)*9+((str[pos+2]-'0')&15));
|
}
|
||||||
pos += 2;
|
return ret.str();
|
||||||
}
|
}
|
||||||
ret << c;
|
|
||||||
}
|
class CTxDump
|
||||||
return ret.str();
|
{
|
||||||
}
|
public:
|
||||||
|
CBlockIndex *pindex;
|
||||||
class CTxDump
|
int64_t nValue;
|
||||||
{
|
bool fSpent;
|
||||||
public:
|
CWalletTx* ptx;
|
||||||
CBlockIndex *pindex;
|
int nOut;
|
||||||
int64_t nValue;
|
CTxDump(CWalletTx* ptx = nullptr, int nOut = -1)
|
||||||
bool fSpent;
|
{
|
||||||
CWalletTx* ptx;
|
pindex = nullptr;
|
||||||
int nOut;
|
nValue = 0;
|
||||||
CTxDump(CWalletTx* ptx = nullptr, int nOut = -1)
|
fSpent = false;
|
||||||
{
|
this->ptx = ptx;
|
||||||
pindex = nullptr;
|
this->nOut = nOut;
|
||||||
nValue = 0;
|
}
|
||||||
fSpent = false;
|
};
|
||||||
this->ptx = ptx;
|
|
||||||
this->nOut = nOut;
|
Value importprivkey(const Array& params, bool fHelp)
|
||||||
}
|
{
|
||||||
};
|
if (fHelp || params.size() < 1 || params.size() > 2)
|
||||||
|
throw runtime_error(
|
||||||
Value importprivkey(const Array& params, bool fHelp)
|
"importprivkey <Trianglesprivkey> [label]\n"
|
||||||
{
|
"Adds a private key (as returned by dumpprivkey) to your wallet.");
|
||||||
if (fHelp || params.size() < 1 || params.size() > 2)
|
|
||||||
throw runtime_error(
|
string strSecret = params[0].get_str();
|
||||||
"importprivkey <Trianglesprivkey> [label]\n"
|
string strLabel = "";
|
||||||
"Adds a private key (as returned by dumpprivkey) to your wallet.");
|
if (params.size() > 1)
|
||||||
|
strLabel = params[1].get_str();
|
||||||
string strSecret = params[0].get_str();
|
CTrianglesSecret vchSecret;
|
||||||
string strLabel = "";
|
bool fGood = vchSecret.SetString(strSecret);
|
||||||
if (params.size() > 1)
|
|
||||||
strLabel = params[1].get_str();
|
if (!fGood) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key");
|
||||||
CTrianglesSecret vchSecret;
|
if (fWalletUnlockStakingOnly)
|
||||||
bool fGood = vchSecret.SetString(strSecret);
|
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
|
||||||
|
|
||||||
if (!fGood) throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key");
|
CKey key;
|
||||||
if (fWalletUnlockStakingOnly)
|
bool fCompressed;
|
||||||
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
|
CSecret secret = vchSecret.GetSecret(fCompressed);
|
||||||
|
key.SetSecret(secret, fCompressed);
|
||||||
CKey key;
|
CKeyID vchAddress = key.GetPubKey().GetID();
|
||||||
bool fCompressed;
|
{
|
||||||
CSecret secret = vchSecret.GetSecret(fCompressed);
|
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||||
key.SetSecret(secret, fCompressed);
|
|
||||||
CKeyID vchAddress = key.GetPubKey().GetID();
|
pwalletMain->MarkDirty();
|
||||||
{
|
pwalletMain->SetAddressBookName(vchAddress, strLabel);
|
||||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
|
||||||
|
if (!pwalletMain->AddKey(key))
|
||||||
pwalletMain->MarkDirty();
|
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet");
|
||||||
pwalletMain->SetAddressBookName(vchAddress, strLabel);
|
|
||||||
|
pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
|
||||||
if (!pwalletMain->AddKey(key))
|
pwalletMain->ReacceptWalletTransactions();
|
||||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet");
|
}
|
||||||
|
|
||||||
pwalletMain->ScanForWalletTransactions(pindexGenesisBlock, true);
|
return Value::null;
|
||||||
pwalletMain->ReacceptWalletTransactions();
|
}
|
||||||
}
|
|
||||||
|
Value importwallet(const Array& params, bool fHelp)
|
||||||
return Value::null;
|
{
|
||||||
}
|
if (fHelp || params.size() != 1)
|
||||||
|
throw runtime_error(
|
||||||
Value importwallet(const Array& params, bool fHelp)
|
"importwallet <filename>\n"
|
||||||
{
|
"Imports keys from a wallet dump file (see dumpwallet).");
|
||||||
if (fHelp || params.size() != 1)
|
|
||||||
throw runtime_error(
|
EnsureWalletIsUnlocked();
|
||||||
"importwallet <filename>\n"
|
|
||||||
"Imports keys from a wallet dump file (see dumpwallet).");
|
ifstream file;
|
||||||
|
file.open(params[0].get_str().c_str());
|
||||||
EnsureWalletIsUnlocked();
|
if (!file.is_open())
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
|
||||||
ifstream file;
|
|
||||||
file.open(params[0].get_str().c_str());
|
int64_t nTimeBegin = pindexBest->nTime;
|
||||||
if (!file.is_open())
|
|
||||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
|
bool fGood = true;
|
||||||
|
|
||||||
int64_t nTimeBegin = pindexBest->nTime;
|
while (file.good()) {
|
||||||
|
std::string line;
|
||||||
bool fGood = true;
|
std::getline(file, line);
|
||||||
|
if (line.empty() || line[0] == '#')
|
||||||
while (file.good()) {
|
continue;
|
||||||
std::string line;
|
|
||||||
std::getline(file, line);
|
auto vstr = SplitString(line, ' ');
|
||||||
if (line.empty() || line[0] == '#')
|
if (vstr.size() < 2)
|
||||||
continue;
|
continue;
|
||||||
|
CTrianglesSecret vchSecret;
|
||||||
auto vstr = SplitString(line, ' ');
|
if (!vchSecret.SetString(vstr[0]))
|
||||||
if (vstr.size() < 2)
|
continue;
|
||||||
continue;
|
|
||||||
CTrianglesSecret vchSecret;
|
bool fCompressed;
|
||||||
if (!vchSecret.SetString(vstr[0]))
|
CKey key;
|
||||||
continue;
|
CSecret secret = vchSecret.GetSecret(fCompressed);
|
||||||
|
key.SetSecret(secret, fCompressed);
|
||||||
bool fCompressed;
|
CKeyID keyid = key.GetPubKey().GetID();
|
||||||
CKey key;
|
|
||||||
CSecret secret = vchSecret.GetSecret(fCompressed);
|
if (pwalletMain->HaveKey(keyid)) {
|
||||||
key.SetSecret(secret, fCompressed);
|
printf("Skipping import of %s (key already present)\n", CTrianglesAddress(keyid).ToString().c_str());
|
||||||
CKeyID keyid = key.GetPubKey().GetID();
|
continue;
|
||||||
|
}
|
||||||
if (pwalletMain->HaveKey(keyid)) {
|
int64_t nTime = DecodeDumpTime(vstr[1]);
|
||||||
printf("Skipping import of %s (key already present)\n", CTrianglesAddress(keyid).ToString().c_str());
|
std::string strLabel;
|
||||||
continue;
|
bool fLabel = true;
|
||||||
}
|
for (unsigned int nStr = 2; nStr < vstr.size(); nStr++) {
|
||||||
int64_t nTime = DecodeDumpTime(vstr[1]);
|
if (vstr[nStr].starts_with("#"))
|
||||||
std::string strLabel;
|
break;
|
||||||
bool fLabel = true;
|
if (vstr[nStr] == "change=1")
|
||||||
for (unsigned int nStr = 2; nStr < vstr.size(); nStr++) {
|
fLabel = false;
|
||||||
if (vstr[nStr].starts_with("#"))
|
if (vstr[nStr] == "reserve=1")
|
||||||
break;
|
fLabel = false;
|
||||||
if (vstr[nStr] == "change=1")
|
if (vstr[nStr].starts_with("label=")) {
|
||||||
fLabel = false;
|
strLabel = DecodeDumpString(vstr[nStr].substr(6));
|
||||||
if (vstr[nStr] == "reserve=1")
|
fLabel = true;
|
||||||
fLabel = false;
|
}
|
||||||
if (vstr[nStr].starts_with("label=")) {
|
}
|
||||||
strLabel = DecodeDumpString(vstr[nStr].substr(6));
|
printf("Importing %s...\n", CTrianglesAddress(keyid).ToString().c_str());
|
||||||
fLabel = true;
|
if (!pwalletMain->AddKey(key)) {
|
||||||
}
|
fGood = false;
|
||||||
}
|
continue;
|
||||||
printf("Importing %s...\n", CTrianglesAddress(keyid).ToString().c_str());
|
}
|
||||||
if (!pwalletMain->AddKey(key)) {
|
pwalletMain->mapKeyMetadata[keyid].nCreateTime = nTime;
|
||||||
fGood = false;
|
if (fLabel)
|
||||||
continue;
|
pwalletMain->SetAddressBookName(keyid, strLabel);
|
||||||
}
|
nTimeBegin = std::min(nTimeBegin, nTime);
|
||||||
pwalletMain->mapKeyMetadata[keyid].nCreateTime = nTime;
|
}
|
||||||
if (fLabel)
|
file.close();
|
||||||
pwalletMain->SetAddressBookName(keyid, strLabel);
|
|
||||||
nTimeBegin = std::min(nTimeBegin, nTime);
|
CBlockIndex *pindex = pindexBest;
|
||||||
}
|
while (pindex && pindex->pprev && pindex->nTime > nTimeBegin - 7200)
|
||||||
file.close();
|
pindex = pindex->pprev;
|
||||||
|
|
||||||
CBlockIndex *pindex = pindexBest;
|
if (!pwalletMain->nTimeFirstKey || nTimeBegin < pwalletMain->nTimeFirstKey)
|
||||||
while (pindex && pindex->pprev && pindex->nTime > nTimeBegin - 7200)
|
pwalletMain->nTimeFirstKey = nTimeBegin;
|
||||||
pindex = pindex->pprev;
|
|
||||||
|
printf("Rescanning last %i blocks\n", pindexBest->nHeight - pindex->nHeight + 1);
|
||||||
if (!pwalletMain->nTimeFirstKey || nTimeBegin < pwalletMain->nTimeFirstKey)
|
pwalletMain->ScanForWalletTransactions(pindex);
|
||||||
pwalletMain->nTimeFirstKey = nTimeBegin;
|
pwalletMain->ReacceptWalletTransactions();
|
||||||
|
pwalletMain->MarkDirty();
|
||||||
printf("Rescanning last %i blocks\n", pindexBest->nHeight - pindex->nHeight + 1);
|
|
||||||
pwalletMain->ScanForWalletTransactions(pindex);
|
if (!fGood)
|
||||||
pwalletMain->ReacceptWalletTransactions();
|
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding some keys to wallet");
|
||||||
pwalletMain->MarkDirty();
|
|
||||||
|
return Value::null;
|
||||||
if (!fGood)
|
}
|
||||||
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding some keys to wallet");
|
|
||||||
|
|
||||||
return Value::null;
|
Value dumpprivkey(const Array& params, bool fHelp)
|
||||||
}
|
{
|
||||||
|
if (fHelp || params.size() != 1)
|
||||||
|
throw runtime_error(
|
||||||
Value dumpprivkey(const Array& params, bool fHelp)
|
"dumpprivkey <Trianglesaddress>\n"
|
||||||
{
|
"Reveals the private key corresponding to <Trianglesaddress>.");
|
||||||
if (fHelp || params.size() != 1)
|
|
||||||
throw runtime_error(
|
EnsureWalletIsUnlocked();
|
||||||
"dumpprivkey <Trianglesaddress>\n"
|
|
||||||
"Reveals the private key corresponding to <Trianglesaddress>.");
|
string strAddress = params[0].get_str();
|
||||||
|
CTrianglesAddress address;
|
||||||
EnsureWalletIsUnlocked();
|
if (!address.SetString(strAddress))
|
||||||
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Triangles address");
|
||||||
string strAddress = params[0].get_str();
|
if (fWalletUnlockStakingOnly)
|
||||||
CTrianglesAddress address;
|
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
|
||||||
if (!address.SetString(strAddress))
|
CKeyID keyID;
|
||||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid Triangles address");
|
if (!address.GetKeyID(keyID))
|
||||||
if (fWalletUnlockStakingOnly)
|
throw JSONRPCError(RPC_TYPE_ERROR, "Address does not refer to a key");
|
||||||
throw JSONRPCError(RPC_WALLET_UNLOCK_NEEDED, "Wallet is unlocked for staking only.");
|
CSecret vchSecret;
|
||||||
CKeyID keyID;
|
bool fCompressed;
|
||||||
if (!address.GetKeyID(keyID))
|
if (!pwalletMain->GetSecret(keyID, vchSecret, fCompressed))
|
||||||
throw JSONRPCError(RPC_TYPE_ERROR, "Address does not refer to a key");
|
throw JSONRPCError(RPC_WALLET_ERROR, "Private key for address " + strAddress + " is not known");
|
||||||
CSecret vchSecret;
|
return CTrianglesSecret(vchSecret, fCompressed).ToString();
|
||||||
bool fCompressed;
|
}
|
||||||
if (!pwalletMain->GetSecret(keyID, vchSecret, fCompressed))
|
|
||||||
throw JSONRPCError(RPC_WALLET_ERROR, "Private key for address " + strAddress + " is not known");
|
Value dumpwallet(const Array& params, bool fHelp)
|
||||||
return CTrianglesSecret(vchSecret, fCompressed).ToString();
|
{
|
||||||
}
|
if (fHelp || params.size() != 1)
|
||||||
|
throw runtime_error(
|
||||||
Value dumpwallet(const Array& params, bool fHelp)
|
"dumpwallet <filename>\n"
|
||||||
{
|
"Dumps all wallet keys in a human-readable format.");
|
||||||
if (fHelp || params.size() != 1)
|
|
||||||
throw runtime_error(
|
EnsureWalletIsUnlocked();
|
||||||
"dumpwallet <filename>\n"
|
|
||||||
"Dumps all wallet keys in a human-readable format.");
|
ofstream file;
|
||||||
|
file.open(params[0].get_str().c_str());
|
||||||
EnsureWalletIsUnlocked();
|
if (!file.is_open())
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
|
||||||
ofstream file;
|
|
||||||
file.open(params[0].get_str().c_str());
|
std::map<CKeyID, int64_t> mapKeyBirth;
|
||||||
if (!file.is_open())
|
|
||||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Cannot open wallet dump file");
|
std::set<CKeyID> setKeyPool;
|
||||||
|
|
||||||
std::map<CKeyID, int64_t> mapKeyBirth;
|
pwalletMain->GetKeyBirthTimes(mapKeyBirth);
|
||||||
|
|
||||||
std::set<CKeyID> setKeyPool;
|
pwalletMain->GetAllReserveKeys(setKeyPool);
|
||||||
|
|
||||||
pwalletMain->GetKeyBirthTimes(mapKeyBirth);
|
// sort time/key pairs
|
||||||
|
std::vector<std::pair<int64_t, CKeyID> > vKeyBirth;
|
||||||
pwalletMain->GetAllReserveKeys(setKeyPool);
|
for (std::map<CKeyID, int64_t>::const_iterator it = mapKeyBirth.begin(); it != mapKeyBirth.end(); it++) {
|
||||||
|
vKeyBirth.push_back({it->second, it->first});
|
||||||
// sort time/key pairs
|
}
|
||||||
std::vector<std::pair<int64_t, CKeyID> > vKeyBirth;
|
mapKeyBirth.clear();
|
||||||
for (std::map<CKeyID, int64_t>::const_iterator it = mapKeyBirth.begin(); it != mapKeyBirth.end(); it++) {
|
std::sort(vKeyBirth.begin(), vKeyBirth.end());
|
||||||
vKeyBirth.push_back({it->second, it->first});
|
|
||||||
}
|
// produce output
|
||||||
mapKeyBirth.clear();
|
file << strprintf("# Wallet dump created by Triangles %s (%s)\n", CLIENT_BUILD.c_str(), CLIENT_DATE.c_str());
|
||||||
std::sort(vKeyBirth.begin(), vKeyBirth.end());
|
file << strprintf("# * Created on %s\n", EncodeDumpTime(GetTime()).c_str());
|
||||||
|
file << strprintf("# * Best block at time of backup was %i (%s),\n", nBestHeight, hashBestChain.ToString().c_str());
|
||||||
// produce output
|
file << strprintf("# mined on %s\n", EncodeDumpTime(pindexBest->nTime).c_str());
|
||||||
file << strprintf("# Wallet dump created by Triangles %s (%s)\n", CLIENT_BUILD.c_str(), CLIENT_DATE.c_str());
|
file << "\n";
|
||||||
file << strprintf("# * Created on %s\n", EncodeDumpTime(GetTime()).c_str());
|
for (std::vector<std::pair<int64_t, CKeyID> >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) {
|
||||||
file << strprintf("# * Best block at time of backup was %i (%s),\n", nBestHeight, hashBestChain.ToString().c_str());
|
const CKeyID &keyid = it->second;
|
||||||
file << strprintf("# mined on %s\n", EncodeDumpTime(pindexBest->nTime).c_str());
|
std::string strTime = EncodeDumpTime(it->first);
|
||||||
file << "\n";
|
std::string strAddr = CTrianglesAddress(keyid).ToString();
|
||||||
for (std::vector<std::pair<int64_t, CKeyID> >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) {
|
bool IsCompressed;
|
||||||
const CKeyID &keyid = it->second;
|
|
||||||
std::string strTime = EncodeDumpTime(it->first);
|
CKey key;
|
||||||
std::string strAddr = CTrianglesAddress(keyid).ToString();
|
if (pwalletMain->GetKey(keyid, key)) {
|
||||||
bool IsCompressed;
|
if (pwalletMain->mapAddressBook.count(keyid)) {
|
||||||
|
CSecret secret = key.GetSecret(IsCompressed);
|
||||||
CKey key;
|
file << strprintf("%s %s label=%s # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), EncodeDumpString(pwalletMain->mapAddressBook[keyid]).c_str(), strAddr.c_str());
|
||||||
if (pwalletMain->GetKey(keyid, key)) {
|
} else if (setKeyPool.count(keyid)) {
|
||||||
if (pwalletMain->mapAddressBook.count(keyid)) {
|
CSecret secret = key.GetSecret(IsCompressed);
|
||||||
CSecret secret = key.GetSecret(IsCompressed);
|
file << strprintf("%s %s reserve=1 # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), strAddr.c_str());
|
||||||
file << strprintf("%s %s label=%s # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), EncodeDumpString(pwalletMain->mapAddressBook[keyid]).c_str(), strAddr.c_str());
|
} else {
|
||||||
} else if (setKeyPool.count(keyid)) {
|
CSecret secret = key.GetSecret(IsCompressed);
|
||||||
CSecret secret = key.GetSecret(IsCompressed);
|
file << strprintf("%s %s change=1 # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), strAddr.c_str());
|
||||||
file << strprintf("%s %s reserve=1 # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), strAddr.c_str());
|
}
|
||||||
} else {
|
}
|
||||||
CSecret secret = key.GetSecret(IsCompressed);
|
}
|
||||||
file << strprintf("%s %s change=1 # addr=%s\n", CTrianglesSecret(secret, IsCompressed).ToString().c_str(), strTime.c_str(), strAddr.c_str());
|
file << "\n";
|
||||||
}
|
file << "# End of dump\n";
|
||||||
}
|
file.close();
|
||||||
}
|
return Value::null;
|
||||||
file << "\n";
|
}
|
||||||
file << "# End of dump\n";
|
|
||||||
file.close();
|
|
||||||
return Value::null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
// Copyright (c) 2026 The Triangles developers.
|
||||||
// Distributed under the MIT/X11 software license, see the accompanying
|
// Distributed under the MIT/X11 software license, see the accompanying
|
||||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
#include "txdb.h"
|
#include "txdb.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Pick the backend once per process. -chaindb is a startup flag; switching at
|
// Pick the backend on every call. The daemon sets -chaindb once at startup
|
||||||
// runtime would require reopening every CTxDB instance, which the codebase
|
// and never changes it, so the per-call cost (a GetArg + tolower loop on a
|
||||||
// doesn't currently support. We cache the resolved choice so subsequent
|
// short string) is negligible compared to the cost of opening the chain DB.
|
||||||
// MakeChainDB calls don't re-parse the argument.
|
// The earlier static-cache version broke test_chaindb_runtime, which
|
||||||
enum class ChainDbKind { LevelDB, RocksDB };
|
// legitimately toggles -chaindb across test cases to exercise both backends
|
||||||
|
// in the same process. Caching would freeze the first-seen choice.
|
||||||
ChainDbKind ResolveChainDbKind()
|
enum class ChainDbKind { LevelDB, RocksDB };
|
||||||
{
|
|
||||||
static const ChainDbKind kKind = []() {
|
ChainDbKind ResolveChainDbKind()
|
||||||
std::string s = GetArg("-chaindb", std::string("leveldb"));
|
{
|
||||||
for (auto& c : s) c = std::tolower(static_cast<unsigned char>(c));
|
// RocksDB is the default backend. LevelDB remains selectable with
|
||||||
|
// -chaindb=leveldb and is retained as the migration source and fallback;
|
||||||
if (s == "leveldb")
|
// its removal is deferred to a later phase after live-chain validation.
|
||||||
return ChainDbKind::LevelDB;
|
std::string s = GetArg("-chaindb", std::string("rocksdb"));
|
||||||
if (s == "rocksdb")
|
for (auto& c : s) c = std::tolower(static_cast<unsigned char>(c));
|
||||||
return ChainDbKind::RocksDB;
|
|
||||||
|
if (s == "leveldb")
|
||||||
throw std::runtime_error(
|
return ChainDbKind::LevelDB;
|
||||||
"-chaindb=" + s + " is not a recognized backend. "
|
if (s == "rocksdb")
|
||||||
"Valid values: leveldb, rocksdb.");
|
return ChainDbKind::RocksDB;
|
||||||
}();
|
|
||||||
return kKind;
|
throw std::runtime_error(
|
||||||
}
|
"-chaindb=" + s + " is not a recognized backend. "
|
||||||
|
"Valid values: leveldb, rocksdb.");
|
||||||
} // anonymous namespace
|
}
|
||||||
|
|
||||||
std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode)
|
} // anonymous namespace
|
||||||
{
|
|
||||||
switch (ResolveChainDbKind()) {
|
std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode)
|
||||||
case ChainDbKind::LevelDB:
|
{
|
||||||
return std::unique_ptr<CTxDBBase>(new CTxDB(pszMode));
|
switch (ResolveChainDbKind()) {
|
||||||
case ChainDbKind::RocksDB:
|
case ChainDbKind::LevelDB:
|
||||||
return std::unique_ptr<CTxDBBase>(new CRocksTxDB(pszMode));
|
return std::unique_ptr<CTxDBBase>(new CTxDB(pszMode));
|
||||||
}
|
case ChainDbKind::RocksDB:
|
||||||
// Unreachable — ResolveChainDbKind throws on bad input.
|
return std::unique_ptr<CTxDBBase>(new CRocksTxDB(pszMode));
|
||||||
return nullptr;
|
}
|
||||||
}
|
// Unreachable — ResolveChainDbKind throws on bad input.
|
||||||
|
return nullptr;
|
||||||
bool IsRocksDbChainBackend()
|
}
|
||||||
{
|
|
||||||
return ResolveChainDbKind() == ChainDbKind::RocksDB;
|
bool IsRocksDbChainBackend()
|
||||||
}
|
{
|
||||||
|
return ResolveChainDbKind() == ChainDbKind::RocksDB;
|
||||||
std::filesystem::path GetChainDataDir()
|
}
|
||||||
{
|
|
||||||
switch (ResolveChainDbKind()) {
|
std::filesystem::path GetChainDataDir()
|
||||||
case ChainDbKind::LevelDB: return GetDataDir() / "txleveldb";
|
{
|
||||||
case ChainDbKind::RocksDB: return GetDataDir() / "rocksdb";
|
switch (ResolveChainDbKind()) {
|
||||||
}
|
case ChainDbKind::LevelDB: return GetDataDir() / "txleveldb";
|
||||||
return GetDataDir() / "txleveldb"; // unreachable
|
case ChainDbKind::RocksDB: return GetDataDir() / "rocksdb";
|
||||||
}
|
}
|
||||||
|
return GetDataDir() / "txleveldb"; // unreachable
|
||||||
void WipeChainDataDir()
|
}
|
||||||
{
|
|
||||||
fs::path p = GetChainDataDir();
|
void WipeChainDataDir()
|
||||||
if (fs::exists(p))
|
{
|
||||||
fs::remove_all(p);
|
fs::path p = GetChainDataDir();
|
||||||
}
|
if (fs::exists(p))
|
||||||
|
fs::remove_all(p);
|
||||||
|
}
|
||||||
|
|||||||
+696
-673
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.
|
// Copyright (c) 2026 The Triangles developers.
|
||||||
// Distributed under the MIT/X11 software license, see the accompanying
|
// Distributed under the MIT/X11 software license, see the accompanying
|
||||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
#ifndef TRIANGLES_TXDB_ROCKSDB_H
|
#ifndef TRIANGLES_TXDB_ROCKSDB_H
|
||||||
#define TRIANGLES_TXDB_ROCKSDB_H
|
#define TRIANGLES_TXDB_ROCKSDB_H
|
||||||
|
|
||||||
#include "txdb-base.h"
|
#include "txdb-base.h"
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
#include <rocksdb/db.h>
|
#include <vector>
|
||||||
#include <rocksdb/options.h>
|
|
||||||
#include <rocksdb/write_batch.h>
|
#include <rocksdb/db.h>
|
||||||
|
#include <rocksdb/options.h>
|
||||||
// RocksDB backend for the chain database.
|
#include <rocksdb/write_batch.h>
|
||||||
//
|
#include <rocksdb/utilities/db_ttl.h>
|
||||||
// Mirrors CTxDB (LevelDB) for byte-level compatibility. CTxDBBase owns all
|
|
||||||
// key serialization, so keys produced by this backend are bit-identical to
|
// RocksDB backend for the chain database.
|
||||||
// the LevelDB backend. That property is what lets the M1.4 dual-backend
|
//
|
||||||
// parity harness verify equivalence.
|
// Mirrors CTxDB (LevelDB) for byte-level compatibility. CTxDBBase owns all
|
||||||
//
|
// key serialization, so keys produced by this backend are bit-identical to
|
||||||
// Data lives under <datadir>/rocksdb/, separate from <datadir>/txleveldb/,
|
// the LevelDB backend. That property is what lets the M1.4 dual-backend
|
||||||
// so both backends can coexist for migration and side-by-side testing.
|
// parity harness verify equivalence.
|
||||||
class CRocksTxDB final : public CTxDBBase
|
//
|
||||||
{
|
// Data lives under <datadir>/rocksdb/, separate from <datadir>/txleveldb/,
|
||||||
public:
|
// so both backends can coexist for migration and side-by-side testing.
|
||||||
CRocksTxDB(const char* pszMode = "r+");
|
class CRocksTxDB final : public CTxDBBase
|
||||||
~CRocksTxDB() override;
|
{
|
||||||
|
public:
|
||||||
void Close() override;
|
CRocksTxDB(const char* pszMode = "r+");
|
||||||
|
~CRocksTxDB() override;
|
||||||
bool TxnBegin() override;
|
|
||||||
bool TxnCommit() override;
|
void Close() override;
|
||||||
bool TxnAbort() override;
|
|
||||||
|
bool TxnBegin() override;
|
||||||
bool LoadBlockIndex() override;
|
bool TxnCommit() override;
|
||||||
|
bool TxnAbort() override;
|
||||||
// Write a raw serialized key/value pair, bypassing the typed Write<>()
|
|
||||||
// overloads. Intended for the chaindb migration utility, which carries
|
bool LoadBlockIndex() override;
|
||||||
// bytes directly across from a CTxDB (LevelDB) iterator. Honors the
|
|
||||||
// active write batch if one is open.
|
// Write a raw serialized key/value pair, bypassing the typed Write<>()
|
||||||
bool WriteRawRecordForMigration(const std::string& key, const std::string& value)
|
// overloads. Intended for the chaindb migration utility, which carries
|
||||||
{
|
// bytes directly across from a CTxDB (LevelDB) iterator. Honors the
|
||||||
return WriteRaw(key, value);
|
// active write batch if one is open.
|
||||||
}
|
bool WriteRawRecordForMigration(const std::string& key, const std::string& value)
|
||||||
|
{
|
||||||
std::unique_ptr<CTxDBIteratorBase> NewIterator() const override;
|
return WriteRaw(key, value);
|
||||||
|
}
|
||||||
protected:
|
|
||||||
bool ReadRaw(const std::string& key, std::string& value) const override;
|
std::unique_ptr<CTxDBIteratorBase> NewIterator() const override;
|
||||||
bool WriteRaw(const std::string& key, const std::string& value) override;
|
|
||||||
bool EraseRaw(const std::string& key) override;
|
// ─── Test-only friend accessor ──────────────────────────────────────────
|
||||||
bool ExistsRaw(const std::string& key) const override;
|
// test_chaindb_runtime exercises the protected raw methods (ReadRaw /
|
||||||
|
// WriteRaw / EraseRaw / ExistsRaw) directly to verify the wrapper layer
|
||||||
private:
|
// that the daemon uses at runtime when launched with -chaindb=rocksdb.
|
||||||
rocksdb::DB* pdb; // Points to the global instance.
|
// We don't widen the public API just for the test — instead the test
|
||||||
rocksdb::WriteBatch* activeBatch; // When non-NULL, writes/deletes go here.
|
// declares a ChainDbRuntimeTestAccessor struct that this class befriends,
|
||||||
rocksdb::Options options;
|
// giving it the same access the class itself has. White-box test pattern,
|
||||||
int nVersion;
|
// zero impact on production callers.
|
||||||
|
friend struct ChainDbRuntimeTestAccessor;
|
||||||
// Parallel record of every pending write (value) or delete (nullopt) on
|
|
||||||
// activeBatch. Used by ScanBatch to answer "is this key already in the
|
protected:
|
||||||
// active batch?" without iterating the WriteBatch via Handler — Ubuntu's
|
bool ReadRaw(const std::string& key, std::string& value) const override;
|
||||||
// librocksdb-dev hides typeinfo for rocksdb::WriteBatch::Handler so a
|
bool WriteRaw(const std::string& key, const std::string& value) override;
|
||||||
// subclass-based scan fails to link there.
|
bool EraseRaw(const std::string& key) override;
|
||||||
std::map<std::string, std::optional<std::string>> pendingBatch;
|
bool ExistsRaw(const std::string& key) const override;
|
||||||
|
|
||||||
bool ScanBatch(const std::string& key, std::string* value, bool* deleted) const;
|
private:
|
||||||
};
|
rocksdb::DB* pdb; // Points to the global instance.
|
||||||
|
rocksdb::WriteBatch* activeBatch; // When non-NULL, writes/deletes go here.
|
||||||
#endif // TRIANGLES_TXDB_ROCKSDB_H
|
rocksdb::Options options;
|
||||||
|
int nVersion;
|
||||||
|
|
||||||
|
// ─── Column family support (DISABLED) ────────────────────────────────────
|
||||||
|
// CF partitioning is intentionally off: the read path (NewIterator /
|
||||||
|
// LoadBlockIndex) only iterates the default CF, so all data must live there
|
||||||
|
// for scans to be correct. GetCF() therefore always returns nullptr (the
|
||||||
|
// default CF). See the long note in txdb-rocksdb.cpp's GetCF definition.
|
||||||
|
// These members are retained for a future CF-aware-iteration phase.
|
||||||
|
enum CfId : int { CF_DEFAULT = 0, CF_BLOCKINDEX, CF_TXINDEX, CF_UTXO, CF_ADDRINDEX, CF_COUNT };
|
||||||
|
rocksdb::ColumnFamilyHandle* cf_handles[CF_COUNT] = {};
|
||||||
|
bool cf_enabled = false; // Always false while CF routing is disabled.
|
||||||
|
|
||||||
|
// Returns the column family a key should live in. While CF partitioning is
|
||||||
|
// disabled this always returns nullptr (= default CF).
|
||||||
|
rocksdb::ColumnFamilyHandle* GetCF(const std::string& key) const;
|
||||||
|
|
||||||
|
// Parallel record of every pending write (value) or delete (nullopt) on
|
||||||
|
// activeBatch. Used by ScanBatch to answer "is this key already in the
|
||||||
|
// active batch?" without iterating the WriteBatch via Handler — Ubuntu's
|
||||||
|
// librocksdb-dev hides typeinfo for rocksdb::WriteBatch::Handler so a
|
||||||
|
// subclass-based scan fails to link there.
|
||||||
|
std::unordered_map<std::string, std::optional<std::string>> pendingBatch;
|
||||||
|
|
||||||
|
bool ScanBatch(const std::string& key, std::string* value, bool* deleted) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // TRIANGLES_TXDB_ROCKSDB_H
|
||||||
|
|||||||
+41
-40
@@ -1,40 +1,41 @@
|
|||||||
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
// Copyright (c) 2009-2010 Satoshi Nakamoto
|
||||||
// Copyright (c) 2009-2012 The Bitcoin developers
|
// Copyright (c) 2009-2012 The Bitcoin developers
|
||||||
// Copyright (c) 2026 The Triangles developers
|
// Copyright (c) 2026 The Triangles developers
|
||||||
// Distributed under the MIT/X11 software license, see the accompanying
|
// Distributed under the MIT/X11 software license, see the accompanying
|
||||||
// file license.txt or http://www.opensource.org/licenses/mit-license.php.
|
// file license.txt or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
#ifndef TRIANGLES_TXDB_H
|
#ifndef TRIANGLES_TXDB_H
|
||||||
#define TRIANGLES_TXDB_H
|
#define TRIANGLES_TXDB_H
|
||||||
|
|
||||||
#include "txdb-base.h"
|
#include "txdb-base.h"
|
||||||
#include "txdb-leveldb.h"
|
#include "txdb-leveldb.h"
|
||||||
#include "txdb-rocksdb.h"
|
#include "txdb-rocksdb.h"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
// Factory: returns a chain-database handle whose concrete backend is chosen
|
// Factory: returns a chain-database handle whose concrete backend is chosen
|
||||||
// by the -chaindb command-line argument:
|
// by the -chaindb command-line argument:
|
||||||
//
|
//
|
||||||
// -chaindb=leveldb (default — pending Phase-4 retirement)
|
// -chaindb=rocksdb (default)
|
||||||
// -chaindb=rocksdb
|
// -chaindb=leveldb (retained as migration source + fallback; pending
|
||||||
//
|
// retirement after live-chain validation)
|
||||||
// Callers receive a CTxDBBase*, so the rest of the codebase stays
|
//
|
||||||
// backend-agnostic. Mode strings ("r", "r+", "cr+") match the pre-existing
|
// Callers receive a CTxDBBase*, so the rest of the codebase stays
|
||||||
// CTxDB constructor convention.
|
// backend-agnostic. Mode strings ("r", "r+", "cr+") match the pre-existing
|
||||||
std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode = "r+");
|
// CTxDB constructor convention.
|
||||||
|
std::unique_ptr<CTxDBBase> MakeChainDB(const char* pszMode = "r+");
|
||||||
// True when the configured chain-DB backend is RocksDB.
|
|
||||||
bool IsRocksDbChainBackend();
|
// True when the configured chain-DB backend is RocksDB.
|
||||||
|
bool IsRocksDbChainBackend();
|
||||||
// On-disk directory of the chain DB for the configured backend, e.g.
|
|
||||||
// <datadir>/txleveldb (LevelDB) or <datadir>/rocksdb (RocksDB).
|
// On-disk directory of the chain DB for the configured backend, e.g.
|
||||||
std::filesystem::path GetChainDataDir();
|
// <datadir>/txleveldb (LevelDB) or <datadir>/rocksdb (RocksDB).
|
||||||
|
std::filesystem::path GetChainDataDir();
|
||||||
// Remove the chain DB directory for the configured backend. Callers that
|
|
||||||
// need a fresh DB (-reindex, snapshot load) must invoke this BEFORE
|
// Remove the chain DB directory for the configured backend. Callers that
|
||||||
// MakeChainDB() opens the global handle for the first time.
|
// need a fresh DB (-reindex, snapshot load) must invoke this BEFORE
|
||||||
void WipeChainDataDir();
|
// MakeChainDB() opens the global handle for the first time.
|
||||||
|
void WipeChainDataDir();
|
||||||
#endif // TRIANGLES_TXDB_H
|
|
||||||
|
#endif // TRIANGLES_TXDB_H
|
||||||
|
|||||||
+1422
-1394
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