Compare commits
12 Commits
autoshield
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 4dc57e80b1 | |||
| 04ac7c1186 | |||
| 65130c3120 | |||
| 698bcf9574 | |||
| a0ccb4be1d | |||
| 358011bd54 | |||
| 7a62fc4877 | |||
| 9a8f17b2c8 | |||
| 1ec6590fb7 | |||
| 92e6c7008d | |||
| 733df964ec | |||
| d12e7dc99d |
@@ -1,4 +1,8 @@
|
||||
FROM ubuntu:20.04
|
||||
# Base image is parameterised so one Dockerfile can produce binaries for several
|
||||
# glibc floors: docker build --build-arg BASE_IMAGE=ubuntu:18.04 ...
|
||||
# The default is unchanged, so `./build.sh --linux-compat` behaves exactly as before.
|
||||
ARG BASE_IMAGE=ubuntu:20.04
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
|
||||
26
build.sh
26
build.sh
@@ -6,10 +6,34 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
VERSION="1.0.3"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
RELEASE_DIR="$SCRIPT_DIR/release"
|
||||
|
||||
# Derive the release version from configure.ac instead of hardcoding it here.
|
||||
# A stale literal names the release directories after the wrong version while the
|
||||
# binaries inside report the real one: this said 1.0.3 while the tree was already
|
||||
# 1.1.0, so `./build.sh --all-release` would have produced
|
||||
# release/dragonx-1.0.3-<platform>/ full of binaries announcing 1.1.0.
|
||||
# Mirrors configure.ac's _CLIENT_VERSION_SUFFIX m4 exactly:
|
||||
# build < 25 -> beta(build+1) build < 50 -> rc(build-24)
|
||||
# build == 50 -> plain release build > 50 -> point release (build-50)
|
||||
_acdef() { sed -n "s/^define(_CLIENT_VERSION_$1, *\([0-9]\{1,\}\))/\1/p" "$SCRIPT_DIR/configure.ac"; }
|
||||
_V_MAJOR="$(_acdef MAJOR)"
|
||||
_V_MINOR="$(_acdef MINOR)"
|
||||
_V_REVISION="$(_acdef REVISION)"
|
||||
_V_BUILD="$(_acdef BUILD)"
|
||||
if [ -z "$_V_MAJOR" ] || [ -z "$_V_MINOR" ] || [ -z "$_V_REVISION" ] || [ -z "$_V_BUILD" ]; then
|
||||
echo "ERROR: could not read the version from $SCRIPT_DIR/configure.ac" >&2
|
||||
echo " refusing to build a release whose directory name would be wrong." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$_V_BUILD" -lt 25 ]; then _V_SUFFIX="$_V_REVISION-beta$((_V_BUILD + 1))"
|
||||
elif [ "$_V_BUILD" -lt 50 ]; then _V_SUFFIX="$_V_REVISION-rc$((_V_BUILD - 24))"
|
||||
elif [ "$_V_BUILD" -eq 50 ]; then _V_SUFFIX="$_V_REVISION"
|
||||
else _V_SUFFIX="$_V_REVISION-$((_V_BUILD - 50))"
|
||||
fi
|
||||
VERSION="$_V_MAJOR.$_V_MINOR.$_V_SUFFIX"
|
||||
|
||||
# Parse release flags
|
||||
BUILD_LINUX_RELEASE=0
|
||||
BUILD_WIN_RELEASE=0
|
||||
|
||||
@@ -2,7 +2,7 @@ dnl require autoconf 2.60 (AS_ECHO/AS_ECHO_N)
|
||||
AC_PREREQ([2.60])
|
||||
define(_CLIENT_VERSION_MAJOR, 1)
|
||||
dnl Must be kept in sync with src/clientversion.h , ugh!
|
||||
define(_CLIENT_VERSION_MINOR, 1)
|
||||
define(_CLIENT_VERSION_MINOR, 2)
|
||||
define(_CLIENT_VERSION_REVISION, 0)
|
||||
define(_CLIENT_VERSION_BUILD, 50)
|
||||
define(_ZC_BUILD_VAL, m4_if(m4_eval(_CLIENT_VERSION_BUILD < 25), 1, m4_incr(_CLIENT_VERSION_BUILD), m4_eval(_CLIENT_VERSION_BUILD < 50), 1, m4_eval(_CLIENT_VERSION_BUILD - 24), m4_eval(_CLIENT_VERSION_BUILD == 50), 1, , m4_eval(_CLIENT_VERSION_BUILD - 50)))
|
||||
|
||||
@@ -1,3 +1,35 @@
|
||||
dragonx (1.2.0) stable; urgency=medium
|
||||
|
||||
* Auto-shield matured coinbase into a wallet-owned Sapling address on a block
|
||||
interval. The destination is derived from the HD seed at m/32'/coin'/i' and
|
||||
is the lowest index inside -mnemonicsaplinggap, so a bare seed-phrase restore
|
||||
re-derives it; auto-shielding refuses to run rather than send anywhere a
|
||||
restore would not find. Enabled only when the seed's provenance is known to be
|
||||
recoverable, so upgraded wallets stay opted out until the operator says
|
||||
otherwise.
|
||||
* Create new wallets from a BIP39 seed phrase by default, byte-compatible with
|
||||
SilentDragonXLite. z_exportmnemonic returns the phrase; -mnemonic restores
|
||||
from it.
|
||||
* New RPC z_autoshieldstatus reports whether auto-shielding is on, the resolved
|
||||
destination, the HD seed's provenance, and why it is off when it is off.
|
||||
* Bound each auto-shield round to 400 inputs and correct the transaction size
|
||||
estimate to account for all three Sapling output descriptions, and lock the
|
||||
selected coins for the duration of proof building so a concurrent
|
||||
z_shieldcoinbase or z_sendmany cannot select them too.
|
||||
* Repair, rather than reject, an hdchain record truncated by an older wallet
|
||||
build. Previously one address generated under a pre-1.1.0 binary left the
|
||||
wallet unopenable with "Wallet corrupted"; the record is now completed and
|
||||
rewritten, and the error text names the seed-phrase remedy when it genuinely
|
||||
cannot be recovered.
|
||||
* Clear a stale sweep flag that could otherwise leave sweeping, consolidation
|
||||
and auto-shielding permanently disabled together, and stop the async queue
|
||||
silently discarding operations at shutdown while reporting success.
|
||||
* Derive the release version from configure.ac in build.sh instead of a
|
||||
hardcoded literal, and document container-based release builds in
|
||||
doc/build-containers.md.
|
||||
|
||||
-- DragonX Developers <dev@dragonx.is> Tue, 25 Aug 2026 19:45:00 +0000
|
||||
|
||||
dragonx (1.1.0) stable; urgency=medium
|
||||
|
||||
* Extend DRAGONX checkpoints to height 3,226,000, enabling the existing
|
||||
|
||||
223
doc/build-containers.md
Normal file
223
doc/build-containers.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Building release binaries in containers
|
||||
|
||||
Release binaries must be built in a container based on an **old** Linux distribution.
|
||||
This document is written to be executed, by a person or an agent, on a machine that
|
||||
has nothing set up yet.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why this exists
|
||||
|
||||
glibc compatibility runs one way only. A binary linked against glibc 2.35 demands
|
||||
symbol versions that glibc 2.31 does not have, and refuses to start. A binary linked
|
||||
against glibc 2.29 runs on 2.29, 2.31 and 2.35 alike.
|
||||
|
||||
Measured on the actual fleet, 2026-08-25:
|
||||
|
||||
| binary | max GLIBC required | runs on |
|
||||
|---|---|---|
|
||||
| what all four 20.04 seeds run today (`v1.0.3-d159e7208`) | `GLIBC_2.29` | 18.04, 20.04, 22.04 |
|
||||
| anything built on seed 176 today (Ubuntu 22.04) | `GLIBC_2.34` | 22.04 only |
|
||||
|
||||
The second binary will not start on four of our own five seeds. The loader reports:
|
||||
|
||||
```
|
||||
/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found
|
||||
/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.30' not found
|
||||
```
|
||||
|
||||
Nothing new is being *called*. glibc 2.34 merged libpthread and libdl into libc and
|
||||
re-versioned every `pthread_*`, `dlsym` and `dladdr` symbol; 2.33 replaced the old
|
||||
`__xstat` inlines with real `stat`/`fstat`/`lstat64`. All of those functions exist in
|
||||
2.31 under older tags. Building against older headers is the entire fix.
|
||||
|
||||
**Do not try to solve this with full static linking.** The daemon calls `getaddrinfo`,
|
||||
`gethostbyname` and `getnameinfo`, and it must resolve `node1..node5.dragonx.is`, which
|
||||
are hard-coded and injected into `-addnode` on every node. Under a fully static glibc
|
||||
binary those go through NSS, which `dlopen`s `libnss_dns.so.2` at run time — it either
|
||||
fails or silently requires the target to have the same glibc you linked against, which
|
||||
defeats the purpose.
|
||||
|
||||
---
|
||||
|
||||
## 2. What already exists in this repo
|
||||
|
||||
Do not write a new build system. Two pieces are already here:
|
||||
|
||||
- **`Dockerfile.compat`** — an Ubuntu base image that installs the toolchain, copies the
|
||||
tree, **deletes any host-built `depends/` and object files**, runs `./util/build.sh`,
|
||||
and strips the three binaries.
|
||||
- **`./build.sh --linux-compat`** — builds that image, creates a throwaway container,
|
||||
copies `dragonxd`, `dragonx-cli` and `dragonx-tx` out into
|
||||
`release/dragonx-<version>-linux-amd64-ubuntu2004/`, adds `bootstrap-dragonx.sh`,
|
||||
`asmap.dat` and the two sapling params, fixes ownership, and prints the binary's
|
||||
maximum required GLIBC version.
|
||||
|
||||
The base image is parameterised via `ARG BASE_IMAGE` (default `ubuntu:20.04`), so the
|
||||
same Dockerfile can target several glibc floors.
|
||||
|
||||
---
|
||||
|
||||
## 3. Prerequisites
|
||||
|
||||
Docker (the scripted path uses `docker` specifically; podman works for the manual path
|
||||
if you alias or substitute it).
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y docker.io git
|
||||
sudo usermod -aG docker "$USER" # then log out and back in, or every command needs sudo
|
||||
```
|
||||
|
||||
Budget, measured on a 4-core box:
|
||||
|
||||
| resource | needs |
|
||||
|---|---|
|
||||
| disk | ~15 GB free (the `depends/` tree alone is ~1.6 GB per target, plus image layers) |
|
||||
| RAM | 4 GB minimum, 8 GB comfortable — the link step is the peak |
|
||||
| time | **1–2 hours per base image on first build.** `depends/` builds boost, BDB, wolfssl, libevent, libsodium, libcurl and rust from source. Later builds reuse Docker layer cache unless the tree changed. |
|
||||
|
||||
`depends/` downloads and builds its own rust toolchain, so the host's rust (or absence
|
||||
of it) is irrelevant.
|
||||
|
||||
---
|
||||
|
||||
## 4. Build one target
|
||||
|
||||
```sh
|
||||
git clone https://git.dragonx.is/DragonX/dragonx
|
||||
cd dragonx
|
||||
git checkout <the tag or branch you are releasing>
|
||||
|
||||
./build.sh --linux-compat
|
||||
```
|
||||
|
||||
Output lands in `release/dragonx-<version>-linux-amd64-ubuntu2004/` and the script
|
||||
prints the max GLIBC at the end. `<version>` is read from `configure.ac`, not
|
||||
hardcoded, so it always matches what the binaries report.
|
||||
|
||||
---
|
||||
|
||||
## 5. Build several targets
|
||||
|
||||
```sh
|
||||
for BASE in ubuntu:18.04 ubuntu:20.04 ubuntu:22.04; do
|
||||
TAG="dragonx-compat-${BASE#ubuntu:}"
|
||||
TAG="${TAG//./}"
|
||||
docker build --build-arg "BASE_IMAGE=$BASE" -f Dockerfile.compat -t "$TAG" .
|
||||
|
||||
OUT="release/dragonx-$(grep -oP 'define\(_CLIENT_VERSION_MAJOR, \K[0-9]+' configure.ac).$(grep -oP 'define\(_CLIENT_VERSION_MINOR, \K[0-9]+' configure.ac).$(grep -oP 'define\(_CLIENT_VERSION_REVISION, \K[0-9]+' configure.ac)-linux-amd64-${BASE#ubuntu:}"
|
||||
mkdir -p "$OUT"
|
||||
CID=$(docker create "$TAG")
|
||||
for b in dragonxd dragonx-cli dragonx-tx; do docker cp "$CID:/build/src/$b" "$OUT/$b"; done
|
||||
docker rm "$CID" >/dev/null
|
||||
cp util/bootstrap-dragonx.sh contrib/asmap/asmap.dat sapling-output.params sapling-spend.params "$OUT/" 2>/dev/null || true
|
||||
done
|
||||
```
|
||||
|
||||
### Which base to choose
|
||||
|
||||
| base | glibc it provides | default GCC | verdict |
|
||||
|---|---|---|---|
|
||||
| `ubuntu:18.04` | 2.27 | 7 | **Verify before relying on it.** The tree is built with `-std=c++17`; GCC 7's C++17 support is incomplete and its cmake (3.10) may be too old for RandomX. Attempt only if you need to reach 18.04 users, and treat a successful build as the proof. |
|
||||
| `ubuntu:20.04` | 2.31 | 9 | **Recommended floor.** GCC 9 covers C++17 fully. Evidence it works: the binary the fleet runs today requires only `GLIBC_2.29`, i.e. the code touches nothing newer, so a 20.04 build reaches 18.04 machines anyway. |
|
||||
| `ubuntu:22.04` | 2.35 | 11 | **Do not ship this.** It is what we already have and what excludes four of our own seeds. Useful only for development. |
|
||||
|
||||
Ubuntu 20.04 left standard support in April 2025, which is precisely why it belongs in
|
||||
a container on a patched host rather than on a build box someone has to maintain.
|
||||
|
||||
---
|
||||
|
||||
## 6. Verify — this step is not optional
|
||||
|
||||
A build that silently targets the wrong glibc looks completely normal until a user
|
||||
reports that nothing starts.
|
||||
|
||||
```sh
|
||||
BIN=release/dragonx-<version>-linux-amd64-ubuntu2004/dragonxd
|
||||
|
||||
# The ceiling. Must be <= the glibc of the OLDEST system you intend to support.
|
||||
objdump -p "$BIN" | grep -oE 'GLIBC_2\.[0-9]+' | sort -t. -k2 -n | tail -1
|
||||
objdump -p "$BIN" | grep -oE 'GLIBCXX_3\.4\.[0-9]+' | sort -t. -k3 -n | tail -1
|
||||
|
||||
# If the ceiling is too high, this names the symbols responsible.
|
||||
readelf --dyn-syms --wide "$BIN" | grep -E '@GLIBC_2\.(3[2-9])'
|
||||
```
|
||||
|
||||
Expected for a 20.04 build: `GLIBC_2.29` or lower, `GLIBCXX_3.4.26` or lower.
|
||||
|
||||
Then actually run it somewhere old. A ceiling check proves the loader will resolve the
|
||||
symbols; it does not prove the binary works. `./dragonxd --version` on a real 20.04 box
|
||||
is a ten-second confirmation.
|
||||
|
||||
---
|
||||
|
||||
## 7. Traps
|
||||
|
||||
Each of these has cost real time.
|
||||
|
||||
**`ETXTBSY` when installing over a running daemon.** `cp` onto the binary fails with
|
||||
"Text file busy" *even after the process has exited* — `pgrep` returning nothing is not
|
||||
sufficient, the kernel still holds the text mapping. Stage into the same directory and
|
||||
`mv` (rename is not blocked), allow ~10 s to settle, and **sha256-verify the installed
|
||||
file before starting it**. A failed copy that goes unnoticed leaves the old binary
|
||||
running and looks like a successful deploy.
|
||||
|
||||
**Never touch `configure.ac` in a configured tree.** Even `cp`-ing back a byte-identical
|
||||
copy updates its mtime, which makes `make` regenerate `aclocal.m4` and `configure` and
|
||||
then re-run `configure`, which fails with `libdb_cxx headers missing` because the
|
||||
depends prefix is not on the command line. If it happens: confirm
|
||||
`git diff --quiet HEAD -- configure.ac`, then restore mtime order oldest-to-newest with
|
||||
one-second gaps — `configure.ac`/`Makefile.am`, then `aclocal.m4`, then
|
||||
`configure`/`Makefile.in`, then `config.status`, then `Makefile`. Inside a container
|
||||
this cannot happen, which is one more reason to build there.
|
||||
|
||||
**Never blind-`touch` a path that might not exist.** `touch src/config/hush-config.h`
|
||||
silently *creates* an empty stray file; the real header is `bitcoin-config.h`. Check
|
||||
`git status` after any timestamp surgery.
|
||||
|
||||
**RandomX must be built with `ARCH=default`.** `util/build.sh` already passes it and the
|
||||
comment there explains why: `ARCH=native` tunes to the build machine, and a build on an
|
||||
AVX-512 host emitted 746 `zmm` instructions into `librandomx.a`, which `SIGILL`s on the
|
||||
entire fleet. If you ever invoke cmake by hand, pass `-DARCH=default`.
|
||||
|
||||
**Strip before distributing.** Unstripped is ~220 MB, stripped ~16 MB. `Dockerfile.compat`
|
||||
already strips inside the container.
|
||||
|
||||
**`util/build-win.sh` discards every argument.** There is no `"$@"` handling in it, so
|
||||
`-j$(nproc)` and `--disable-tests` are dropped on the floor and the Windows build is
|
||||
single-threaded. Expect it to be far slower than you planned.
|
||||
|
||||
**Windows also needs `-Wa,-mbig-obj` and `-DARCH=default`.** Both are in
|
||||
`util/build-win.sh` today. The mingw flag was missing from `dev` for a month; without it
|
||||
the cross-compile fails at link because boost-heavy translation units exceed the
|
||||
PE/COFF section limit. Do not lose it on a re-branch.
|
||||
|
||||
**macOS cannot be containerised.** `util/build-mac.sh` is a native-Mac script, and there
|
||||
is no darwin cross-compile path in `depends/` at all: `hosts/darwin.mk` wants
|
||||
`native_cctools`, which has no package definition, and there is no SDK in the tree. It
|
||||
also hardcodes an Intel Homebrew GCC path, so it produces x86_64 only — no arm64, no
|
||||
universal binary. macOS needs a real Mac.
|
||||
|
||||
**The `contrib/gitian-descriptors/` files are not a build path.** They are unmodified
|
||||
upstream Bitcoin files (`name: "bitcoin-win-0.11"`, suite `trusty`) with zero DragonX
|
||||
content. Ignore them.
|
||||
|
||||
---
|
||||
|
||||
## 8. Handoff checklist
|
||||
|
||||
- [ ] Docker installed, user in the `docker` group, ~15 GB free
|
||||
- [ ] Correct tag or branch checked out, tree clean (`git status`)
|
||||
- [ ] Version in `configure.ac` is the one you intend to release
|
||||
- [ ] `./build.sh --linux-compat` completes
|
||||
- [ ] GLIBC ceiling is **2.31 or lower** (2.29 expected)
|
||||
- [ ] GLIBCXX ceiling is **3.4.28 or lower** (3.4.26 expected)
|
||||
- [ ] `dragonxd --version` runs on a real machine of the oldest supported distro
|
||||
- [ ] Binaries stripped, `release/` contains the bootstrap script, `asmap.dat` and both sapling params
|
||||
- [ ] sha256 recorded for each artifact
|
||||
|
||||
One more thing that is not a build step but belongs in the same conversation: the
|
||||
in-app daemon updater refuses any release without a detached signature
|
||||
(`kDaemonRequireSignature = true`). Publishing checksums alone means no existing user
|
||||
can update in place.
|
||||
@@ -96,18 +96,21 @@ void AsyncRPCQueue::run(size_t workerId) {
|
||||
*
|
||||
* Don't use std::make_shared<AsyncRPCOperation>().
|
||||
*/
|
||||
void AsyncRPCQueue::addOperation(const std::shared_ptr<AsyncRPCOperation> &ptrOperation) {
|
||||
bool AsyncRPCQueue::addOperation(const std::shared_ptr<AsyncRPCOperation> &ptrOperation) {
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
|
||||
// Don't add if queue is closed or finishing
|
||||
// Don't add if queue is closed or finishing. Report it: silently dropping the
|
||||
// operation made callers announce work that would never run.
|
||||
// (isClosed/isFinishing read atomics, so calling them under the guard is safe.)
|
||||
if (isClosed() || isFinishing()) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
AsyncRPCOperationId id = ptrOperation->getId();
|
||||
operation_map_.emplace(id, ptrOperation);
|
||||
operation_id_queue_.push(id);
|
||||
this->condition_.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -63,7 +63,12 @@ public:
|
||||
size_t getOperationCount() const;
|
||||
std::shared_ptr<AsyncRPCOperation> getOperationForId(AsyncRPCOperationId) const;
|
||||
std::shared_ptr<AsyncRPCOperation> popOperationForId(AsyncRPCOperationId);
|
||||
void addOperation(const std::shared_ptr<AsyncRPCOperation> &ptrOperation);
|
||||
// Returns false if the queue is closed or finishing, in which case the
|
||||
// operation was NOT queued and will never run. Callers must react: a caller
|
||||
// that ignores this both reports success for work that will not happen and
|
||||
// leaves any state it set for the operation (running flags, coin locks)
|
||||
// stranded for the life of the process.
|
||||
bool addOperation(const std::shared_ptr<AsyncRPCOperation> &ptrOperation);
|
||||
std::vector<AsyncRPCOperationId> getAllOperationIds() const;
|
||||
|
||||
private:
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
//! These need to be macros, as clientversion.cpp's and bitcoin*-res.rc's voodoo requires it
|
||||
// Must be kept in sync with configure.ac , ugh!
|
||||
#define CLIENT_VERSION_MAJOR 1
|
||||
#define CLIENT_VERSION_MINOR 1
|
||||
#define CLIENT_VERSION_MINOR 2
|
||||
#define CLIENT_VERSION_REVISION 0
|
||||
#define CLIENT_VERSION_BUILD 50
|
||||
|
||||
|
||||
25
src/init.cpp
25
src/init.cpp
@@ -60,6 +60,7 @@
|
||||
#include "wallet/wallet.h"
|
||||
#include "wallet/walletdb.h"
|
||||
#include "wallet/asyncrpcoperation_saplingconsolidation.h"
|
||||
#include "wallet/asyncrpcoperation_autoshieldcoinbase.h"
|
||||
#include "wallet/asyncrpcoperation_sweep.h"
|
||||
#endif
|
||||
#include <stdint.h>
|
||||
@@ -2270,7 +2271,10 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
||||
if (nLoadWalletRet != DB_LOAD_OK)
|
||||
{
|
||||
if (nLoadWalletRet == DB_CORRUPT)
|
||||
strErrors << _("Error loading wallet.dat: Wallet corrupted") << "\n";
|
||||
strErrors << _("Error loading wallet.dat: Wallet corrupted. If this wallet was last opened "
|
||||
"by an older version, move wallet.dat aside and restore from your seed "
|
||||
"phrase with -mnemonic=\"<your seed phrase>\" -rescan (see debug.log for "
|
||||
"the specific record at fault).") << "\n";
|
||||
else if (nLoadWalletRet == DB_NONCRITICAL_ERROR)
|
||||
{
|
||||
string msg(_("Warning: error reading wallet.dat! All keys read correctly, but transaction data"
|
||||
@@ -2556,6 +2560,25 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
||||
return InitError("Wallet must hold the spending key of -autoshieldaddress (else shielded coinbase would be unrecoverable)");
|
||||
}
|
||||
pwalletMain->autoShieldAddress = autoShieldAddress;
|
||||
} else {
|
||||
// No explicit destination. Resolve the seed-derived one now, read-only,
|
||||
// so z_autoshieldstatus can say where coinbase will go BEFORE the first
|
||||
// round rather than reporting an empty string until one fires. This
|
||||
// never generates a key: a fresh account must not be a side effect of
|
||||
// populating a status field. A brand-new wallet holds nothing in-gap
|
||||
// yet, so the field stays empty and the RPC explains why.
|
||||
LOCK(pwalletMain->cs_wallet);
|
||||
if (!pwalletMain->IsLocked()) {
|
||||
libzcash::SaplingPaymentAddress destAddr;
|
||||
std::string destStr;
|
||||
uint32_t destAccount = AUTOSHIELD_ACCOUNT_NONE;
|
||||
if (ResolveAutoShieldDestinationReadOnly(destAddr, destStr, destAccount)
|
||||
== AutoShieldDestStatus::Resolved) {
|
||||
pwalletMain->autoShieldAddress = destStr;
|
||||
LogPrintf("%s: autoshield destination %s (seed-derived sapling account %u)\n",
|
||||
__func__, destStr, (unsigned)destAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -369,6 +369,7 @@ extern UniValue z_gettotalbalance(const UniValue& params, bool fHelp, const CPub
|
||||
extern UniValue z_mergetoaddress(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
|
||||
extern UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
|
||||
extern UniValue z_sweepstatus(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
|
||||
extern UniValue z_autoshieldstatus(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
|
||||
extern UniValue z_consolidationstatus(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
|
||||
extern UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
|
||||
extern UniValue z_getoperationstatus(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
|
||||
|
||||
@@ -22,6 +22,20 @@ extern std::string randomSietchZaddr();
|
||||
|
||||
// Serialized-size estimates for one spent input (kept in sync with rpcwallet.cpp)
|
||||
static const size_t AUTOSHIELD_CTXIN_DUST_SIZE = 148;
|
||||
// Every autoshield tx carries THREE Sapling OutputDescriptions -- the change
|
||||
// note to destZaddr plus the two Sietch dummies -- at ~948 bytes each. Reserving
|
||||
// 2000 for "header + sietch outputs" was ~900 bytes short before a single input
|
||||
// was counted, so a large enough round could build a tx over MAX_TX_SIZE.
|
||||
static const size_t AUTOSHIELD_SAPLING_OUTPUT_SIZE = 948;
|
||||
static const size_t AUTOSHIELD_TX_OVERHEAD = (3 * AUTOSHIELD_SAPLING_OUTPUT_SIZE) + 256;
|
||||
// Hard cap on inputs per round, mirroring z_shieldcoinbase's
|
||||
// SHIELD_COINBASE_DEFAULT_LIMIT. The byte estimate alone is not a safe bound:
|
||||
// with a P2PKH coinbase (-mineraddress) the 148-byte figure is exact rather than
|
||||
// conservative, so an under-estimate translates directly into an oversize tx.
|
||||
// The remainder is simply shielded on the next round.
|
||||
static const size_t AUTOSHIELD_MAX_INPUTS = 400;
|
||||
// Unrelated to the cap above despite sharing the value: this is a SIZE IN BYTES for
|
||||
// one spent P2SH input, mirroring CTXIN_SPEND_P2SH_SIZE in rpcwallet.cpp.
|
||||
static const size_t AUTOSHIELD_CTXIN_P2SH_SIZE = 400;
|
||||
// Expire unmined autoshield txs after this many blocks, so a tx cannot straddle
|
||||
// a network-upgrade activation.
|
||||
@@ -93,50 +107,34 @@ void AsyncRPCOperation_autoshieldcoinbase::main() {
|
||||
getId(), getStateAsString(), numTxCreated_, FormatMoney(amountShielded_));
|
||||
}
|
||||
|
||||
// Resolve the Sapling destination for auto-shielded coinbase.
|
||||
//
|
||||
// Recoverability is the hard requirement: coinbase we shield must land in an
|
||||
// address that a bare -mnemonic/-hdseed restore of THIS wallet's seed re-derives
|
||||
// on its own. A restore pre-derives exactly -mnemonicsaplinggap sapling accounts
|
||||
// starting at index 0, with saplingAccountCounter reset to 0 (init.cpp:2349-2355),
|
||||
// so the only self-recoverable destinations are the default addresses of
|
||||
// m/32'/<coin>'/i' for i < gap.
|
||||
//
|
||||
// We therefore DERIVE those accounts from the seed and pick the lowest index the
|
||||
// wallet already holds. Deriving is the only authoritative test. In particular
|
||||
// CKeyMetadata is NOT evidence of provenance: z_importkey / z_importwallet copy
|
||||
// both hdKeypath and seedFp verbatim out of the import source
|
||||
// (wallet.cpp:5522-5529 <- rpcdump.cpp:511-516), so a foreign key can claim any
|
||||
// keypath and any seed fingerprint. Filtering on metadata would let an imported
|
||||
// key win as "account 0" and silently receive every shielded reward.
|
||||
//
|
||||
// Caller must hold cs_wallet and must already have checked the wallet is unlocked.
|
||||
bool AsyncRPCOperation_autoshieldcoinbase::resolveDestination(
|
||||
libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut) {
|
||||
// Read-only half of destination resolution, shared with init.cpp so the answer to
|
||||
// "where will auto-shielding send?" is available before the first round runs rather
|
||||
// than only after one has fired. Mutates nothing: no key generation, no caching.
|
||||
AutoShieldDestStatus ResolveAutoShieldDestinationReadOnly(
|
||||
libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut, uint32_t& accountOut) {
|
||||
accountOut = AUTOSHIELD_ACCOUNT_NONE;
|
||||
|
||||
// 1. Explicit -autoshieldaddress override (validated as a spendable Sapling
|
||||
// zaddr at init.cpp:2494-2506). This also serves as the per-process cache
|
||||
// for whatever step 2/3 resolved.
|
||||
// zaddr in init.cpp). This doubles as the per-process cache for whatever
|
||||
// the derivation below resolved on an earlier round.
|
||||
if (!pwalletMain->autoShieldAddress.empty()) {
|
||||
auto decoded = DecodePaymentAddress(pwalletMain->autoShieldAddress);
|
||||
if (boost::get<libzcash::SaplingPaymentAddress>(&decoded) != nullptr) {
|
||||
destOut = boost::get<libzcash::SaplingPaymentAddress>(decoded);
|
||||
destStrOut = pwalletMain->autoShieldAddress;
|
||||
return true;
|
||||
return AutoShieldDestStatus::Resolved;
|
||||
}
|
||||
LogPrintf("%s: configured -autoshieldaddress is not a valid Sapling address\n", getId());
|
||||
return false;
|
||||
return AutoShieldDestStatus::InvalidOverride;
|
||||
}
|
||||
|
||||
// 2. Walk the restore window m/32'/coin'/[0, gap)' derived from the seed.
|
||||
HDSeed seed;
|
||||
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
|
||||
LogPrintf("%s: no HD seed available; refusing to pick an autoshield destination\n", getId());
|
||||
return false;
|
||||
return AutoShieldDestStatus::NoSeed;
|
||||
}
|
||||
|
||||
// Mirror init.cpp:2349-2350's own clamp, and cap into the hardened index
|
||||
// space so (i | ZIP32_HARDENED_KEY_LIMIT) below stays well formed.
|
||||
// Mirror init.cpp's own clamp, and cap into the hardened index space so
|
||||
// (i | ZIP32_HARDENED_KEY_LIMIT) below stays well formed.
|
||||
int64_t gapArg = GetArg("-mnemonicsaplinggap", 100);
|
||||
if (gapArg < 0) {
|
||||
gapArg = 0;
|
||||
@@ -167,15 +165,77 @@ bool AsyncRPCOperation_autoshieldcoinbase::resolveDestination(
|
||||
if (pwalletMain->GetSaplingExtendedSpendingKey(addr, held)) {
|
||||
destOut = addr;
|
||||
destStrOut = EncodePaymentAddress(addr);
|
||||
// Cache for the life of the process; step 1 short-circuits later
|
||||
// rounds. Safe: we only cache post-validation.
|
||||
pwalletMain->autoShieldAddress = destStrOut;
|
||||
LogPrintf("%s: autoshield destination %s (seed-derived sapling account %u, gap %u)\n",
|
||||
getId(), destStrOut, (unsigned)i, (unsigned)saplingGap);
|
||||
return true;
|
||||
accountOut = i;
|
||||
return AutoShieldDestStatus::Resolved;
|
||||
}
|
||||
}
|
||||
|
||||
return AutoShieldDestStatus::NotFound;
|
||||
}
|
||||
|
||||
// Resolve the Sapling destination for auto-shielded coinbase.
|
||||
//
|
||||
// Recoverability is the hard requirement: coinbase we shield must land in an
|
||||
// address that a bare -mnemonic/-hdseed restore of THIS wallet's seed re-derives
|
||||
// on its own. A restore pre-derives exactly -mnemonicsaplinggap sapling accounts
|
||||
// starting at index 0, with saplingAccountCounter reset to 0 (init.cpp:2349-2355),
|
||||
// so the only self-recoverable destinations are the default addresses of
|
||||
// m/32'/<coin>'/i' for i < gap.
|
||||
//
|
||||
// We therefore DERIVE those accounts from the seed and pick the lowest index the
|
||||
// wallet already holds. Deriving is the only authoritative test. In particular
|
||||
// CKeyMetadata is NOT evidence of provenance: z_importkey / z_importwallet copy
|
||||
// both hdKeypath and seedFp verbatim out of the import source
|
||||
// (wallet.cpp:5522-5529 <- rpcdump.cpp:511-516), so a foreign key can claim any
|
||||
// keypath and any seed fingerprint. Filtering on metadata would let an imported
|
||||
// key win as "account 0" and silently receive every shielded reward.
|
||||
//
|
||||
// Caller must hold cs_wallet and must already have checked the wallet is unlocked.
|
||||
bool AsyncRPCOperation_autoshieldcoinbase::resolveDestination(
|
||||
libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut) {
|
||||
|
||||
uint32_t account = AUTOSHIELD_ACCOUNT_NONE;
|
||||
switch (ResolveAutoShieldDestinationReadOnly(destOut, destStrOut, account)) {
|
||||
case AutoShieldDestStatus::Resolved:
|
||||
// Cache for the life of the process; the override branch of the resolver
|
||||
// short-circuits later rounds. Safe: we only cache post-validation.
|
||||
pwalletMain->autoShieldAddress = destStrOut;
|
||||
if (account == AUTOSHIELD_ACCOUNT_NONE) {
|
||||
LogPrintf("%s: autoshield destination %s (configured)\n", getId(), destStrOut);
|
||||
} else {
|
||||
LogPrintf("%s: autoshield destination %s (seed-derived sapling account %u)\n",
|
||||
getId(), destStrOut, (unsigned)account);
|
||||
}
|
||||
return true;
|
||||
case AutoShieldDestStatus::InvalidOverride:
|
||||
LogPrintf("%s: configured -autoshieldaddress is not a valid Sapling address\n", getId());
|
||||
return false;
|
||||
case AutoShieldDestStatus::NoSeed:
|
||||
LogPrintf("%s: no HD seed available; refusing to pick an autoshield destination\n", getId());
|
||||
return false;
|
||||
case AutoShieldDestStatus::NotFound:
|
||||
break; // nothing in the window yet: fall through and derive one
|
||||
}
|
||||
|
||||
// Re-establish the derivation context the resolver used, for step 3 below.
|
||||
HDSeed seed;
|
||||
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
|
||||
LogPrintf("%s: no HD seed available; refusing to pick an autoshield destination\n", getId());
|
||||
return false;
|
||||
}
|
||||
int64_t gapArg = GetArg("-mnemonicsaplinggap", 100);
|
||||
if (gapArg < 0) {
|
||||
gapArg = 0;
|
||||
}
|
||||
if (gapArg > (int64_t)ZIP32_HARDENED_KEY_LIMIT) {
|
||||
gapArg = (int64_t)ZIP32_HARDENED_KEY_LIMIT;
|
||||
}
|
||||
const uint32_t saplingGap = (uint32_t)gapArg;
|
||||
const uint32_t bip44CoinType = Params().BIP44CoinType();
|
||||
auto m = libzcash::SaplingExtendedSpendingKey::Master(seed);
|
||||
auto m_32h = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT);
|
||||
auto m_32h_cth = m_32h.Derive(bip44CoinType | ZIP32_HARDENED_KEY_LIMIT);
|
||||
|
||||
// 3. Nothing usable in the window yet: derive the next account, but only if
|
||||
// GenerateNewSaplingZKey will land INSIDE the window. It does NOT derive
|
||||
// at saplingAccountCounter: its do/while skips every index whose spending
|
||||
@@ -258,6 +318,27 @@ bool AsyncRPCOperation_autoshieldcoinbase::main_impl() {
|
||||
libzcash::SaplingPaymentAddress destZaddr;
|
||||
std::string destStr;
|
||||
std::vector<ShieldCoinbaseUTXO> inputs;
|
||||
|
||||
// Proof building below runs WITHOUT cs_wallet (deliberately, so wallet RPCs
|
||||
// are not stalled), which leaves a multi-second window in which a manual
|
||||
// z_shieldcoinbase or z_sendmany over the same miner address would re-select
|
||||
// these same coinbase outputs. AvailableCoins honours IsLockedCoin, so lock
|
||||
// them for the duration exactly as z_shieldcoinbase does. RAII because there
|
||||
// are several early returns between here and commit, and a leaked lock would
|
||||
// silently exclude those coins from every future round.
|
||||
struct ScopedCoinLocks {
|
||||
std::vector<COutPoint> locked;
|
||||
~ScopedCoinLocks() {
|
||||
// A destructor is noexcept by default; letting the lock acquisition
|
||||
// escape would turn a contended mutex into std::terminate.
|
||||
try {
|
||||
if (locked.empty()) return;
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
// UnlockCoin takes a non-const reference (upstream signature).
|
||||
for (COutPoint& op : locked) pwalletMain->UnlockCoin(op);
|
||||
} catch (...) {}
|
||||
}
|
||||
} coinLocks;
|
||||
CAmount shieldedValue = 0;
|
||||
unsigned int max_tx_size = MAX_TX_SIZE_AFTER_SAPLING;
|
||||
|
||||
@@ -277,10 +358,11 @@ bool AsyncRPCOperation_autoshieldcoinbase::main_impl() {
|
||||
}
|
||||
|
||||
// Gather matured, spendable coinbase UTXOs, byte-capped to a single tx.
|
||||
// AvailableCoins with fOnlySpendable already excludes immature coinbase
|
||||
// (< COINBASE_MATURITY) and outputs we don't own, so external
|
||||
// -mineraddress / pool coinbase naturally yields zero inputs.
|
||||
size_t estimatedTxSize = 2000; // header + sietch outputs headroom
|
||||
// AvailableCoins excludes immature coinbase unconditionally (wallet.cpp,
|
||||
// `IsCoinBase() && GetBlocksToMaturity() > 0`) and only ever returns outputs
|
||||
// we own, so external -mineraddress / pool coinbase yields zero inputs. The
|
||||
// second argument here is fOnlyConfirmed, not fOnlySpendable.
|
||||
size_t estimatedTxSize = AUTOSHIELD_TX_OVERHEAD;
|
||||
std::vector<COutput> vecOutputs;
|
||||
pwalletMain->AvailableCoins(vecOutputs, true, NULL, false, true);
|
||||
for (const COutput& out : vecOutputs) {
|
||||
@@ -293,6 +375,11 @@ bool AsyncRPCOperation_autoshieldcoinbase::main_impl() {
|
||||
}
|
||||
size_t increase = (boost::get<CScriptID>(&address) != nullptr)
|
||||
? AUTOSHIELD_CTXIN_P2SH_SIZE : AUTOSHIELD_CTXIN_DUST_SIZE;
|
||||
if (inputs.size() >= AUTOSHIELD_MAX_INPUTS) {
|
||||
LogPrintf("%s: reached per-round input cap (%d); deferring remaining coinbase to next round\n",
|
||||
opid, (int)AUTOSHIELD_MAX_INPUTS);
|
||||
break;
|
||||
}
|
||||
if (estimatedTxSize + increase >= max_tx_size) {
|
||||
// Size-safe batch; the remainder is shielded next round.
|
||||
LogPrintf("%s: reached per-tx size cap; deferring remaining coinbase to next round\n", opid);
|
||||
@@ -305,6 +392,12 @@ bool AsyncRPCOperation_autoshieldcoinbase::main_impl() {
|
||||
inputs.push_back(utxo);
|
||||
shieldedValue += out.tx->vout[out.i].nValue;
|
||||
}
|
||||
|
||||
for (const ShieldCoinbaseUTXO& t : inputs) {
|
||||
COutPoint outpt(t.txid, t.vout);
|
||||
pwalletMain->LockCoin(outpt);
|
||||
coinLocks.locked.push_back(outpt);
|
||||
}
|
||||
}
|
||||
|
||||
CAmount fee = pwalletMain->autoShieldFee;
|
||||
@@ -331,7 +424,12 @@ bool AsyncRPCOperation_autoshieldcoinbase::main_impl() {
|
||||
|
||||
// Build the t->z shield tx. Proof generation happens in Build() WITHOUT
|
||||
// holding cs_wallet (mirrors the sweep op) so we don't stall wallet RPCs.
|
||||
auto builder = TransactionBuilder(consensusParams, targetHeight_, pwalletMain);
|
||||
// tipHeight, not targetHeight_: the builder's height selects the consensus
|
||||
// branch id (transaction_builder.cpp CurrentEpochBranchId), and the NU-straddle
|
||||
// guard above plus SetExpiryHeight below are both keyed off tipHeight. Using the
|
||||
// stale enqueue-time height here meant the guard was checking a height the
|
||||
// transaction was not actually signed against.
|
||||
auto builder = TransactionBuilder(consensusParams, tipHeight, pwalletMain);
|
||||
builder.SetExpiryHeight(tipHeight + AUTOSHIELD_EXPIRY_DELTA);
|
||||
builder.SetFee(fee);
|
||||
|
||||
@@ -395,6 +493,13 @@ void AsyncRPCOperation_autoshieldcoinbase::setResult() {
|
||||
}
|
||||
|
||||
void AsyncRPCOperation_autoshieldcoinbase::cancel() {
|
||||
// Cancelling is how the scheduler stops an in-flight round, so unlike the base
|
||||
// class this must be able to move an EXECUTING operation to CANCELLED. What it
|
||||
// must not do is overwrite a state that is already terminal: the scheduler
|
||||
// cancels the previous operation when it enqueues the next one, and that one may
|
||||
// have already SUCCEEDED, whose result would otherwise be relabelled as cancelled.
|
||||
if (isSuccess() || isFailed() || isCancelled())
|
||||
return;
|
||||
set_state(OperationStatus::CANCELLED);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,26 @@
|
||||
// Default fee for automatic coinbase-shielding transactions
|
||||
static const CAmount DEFAULT_AUTOSHIELD_FEE = 10000;
|
||||
|
||||
// Sentinel for "not a derived account" (i.e. the configured -autoshieldaddress).
|
||||
static const uint32_t AUTOSHIELD_ACCOUNT_NONE = UINT32_MAX;
|
||||
|
||||
enum class AutoShieldDestStatus {
|
||||
Resolved, // destOut/destStrOut are set
|
||||
NotFound, // no in-gap account held yet; the operation will derive one
|
||||
InvalidOverride, // -autoshieldaddress is set but is not a Sapling address
|
||||
NoSeed, // no HD seed available (e.g. locked wallet)
|
||||
};
|
||||
|
||||
// Resolve the auto-shield destination WITHOUT mutating the wallet: the configured
|
||||
// -autoshieldaddress if set, else the lowest in-gap seed-derived account the wallet
|
||||
// already holds. It deliberately does NOT generate a key, so init can call it purely
|
||||
// to answer "where will this send?" -- deriving a fresh account as a side effect of
|
||||
// populating a status field would be wrong. The operation's own resolveDestination
|
||||
// falls through to generation when this returns NotFound.
|
||||
// Caller must hold cs_wallet.
|
||||
AutoShieldDestStatus ResolveAutoShieldDestinationReadOnly(
|
||||
libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut, uint32_t& accountOut);
|
||||
|
||||
// A periodic, wallet-local operation that drains matured *transparent* coinbase
|
||||
// UTXOs into a wallet-owned Sapling z-address in size-bounded batches. It is the
|
||||
// automatic sibling of the manual z_shieldcoinbase RPC and mirrors the dispatch
|
||||
|
||||
@@ -305,6 +305,13 @@ void AsyncRPCOperation_saplingconsolidation::setConsolidationResult(int numTxCre
|
||||
}
|
||||
|
||||
void AsyncRPCOperation_saplingconsolidation::cancel() {
|
||||
// Cancelling is how the scheduler stops an in-flight round, so unlike the base
|
||||
// class this must be able to move an EXECUTING operation to CANCELLED. What it
|
||||
// must not do is overwrite a state that is already terminal: the scheduler
|
||||
// cancels the previous operation when it enqueues the next one, and that one may
|
||||
// have already SUCCEEDED, whose result would otherwise be relabelled as cancelled.
|
||||
if (isSuccess() || isFailed() || isCancelled())
|
||||
return;
|
||||
set_state(OperationStatus::CANCELLED);
|
||||
}
|
||||
|
||||
|
||||
@@ -364,6 +364,13 @@ void AsyncRPCOperation_sweep::setSweepResult(int numTxCreated, const CAmount& am
|
||||
}
|
||||
|
||||
void AsyncRPCOperation_sweep::cancel() {
|
||||
// Cancelling is how the scheduler stops an in-flight round, so unlike the base
|
||||
// class this must be able to move an EXECUTING operation to CANCELLED. What it
|
||||
// must not do is overwrite a state that is already terminal: the scheduler
|
||||
// cancels the previous operation when it enqueues the next one, and that one may
|
||||
// have already SUCCEEDED, whose result would otherwise be relabelled as cancelled.
|
||||
if (isSuccess() || isFailed() || isCancelled())
|
||||
return;
|
||||
set_state(OperationStatus::CANCELLED);
|
||||
}
|
||||
|
||||
|
||||
@@ -3345,6 +3345,83 @@ UniValue z_sweepstatus(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
||||
return ret;
|
||||
}
|
||||
|
||||
UniValue z_autoshieldstatus(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
||||
{
|
||||
if (!EnsureWalletIsAvailable(fHelp))
|
||||
return NullUniValue;
|
||||
|
||||
if (fHelp || params.size() > 0)
|
||||
throw runtime_error(
|
||||
"z_autoshieldstatus\n"
|
||||
"\nReport the state of automatic coinbase shielding: whether it is on, where it sends,\n"
|
||||
"and -- when it is off -- why.\n"
|
||||
"\nResult:\n"
|
||||
"{\n"
|
||||
" \"autoshield\" : true|false, (boolean) whether auto-shielding is enabled\n"
|
||||
" \"running\" : true|false, (boolean) whether a round is in flight\n"
|
||||
" \"next_autoshield\" : n, (numeric) height of the next round\n"
|
||||
" \"autoshieldinterval\" : n, (numeric) blocks between rounds\n"
|
||||
" \"autoshieldaddress\" : \"zaddr\", (string) resolved destination; empty until first resolved\n"
|
||||
" \"autoshieldfee\" : n, (numeric) fee in puposhis\n"
|
||||
" \"autoshieldminutxos\" : n, (numeric) minimum matured coinbase utxos per round\n"
|
||||
" \"hdseedorigin\" : n, (numeric) 0 unrecorded, 1 created, 2 restored, 3 retrofit, 4 unknown\n"
|
||||
" \"hdseedorigin_desc\" : \"...\", (string) readable form of hdseedorigin\n"
|
||||
" \"seed_recoverable\" : true|false, (boolean) whether a seed phrase can be exported\n"
|
||||
" \"disabled_reason\" : \"...\" (string) why auto-shielding is not running, if it is not\n"
|
||||
"}\n"
|
||||
"\nExamples:\n"
|
||||
+ HelpExampleCli("z_autoshieldstatus", "")
|
||||
+ HelpExampleRpc("z_autoshieldstatus", "")
|
||||
);
|
||||
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
|
||||
UniValue ret(UniValue::VOBJ);
|
||||
ret.push_back(Pair("autoshield", pwalletMain->fAutoShieldEnabled));
|
||||
ret.push_back(Pair("running", pwalletMain->fAutoShieldRunning));
|
||||
ret.push_back(Pair("next_autoshield", pwalletMain->nextAutoShield));
|
||||
ret.push_back(Pair("autoshieldinterval", pwalletMain->autoShieldInterval));
|
||||
ret.push_back(Pair("autoshieldaddress", pwalletMain->autoShieldAddress));
|
||||
ret.push_back(Pair("autoshieldfee", pwalletMain->autoShieldFee));
|
||||
ret.push_back(Pair("autoshieldminutxos", pwalletMain->autoShieldMinUtxos));
|
||||
|
||||
int origin = pwalletMain->hdSeedOrigin;
|
||||
std::string desc;
|
||||
switch (origin) {
|
||||
case CWallet::HDSEED_ORIGIN_CREATED: desc = "created on an empty wallet"; break;
|
||||
case CWallet::HDSEED_ORIGIN_RESTORED: desc = "restored from -mnemonic/-hdseed"; break;
|
||||
case CWallet::HDSEED_ORIGIN_RETROFIT: desc = "retrofitted onto a pre-existing wallet"; break;
|
||||
case CWallet::HDSEED_ORIGIN_UNKNOWN: desc = "predates provenance recording"; break;
|
||||
default: desc = "not yet recorded"; break;
|
||||
}
|
||||
ret.push_back(Pair("hdseedorigin", origin));
|
||||
ret.push_back(Pair("hdseedorigin_desc", desc));
|
||||
ret.push_back(Pair("seed_recoverable", pwalletMain->IsMnemonicSeed()));
|
||||
|
||||
// Say why it is off. A silent "false" is exactly what made the destination
|
||||
// un-inspectable in the first place.
|
||||
std::string why = "";
|
||||
if (!pwalletMain->fAutoShieldEnabled) {
|
||||
if (origin != CWallet::HDSEED_ORIGIN_CREATED && origin != CWallet::HDSEED_ORIGIN_RESTORED)
|
||||
why = "HD seed origin is not known-recoverable; back the seed up and pass -autoshield=1";
|
||||
else
|
||||
why = "disabled by -autoshield=0";
|
||||
} else if (pwalletMain->IsLocked()) {
|
||||
why = "wallet is locked; rounds are skipped until it is unlocked";
|
||||
} else if (pwalletMain->fSweepRunning || pwalletMain->fConsolidationRunning) {
|
||||
// Autoshield is mutually exclusive with sweep and consolidation. Without
|
||||
// this the RPC reports autoshield=true, running=false and an empty
|
||||
// reason while no round can actually start.
|
||||
why = strprintf("deferred while %s is running; rounds resume when it finishes",
|
||||
pwalletMain->fSweepRunning ? "z_sweep" : "sapling consolidation");
|
||||
} else if (pwalletMain->autoShieldAddress.empty()) {
|
||||
why = "no destination resolved yet; one will be derived from the HD seed on the first round";
|
||||
}
|
||||
ret.push_back(Pair("disabled_reason", why));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
UniValue z_listreceivedaddress(const UniValue& params, bool fHelp,const CPubKey&)
|
||||
{
|
||||
if (!EnsureWalletIsAvailable(fHelp))
|
||||
@@ -5466,7 +5543,10 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
||||
// Create operation and add to global queue
|
||||
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(builder, contextualTx, fromaddress, taddrRecipients, zaddrRecipients, saplingNoteInputs, nMinDepth, nFee, contextInfo, opret) );
|
||||
q->addOperation(operation);
|
||||
if (!q->addOperation(operation)) {
|
||||
throw JSONRPCError(RPC_INTERNAL_ERROR,
|
||||
"Async RPC queue is shutting down; the operation was not queued");
|
||||
}
|
||||
|
||||
if(fZdebug)
|
||||
LogPrintf("%s: Submitted to async queue\n", __FUNCTION__);
|
||||
@@ -5694,7 +5774,13 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& myp
|
||||
// Create operation and add to global queue
|
||||
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
|
||||
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_shieldcoinbase(builder, contextualTx, inputs, destaddress, nFee, donation, contextInfo) );
|
||||
q->addOperation(operation);
|
||||
// The constructor has already locked the selected coins. Coin locks are
|
||||
// memory-only, so a refused queue at shutdown reclaims them with the process;
|
||||
// what must not happen is returning an opid for work that will never run.
|
||||
if (!q->addOperation(operation)) {
|
||||
throw JSONRPCError(RPC_INTERNAL_ERROR,
|
||||
"Async RPC queue is shutting down; the operation was not queued");
|
||||
}
|
||||
AsyncRPCOperationId operationId = operation->getId();
|
||||
|
||||
// Return continuation information
|
||||
@@ -6048,7 +6134,10 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp, const CPubKey& myp
|
||||
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
|
||||
std::shared_ptr<AsyncRPCOperation> operation(
|
||||
new AsyncRPCOperation_mergetoaddress(builder, contextualTx, utxoInputs, saplingNoteInputs, recipient, nFee, contextInfo) );
|
||||
q->addOperation(operation);
|
||||
if (!q->addOperation(operation)) {
|
||||
throw JSONRPCError(RPC_INTERNAL_ERROR,
|
||||
"Async RPC queue is shutting down; the operation was not queued");
|
||||
}
|
||||
AsyncRPCOperationId operationId = operation->getId();
|
||||
|
||||
// Return continuation information
|
||||
@@ -6349,6 +6438,7 @@ static const CRPCCommand commands[] =
|
||||
{ "wallet", "z_gettotalbalance", &z_gettotalbalance, false },
|
||||
{ "wallet", "z_mergetoaddress", &z_mergetoaddress, false },
|
||||
{ "wallet", "z_sweepstatus", &z_sweepstatus, true },
|
||||
{ "wallet", "z_autoshieldstatus", &z_autoshieldstatus, true },
|
||||
{ "wallet", "z_consolidationstatus", &z_consolidationstatus, true },
|
||||
{ "wallet", "z_sendmany", &z_sendmany, false },
|
||||
{ "wallet", "z_shieldcoinbase", &z_shieldcoinbase, false },
|
||||
|
||||
@@ -592,6 +592,31 @@ void CWallet::RunSaplingSweep(int blockHeight) {
|
||||
// masked an unsynchronized mutation.) cs_wallet is recursive, so this is
|
||||
// safe even on any path that already holds it.
|
||||
LOCK(cs_wallet);
|
||||
|
||||
// Stale-baton guard. A successful-but-incomplete sweep round deliberately
|
||||
// returns with fSweepRunning still set and nextSweep unadvanced (see
|
||||
// AsyncRPCOperation_sweep::main), as a "continue draining next block" baton.
|
||||
// But every early return below leaves that baton set WITHOUT re-dispatching,
|
||||
// and RunSaplingConsolidation -- which is gated on fSweepRunning -- then
|
||||
// returns without advancing nextConsolidation, so the "consolidation is
|
||||
// within 5 blocks" blackout at the top of this function never lifts. That
|
||||
// is a self-sustaining three-way deadlock: sweep waits on consolidation,
|
||||
// consolidation waits on sweep, and autoshield shares the same gate, so a
|
||||
// wedged sweep silently disables coinbase shielding forever.
|
||||
// Only honour the baton while a sweep operation genuinely is in flight.
|
||||
if (fSweepRunning) {
|
||||
std::shared_ptr<AsyncRPCQueue> sweepQueue = getAsyncRPCQueue();
|
||||
std::shared_ptr<AsyncRPCOperation> inFlightSweep =
|
||||
(sweepQueue != nullptr) ? sweepQueue->getOperationForId(saplingSweepOperationId) : nullptr;
|
||||
bool inFlight = (inFlightSweep != nullptr) &&
|
||||
(inFlightSweep->isReady() || inFlightSweep->isExecuting());
|
||||
if (!inFlight) {
|
||||
LogPrintf("%s: clearing stale fSweepRunning at blockHeight=%d (no sweep operation in flight)\n",
|
||||
__func__, blockHeight);
|
||||
fSweepRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fSweepEnabled) {
|
||||
return;
|
||||
}
|
||||
@@ -626,11 +651,21 @@ void CWallet::RunSaplingSweep(int blockHeight) {
|
||||
std::shared_ptr<AsyncRPCOperation> lastOperation = q->getOperationForId(saplingSweepOperationId);
|
||||
if (lastOperation != nullptr) {
|
||||
lastOperation->cancel();
|
||||
// Drop it from the queue's map as well. Nothing else ever removes these:
|
||||
// popOperationForId is only reached from z_getoperationresult, so on a node
|
||||
// running this every interval the map grew without bound.
|
||||
q->popOperationForId(saplingSweepOperationId);
|
||||
}
|
||||
pendingSaplingSweepTxs.clear();
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sweep(blockHeight + 5));
|
||||
saplingSweepOperationId = operation->getId();
|
||||
q->addOperation(operation);
|
||||
if (!q->addOperation(operation)) {
|
||||
// Queue is closing (shutdown). Release the flag we just set, or it stays
|
||||
// set with no operation in flight and blocks every later round.
|
||||
LogPrintf("%s: async queue is not accepting operations; skipping this round\n", __func__);
|
||||
fSweepRunning = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CWallet::RunSaplingConsolidation(int blockHeight) {
|
||||
@@ -674,11 +709,21 @@ void CWallet::RunSaplingConsolidation(int blockHeight) {
|
||||
std::shared_ptr<AsyncRPCOperation> lastOperation = q->getOperationForId(saplingConsolidationOperationId);
|
||||
if (lastOperation != nullptr) {
|
||||
lastOperation->cancel();
|
||||
// Drop it from the queue's map as well. Nothing else ever removes these:
|
||||
// popOperationForId is only reached from z_getoperationresult, so on a node
|
||||
// running this every interval the map grew without bound.
|
||||
q->popOperationForId(saplingConsolidationOperationId);
|
||||
}
|
||||
pendingSaplingConsolidationTxs.clear();
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_saplingconsolidation(blockHeight + 5));
|
||||
saplingConsolidationOperationId = operation->getId();
|
||||
q->addOperation(operation);
|
||||
if (!q->addOperation(operation)) {
|
||||
// Queue is closing (shutdown). Release the flag we just set, or it stays
|
||||
// set with no operation in flight and blocks every later round.
|
||||
LogPrintf("%s: async queue is not accepting operations; skipping this round\n", __func__);
|
||||
fConsolidationRunning = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Periodically drain matured transparent coinbase into a wallet-owned Sapling
|
||||
@@ -729,16 +774,28 @@ void CWallet::RunAutoShieldCoinbase(int blockHeight) {
|
||||
std::shared_ptr<AsyncRPCOperation> lastOperation = q->getOperationForId(saplingAutoShieldOperationId);
|
||||
if (lastOperation != nullptr) {
|
||||
lastOperation->cancel();
|
||||
// Drop it from the queue's map as well. Nothing else ever removes these:
|
||||
// popOperationForId is only reached from z_getoperationresult, so on a node
|
||||
// running this every interval the map grew without bound.
|
||||
q->popOperationForId(saplingAutoShieldOperationId);
|
||||
}
|
||||
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_autoshieldcoinbase(blockHeight + 5));
|
||||
saplingAutoShieldOperationId = operation->getId();
|
||||
q->addOperation(operation);
|
||||
if (!q->addOperation(operation)) {
|
||||
// Queue is closing (shutdown). Release the flag we just set, or it stays
|
||||
// set with no operation in flight and blocks every later round.
|
||||
LogPrintf("%s: async queue is not accepting operations; skipping this round\n", __func__);
|
||||
fAutoShieldRunning = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool CWallet::CommitAutomatedTx(const CTransaction& tx) {
|
||||
CWalletTx wtx(this, tx);
|
||||
CReserveKey reservekey(pwalletMain);
|
||||
fprintf(stderr,"%s: %s\n",__func__,tx.ToString().c_str());
|
||||
// No tx dump here: CommitTransaction already LogPrintf's the same wtx.ToString(),
|
||||
// and ToString() emits a line per vin, so with the 400-input autoshield cap this
|
||||
// printed tens of KB to stderr on every automated round.
|
||||
return CommitTransaction(wtx, reservekey);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,15 @@
|
||||
#include <boost/scoped_ptr.hpp>
|
||||
#include <boost/thread.hpp>
|
||||
|
||||
// Out-of-line definitions for CHDChain's in-class static constants. These are
|
||||
// only initialised in the class body, so any ODR use -- binding one to a const
|
||||
// reference, which is exactly what gtest's EXPECT_*/ASSERT_* macros do -- needs
|
||||
// a definition or the link fails. hush-gtest hit this on VERSION_HD_MNEMONIC.
|
||||
const int CHDChain::VERSION_HD_BASE;
|
||||
const int CHDChain::VERSION_HD_TRANSPARENT;
|
||||
const int CHDChain::VERSION_HD_MNEMONIC;
|
||||
const int CHDChain::CURRENT_VERSION;
|
||||
|
||||
using namespace std;
|
||||
|
||||
static uint64_t nAccountingEntryNumber = 0;
|
||||
@@ -411,6 +420,9 @@ public:
|
||||
vector<uint256> vWalletUpgrade;
|
||||
// True once a well-formed "hdchain" record has been loaded.
|
||||
bool fHDChainRead;
|
||||
// True when that record had to be repaired on read (see the "hdchain" case
|
||||
// in ReadKeyValue); LoadWallet rewrites it in full form afterwards.
|
||||
bool fHDChainRepaired;
|
||||
|
||||
CWalletScanState() {
|
||||
nKeys = nCKeys = nKeyMeta = nZKeys = nCZKeys = nZKeyMeta = nSapZAddrs = 0;
|
||||
@@ -418,6 +430,7 @@ public:
|
||||
fAnyUnordered = false;
|
||||
nFileVersion = 0;
|
||||
fHDChainRead = false;
|
||||
fHDChainRepaired = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -842,14 +855,48 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
||||
else if (strType == "hdchain")
|
||||
{
|
||||
CHDChain chain;
|
||||
// Keep an untouched copy: a failed >> has already consumed part of ssValue.
|
||||
CDataStream ssRetry(ssValue.begin(), ssValue.end(), ssValue.GetType(), ssValue.GetVersion());
|
||||
try {
|
||||
ssValue >> chain;
|
||||
} catch (...) {
|
||||
// Do not let this land in the "user can live with it" bucket:
|
||||
// report it, and leave wss.fHDChainRead false so LoadWallet
|
||||
// turns it into DB_CORRUPT when a seed is present.
|
||||
strErr = "Error reading wallet database: hdchain record is corrupt";
|
||||
return false;
|
||||
// Downgrade repair. A build predating VERSION_HD_TRANSPARENT writes
|
||||
// this record back with only the four base fields while leaving
|
||||
// nVersion at whatever it read, so the version-gated reads above run
|
||||
// off the end. Without this, one address generated under such a build
|
||||
// makes the wallet unopenable here ("Wallet corrupted") even though
|
||||
// nothing is actually lost.
|
||||
//
|
||||
// Recovering is safe for a v1/v2 record: everything derivation needs
|
||||
// is either in the four-field prefix or in the separate hdseed record,
|
||||
// and the trailing counters are self-healing -- DeriveNewChildKey and
|
||||
// GenerateNewSaplingZKey both skip indices whose key the wallet
|
||||
// already holds, so restarting a counter at 0 re-walks past existing
|
||||
// keys instead of reissuing them.
|
||||
chain = CHDChain();
|
||||
try {
|
||||
ssRetry >> chain.nVersion;
|
||||
ssRetry >> chain.seedFp;
|
||||
ssRetry >> chain.nCreateTime;
|
||||
ssRetry >> chain.saplingAccountCounter;
|
||||
} catch (...) {
|
||||
// Short even in the base fields: genuinely corrupt.
|
||||
strErr = "Error reading wallet database: hdchain record is corrupt";
|
||||
return false;
|
||||
}
|
||||
if (chain.nVersion >= CHDChain::VERSION_HD_MNEMONIC) {
|
||||
// A record claiming to carry fMnemonicSeed must not have it
|
||||
// guessed: that flag selects the derivation input, so defaulting
|
||||
// it wrong yields a different key tree in silence. Fail loud, as
|
||||
// this branch always did.
|
||||
strErr = "Error reading wallet database: hdchain record is corrupt";
|
||||
return false;
|
||||
}
|
||||
chain.transparentChildCounter = 0;
|
||||
chain.fMnemonicSeed = false;
|
||||
wss.fHDChainRepaired = true;
|
||||
LogPrintf("Repairing a truncated hdchain record (nVersion=%d): it was last written by "
|
||||
"a wallet build that predates the transparent HD counter\n", chain.nVersion);
|
||||
}
|
||||
wss.fHDChainRead = true;
|
||||
pwallet->SetHDChain(chain, true);
|
||||
@@ -1007,6 +1054,18 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
|
||||
if (fNoncriticalErrors && result == DB_LOAD_OK)
|
||||
result = DB_NONCRITICAL_ERROR;
|
||||
|
||||
// Rewrite a repaired record in full form so the next load is clean and the
|
||||
// transparent counter starts being persisted again.
|
||||
if (wss.fHDChainRepaired && pwallet->HaveHDSeed())
|
||||
{
|
||||
try {
|
||||
pwallet->SetHDChain(pwallet->GetHDChain(), false);
|
||||
LogPrintf("Rewrote the repaired hdchain record in full form\n");
|
||||
} catch (const std::exception& e) {
|
||||
LogPrintf("Could not rewrite the repaired hdchain record: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
// A wallet that holds an HD seed but whose hdchain record is missing or
|
||||
// unreadable is NOT safe to run. hdChain would fall back to its SetNull
|
||||
// defaults (walletdb.h:105-113), which (a) clears fMnemonicSeed, switching
|
||||
@@ -1018,7 +1077,9 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
|
||||
// Fail loud instead of quietly deriving into the wrong tree.
|
||||
if (pwallet->HaveHDSeed() && !wss.fHDChainRead)
|
||||
{
|
||||
LogPrintf("Error loading wallet.dat: HD seed present but the hdchain record is missing or corrupt\n");
|
||||
LogPrintf("Error loading wallet.dat: HD seed present but the hdchain record is missing or corrupt. "
|
||||
"Recover by restoring from the seed phrase: move wallet.dat aside and start with "
|
||||
"-mnemonic=\"<your seed phrase>\" -rescan\n");
|
||||
return DB_CORRUPT;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user