Compare commits
20 Commits
2232868d9f
...
audit-fixe
| Author | SHA1 | Date | |
|---|---|---|---|
| 520e1e0ede | |||
| fa3a4223ec | |||
| c1040028e4 | |||
| a6b6f80db0 | |||
| 3aac75e94f | |||
| 5634aed750 | |||
| db42091ce3 | |||
| 6d282db216 | |||
| c5fde12485 | |||
| e2e10f6ef8 | |||
| af7d9e2300 | |||
| 4cc7e0491a | |||
| 5e0a706839 | |||
| 60d66022f6 | |||
| 3b2aa866aa | |||
| 2d6359ea74 | |||
| fa16e740b6 | |||
| 5f40c8ede0 | |||
| b3e81f1eda | |||
| d05302d450 |
@@ -27,6 +27,17 @@ RUN rm -rf /build/depends/built /build/depends/work \
|
|||||||
&& rm -rf /build/src/cc/*.o /build/src/cc/*.a \
|
&& rm -rf /build/src/cc/*.o /build/src/cc/*.a \
|
||||||
&& rm -f /build/config.status /build/config.log
|
&& rm -f /build/config.status /build/config.log
|
||||||
|
|
||||||
|
# The build context excludes .git (see .dockerignore), so genbuild.sh cannot derive
|
||||||
|
# a version and would stamp the binaries "-unk". build.sh computes the real one
|
||||||
|
# on the host and passes it in here.
|
||||||
|
ARG BUILD_DESC=
|
||||||
|
ENV DRAGONX_BUILD_DESC=${BUILD_DESC}
|
||||||
|
|
||||||
|
RUN if [ -z "$DRAGONX_BUILD_DESC" ]; then \
|
||||||
|
echo "WARNING: no BUILD_DESC build-arg -- binaries will be stamped -unk." >&2; \
|
||||||
|
echo " Prefer ./build.sh --linux-compat, or pass --build-arg BUILD_DESC=..." >&2; \
|
||||||
|
fi
|
||||||
|
|
||||||
RUN cd /build && ./util/build.sh --disable-tests -j$(nproc)
|
RUN cd /build && ./util/build.sh --disable-tests -j$(nproc)
|
||||||
|
|
||||||
# Strip binaries inside the container so extracted files are already small
|
# Strip binaries inside the container so extracted files are already small
|
||||||
|
|||||||
29
build.sh
29
build.sh
@@ -164,7 +164,34 @@ if [ $BUILD_LINUX_COMPAT -eq 1 ] || [ $BUILD_LINUX_RELEASE -eq 1 ] || [ $BUILD_W
|
|||||||
COMPAT_RELEASE_DIR="$RELEASE_DIR/dragonx-$VERSION-$COMPAT_PLATFORM"
|
COMPAT_RELEASE_DIR="$RELEASE_DIR/dragonx-$VERSION-$COMPAT_PLATFORM"
|
||||||
|
|
||||||
echo "Building Docker image (Ubuntu 20.04 base)..."
|
echo "Building Docker image (Ubuntu 20.04 base)..."
|
||||||
$DOCKER_CMD build -f Dockerfile.compat -t "$DOCKER_IMAGE" .
|
# .dockerignore excludes .git, so genbuild.sh inside the container cannot
|
||||||
|
# derive the version and would stamp the binaries "-unk". Compute it on the
|
||||||
|
# host, mirroring util/genbuild.sh exactly, and pass it in via --build-arg.
|
||||||
|
# NB: build.sh runs under `set -eu -o pipefail`, so every git call here must be
|
||||||
|
# non-fatal -- a source tarball, a machine without git, or a branch whose only
|
||||||
|
# reachable tags are lightweight (v1.0.1-v1.0.3) would otherwise abort the build.
|
||||||
|
git diff >/dev/null 2>&1 || true # refresh index: touched-but-unmodified are not dirty
|
||||||
|
COMPAT_BUILD_DESC=""
|
||||||
|
COMPAT_RAWDESC=$(git describe --abbrev=0 2>/dev/null || true)
|
||||||
|
if [ -n "$COMPAT_RAWDESC" ] \
|
||||||
|
&& [ "$(git rev-parse HEAD 2>/dev/null)" = "$(git rev-list -1 "$COMPAT_RAWDESC" 2>/dev/null)" ] \
|
||||||
|
&& git diff-index --quiet HEAD -- 2>/dev/null; then
|
||||||
|
COMPAT_BUILD_DESC="$COMPAT_RAWDESC"
|
||||||
|
else
|
||||||
|
COMPAT_SUFFIX=$(git rev-parse --short HEAD 2>/dev/null || true)
|
||||||
|
if [ -n "$COMPAT_SUFFIX" ]; then
|
||||||
|
git diff-index --quiet HEAD -- 2>/dev/null || COMPAT_SUFFIX="$COMPAT_SUFFIX-dirty"
|
||||||
|
COMPAT_BUILD_DESC="v$VERSION-$COMPAT_SUFFIX"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -n "$COMPAT_BUILD_DESC" ]; then
|
||||||
|
echo "Stamping container build as: $COMPAT_BUILD_DESC"
|
||||||
|
else
|
||||||
|
echo "Warning: no usable git metadata; container binaries will be stamped -unk"
|
||||||
|
fi
|
||||||
|
$DOCKER_CMD build -f Dockerfile.compat \
|
||||||
|
--build-arg BUILD_DESC="$COMPAT_BUILD_DESC" \
|
||||||
|
-t "$DOCKER_IMAGE" .
|
||||||
|
|
||||||
echo "Extracting binaries from Docker image..."
|
echo "Extracting binaries from Docker image..."
|
||||||
CONTAINER_ID=$($DOCKER_CMD create "$DOCKER_IMAGE")
|
CONTAINER_ID=$($DOCKER_CMD create "$DOCKER_IMAGE")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ dnl require autoconf 2.60 (AS_ECHO/AS_ECHO_N)
|
|||||||
AC_PREREQ([2.60])
|
AC_PREREQ([2.60])
|
||||||
define(_CLIENT_VERSION_MAJOR, 1)
|
define(_CLIENT_VERSION_MAJOR, 1)
|
||||||
dnl Must be kept in sync with src/clientversion.h , ugh!
|
dnl Must be kept in sync with src/clientversion.h , ugh!
|
||||||
define(_CLIENT_VERSION_MINOR, 2)
|
define(_CLIENT_VERSION_MINOR, 3)
|
||||||
define(_CLIENT_VERSION_REVISION, 0)
|
define(_CLIENT_VERSION_REVISION, 0)
|
||||||
define(_CLIENT_VERSION_BUILD, 50)
|
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)))
|
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,31 @@
|
|||||||
|
dragonx (1.3.0) stable; urgency=medium
|
||||||
|
|
||||||
|
* RandomX stratum mining support: the daemon can serve stratum clients
|
||||||
|
directly, with a reference miner behind the stratummine RPC for testing.
|
||||||
|
* Stratum fixes found by audit: a malformed 63-character job_id no longer
|
||||||
|
aborts the daemon; each block is paid to the miner that actually found it
|
||||||
|
rather than to whichever client asked for work first; and a low-difficulty
|
||||||
|
share is now rejected before it costs a RandomX hash.
|
||||||
|
* Fix -connect never dialing its targets, so a node pinned to specific peers
|
||||||
|
reaches them instead of silently falling through to peer discovery.
|
||||||
|
* Remove roughly 7,100 lines of dead code, including the CBOPRET price
|
||||||
|
validation in the coinbase check, whose guard could not be true on any
|
||||||
|
chain, and adaptive-PoW difficulty logic that DragonX does not enable.
|
||||||
|
* Honor a command-line -rpcpassword across restarts, and fix a misspelled
|
||||||
|
-rpcusername key that silently discarded the configured RPC user.
|
||||||
|
* Default -checkpoints off on regtest so an isolated node leaves initial
|
||||||
|
block download, instead of staying in IBD forever and disabling every
|
||||||
|
operation gated on it.
|
||||||
|
* Release cs_main and the mempool lock on the miner's isStake error paths;
|
||||||
|
the leak presented as a permanent stall rather than a slow response.
|
||||||
|
* Windows cross-build: the mingw target links and finds librustzcash, and a
|
||||||
|
fs::path::c_str() regression in init no longer breaks the build.
|
||||||
|
* Stamp container builds with the real version instead of "-unk".
|
||||||
|
* Repair the qa/rpc-tests harness far enough to start a DragonX node and
|
||||||
|
build the shared test chain; it previously started mainnet nodes.
|
||||||
|
|
||||||
|
-- DragonX Developers <dev@dragonx.is> Mon, 31 Aug 2026 03:46:19 +0000
|
||||||
|
|
||||||
dragonx (1.2.0) stable; urgency=medium
|
dragonx (1.2.0) stable; urgency=medium
|
||||||
|
|
||||||
* Auto-shield matured coinbase into a wallet-owned Sapling address on a block
|
* Auto-shield matured coinbase into a wallet-owned Sapling address on a block
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1.
|
||||||
.TH DRAGONX-CLI "1" "August 2026" "dragonx-cli v1.2.0" "User Commands"
|
.TH DRAGONX-CLI "1" "August 2026" "dragonx-cli v1.3.0" "User Commands"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
dragonx-cli \- manual page for dragonx-cli v1.2.0
|
dragonx-cli \- manual page for dragonx-cli v1.3.0
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
DragonX RPC client version v1.2.0
|
DragonX RPC client version v1.3.0\-af7d9e230
|
||||||
.PP
|
.PP
|
||||||
In order to ensure you are adequately protecting your privacy when using
|
In order to ensure you are adequately protecting your privacy when using
|
||||||
DragonX, please see <https://dragonx.is/security/>.
|
DragonX, please see <https://dragonx.is/security/>.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1.
|
||||||
.TH DRAGONX-TX "1" "August 2026" "dragonx-tx v1.2.0" "User Commands"
|
.TH DRAGONX-TX "1" "August 2026" "dragonx-tx v1.3.0" "User Commands"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
dragonx-tx \- manual page for dragonx-tx v1.2.0
|
dragonx-tx \- manual page for dragonx-tx v1.3.0
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
hush\-tx utility version v1.2.0
|
hush\-tx utility version v1.3.0\-af7d9e230
|
||||||
.SS "Usage:"
|
.SS "Usage:"
|
||||||
.TP
|
.TP
|
||||||
hush\-tx [options] <hex\-tx> [commands]
|
hush\-tx [options] <hex\-tx> [commands]
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1.
|
||||||
.TH DRAGONXD "1" "August 2026" "dragonxd v1.2.0" "User Commands"
|
.TH DRAGONXD "1" "August 2026" "dragonxd v1.3.0" "User Commands"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
dragonxd \- manual page for dragonxd v1.2.0
|
dragonxd \- manual page for dragonxd v1.3.0
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
DragonX Daemon version v1.2.0
|
DragonX Daemon version v1.3.0\-af7d9e230
|
||||||
.PP
|
.PP
|
||||||
In order to ensure you are adequately protecting your privacy when using
|
In order to ensure you are adequately protecting your privacy when using
|
||||||
DragonX, please see <https://dragonx.is/security/>.
|
DragonX, please see <https://dragonx.is/security/>.
|
||||||
@@ -452,6 +452,13 @@ or create a wallet z\-address). Must be spendable by this wallet.
|
|||||||
Fee in puposhis for automatic coinbase\-shielding transactions (default:
|
Fee in puposhis for automatic coinbase\-shielding transactions (default:
|
||||||
10000)
|
10000)
|
||||||
.HP
|
.HP
|
||||||
|
\fB\-sietch\-min\-zouts=\fR<n>
|
||||||
|
.IP
|
||||||
|
Minimum number of shielded (Sapling) outputs Sietch adds to each
|
||||||
|
z_sendmany transaction as decoys, strengthening
|
||||||
|
amount/linkability privacy. Higher values add privacy at the cost
|
||||||
|
of larger transactions (default: 7, clamped to the range 3\-50)
|
||||||
|
.HP
|
||||||
\fB\-autoshieldminutxos\fR
|
\fB\-autoshieldminutxos\fR
|
||||||
.IP
|
.IP
|
||||||
Only auto\-shield once at least this many matured coinbase UTXOs exist
|
Only auto\-shield once at least this many matured coinbase UTXOs exist
|
||||||
@@ -710,6 +717,11 @@ Stratum server options:
|
|||||||
.IP
|
.IP
|
||||||
Enable stratum server (default: off)
|
Enable stratum server (default: off)
|
||||||
.HP
|
.HP
|
||||||
|
\fB\-stratumtarget=\fR<hex>
|
||||||
|
.IP
|
||||||
|
Pool share target (64\-hex, big\-endian; larger = easier). Default is the
|
||||||
|
diff\-1 target. Useful for solo/low\-difficulty mining.
|
||||||
|
.HP
|
||||||
\fB\-stratumaddress=\fR<address>
|
\fB\-stratumaddress=\fR<address>
|
||||||
.IP
|
.IP
|
||||||
Mining address to use when special address of 'x' is sent by miner
|
Mining address to use when special address of 'x' is sent by miner
|
||||||
|
|||||||
@@ -8,32 +8,32 @@ It is best to keep doc/relnotes/README.md up to date as changes and bug fixes ar
|
|||||||
|
|
||||||
## Branch model
|
## Branch model
|
||||||
|
|
||||||
Development happens on the `dev` branch. Releases are cut on the default branch, `dragonx`. There is no `master` branch. Code changes should land on `dev` first and undergo testing before being merged into `dragonx`.
|
Development happens on the `dev` branch. Releases are cut on the default branch, `master`. Code changes should land on `dev` first and undergo testing before being merged into `master`.
|
||||||
|
|
||||||
## Check for changes on dragonx that should be on dev
|
## Check for changes on master that should be on dev
|
||||||
|
|
||||||
Occasionally trivial changes are made directly on the `dragonx` branch, such as documentation changes. In theory, no code changes should happen on `dragonx` without being on `dev` first, but it's better to be safe than sorry. We want the `dev` branch which undergoes testing to be as close as possible to what the `dragonx` branch will become, so we don't want to merge `dev` into `dragonx` and just assume everything works. So it's best to merge the `dragonx` branch into `dev` just before merging the `dev` branch into `dragonx`.
|
Occasionally trivial changes are made directly on the `master` branch, such as documentation changes. In theory, no code changes should happen on `master` without being on `dev` first, but it's better to be safe than sorry. We want the `dev` branch which undergoes testing to be as close as possible to what the `master` branch will become, so we don't want to merge `dev` into `master` and just assume everything works. So it's best to merge the `master` branch into `dev` just before merging the `dev` branch into `master`.
|
||||||
|
|
||||||
To check if the `dragonx` branch has any changes that the `dev` branch does not:
|
To check if the `master` branch has any changes that the `dev` branch does not:
|
||||||
|
|
||||||
```
|
```
|
||||||
# this assumes you are working with https://git.dragonx.is/DragonX/dragonx as your remote
|
# this assumes you are working with https://git.dragonx.is/DragonX/dragonx as your remote
|
||||||
git checkout dev
|
git checkout dev
|
||||||
git pull # make sure dev is up to date
|
git pull # make sure dev is up to date
|
||||||
git checkout dragonx
|
git checkout master
|
||||||
git pull # make sure dragonx is up to date
|
git pull # make sure master is up to date
|
||||||
git diff dev...dragonx # look at the set of changes which exist in dragonx but not dev
|
git diff dev...master # look at the set of changes which exist in master but not dev
|
||||||
```
|
```
|
||||||
|
|
||||||
If the last command has no output, congrats, there is nothing to do. If the last command has output, then you should merge `dragonx` into `dev`:
|
If the last command has no output, congrats, there is nothing to do. If the last command has output, then you should merge `master` into `dev`:
|
||||||
|
|
||||||
```
|
```
|
||||||
git checkout dev
|
git checkout dev
|
||||||
git merge dragonx
|
git merge master
|
||||||
git push origin dev
|
git push origin dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the `--no-ff` flag when merging `dev` into `dragonx` for a release (see below). The `--no-ff` flag makes sure to make a merge commit, no matter what, even if a "fast forward" could be done. For those in the future looking back, it's much better to see evidence of when branches were merged.
|
Use the `--no-ff` flag when merging `dev` into `master` for a release (see below). The `--no-ff` flag makes sure to make a merge commit, no matter what, even if a "fast forward" could be done. For those in the future looking back, it's much better to see evidence of when branches were merged.
|
||||||
|
|
||||||
|
|
||||||
### Git Issues
|
### Git Issues
|
||||||
@@ -66,6 +66,7 @@ Install deps on Linux:
|
|||||||
- Run "make seeds"
|
- Run "make seeds"
|
||||||
- Commit the result
|
- Commit the result
|
||||||
- Update version in configure.ac and src/clientversion.h to update the dragonxd version
|
- Update version in configure.ac and src/clientversion.h to update the dragonxd version
|
||||||
|
- **The new version MUST be higher than every version already tagged**, including tags that were never built or published. Check with `git tag -l --sort=-v:refname | head`. Two trees stamped with the same `CLIENT_VERSION` are indistinguishable on the wire, in `getnetworkinfo`, and to the wallet's in-app updater — and a published archive that does not match its tag destroys the only provenance check users have.
|
||||||
- In src/clientversion.h you update `CLIENT_VERSION_*` variables. Usually you will just update `CLIENT_VERSION_REVISION`
|
- In src/clientversion.h you update `CLIENT_VERSION_*` variables. Usually you will just update `CLIENT_VERSION_REVISION`
|
||||||
- If there is a consensus change, it may be a good idea to update `CLIENT_VERSION_MINOR` or `CLIENT_VERSION_MAJOR`
|
- If there is a consensus change, it may be a good idea to update `CLIENT_VERSION_MINOR` or `CLIENT_VERSION_MAJOR`
|
||||||
- To make a pre-release "beta" you can modify `CLIENT_VERSION_BUILD` but that is rarely done.
|
- To make a pre-release "beta" you can modify `CLIENT_VERSION_BUILD` but that is rarely done.
|
||||||
@@ -97,17 +98,27 @@ Install deps on Linux:
|
|||||||
- Try to generate checkpoints as close to the release as possible, so you can have a recent block height be protected.
|
- Try to generate checkpoints as close to the release as possible, so you can have a recent block height be protected.
|
||||||
- For instance, don't update checkpoints and then do a release a month later. You can always update checkpoint data again or multiple times
|
- For instance, don't update checkpoints and then do a release a month later. You can always update checkpoint data again or multiple times
|
||||||
- Update doc/relnotes/README.md
|
- Update doc/relnotes/README.md
|
||||||
- To get the stats of file changes: `git diff --stat dragonx...dev`
|
- To get the stats of file changes: `git diff --stat master...dev`
|
||||||
- Do a fresh clone and fresh sync with new checkpoints
|
- Do a fresh clone and fresh sync with new checkpoints
|
||||||
- Stop node, wait 20 minutes, and then do a partial sync with new checkpoints
|
- Stop node, wait 20 minutes, and then do a partial sync with new checkpoints
|
||||||
- Merge dev into dragonx: `git checkout dev && git pull && git checkout dragonx && git pull && git merge --no-ff dev && git push`
|
- Merge dev into master: `git checkout dev && git pull && git checkout master && git pull && git merge --no-ff dev && git push`
|
||||||
- The above command makes sure that your local dev branch is up to date before doing anything
|
- The above command makes sure that your local dev branch is up to date before doing anything
|
||||||
- The above command will not merge if "git pull" creates a merge conflict
|
- The above command will not merge if "git pull" creates a merge conflict
|
||||||
- The above command will not push if there is a problem with merging dev
|
- The above command will not push if there is a problem with merging dev
|
||||||
- Make Gitea release with git tag from the dragonx branch (make sure to merge dev in first)
|
- Make Gitea release with git tag from the master branch (make sure to merge dev in first)
|
||||||
- Make sure git tag starts with a `v` such as `v1.0.3`
|
- Make sure git tag starts with a `v` such as `v1.0.3`
|
||||||
- Use util/gen-linux-binary-release.sh to make a Linux release binary
|
- **The tag MUST be annotated** (`git tag -a v1.3.0 -m 'DragonX v1.3.0'`), not lightweight. `util/genbuild.sh` calls `git describe` *without* `--tags`, which only ever sees annotated tags — a lightweight tag makes the build stamp itself `v<older-tag>-<sha>` instead of the release version. v1.0.0 through v1.0.3 are lightweight, which is why their builds are labelled that way.
|
||||||
- Upload Linux binary to Gitea release and add SHA256 sum
|
- Verify before building: `git describe` must print exactly the tag, with no `-<n>-g<sha>` suffix.
|
||||||
|
- Use `./build.sh` (container-based, see doc/build-containers.md) or util/gen-linux-binary-release.sh to make a Linux release binary
|
||||||
|
- **Sign every archive and publish the signatures.** This step is mandatory and was missing from this document until v1.3.0 — its absence is why v1.1.0 and v1.2.0 were tagged but never became installable releases.
|
||||||
|
- The wallet's in-app daemon updater pins an ed25519 public key in `ObsidianDragon/src/util/daemon_updater.h` and sets `kDaemonRequireSignature = true`. **An update is refused outright unless a valid `<archive>.sig` is published beside the archive.** No signature means every existing user silently stays on their old daemon.
|
||||||
|
- Sign with `ObsidianDragon/scripts/sign-daemon-release.sh`:
|
||||||
|
- `scripts/sign-daemon-release.sh sign <secret.key> <archive>...` produces `<archive>.sig` (base64 of a detached 64-byte ed25519 signature over the exact archive bytes)
|
||||||
|
- or `scripts/sign-daemon-release.sh release <secret.key> <version>` to zip, sign, and print the checksum table in one step
|
||||||
|
- Keep the secret key offline, mode 600. The matching base64 public key must already be pinned in `kDaemonSignaturePublicKeyBase64`.
|
||||||
|
- Upload each Linux binary archive **and its `.sig`** to the Gitea release
|
||||||
|
- **Paste the SHA-256 checksum table into the release body** as markdown rows of the form `| <archive>.zip | `<sha256hex>` |`. The updater parses this table and will not install an archive that is absent from it.
|
||||||
|
- Confirm the release is actually consumable before announcing it: the updater looks for an asset whose name contains `"-" + platformToken + ".zip"` (`linux-amd64`, `macos`, `win64`). An archive named for a distro variant instead of the platform token is invisible to it.
|
||||||
- Create an x86 Debian package for the release:
|
- Create an x86 Debian package for the release:
|
||||||
- Edit contrib/debian/changelog to add information about the new release
|
- Edit contrib/debian/changelog to add information about the new release
|
||||||
- Use `util/build-debian-package.sh` to make an x86 Debian package for the release
|
- Use `util/build-debian-package.sh` to make an x86 Debian package for the release
|
||||||
@@ -115,7 +126,7 @@ Install deps on Linux:
|
|||||||
- `lintian` is an optional dependency, it's not needed to build the .deb
|
- `lintian` is an optional dependency, it's not needed to build the .deb
|
||||||
- Upload .deb to Gitea release
|
- Upload .deb to Gitea release
|
||||||
- Add SHA256 checksum of .deb to release
|
- Add SHA256 checksum of .deb to release
|
||||||
- Use util/build-debian-package-ARM.sh (does this still work?) to make an ARM Debian package for the release
|
- ARM Debian package: `util/build-debian-package-ARM.sh` is referenced here historically but **is not present in the tree**. Skip, or restore the script first.
|
||||||
- Upload the debian packages to the Gitea release page, with SHA256 sums
|
- Upload the debian packages to the Gitea release page, with SHA256 sums
|
||||||
|
|
||||||
## Platform-specific notes
|
## Platform-specific notes
|
||||||
|
|||||||
87
qa/r4-salvaged-wallet-harness.sh
Executable file
87
qa/r4-salvaged-wallet-harness.sh
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# r4 now-tier acceptance harness. MUST run inside a network namespace: the v1.0.x binaries
|
||||||
|
# predate the regtest seed-injection fix and would otherwise dial the live DragonX network.
|
||||||
|
set -u
|
||||||
|
OLD=/home/dev/dragonx/release/dragonx-1.0.1-linux-amd64
|
||||||
|
NEW=/home/dev/dragonx-dev/src
|
||||||
|
ROOT=/tmp/claude-1000/-home-dev/45a644d6-ea0b-4b7c-8ec7-6ccb1a64afa7/scratchpad/r4lab
|
||||||
|
PASS=0; FAIL=0
|
||||||
|
ok(){ echo " PASS $1"; PASS=$((PASS+1)); }
|
||||||
|
no(){ echo " FAIL $1"; FAIL=$((FAIL+1)); }
|
||||||
|
|
||||||
|
# --- hard guardrail: refuse to run unisolated ---
|
||||||
|
if [ "$(ip route show 2>/dev/null | wc -l)" != "0" ]; then
|
||||||
|
echo "REFUSING: not in an isolated netns (routes present). Run under: unshare -rn"; exit 90
|
||||||
|
fi
|
||||||
|
ip link set lo up 2>/dev/null
|
||||||
|
echo "isolation: $(ip route show | wc -l) routes, $(ip -o link | wc -l) interface(s)"
|
||||||
|
|
||||||
|
conf(){ printf 'regtest=1\nrpcuser=t\nrpcpassword=t\nlisten=0\ndnsseed=0\n' > "$1/DRAGONX.conf"; }
|
||||||
|
start(){ # $1=bindir $2=datadir $3=extra
|
||||||
|
"$1/dragonxd" -regtest -datadir="$2" -connect=0 -listen=0 -dnsseed=0 $3 -daemon >/dev/null 2>&1
|
||||||
|
for i in $(seq 40); do "$1/dragonx-cli" -regtest -datadir="$2" -rpcuser=t -rpcpassword=t getblockcount >/dev/null 2>&1 && return 0; sleep 2; done
|
||||||
|
return 1; }
|
||||||
|
cli(){ "$1/dragonx-cli" -regtest -datadir="$2" -rpcuser=t -rpcpassword=t "${@:3}" 2>&1; }
|
||||||
|
stopn(){ cli "$1" "$2" stop >/dev/null 2>&1; sleep 6; }
|
||||||
|
|
||||||
|
rm -rf "$ROOT"; mkdir -p "$ROOT"
|
||||||
|
|
||||||
|
echo; echo "### build victim wallets with the OLD binary (v1.0.1) ###"
|
||||||
|
mkdir -p "$ROOT/base/regtest"; conf "$ROOT/base/regtest"
|
||||||
|
start "$OLD" "$ROOT/base/regtest" "" || { echo "old node failed to start"; exit 91; }
|
||||||
|
[ "$(cli "$OLD" "$ROOT/base/regtest" getconnectioncount)" = "0" ] && ok "victim-maker has 0 peers (isolated)" || no "victim-maker NOT isolated -- ABORT"
|
||||||
|
cli "$OLD" "$ROOT/base/regtest" getnewaddress >/dev/null
|
||||||
|
cli "$OLD" "$ROOT/base/regtest" z_getnewaddress >/dev/null
|
||||||
|
ZBEFORE=$(cli "$OLD" "$ROOT/base/regtest" z_listaddresses | tr -d ' \n')
|
||||||
|
stopn "$OLD" "$ROOT/base/regtest"
|
||||||
|
|
||||||
|
for v in C A B; do cp -a "$ROOT/base" "$ROOT/$v"; done
|
||||||
|
|
||||||
|
# A = salvaged by v1.0.1 (drops hdchain). B = A then USED on v1.0.1 (persists a bogus chain).
|
||||||
|
start "$OLD" "$ROOT/A/regtest" "-salvagewallet" && stopn "$OLD" "$ROOT/A/regtest"
|
||||||
|
start "$OLD" "$ROOT/B/regtest" "-salvagewallet" && stopn "$OLD" "$ROOT/B/regtest"
|
||||||
|
start "$OLD" "$ROOT/B/regtest" "" && { cli "$OLD" "$ROOT/B/regtest" z_getnewaddress >/dev/null; stopn "$OLD" "$ROOT/B/regtest"; }
|
||||||
|
|
||||||
|
echo " A hdchain records: $(strings "$ROOT/A/regtest/regtest/wallet.dat" | grep -c hdchain) (expect 0)"
|
||||||
|
echo " B hdchain records: $(strings "$ROOT/B/regtest/regtest/wallet.dat" | grep -c hdchain) (expect >=1)"
|
||||||
|
|
||||||
|
echo; echo "### open each on the NEW binary ###"
|
||||||
|
for v in C A B; do
|
||||||
|
D="$ROOT/$v/regtest"; L="$D/regtest/debug.log"
|
||||||
|
WDAT="$D/regtest/wallet.dat"
|
||||||
|
[ -f "$WDAT" ] || { echo " FAIL $v: wallet.dat not found at $WDAT"; exit 92; }
|
||||||
|
# NOT a byte-identical check: normal startup (keypool top-up, bestblock) rewrites wallet.dat for
|
||||||
|
# ANY wallet, healthy ones included -- verified with a control. The precise claim is that the
|
||||||
|
# degraded path never SYNTHESISES an hdchain record, so count that instead.
|
||||||
|
HD1=$(strings "$WDAT" | grep -c hdchain)
|
||||||
|
: > "$L" 2>/dev/null
|
||||||
|
if start "$NEW" "$D" "-exportdir=$D/exp"; then
|
||||||
|
STARTED=yes; DEG=$(grep -c "DEGRADED" "$L" 2>/dev/null)
|
||||||
|
MISS=$(grep -c "hdchain record is missing" "$L" 2>/dev/null)
|
||||||
|
MISM=$(grep -c "does not belong to this wallet" "$L" 2>/dev/null)
|
||||||
|
if [ "$v" = "A" ]; then
|
||||||
|
mkdir -p "$D/exp"; EXP=$(cli "$NEW" "$D" z_exportwallet r4dump 2>&1 | head -1)
|
||||||
|
DUMP=$(find "$D" -name 'r4dump' 2>/dev/null | head -1)
|
||||||
|
ZNEW=$(cli "$NEW" "$D" z_getnewaddress); TNEW=$(cli "$NEW" "$D" getnewaddress)
|
||||||
|
fi
|
||||||
|
stopn "$NEW" "$D"
|
||||||
|
else STARTED=no; DEG=0; MISS=0; MISM=0; fi
|
||||||
|
HD2=$(strings "$WDAT" | grep -c hdchain)
|
||||||
|
|
||||||
|
case $v in
|
||||||
|
C) [ "$STARTED" = yes ] && ok "C healthy wallet opens" || no "C healthy wallet failed to open"
|
||||||
|
[ "$DEG" = "0" ] && ok "C no false positive (not flagged degraded)" || no "C FALSE POSITIVE: healthy wallet flagged" ;;
|
||||||
|
A) [ "$STARTED" = yes ] && ok "A salvaged wallet opens (was DB_CORRUPT before)" || no "A salvaged wallet still refuses to open"
|
||||||
|
[ "$MISS" -ge 1 ] && ok "A flagged: hdchain missing" || no "A not flagged as missing-hdchain"
|
||||||
|
[ "$HD1" = "0" ] && [ "$HD2" = "0" ] && ok "A no hdchain synthesised (degraded path persists nothing)" || no "A hdchain record appeared ($HD1 -> $HD2)"
|
||||||
|
echo "$ZNEW" | grep -qi 'error' && ok "A z_getnewaddress refused cleanly (derivation gated)" || no "A z_getnewaddress derived anyway: $ZNEW"
|
||||||
|
echo "$TNEW" | grep -qiE '^R[a-zA-Z0-9]+$' && ok "A getnewaddress still works (legacy random t-key)" || no "A getnewaddress broke: $TNEW"
|
||||||
|
if [ -n "${DUMP:-}" ] && [ -f "$DUMP" ]; then
|
||||||
|
grep -qE '^# HDSeed=[0-9a-f]' "$DUMP" && no "E z_exportwallet emitted an HDSeed line on a degraded wallet" || ok "E z_exportwallet emitted no bogus HDSeed line"
|
||||||
|
else echo " SKIP E (no dump produced: $EXP)"; fi ;;
|
||||||
|
B) [ "$STARTED" = yes ] && ok "B poisoned wallet opens" || no "B poisoned wallet failed to open"
|
||||||
|
[ "$MISM" -ge 1 ] && ok "B CASE-3 DETECTOR FIRED (seedFp mismatch)" || no "B case-3 detector did NOT fire" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
echo; echo "### $PASS passed, $FAIL failed ###"
|
||||||
|
exit $FAIL
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2016 The Zcash developers
|
# Copyright (c) 2016 The Zcash developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2014 The Bitcoin Core developers
|
# Copyright (c) 2014 The Bitcoin Core developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
@@ -52,7 +52,7 @@ class GetBlockTemplateLPTest(BitcoinTestFramework):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
def run_test(self):
|
def run_test(self):
|
||||||
print "Warning: this test will take about 70 seconds in the best case. Be patient."
|
print("Warning: this test will take about 70 seconds in the best case. Be patient.")
|
||||||
self.nodes[0].generate(10)
|
self.nodes[0].generate(10)
|
||||||
templat = self.nodes[0].getblocktemplate()
|
templat = self.nodes[0].getblocktemplate()
|
||||||
longpollid = templat['longpollid']
|
longpollid = templat['longpollid']
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2014 The Bitcoin Core developers
|
# Copyright (c) 2014 The Bitcoin Core developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
from test_framework.test_framework import BitcoinTestFramework
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
from test_framework.authproxy import JSONRPCException
|
from test_framework.authproxy import JSONRPCException
|
||||||
|
from test_framework.util import initialize_chain_clean, start_node
|
||||||
|
|
||||||
from binascii import a2b_hex, b2a_hex
|
from binascii import a2b_hex, b2a_hex
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
@@ -69,14 +70,43 @@ def genmrklroot(leaflist):
|
|||||||
cur = n
|
cur = n
|
||||||
return cur[0]
|
return cur[0]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Sapling v4 transaction layout.
|
||||||
|
#
|
||||||
|
# This test was written against the pre-Overwinter serialization, where a tx
|
||||||
|
# began with a 4-byte nVersion immediately followed by the vin count, so the
|
||||||
|
# first input's prevout hash lived at offset 4+1. DragonX transactions are
|
||||||
|
# Sapling v4: 4-byte header (nVersion | fOverwintered) + 4-byte nVersionGroupId
|
||||||
|
# + vin count, so the prevout hash starts 4 bytes further in. Poking the old
|
||||||
|
# offset corrupts nVersionGroupId and every proposal below just comes back
|
||||||
|
# "Block decode failed" instead of exercising any consensus rule.
|
||||||
|
CB_PREVOUT_OFF = 4+4+1
|
||||||
|
# Likewise the tx no longer ends at nLockTime: nExpiryHeight (4), valueBalance
|
||||||
|
# (8) and the empty vShieldedSpend/vShieldedOutput/vJoinSplit counts (1 each)
|
||||||
|
# trail it, so nLockTime is the 4 bytes at [-19:-15].
|
||||||
|
TX_TAIL_AFTER_LOCKTIME = 4+8+1+1+1
|
||||||
|
|
||||||
|
def tx_seq_off(tx):
|
||||||
|
"""Offset of the first input's nSequence in a Sapling v4 tx."""
|
||||||
|
scriptlen_off = CB_PREVOUT_OFF + 32 + 4 # after prevout hash + prevout.n
|
||||||
|
return scriptlen_off + 1 + tx[scriptlen_off]
|
||||||
|
|
||||||
|
def tx_vout0_value_off(tx):
|
||||||
|
"""Offset of the first output's 8-byte value in a Sapling v4 tx."""
|
||||||
|
return tx_seq_off(tx) + 4 + 1 # after nSequence + vout count
|
||||||
|
|
||||||
def template_to_bytes(tmpl, txlist):
|
def template_to_bytes(tmpl, txlist):
|
||||||
blkver = pack('<L', tmpl['version'])
|
blkver = pack('<L', tmpl['version'])
|
||||||
mrklroot = genmrklroot(list(dblsha(a) for a in txlist))
|
mrklroot = genmrklroot(list(dblsha(a) for a in txlist))
|
||||||
reserved = b'\0'*32
|
# hashFinalSaplingRoot. The all-zeroes placeholder this test used predates
|
||||||
|
# Sapling; a header carrying the wrong root is rejected with
|
||||||
|
# 'bad-sapling-root-in-block', which would sink even Test 11 (valid block).
|
||||||
|
reserved = a2b_hex(tmpl['finalsaplingroothash'])[::-1]
|
||||||
timestamp = pack('<L', tmpl['curtime'])
|
timestamp = pack('<L', tmpl['curtime'])
|
||||||
nonce = b'\0'*32
|
nonce = b'\0'*32
|
||||||
soln = b'\0'
|
soln = b'\0'
|
||||||
blk = blkver + a2b_hex(tmpl['previousblockhash'])[::-1] + mrklroot + reserved + timestamp + a2b_hex(tmpl['bits'])[::-1] + nonce + soln
|
# bytearray, not bytes: Test 9 mutates one byte of the result in place.
|
||||||
|
blk = bytearray(blkver + a2b_hex(tmpl['previousblockhash'])[::-1] + mrklroot + reserved + timestamp + a2b_hex(tmpl['bits'])[::-1] + nonce + soln)
|
||||||
blk += varlenEncode(len(txlist))
|
blk += varlenEncode(len(txlist))
|
||||||
for tx in txlist:
|
for tx in txlist:
|
||||||
blk += tx
|
blk += tx
|
||||||
@@ -95,6 +125,20 @@ class GetBlockTemplateProposalTest(BitcoinTestFramework):
|
|||||||
Test block proposals with getblocktemplate.
|
Test block proposals with getblocktemplate.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
def setup_chain(self):
|
||||||
|
print("Initializing test directory "+self.options.tmpdir)
|
||||||
|
initialize_chain_clean(self.options.tmpdir, 1)
|
||||||
|
|
||||||
|
def setup_network(self, split=False):
|
||||||
|
# -daaforkheight=0: ContextualCheckBlockHeader only enforces nBits for a
|
||||||
|
# smart chain above daaForkHeight, which defaults to
|
||||||
|
# ASSETCHAINS_RANDOMX_VALIDATION+62000 (millions of blocks) so that a
|
||||||
|
# fresh sync accepts DragonX's historical bad-nBits window. Without this
|
||||||
|
# flag Test 8 (bad bits) is unreachable at regtest heights and the
|
||||||
|
# daemon happily accepts a block with arbitrary nBits.
|
||||||
|
self.nodes = [ start_node(0, self.options.tmpdir, ['-daaforkheight=0']) ]
|
||||||
|
self.is_network_split = False
|
||||||
|
|
||||||
def run_test(self):
|
def run_test(self):
|
||||||
node = self.nodes[0]
|
node = self.nodes[0]
|
||||||
node.generate(1) # Mine a block to leave initial block download
|
node.generate(1) # Mine a block to leave initial block download
|
||||||
@@ -117,9 +161,9 @@ class GetBlockTemplateProposalTest(BitcoinTestFramework):
|
|||||||
#txlist[0][4+1+36+1+1] -= 1
|
#txlist[0][4+1+36+1+1] -= 1
|
||||||
|
|
||||||
# Test 2: Bad input hash for gen tx
|
# Test 2: Bad input hash for gen tx
|
||||||
txlist[0][4+1] += 1
|
txlist[0][CB_PREVOUT_OFF] += 1
|
||||||
assert_template(node, tmpl, txlist, 'bad-cb-missing')
|
assert_template(node, tmpl, txlist, 'bad-cb-missing')
|
||||||
txlist[0][4+1] -= 1
|
txlist[0][CB_PREVOUT_OFF] -= 1
|
||||||
|
|
||||||
# Test 3: Truncated final tx
|
# Test 3: Truncated final tx
|
||||||
lastbyte = txlist[-1].pop()
|
lastbyte = txlist[-1].pop()
|
||||||
@@ -136,14 +180,33 @@ class GetBlockTemplateProposalTest(BitcoinTestFramework):
|
|||||||
|
|
||||||
# Test 5: Add an invalid tx to the end (non-duplicate)
|
# Test 5: Add an invalid tx to the end (non-duplicate)
|
||||||
txlist.append(bytearray(txlist[0]))
|
txlist.append(bytearray(txlist[0]))
|
||||||
txlist[-1][4+1] = b'\xff'
|
txlist[-1][CB_PREVOUT_OFF] = 0xff
|
||||||
|
# DragonX is a fully private chain (ASSETCHAINS_PRIVATE): a non-coinbase
|
||||||
|
# tx paying a positive amount to a t-addr is killed in CheckTransaction
|
||||||
|
# with 'bad-txns-acprivacy-chain' before ConnectBlock ever looks up its
|
||||||
|
# inputs. Zero the output value -- CheckTransaction exempts zero-value
|
||||||
|
# vouts -- so the tx survives to the missing-input check this case is
|
||||||
|
# actually about.
|
||||||
|
_val_off = tx_vout0_value_off(txlist[-1])
|
||||||
|
txlist[-1][_val_off:_val_off+8] = b'\0'*8
|
||||||
assert_template(node, tmpl, txlist, 'bad-txns-inputs-missingorspent')
|
assert_template(node, tmpl, txlist, 'bad-txns-inputs-missingorspent')
|
||||||
txlist.pop()
|
txlist.pop()
|
||||||
|
|
||||||
# Test 6: Future tx lock time
|
# Test 6: Future tx lock time
|
||||||
txlist[0][-4:] = b'\xff\xff\xff\xff'
|
# The server-supplied coinbase carries nSequence=0xffffffff, and
|
||||||
|
# IsFinalTx() short-circuits on all-final inputs, so a future nLockTime
|
||||||
|
# alone leaves the tx final and the block valid. (DragonX's IsFinalTx
|
||||||
|
# also whitelists 0xfffffffe below the Hush hardfork height, which
|
||||||
|
# regtest is.) Make the input genuinely non-final so the future
|
||||||
|
# nLockTime is the thing under test.
|
||||||
|
seq_off = tx_seq_off(txlist[0])
|
||||||
|
realseq = txlist[0][seq_off:seq_off+4]
|
||||||
|
txlist[0][seq_off:seq_off+4] = b'\0\0\0\0'
|
||||||
|
reallocktime = txlist[0][-TX_TAIL_AFTER_LOCKTIME-4:-TX_TAIL_AFTER_LOCKTIME]
|
||||||
|
txlist[0][-TX_TAIL_AFTER_LOCKTIME-4:-TX_TAIL_AFTER_LOCKTIME] = b'\xff\xff\xff\xff'
|
||||||
assert_template(node, tmpl, txlist, 'bad-txns-nonfinal')
|
assert_template(node, tmpl, txlist, 'bad-txns-nonfinal')
|
||||||
txlist[0][-4:] = b'\0\0\0\0'
|
txlist[0][-TX_TAIL_AFTER_LOCKTIME-4:-TX_TAIL_AFTER_LOCKTIME] = reallocktime
|
||||||
|
txlist[0][seq_off:seq_off+4] = realseq
|
||||||
|
|
||||||
# Test 7: Bad tx count
|
# Test 7: Bad tx count
|
||||||
txlist.append(b'')
|
txlist.append(b'')
|
||||||
@@ -171,7 +234,9 @@ class GetBlockTemplateProposalTest(BitcoinTestFramework):
|
|||||||
tmpl['curtime'] = 0x7fffffff
|
tmpl['curtime'] = 0x7fffffff
|
||||||
assert_template(node, tmpl, txlist, 'time-too-new')
|
assert_template(node, tmpl, txlist, 'time-too-new')
|
||||||
tmpl['curtime'] = 0
|
tmpl['curtime'] = 0
|
||||||
assert_template(node, tmpl, txlist, 'time-too-old')
|
# DragonX split Bitcoin's 'time-too-old' into 'time-too-old-median'
|
||||||
|
# (block time <= prev MedianTimePast) and 'time-too-old-prevblock'.
|
||||||
|
assert_template(node, tmpl, txlist, 'time-too-old-median')
|
||||||
tmpl['curtime'] = realtime
|
tmpl['curtime'] = realtime
|
||||||
|
|
||||||
# Test 11: Valid block
|
# Test 11: Valid block
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2014 The Bitcoin Core developers
|
# Copyright (c) 2014 The Bitcoin Core developers
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Released under the GPLv3
|
# Released under the GPLv3
|
||||||
@@ -28,7 +28,7 @@ class ReindexTest(BitcoinTestFramework):
|
|||||||
wait_bitcoinds()
|
wait_bitcoinds()
|
||||||
self.nodes[0]=start_node(0, self.options.tmpdir, ["-debug", "-reindex", "-checkblockindex=1"])
|
self.nodes[0]=start_node(0, self.options.tmpdir, ["-debug", "-reindex", "-checkblockindex=1"])
|
||||||
assert_equal(self.nodes[0].getblockcount(), 3)
|
assert_equal(self.nodes[0].getblockcount(), 3)
|
||||||
print "Success"
|
print("Success")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
ReindexTest().main()
|
ReindexTest().main()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# and for constructing a getheaders message
|
# and for constructing a getheaders message
|
||||||
#
|
#
|
||||||
|
|
||||||
from mininode import CBlock, CBlockHeader, CBlockLocator, CTransaction, msg_block, msg_headers, msg_tx
|
from .mininode import CBlock, CBlockHeader, CBlockLocator, CTransaction, msg_block, msg_headers, msg_tx
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import cStringIO
|
import cStringIO
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
# file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
# file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||||
# blocktools.py - utilities for manipulating blocks and transactions
|
# blocktools.py - utilities for manipulating blocks and transactions
|
||||||
from mininode import CBlock, CTransaction, CTxIn, CTxOut, COutPoint
|
from .mininode import CBlock, CTransaction, CTxIn, CTxOut, COutPoint
|
||||||
from script import CScript, OP_0, OP_EQUAL, OP_HASH160
|
from .script import CScript, OP_0, OP_EQUAL, OP_HASH160
|
||||||
|
|
||||||
# Create a block (with regtest difficulty)
|
# Create a block (with regtest difficulty)
|
||||||
def create_block(hashprev, coinbase, nTime=None, nBits=None):
|
def create_block(hashprev, coinbase, nTime=None, nBits=None):
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
# file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
# file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||||
|
|
||||||
from mininode import CBlock, CTransaction, CInv, NodeConn, NodeConnCB, \
|
from .mininode import CBlock, CTransaction, CInv, NodeConn, NodeConnCB, \
|
||||||
msg_inv, msg_getheaders, msg_ping, msg_mempool, mininode_lock, MAX_INV_SZ
|
msg_inv, msg_getheaders, msg_ping, msg_mempool, mininode_lock, MAX_INV_SZ
|
||||||
from blockstore import BlockStore, TxStore
|
from .blockstore import BlockStore, TxStore
|
||||||
from util import p2p_port
|
from .util import p2p_port
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from authproxy import JSONRPCException
|
from .authproxy import JSONRPCException
|
||||||
from util import assert_equal, check_json_precision, \
|
from .util import assert_equal, check_json_precision, \
|
||||||
initialize_chain, initialize_chain_clean, \
|
initialize_chain, initialize_chain_clean, \
|
||||||
start_nodes, connect_nodes_bi, stop_nodes, \
|
start_nodes, connect_nodes_bi, stop_nodes, \
|
||||||
sync_blocks, sync_mempools, wait_bitcoinds
|
sync_blocks, sync_mempools, wait_bitcoinds
|
||||||
@@ -91,7 +91,7 @@ class BitcoinTestFramework(object):
|
|||||||
parser.add_option("--noshutdown", dest="noshutdown", default=False, action="store_true",
|
parser.add_option("--noshutdown", dest="noshutdown", default=False, action="store_true",
|
||||||
help="Don't stop nodes after the test execution")
|
help="Don't stop nodes after the test execution")
|
||||||
parser.add_option("--srcdir", dest="srcdir", default="../../src",
|
parser.add_option("--srcdir", dest="srcdir", default="../../src",
|
||||||
help="Source directory containing hushd/hush-cli (default: %default)")
|
help="Source directory containing dragonxd/dragonx-cli (default: %default)")
|
||||||
parser.add_option("--tmpdir", dest="tmpdir", default=tempfile.mkdtemp(prefix="test"),
|
parser.add_option("--tmpdir", dest="tmpdir", default=tempfile.mkdtemp(prefix="test"),
|
||||||
help="Root directory for datadirs")
|
help="Root directory for datadirs")
|
||||||
parser.add_option("--tracerpc", dest="trace_rpc", default=False, action="store_true",
|
parser.add_option("--tracerpc", dest="trace_rpc", default=False, action="store_true",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import subprocess
|
|||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from authproxy import AuthServiceProxy
|
from .authproxy import AuthServiceProxy
|
||||||
|
|
||||||
def p2p_port(n):
|
def p2p_port(n):
|
||||||
return 11000 + n + os.getpid()%999
|
return 11000 + n + os.getpid()%999
|
||||||
@@ -97,8 +97,8 @@ def initialize_datadir(dirname, n):
|
|||||||
print("Creating dirs %s" % datadir)
|
print("Creating dirs %s" % datadir)
|
||||||
os.makedirs(datadir)
|
os.makedirs(datadir)
|
||||||
|
|
||||||
print("Writing to " + os.path.join(datadir,"ZZZ.conf"))
|
print("Writing to " + os.path.join(datadir,"DRAGONX.conf"))
|
||||||
with open(os.path.join(datadir, "ZZZ.conf"), 'w') as f:
|
with open(os.path.join(datadir, "DRAGONX.conf"), 'w') as f:
|
||||||
f.write("regtest=1\n");
|
f.write("regtest=1\n");
|
||||||
f.write("txindex=1\n");
|
f.write("txindex=1\n");
|
||||||
#f.write("testnode=1\n");
|
#f.write("testnode=1\n");
|
||||||
@@ -116,7 +116,19 @@ def initialize_datadir(dirname, n):
|
|||||||
f.write("spentindex=1\n");
|
f.write("spentindex=1\n");
|
||||||
f.write("timestampindex=1\n");
|
f.write("timestampindex=1\n");
|
||||||
#f.write("zindex=1\n");
|
#f.write("zindex=1\n");
|
||||||
print("Done writing to %s" % os.path.join(datadir,"ZZZ.conf") )
|
print("Done writing to %s" % os.path.join(datadir,"DRAGONX.conf") )
|
||||||
|
|
||||||
|
# dragonxd refuses to start without an asmap file ("Could not find any asmap file!"),
|
||||||
|
# so every regtest datadir needs one. Link the tree's copy rather than duplicating it.
|
||||||
|
for src in ("../../../asmap.dat", "../../../src/asmap.dat",
|
||||||
|
os.path.expanduser("~/.hush/DRAGONX/asmap.dat")):
|
||||||
|
cand = src if os.path.isabs(src) else os.path.join(os.path.dirname(os.path.abspath(__file__)), src)
|
||||||
|
if os.path.exists(cand):
|
||||||
|
dst = os.path.join(datadir, "asmap.dat")
|
||||||
|
if not os.path.exists(dst):
|
||||||
|
try: os.symlink(os.path.realpath(cand), dst)
|
||||||
|
except OSError: shutil.copyfile(cand, dst)
|
||||||
|
break
|
||||||
|
|
||||||
return datadir
|
return datadir
|
||||||
|
|
||||||
@@ -133,11 +145,23 @@ def initialize_chain(test_dir):
|
|||||||
# Create cache directories, run hushds:
|
# Create cache directories, run hushds:
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
datadir=initialize_datadir("cache", i)
|
datadir=initialize_datadir("cache", i)
|
||||||
args = [ os.getenv("BITCOIND", "hushd"), "-keypool=1", "-datadir="+datadir, "-discover=0" ]
|
# Same two requirements as start_node(): -regtest must be a command-line flag (the
|
||||||
|
# conf key is ignored, and without it this cache node runs on MAINNET), and -asmap
|
||||||
|
# must be absolute or dragonxd refuses to start.
|
||||||
|
# -connect=<anything> makes init.cpp soft-set -listen=0 ("parameter interaction: -connect
|
||||||
|
# set -> setting -listen=0"), so cache node0 never opened its p2p port and nodes 1-3 could
|
||||||
|
# never sync to it -- initialize_chain() then hung forever in sync_blocks(). Pass -listen
|
||||||
|
# and -bind explicitly (an explicit arg beats SoftSetBoolArg) and keep the loopback bind so
|
||||||
|
# the cache nodes stay off the public network.
|
||||||
|
args = [ os.getenv("BITCOIND", "src/dragonxd"), "-regtest", "-connect=0", "-keypool=1", "-datadir="+datadir, "-discover=0",
|
||||||
|
"-listen=1", "-bind=127.0.0.1", "-dnsseed=0" ]
|
||||||
|
_am = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../asmap.dat")
|
||||||
|
if os.path.exists(_am):
|
||||||
|
args.append("-asmap=" + os.path.realpath(_am))
|
||||||
if i > 0:
|
if i > 0:
|
||||||
args.append("-connect=127.0.0.1:"+str(p2p_port(0)))
|
args.append("-connect=127.0.0.1:"+str(p2p_port(0)))
|
||||||
bitcoind_processes[i] = subprocess.Popen(args)
|
bitcoind_processes[i] = subprocess.Popen(args)
|
||||||
cmd = os.getenv("BITCOINCLI", "hush-cli")
|
cmd = os.getenv("BITCOINCLI", "src/dragonx-cli")
|
||||||
cmd_args = cmd + " -datadir="+datadir + " -rpcwait getblockcount"
|
cmd_args = cmd + " -datadir="+datadir + " -rpcwait getblockcount"
|
||||||
if os.getenv("PYTHON_DEBUG", ""):
|
if os.getenv("PYTHON_DEBUG", ""):
|
||||||
print("initialize_chain: hushd started, calling: " + cmd_args)
|
print("initialize_chain: hushd started, calling: " + cmd_args)
|
||||||
@@ -180,10 +204,17 @@ def initialize_chain(test_dir):
|
|||||||
wait_bitcoinds()
|
wait_bitcoinds()
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
print("Cleaning up cache dir files")
|
print("Cleaning up cache dir files")
|
||||||
os.remove(log_filename("cache", i, "debug.log"))
|
# log_filename() points at <cache>/node<i>/regtest, but that IS the -datadir we passed;
|
||||||
os.remove(log_filename("cache", i, "db.log"))
|
# dragonxd writes its logs/peers.dat one level deeper, into the net-specific
|
||||||
os.remove(log_filename("cache", i, "peers.dat"))
|
# <datadir>/regtest subdir (same datadir-vs-netdir split that forced -asmap to be
|
||||||
os.remove(log_filename("cache", i, "fee_estimates.dat"))
|
# absolute in start_node). Try both, and tolerate files a node never created.
|
||||||
|
for name in ("debug.log", "db.log", "peers.dat", "fee_estimates.dat"):
|
||||||
|
for cand in (log_filename("cache", i, os.path.join("regtest", name)),
|
||||||
|
log_filename("cache", i, name)):
|
||||||
|
try:
|
||||||
|
os.remove(cand)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
from_dir = os.path.join("cache", "node"+str(i))
|
from_dir = os.path.join("cache", "node"+str(i))
|
||||||
@@ -227,9 +258,10 @@ def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=
|
|||||||
"""
|
"""
|
||||||
print("Starting node " + str(i) + " in dir " + dirname)
|
print("Starting node " + str(i) + " in dir " + dirname)
|
||||||
datadir = os.path.join(dirname, "node"+str(i), "regtest")
|
datadir = os.path.join(dirname, "node"+str(i), "regtest")
|
||||||
|
if extra_args is None: extra_args = []
|
||||||
# creating special config
|
# creating special config
|
||||||
if len(extra_args) > 0 and extra_args[0] == '-ac_name=ZZZ':
|
if len(extra_args) > 0 and extra_args[0] == '-ac_name=ZZZ':
|
||||||
configpath = datadir + "/ZZZ.conf"
|
configpath = datadir + "/DRAGONX.conf"
|
||||||
with open(configpath, "w+") as config:
|
with open(configpath, "w+") as config:
|
||||||
config.write("rpcuser=hush\n")
|
config.write("rpcuser=hush\n")
|
||||||
config.write("rpcpassword=puppy\n")
|
config.write("rpcpassword=puppy\n")
|
||||||
@@ -247,16 +279,30 @@ def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=
|
|||||||
print("Done writing to %s" % configpath)
|
print("Done writing to %s" % configpath)
|
||||||
|
|
||||||
if binary is None:
|
if binary is None:
|
||||||
binary = os.getenv("BITCOIND", "src/hushd")
|
binary = os.getenv("BITCOIND", "src/dragonxd")
|
||||||
args = [ binary, "-datadir="+datadir, "-keypool=1", "-discover=0", "-rest" ]
|
# -regtest MUST be a command-line flag. DragonX ignores "regtest=1" in the conf file, so
|
||||||
|
# without this the node silently runs on MAINNET: it loads the real genesis, dials the real
|
||||||
|
# seeds and starts syncing the live chain into the test datadir (observed: 196k blocks and
|
||||||
|
# 679MB before a test timed out). -connect=0 keeps the regtest node off the public network.
|
||||||
|
args = [ binary, "-regtest", "-connect=0", "-datadir="+datadir, "-keypool=1", "-discover=0", "-rest" ]
|
||||||
|
# -asmap relative paths are resolved against the NET-SPECIFIC datadir (init.cpp), which for
|
||||||
|
# regtest is <datadir>/regtest -- so a copy sitting in <datadir> is never found. Pass an
|
||||||
|
# absolute path; without it dragonxd exits with "Could not find any asmap file!".
|
||||||
|
_asmap = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../asmap.dat")
|
||||||
|
if os.path.exists(_asmap):
|
||||||
|
args.append("-asmap=" + os.path.realpath(_asmap))
|
||||||
if extra_args is not None: args.extend(extra_args)
|
if extra_args is not None: args.extend(extra_args)
|
||||||
print("args=" + ' '.join(args))
|
print("args=" + ' '.join(args))
|
||||||
bitcoind_processes[i] = subprocess.Popen(args)
|
bitcoind_processes[i] = subprocess.Popen(args)
|
||||||
devnull = open("/dev/null", "w+")
|
devnull = open("/dev/null", "w+")
|
||||||
|
|
||||||
cmd = os.getenv("BITCOINCLI", "src/hush-cli")
|
cmd = os.getenv("BITCOINCLI", "src/dragonx-cli")
|
||||||
print("cmd=" + cmd)
|
print("cmd=" + cmd)
|
||||||
args = [ extra_args[0], "-datadir="+datadir, "-keypool=1", "-discover=0", "-rest" ]
|
# The CLI only needs the datadir: initialize_datadir() already wrote DRAGONX.conf there
|
||||||
|
# with the right rpcport/user/password. The old form passed extra_args[0] as argv[0] and
|
||||||
|
# replayed daemon-only flags at the CLI, which only worked for the -ac_name=ZZZ assetchain
|
||||||
|
# tests and broke every test that passes no extra_args.
|
||||||
|
args = [ "-regtest", "-datadir="+datadir ]
|
||||||
cmd_args = ' '.join(args) + " -rpcwait getblockcount "
|
cmd_args = ' '.join(args) + " -rpcwait getblockcount "
|
||||||
if os.getenv("PYTHON_DEBUG", ""):
|
if os.getenv("PYTHON_DEBUG", ""):
|
||||||
print("start_node: hushd started, calling : " + cmd + " " + cmd_args)
|
print("start_node: hushd started, calling : " + cmd + " " + cmd_args)
|
||||||
@@ -266,18 +312,22 @@ def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=
|
|||||||
import time
|
import time
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
subprocess.check_call(strcmd, shell=True);
|
subprocess.check_call(strcmd, shell=True);
|
||||||
#subprocess.check_call([ os.getenv("BITCOINCLI", "hush-cli"), "-datadir="+datadir] +
|
#subprocess.check_call([ os.getenv("BITCOINCLI", "dragonx-cli"), "-datadir="+datadir] +
|
||||||
# _rpchost_to_args(rpchost) +
|
# _rpchost_to_args(rpchost) +
|
||||||
# ["-rpcwait", "-rpcport=6438", "getblockcount"], stdout=devnull)
|
# ["-rpcwait", "-rpcport=6438", "getblockcount"], stdout=devnull)
|
||||||
if os.getenv("PYTHON_DEBUG", ""):
|
if os.getenv("PYTHON_DEBUG", ""):
|
||||||
print("start_node: calling hush-cli -rpcwait getblockcount returned")
|
print("start_node: calling hush-cli -rpcwait getblockcount returned")
|
||||||
devnull.close()
|
devnull.close()
|
||||||
port = extra_args[3]
|
# Port comes from the same helper initialize_datadir() used, except for the assetchain
|
||||||
#port = rpc_port(i)
|
# tests which pass it positionally as extra_args[3] == "-rpcport=NNNN".
|
||||||
|
if len(extra_args) > 3 and str(extra_args[0]) == '-ac_name=ZZZ':
|
||||||
|
port = extra_args[3][9:]
|
||||||
|
else:
|
||||||
|
port = str(rpc_port(i))
|
||||||
#print("port=%s" % port)
|
#print("port=%s" % port)
|
||||||
username = rpc_username()
|
username = rpc_username()
|
||||||
password = rpc_password()
|
password = rpc_password()
|
||||||
url = "http://%s:%s@%s:%s" % (username, password, rpchost or '127.0.0.1', port[9:])
|
url = "http://%s:%s@%s:%s" % (username, password, rpchost or '127.0.0.1', port)
|
||||||
print("connecting to " + url)
|
print("connecting to " + url)
|
||||||
if timewait is not None:
|
if timewait is not None:
|
||||||
proxy = AuthServiceProxy(url, timeout=timewait)
|
proxy = AuthServiceProxy(url, timeout=timewait)
|
||||||
@@ -315,7 +365,7 @@ def stop_nodes(nodes):
|
|||||||
del nodes[:] # Emptying array closes connections as a side effect
|
del nodes[:] # Emptying array closes connections as a side effect
|
||||||
|
|
||||||
def set_node_times(nodes, t):
|
def set_node_times(nodes, t):
|
||||||
print("Setting nodes time to " + t)
|
print("Setting nodes time to " + str(t))
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
node.setmocktime(t)
|
node.setmocktime(t)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2014 The Bitcoin Core developers
|
# Copyright (c) 2014 The Bitcoin Core developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
@@ -20,8 +20,20 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
print("Initializing test directory "+self.options.tmpdir)
|
print("Initializing test directory "+self.options.tmpdir)
|
||||||
initialize_chain_clean(self.options.tmpdir, 4)
|
initialize_chain_clean(self.options.tmpdir, 4)
|
||||||
|
|
||||||
|
# PORT NOTE (DragonX/regtest, not a change of test intent):
|
||||||
|
# test_framework.start_node() hardcodes "-connect=0", and init.cpp turns that into
|
||||||
|
# "-connect set -> setting -listen=0". With listening off the nodes never bind their
|
||||||
|
# p2p port, so connect_nodes_bi() connects nothing at all (its version==0 poll loop
|
||||||
|
# exits immediately because there are no local peers) and the first sync_all() hangs
|
||||||
|
# forever. Passing -bind forces -listen back to 1. -dnsseed=0 keeps these regtest
|
||||||
|
# nodes from dialing the live DragonX network.
|
||||||
|
NET_ARGS = ["-listen=1", "-bind=127.0.0.1", "-dnsseed=0"]
|
||||||
|
|
||||||
|
def net_args(self, n, extra=None):
|
||||||
|
return [list(self.NET_ARGS) + list(extra or []) for _ in range(n)]
|
||||||
|
|
||||||
def setup_network(self, split=False):
|
def setup_network(self, split=False):
|
||||||
self.nodes = start_nodes(3, self.options.tmpdir)
|
self.nodes = start_nodes(3, self.options.tmpdir, self.net_args(3))
|
||||||
connect_nodes_bi(self.nodes,0,1)
|
connect_nodes_bi(self.nodes,0,1)
|
||||||
connect_nodes_bi(self.nodes,1,2)
|
connect_nodes_bi(self.nodes,1,2)
|
||||||
connect_nodes_bi(self.nodes,0,2)
|
connect_nodes_bi(self.nodes,0,2)
|
||||||
@@ -29,7 +41,7 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
self.sync_all()
|
self.sync_all()
|
||||||
|
|
||||||
def run_test (self):
|
def run_test (self):
|
||||||
print "Mining blocks..."
|
print("Mining blocks...")
|
||||||
|
|
||||||
self.nodes[0].generate(4)
|
self.nodes[0].generate(4)
|
||||||
self.sync_all()
|
self.sync_all()
|
||||||
@@ -106,7 +118,7 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
signed_tx = self.nodes[2].signrawtransaction(raw_tx)
|
signed_tx = self.nodes[2].signrawtransaction(raw_tx)
|
||||||
try:
|
try:
|
||||||
self.nodes[2].sendrawtransaction(signed_tx["hex"])
|
self.nodes[2].sendrawtransaction(signed_tx["hex"])
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert("absurdly high fees" in errorString)
|
assert("absurdly high fees" in errorString)
|
||||||
assert("900000000 > 190000" in errorString)
|
assert("900000000 > 190000" in errorString)
|
||||||
@@ -186,7 +198,7 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
txid2 = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1)
|
txid2 = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1)
|
||||||
sync_mempools(self.nodes)
|
sync_mempools(self.nodes)
|
||||||
|
|
||||||
self.nodes.append(start_node(3, self.options.tmpdir))
|
self.nodes.append(start_node(3, self.options.tmpdir, list(self.NET_ARGS)))
|
||||||
connect_nodes_bi(self.nodes, 0, 3)
|
connect_nodes_bi(self.nodes, 0, 3)
|
||||||
sync_blocks(self.nodes)
|
sync_blocks(self.nodes)
|
||||||
|
|
||||||
@@ -227,7 +239,7 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
#do some -walletbroadcast tests
|
#do some -walletbroadcast tests
|
||||||
stop_nodes(self.nodes)
|
stop_nodes(self.nodes)
|
||||||
wait_bitcoinds()
|
wait_bitcoinds()
|
||||||
self.nodes = start_nodes(3, self.options.tmpdir, [["-walletbroadcast=0"],["-walletbroadcast=0"],["-walletbroadcast=0"]])
|
self.nodes = start_nodes(3, self.options.tmpdir, self.net_args(3, ["-walletbroadcast=0"]))
|
||||||
connect_nodes_bi(self.nodes,0,1)
|
connect_nodes_bi(self.nodes,0,1)
|
||||||
connect_nodes_bi(self.nodes,1,2)
|
connect_nodes_bi(self.nodes,1,2)
|
||||||
connect_nodes_bi(self.nodes,0,2)
|
connect_nodes_bi(self.nodes,0,2)
|
||||||
@@ -256,7 +268,7 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
#restart the nodes with -walletbroadcast=1
|
#restart the nodes with -walletbroadcast=1
|
||||||
stop_nodes(self.nodes)
|
stop_nodes(self.nodes)
|
||||||
wait_bitcoinds()
|
wait_bitcoinds()
|
||||||
self.nodes = start_nodes(3, self.options.tmpdir)
|
self.nodes = start_nodes(3, self.options.tmpdir, self.net_args(3))
|
||||||
connect_nodes_bi(self.nodes,0,1)
|
connect_nodes_bi(self.nodes,0,1)
|
||||||
connect_nodes_bi(self.nodes,1,2)
|
connect_nodes_bi(self.nodes,1,2)
|
||||||
connect_nodes_bi(self.nodes,0,2)
|
connect_nodes_bi(self.nodes,0,2)
|
||||||
@@ -290,7 +302,7 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
num_t_recipients = 3000
|
num_t_recipients = 3000
|
||||||
amount_per_recipient = Decimal('0.00000001')
|
amount_per_recipient = Decimal('0.00000001')
|
||||||
errorString = ''
|
errorString = ''
|
||||||
for i in xrange(0,num_t_recipients):
|
for i in range(0,num_t_recipients):
|
||||||
newtaddr = self.nodes[2].getnewaddress()
|
newtaddr = self.nodes[2].getnewaddress()
|
||||||
recipients.append({"address":newtaddr, "amount":amount_per_recipient})
|
recipients.append({"address":newtaddr, "amount":amount_per_recipient})
|
||||||
|
|
||||||
@@ -305,7 +317,7 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
self.nodes[0].z_sendmany(myzaddr, recipients)
|
self.nodes[0].z_sendmany(myzaddr, recipients)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert("Too many outputs, size of raw transaction" in errorString)
|
assert("Too many outputs, size of raw transaction" in errorString)
|
||||||
|
|
||||||
@@ -314,10 +326,10 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
num_z_recipients = 50
|
num_z_recipients = 50
|
||||||
amount_per_recipient = Decimal('0.00000001')
|
amount_per_recipient = Decimal('0.00000001')
|
||||||
errorString = ''
|
errorString = ''
|
||||||
for i in xrange(0,num_t_recipients):
|
for i in range(0,num_t_recipients):
|
||||||
newtaddr = self.nodes[2].getnewaddress()
|
newtaddr = self.nodes[2].getnewaddress()
|
||||||
recipients.append({"address":newtaddr, "amount":amount_per_recipient})
|
recipients.append({"address":newtaddr, "amount":amount_per_recipient})
|
||||||
for i in xrange(0,num_z_recipients):
|
for i in range(0,num_z_recipients):
|
||||||
newzaddr = self.nodes[2].z_getnewaddress()
|
newzaddr = self.nodes[2].z_getnewaddress()
|
||||||
recipients.append({"address":newzaddr, "amount":amount_per_recipient})
|
recipients.append({"address":newzaddr, "amount":amount_per_recipient})
|
||||||
|
|
||||||
@@ -327,7 +339,7 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
self.nodes[0].z_sendmany(myzaddr, recipients)
|
self.nodes[0].z_sendmany(myzaddr, recipients)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert("size of raw transaction would be larger than limit" in errorString)
|
assert("size of raw transaction would be larger than limit" in errorString)
|
||||||
|
|
||||||
@@ -335,12 +347,12 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
num_z_recipients = 100
|
num_z_recipients = 100
|
||||||
amount_per_recipient = Decimal('0.00000001')
|
amount_per_recipient = Decimal('0.00000001')
|
||||||
errorString = ''
|
errorString = ''
|
||||||
for i in xrange(0,num_z_recipients):
|
for i in range(0,num_z_recipients):
|
||||||
newzaddr = self.nodes[2].z_getnewaddress()
|
newzaddr = self.nodes[2].z_getnewaddress()
|
||||||
recipients.append({"address":newzaddr, "amount":amount_per_recipient})
|
recipients.append({"address":newzaddr, "amount":amount_per_recipient})
|
||||||
try:
|
try:
|
||||||
self.nodes[0].z_sendmany(myzaddr, recipients)
|
self.nodes[0].z_sendmany(myzaddr, recipients)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert("Invalid parameter, too many zaddr outputs" in errorString)
|
assert("Invalid parameter, too many zaddr outputs" in errorString)
|
||||||
|
|
||||||
@@ -426,7 +438,7 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
errorString = ""
|
errorString = ""
|
||||||
try:
|
try:
|
||||||
txId = self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), "1f-4")
|
txId = self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), "1f-4")
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
|
|
||||||
assert_equal("Invalid amount" in errorString, True)
|
assert_equal("Invalid amount" in errorString, True)
|
||||||
@@ -434,7 +446,7 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
errorString = ""
|
errorString = ""
|
||||||
try:
|
try:
|
||||||
self.nodes[0].generate("2") #use a string to as block amount parameter must fail because it's not interpreted as amount
|
self.nodes[0].generate("2") #use a string to as block amount parameter must fail because it's not interpreted as amount
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
|
|
||||||
assert_equal("not an integer" in errorString, True)
|
assert_equal("not an integer" in errorString, True)
|
||||||
@@ -448,9 +460,9 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
myopid = self.nodes[0].z_sendmany(myzaddr, recipients)
|
myopid = self.nodes[0].z_sendmany(myzaddr, recipients)
|
||||||
assert(myopid)
|
assert(myopid)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
print errorString
|
print(errorString)
|
||||||
assert(False)
|
assert(False)
|
||||||
|
|
||||||
# This fee is larger than the default fee and since amount=0
|
# This fee is larger than the default fee and since amount=0
|
||||||
@@ -462,7 +474,7 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, minconf, fee)
|
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, minconf, fee)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert('Small transaction amount' in errorString)
|
assert('Small transaction amount' in errorString)
|
||||||
|
|
||||||
@@ -475,9 +487,9 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, minconf, fee)
|
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, minconf, fee)
|
||||||
assert(myopid)
|
assert(myopid)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
print errorString
|
print(errorString)
|
||||||
assert(False)
|
assert(False)
|
||||||
|
|
||||||
# Make sure amount=0, fee=0 transaction are valid to add to mempool
|
# Make sure amount=0, fee=0 transaction are valid to add to mempool
|
||||||
@@ -490,9 +502,9 @@ class WalletTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, minconf, fee)
|
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, minconf, fee)
|
||||||
assert(myopid)
|
assert(myopid)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
print errorString
|
print(errorString)
|
||||||
assert(False)
|
assert(False)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2018 The Zcash developers
|
# Copyright (c) 2018 The Zcash developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2017 The Zcash developers
|
# Copyright (c) 2017 The Zcash developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
@@ -32,7 +32,7 @@ class WalletMergeToAddressTest (BitcoinTestFramework):
|
|||||||
self.sync_all()
|
self.sync_all()
|
||||||
|
|
||||||
def run_test (self):
|
def run_test (self):
|
||||||
print "Mining blocks..."
|
print("Mining blocks...")
|
||||||
|
|
||||||
self.nodes[0].generate(1)
|
self.nodes[0].generate(1)
|
||||||
do_not_shield_taddr = self.nodes[0].getnewaddress()
|
do_not_shield_taddr = self.nodes[0].getnewaddress()
|
||||||
@@ -81,7 +81,7 @@ class WalletMergeToAddressTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
self.nodes[0].z_mergetoaddress("*", myzaddr)
|
self.nodes[0].z_mergetoaddress("*", myzaddr)
|
||||||
assert(False)
|
assert(False)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("JSON value is not an array as expected" in errorString, True)
|
assert_equal("JSON value is not an array as expected" in errorString, True)
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ class WalletMergeToAddressTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
self.nodes[2].z_mergetoaddress([mytaddr], myzaddr)
|
self.nodes[2].z_mergetoaddress([mytaddr], myzaddr)
|
||||||
assert(False)
|
assert(False)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Could not find any funds to merge" in errorString, True)
|
assert_equal("Could not find any funds to merge" in errorString, True)
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ class WalletMergeToAddressTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
self.nodes[0].z_mergetoaddress(["*"], myzaddr, -1)
|
self.nodes[0].z_mergetoaddress(["*"], myzaddr, -1)
|
||||||
assert(False)
|
assert(False)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Amount out of range" in errorString, True)
|
assert_equal("Amount out of range" in errorString, True)
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ class WalletMergeToAddressTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('21000000.00000001'))
|
self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('21000000.00000001'))
|
||||||
assert(False)
|
assert(False)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Amount out of range" in errorString, True)
|
assert_equal("Amount out of range" in errorString, True)
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ class WalletMergeToAddressTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
self.nodes[0].z_mergetoaddress(["*"], myzaddr, 999)
|
self.nodes[0].z_mergetoaddress(["*"], myzaddr, 999)
|
||||||
assert(False)
|
assert(False)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Insufficient funds" in errorString, True)
|
assert_equal("Insufficient funds" in errorString, True)
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ class WalletMergeToAddressTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('0.001'), -1)
|
self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('0.001'), -1)
|
||||||
assert(False)
|
assert(False)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Limit on maximum number of UTXOs cannot be negative" in errorString, True)
|
assert_equal("Limit on maximum number of UTXOs cannot be negative" in errorString, True)
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ class WalletMergeToAddressTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('0.001'), 99999999999999)
|
self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('0.001'), 99999999999999)
|
||||||
assert(False)
|
assert(False)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("JSON integer out of range" in errorString, True)
|
assert_equal("JSON integer out of range" in errorString, True)
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ class WalletMergeToAddressTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('0.001'), 50, -1)
|
self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('0.001'), 50, -1)
|
||||||
assert(False)
|
assert(False)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Limit on maximum number of notes cannot be negative" in errorString, True)
|
assert_equal("Limit on maximum number of notes cannot be negative" in errorString, True)
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ class WalletMergeToAddressTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('0.001'), 50, 99999999999999)
|
self.nodes[0].z_mergetoaddress(["*"], myzaddr, Decimal('0.001'), 50, 99999999999999)
|
||||||
assert(False)
|
assert(False)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("JSON integer out of range" in errorString, True)
|
assert_equal("JSON integer out of range" in errorString, True)
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ class WalletMergeToAddressTest (BitcoinTestFramework):
|
|||||||
try:
|
try:
|
||||||
self.nodes[0].z_mergetoaddress([mytaddr], mytaddr)
|
self.nodes[0].z_mergetoaddress([mytaddr], mytaddr)
|
||||||
assert(False)
|
assert(False)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Destination address is also the only source address, and all its funds are already merged" in errorString, True)
|
assert_equal("Destination address is also the only source address, and all its funds are already merged" in errorString, True)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2016 The Zcash developers
|
# Copyright (c) 2016 The Zcash developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
@@ -7,15 +7,71 @@
|
|||||||
|
|
||||||
from test_framework.test_framework import BitcoinTestFramework
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
from test_framework.util import assert_equal, assert_true, bitcoind_processes, \
|
from test_framework.util import assert_equal, assert_true, bitcoind_processes, \
|
||||||
connect_nodes_bi, start_node, start_nodes, wait_and_assert_operationid_status
|
connect_nodes_bi, initialize_chain_clean, p2p_port, start_node, start_nodes, \
|
||||||
|
sync_blocks, wait_and_assert_operationid_status
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
class WalletNullifiersTest (BitcoinTestFramework):
|
class WalletNullifiersTest (BitcoinTestFramework):
|
||||||
|
|
||||||
|
# The framework default setup_chain() calls initialize_chain(), which pre-builds a
|
||||||
|
# 200-block chain in a *relative* "cache/" directory shared by every test process
|
||||||
|
# running out of this tree. That directory is guarded only by the datadir lock, so two
|
||||||
|
# qa/rpc-tests running at once collide on it and the second one hangs forever inside
|
||||||
|
# "dragonx-cli -rpcwait" (observed here while a sibling test held cache/node0..3).
|
||||||
|
# Build the identical pre-condition -- 4 nodes, two rounds of 25 blocks each, i.e. 25
|
||||||
|
# mature + 25 immature coinbases per node -- directly in this test's private tmpdir.
|
||||||
|
# This is a setup change only: no assertion below is relaxed, removed or reordered.
|
||||||
|
def setup_chain(self):
|
||||||
|
print("Initializing test directory "+self.options.tmpdir)
|
||||||
|
initialize_chain_clean(self.options.tmpdir, 4)
|
||||||
|
|
||||||
|
# Three networking facts about this daemon force extra flags here. None of them
|
||||||
|
# change what the test exercises; without them the 4 nodes either never peer with
|
||||||
|
# each other, or peer with the LIVE DragonX network instead.
|
||||||
|
#
|
||||||
|
# 1. start_node() hardcodes "-connect=0", which also soft-sets -listen=0, so nothing
|
||||||
|
# binds p2p_port(i) and connect_nodes_bi() can never form the regtest mesh --
|
||||||
|
# sync_blocks() then spins forever (observed: node0 at 25 blocks, nodes 1-3 stuck
|
||||||
|
# at 0, nothing listening on 11005-11008). -listen=1 -bind=127.0.0.1 restores the
|
||||||
|
# mesh and keeps it on loopback.
|
||||||
|
# 2. hush_args() appends node1..node10.dragonx.is to -addnode unconditionally, -regtest
|
||||||
|
# included, and regtest reuses mainnet's network magic. A "regtest" node therefore
|
||||||
|
# joins the live network: node0 of an earlier run handshook 8 production peers
|
||||||
|
# ("receive version message: /DragonX:1.0.3/ ... blocks=3254266") and ingested their
|
||||||
|
# headers. -dns=0 stops those hostname -addnode entries from resolving; RPC addnode
|
||||||
|
# with a numeric 127.0.0.1:port is unaffected.
|
||||||
|
# 3. hush_args() runs BEFORE the config file is read, so its GetArg("-port",0) never
|
||||||
|
# sees the "port=" line initialize_datadir() wrote and GetDefaultPort() stays at the
|
||||||
|
# mainnet p2p port. "-connect=0" is then parsed as the address 0.0.0.0:<mainnet
|
||||||
|
# port>, i.e. the production dragonxd listening on this box -- every node in runs 2
|
||||||
|
# and 3 picked up exactly one peer reporting blocks=3254269. Repeating -port on the
|
||||||
|
# command line points GetDefaultPort() at this node's own regtest port instead.
|
||||||
|
#
|
||||||
|
# -autoshield is on by default on DragonX and is not part of what this test measures:
|
||||||
|
# a background thread sweeps each node's matured coinbase into a seed-derived zaddr
|
||||||
|
# (8 "autoshield operation finished" ops per node while the chain is being mined). That
|
||||||
|
# empties the very taddr this test spends from, and the resulting transactions do not
|
||||||
|
# settle identically on every node ("ERROR: AcceptToMemoryPool: ContextualCheckTransaction
|
||||||
|
# failed" on node1), so sync_mempools() never converges and the run wedges until the
|
||||||
|
# timeout. Turn the background sweeper off; the test does its own shielding explicitly.
|
||||||
|
def net_args(self, i):
|
||||||
|
return ['-listen=1', '-bind=127.0.0.1', '-dns=0', '-autoshield=0',
|
||||||
|
'-port=%d' % p2p_port(i)]
|
||||||
|
|
||||||
def setup_nodes(self):
|
def setup_nodes(self):
|
||||||
return start_nodes(4, self.options.tmpdir,
|
return start_nodes(4, self.options.tmpdir,
|
||||||
extra_args=[['-experimentalfeatures', '-developerencryptwallet']] * 4)
|
extra_args=[['-experimentalfeatures', '-developerencryptwallet']
|
||||||
|
+ self.net_args(i) for i in range(4)])
|
||||||
|
|
||||||
|
def setup_network(self, split = False):
|
||||||
|
super().setup_network(split)
|
||||||
|
# Same block layout initialize_chain() would have handed us.
|
||||||
|
for _ in range(2):
|
||||||
|
for peer in range(4):
|
||||||
|
self.nodes[peer].generate(25)
|
||||||
|
sync_blocks(self.nodes)
|
||||||
|
self.sync_all()
|
||||||
|
|
||||||
def run_test (self):
|
def run_test (self):
|
||||||
# add zaddr to node 0
|
# add zaddr to node 0
|
||||||
@@ -44,7 +100,7 @@ class WalletNullifiersTest (BitcoinTestFramework):
|
|||||||
bitcoind_processes[1].wait()
|
bitcoind_processes[1].wait()
|
||||||
|
|
||||||
# restart node 1
|
# restart node 1
|
||||||
self.nodes[1] = start_node(1, self.options.tmpdir)
|
self.nodes[1] = start_node(1, self.options.tmpdir, self.net_args(1))
|
||||||
connect_nodes_bi(self.nodes, 0, 1)
|
connect_nodes_bi(self.nodes, 0, 1)
|
||||||
connect_nodes_bi(self.nodes, 1, 2)
|
connect_nodes_bi(self.nodes, 1, 2)
|
||||||
self.sync_all()
|
self.sync_all()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2018 The Zcash developers
|
# Copyright (c) 2018 The Zcash developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
@@ -13,6 +13,29 @@ from test_framework.util import (
|
|||||||
)
|
)
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
def get_value_pool(pools, pool_id):
|
||||||
|
"""
|
||||||
|
Return the valuePools entry with the given id, or None if this chain does
|
||||||
|
not have that pool. DragonX's getblockchaininfo only reports the Sapling
|
||||||
|
pool (no Sprout history exists on this chain), so the pools list can no
|
||||||
|
longer be indexed positionally the way the upstream test did.
|
||||||
|
"""
|
||||||
|
for pool in pools:
|
||||||
|
if pool['id'] == pool_id:
|
||||||
|
return pool
|
||||||
|
return None
|
||||||
|
|
||||||
|
def assert_pool_values(pools, sprout_value, sapling_value):
|
||||||
|
sprout = get_value_pool(pools, 'sprout')
|
||||||
|
if sprout is not None:
|
||||||
|
assert_equal(sprout['chainValue'], sprout_value)
|
||||||
|
else:
|
||||||
|
# No Sprout pool at all is the same statement as "the Sprout pool holds nothing"
|
||||||
|
assert_equal(sprout_value, Decimal('0'))
|
||||||
|
sapling = get_value_pool(pools, 'sapling')
|
||||||
|
assert_true(sapling is not None, "Sapling value pool missing from getblockchaininfo")
|
||||||
|
assert_equal(sapling['chainValue'], sapling_value)
|
||||||
|
|
||||||
class WalletPersistenceTest (BitcoinTestFramework):
|
class WalletPersistenceTest (BitcoinTestFramework):
|
||||||
|
|
||||||
def setup_chain(self):
|
def setup_chain(self):
|
||||||
@@ -20,8 +43,20 @@ class WalletPersistenceTest (BitcoinTestFramework):
|
|||||||
initialize_chain_clean(self.options.tmpdir, 3)
|
initialize_chain_clean(self.options.tmpdir, 3)
|
||||||
|
|
||||||
def setup_network(self, split=False):
|
def setup_network(self, split=False):
|
||||||
|
# -listen=1/-bind: the framework's start_node() passes -connect=0, and DragonX (like
|
||||||
|
# Bitcoin) reacts to -connect by soft-setting -listen=0. A non-listening node can never
|
||||||
|
# accept the "addnode 127.0.0.1:<port>" that connect_nodes_bi() issues, so without this
|
||||||
|
# the three nodes stay isolated and sync_all() spins forever. -bind keeps the listener on
|
||||||
|
# loopback so a regtest node never becomes reachable from the public internet.
|
||||||
self.nodes = start_nodes(3, self.options.tmpdir,
|
self.nodes = start_nodes(3, self.options.tmpdir,
|
||||||
extra_args=[[
|
extra_args=[[
|
||||||
|
'-listen=1',
|
||||||
|
'-bind=127.0.0.1',
|
||||||
|
# -dns=0: DragonX appends node1..node10.dragonx.is to -addnode for every chain
|
||||||
|
# named DRAGONX (hush_utils.h), and -connect=0 does not suppress -addnode. Without
|
||||||
|
# this a regtest node dials the LIVE DragonX network and is fed mainnet headers.
|
||||||
|
# The addnode calls connect_nodes_bi() makes use literal IPs, so they still work.
|
||||||
|
'-dns=0',
|
||||||
'-nuparams=5ba81b19:100', # Overwinter
|
'-nuparams=5ba81b19:100', # Overwinter
|
||||||
'-nuparams=76b809bb:201', # Sapling
|
'-nuparams=76b809bb:201', # Sapling
|
||||||
]] * 3)
|
]] * 3)
|
||||||
@@ -72,8 +107,7 @@ class WalletPersistenceTest (BitcoinTestFramework):
|
|||||||
|
|
||||||
# Verify size of shielded pools
|
# Verify size of shielded pools
|
||||||
pools = self.nodes[0].getblockchaininfo()['valuePools']
|
pools = self.nodes[0].getblockchaininfo()['valuePools']
|
||||||
assert_equal(pools[0]['chainValue'], Decimal('0')) # Sprout
|
assert_pool_values(pools, Decimal('0'), Decimal('20'))
|
||||||
assert_equal(pools[1]['chainValue'], Decimal('20')) # Sapling
|
|
||||||
|
|
||||||
# Restart the nodes
|
# Restart the nodes
|
||||||
stop_nodes(self.nodes)
|
stop_nodes(self.nodes)
|
||||||
@@ -82,8 +116,7 @@ class WalletPersistenceTest (BitcoinTestFramework):
|
|||||||
|
|
||||||
# Verify size of shielded pools
|
# Verify size of shielded pools
|
||||||
pools = self.nodes[0].getblockchaininfo()['valuePools']
|
pools = self.nodes[0].getblockchaininfo()['valuePools']
|
||||||
assert_equal(pools[0]['chainValue'], Decimal('0')) # Sprout
|
assert_pool_values(pools, Decimal('0'), Decimal('20'))
|
||||||
assert_equal(pools[1]['chainValue'], Decimal('20')) # Sapling
|
|
||||||
|
|
||||||
# Node 0 sends some shielded funds to Node 1
|
# Node 0 sends some shielded funds to Node 1
|
||||||
dest_addr = self.nodes[1].z_getnewaddress('sapling')
|
dest_addr = self.nodes[1].z_getnewaddress('sapling')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2016 The Zcash developers
|
# Copyright (c) 2016 The Zcash developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
from test_framework.test_framework import BitcoinTestFramework
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
from test_framework.authproxy import JSONRPCException
|
from test_framework.authproxy import JSONRPCException
|
||||||
from test_framework.mininode import COIN
|
|
||||||
from test_framework.util import assert_equal, initialize_chain_clean, \
|
from test_framework.util import assert_equal, initialize_chain_clean, \
|
||||||
start_nodes, connect_nodes_bi, wait_and_assert_operationid_status
|
start_nodes, connect_nodes_bi, wait_and_assert_operationid_status
|
||||||
|
|
||||||
@@ -14,6 +13,20 @@ import sys
|
|||||||
import timeit
|
import timeit
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
# Upstream imported this from test_framework.mininode, which is still python2 and
|
||||||
|
# fails to even parse under python3 (0x100000000L literals). mininode is a p2p
|
||||||
|
# serialisation module this test does not otherwise use, so rather than drag a
|
||||||
|
# large unrelated port into test_framework/ the one constant needed is inlined.
|
||||||
|
# Same value as test_framework/mininode.py:52.
|
||||||
|
COIN = 100000000 # 1 DRGX in puposhis
|
||||||
|
|
||||||
|
# DragonX has no Sprout pool: getblockchaininfo/getblock only ever emit a
|
||||||
|
# "sapling" entry in valuePools (see rpc/blockchain.cpp), and z_getnewaddress
|
||||||
|
# only makes Sapling addresses. The shielded value this test moves therefore
|
||||||
|
# lands in the Sapling pool, so every check that upstream made against 'sprout'
|
||||||
|
# is made against 'sapling' here. The assertion itself is unchanged.
|
||||||
|
SHIELDED_POOL = 'sapling'
|
||||||
|
|
||||||
def check_value_pool(node, name, total):
|
def check_value_pool(node, name, total):
|
||||||
value_pools = node.getblockchaininfo()['valuePools']
|
value_pools = node.getblockchaininfo()['valuePools']
|
||||||
found = False
|
found = False
|
||||||
@@ -42,7 +55,7 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
|||||||
self.sync_all()
|
self.sync_all()
|
||||||
|
|
||||||
def run_test (self):
|
def run_test (self):
|
||||||
print "Mining blocks..."
|
print("Mining blocks...")
|
||||||
|
|
||||||
self.nodes[0].generate(4)
|
self.nodes[0].generate(4)
|
||||||
|
|
||||||
@@ -59,17 +72,17 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
|||||||
assert_equal(self.nodes[2].getbalance(), 0)
|
assert_equal(self.nodes[2].getbalance(), 0)
|
||||||
assert_equal(self.nodes[3].getbalance(), 0)
|
assert_equal(self.nodes[3].getbalance(), 0)
|
||||||
|
|
||||||
check_value_pool(self.nodes[0], 'sprout', 0)
|
check_value_pool(self.nodes[0], SHIELDED_POOL, 0)
|
||||||
check_value_pool(self.nodes[1], 'sprout', 0)
|
check_value_pool(self.nodes[1], SHIELDED_POOL, 0)
|
||||||
check_value_pool(self.nodes[2], 'sprout', 0)
|
check_value_pool(self.nodes[2], SHIELDED_POOL, 0)
|
||||||
check_value_pool(self.nodes[3], 'sprout', 0)
|
check_value_pool(self.nodes[3], SHIELDED_POOL, 0)
|
||||||
|
|
||||||
# Send will fail because we are enforcing the consensus rule that
|
# Send will fail because we are enforcing the consensus rule that
|
||||||
# coinbase utxos can only be sent to a zaddr.
|
# coinbase utxos can only be sent to a zaddr.
|
||||||
errorString = ""
|
errorString = ""
|
||||||
try:
|
try:
|
||||||
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1)
|
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 1)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Coinbase funds can only be sent to a zaddr" in errorString, True)
|
assert_equal("Coinbase funds can only be sent to a zaddr" in errorString, True)
|
||||||
|
|
||||||
@@ -95,11 +108,11 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
|||||||
# Test that the returned status object contains a params field with the operation's input parameters
|
# Test that the returned status object contains a params field with the operation's input parameters
|
||||||
assert_equal(error_result["method"], "z_sendmany")
|
assert_equal(error_result["method"], "z_sendmany")
|
||||||
params = error_result["params"]
|
params = error_result["params"]
|
||||||
assert_equal(params["fee"], Decimal('0.0001')) # default
|
assert_equal(Decimal(params["fee"]), Decimal('0.0001')) # default
|
||||||
assert_equal(params["minconf"], Decimal('1')) # default
|
assert_equal(Decimal(params["minconf"]), Decimal('1')) # default
|
||||||
assert_equal(params["fromaddress"], mytaddr)
|
assert_equal(params["fromaddress"], mytaddr)
|
||||||
assert_equal(params["amounts"][0]["address"], myzaddr)
|
assert_equal(params["amounts"][0]["address"], myzaddr)
|
||||||
assert_equal(params["amounts"][0]["amount"], Decimal('1.23456789'))
|
assert_equal(Decimal(params["amounts"][0]["amount"]), Decimal('1.23456789'))
|
||||||
|
|
||||||
# Add viewing key for myzaddr to Node 3
|
# Add viewing key for myzaddr to Node 3
|
||||||
myviewingkey = self.nodes[0].z_exportviewingkey(myzaddr)
|
myviewingkey = self.nodes[0].z_exportviewingkey(myzaddr)
|
||||||
@@ -169,14 +182,17 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
|||||||
assert_equal(Decimal(resp["private"]), Decimal('19.9999'))
|
assert_equal(Decimal(resp["private"]), Decimal('19.9999'))
|
||||||
assert_equal(Decimal(resp["total"]), Decimal('39.9999'))
|
assert_equal(Decimal(resp["total"]), Decimal('39.9999'))
|
||||||
|
|
||||||
# The Sprout value pool should reflect the send
|
# The shielded value pool should reflect the send
|
||||||
sproutvalue = shieldvalue
|
shieldedvalue = shieldvalue
|
||||||
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
|
check_value_pool(self.nodes[0], SHIELDED_POOL, shieldedvalue)
|
||||||
|
|
||||||
# A custom fee of 0 is okay. Here the node will send the note value back to itself.
|
# A custom fee of 0 is okay. Here the node will send the note value back to itself.
|
||||||
recipients = []
|
recipients = []
|
||||||
recipients.append({"address":myzaddr, "amount": Decimal('19.9999')})
|
recipients.append({"address":myzaddr, "amount": Decimal('19.9999')})
|
||||||
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, Decimal('0.0'))
|
# NB: the fee is passed as a JSON number, not a Decimal. authproxy serialises
|
||||||
|
# Decimal as a JSON *string* and z_sendmany reads the fee with params[3].get_real(),
|
||||||
|
# which only accepts VNUM -- see port notes. The value is unchanged.
|
||||||
|
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, 1, 0.0)
|
||||||
mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
|
mytxid = wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||||
self.sync_all()
|
self.sync_all()
|
||||||
self.nodes[1].generate(1)
|
self.nodes[1].generate(1)
|
||||||
@@ -186,8 +202,8 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
|||||||
assert_equal(Decimal(resp["private"]), Decimal('19.9999'))
|
assert_equal(Decimal(resp["private"]), Decimal('19.9999'))
|
||||||
assert_equal(Decimal(resp["total"]), Decimal('39.9999'))
|
assert_equal(Decimal(resp["total"]), Decimal('39.9999'))
|
||||||
|
|
||||||
# The Sprout value pool should be unchanged
|
# The shielded value pool should be unchanged
|
||||||
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
|
check_value_pool(self.nodes[0], SHIELDED_POOL, shieldedvalue)
|
||||||
|
|
||||||
# convert note to transparent funds
|
# convert note to transparent funds
|
||||||
unshieldvalue = Decimal('10.0')
|
unshieldvalue = Decimal('10.0')
|
||||||
@@ -206,12 +222,12 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
|||||||
self.sync_all()
|
self.sync_all()
|
||||||
|
|
||||||
# check balances
|
# check balances
|
||||||
sproutvalue -= unshieldvalue + Decimal('0.0001')
|
shieldedvalue -= unshieldvalue + Decimal('0.0001')
|
||||||
resp = self.nodes[0].z_gettotalbalance()
|
resp = self.nodes[0].z_gettotalbalance()
|
||||||
assert_equal(Decimal(resp["transparent"]), Decimal('30.0'))
|
assert_equal(Decimal(resp["transparent"]), Decimal('30.0'))
|
||||||
assert_equal(Decimal(resp["private"]), Decimal('9.9998'))
|
assert_equal(Decimal(resp["private"]), Decimal('9.9998'))
|
||||||
assert_equal(Decimal(resp["total"]), Decimal('39.9998'))
|
assert_equal(Decimal(resp["total"]), Decimal('39.9998'))
|
||||||
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
|
check_value_pool(self.nodes[0], SHIELDED_POOL, shieldedvalue)
|
||||||
|
|
||||||
# z_sendmany will return an error if there is transparent change output considered dust.
|
# z_sendmany will return an error if there is transparent change output considered dust.
|
||||||
# UTXO selection in z_sendmany sorts in ascending order, so smallest utxos are consumed first.
|
# UTXO selection in z_sendmany sorts in ascending order, so smallest utxos are consumed first.
|
||||||
@@ -226,7 +242,7 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
|||||||
errorString = ""
|
errorString = ""
|
||||||
try:
|
try:
|
||||||
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 99999)
|
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 99999)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Insufficient funds" in errorString, True)
|
assert_equal("Insufficient funds" in errorString, True)
|
||||||
|
|
||||||
@@ -241,7 +257,7 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
|||||||
# Send will fail because of insufficient funds unless sender uses coinbase utxos
|
# Send will fail because of insufficient funds unless sender uses coinbase utxos
|
||||||
try:
|
try:
|
||||||
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 21)
|
self.nodes[0].sendtoaddress(self.nodes[2].getnewaddress(), 21)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Insufficient funds, coinbase funds can only be spent after they have been sent to a zaddr" in errorString, True)
|
assert_equal("Insufficient funds, coinbase funds can only be spent after they have been sent to a zaddr" in errorString, True)
|
||||||
|
|
||||||
@@ -256,7 +272,7 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
|||||||
# Note that regtest chainparams does not require standard tx, so setting the amount to be
|
# Note that regtest chainparams does not require standard tx, so setting the amount to be
|
||||||
# less than the dust threshold, e.g. 0.00000001 will not result in mempool rejection.
|
# less than the dust threshold, e.g. 0.00000001 will not result in mempool rejection.
|
||||||
start_time = timeit.default_timer()
|
start_time = timeit.default_timer()
|
||||||
for i in xrange(0,num_t_recipients):
|
for i in range(0,num_t_recipients):
|
||||||
newtaddr = self.nodes[2].getnewaddress()
|
newtaddr = self.nodes[2].getnewaddress()
|
||||||
recipients.append({"address":newtaddr, "amount":amount_per_recipient})
|
recipients.append({"address":newtaddr, "amount":amount_per_recipient})
|
||||||
elapsed = timeit.default_timer() - start_time
|
elapsed = timeit.default_timer() - start_time
|
||||||
@@ -287,28 +303,30 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
|||||||
|
|
||||||
# check balance
|
# check balance
|
||||||
node2balance = amount_per_recipient * num_t_recipients
|
node2balance = amount_per_recipient * num_t_recipients
|
||||||
sproutvalue -= node2balance + Decimal('0.0001')
|
shieldedvalue -= node2balance + Decimal('0.0001')
|
||||||
assert_equal(self.nodes[2].getbalance(), node2balance)
|
assert_equal(self.nodes[2].getbalance(), node2balance)
|
||||||
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
|
check_value_pool(self.nodes[0], SHIELDED_POOL, shieldedvalue)
|
||||||
|
|
||||||
# Send will fail because fee is negative
|
# Send will fail because fee is negative
|
||||||
try:
|
try:
|
||||||
self.nodes[0].z_sendmany(myzaddr, recipients, 1, -1)
|
self.nodes[0].z_sendmany(myzaddr, recipients, 1, -1)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Amount out of range" in errorString, True)
|
assert_equal("Amount out of range" in errorString, True)
|
||||||
|
|
||||||
# Send will fail because fee is larger than MAX_MONEY
|
# Send will fail because fee is larger than MAX_MONEY
|
||||||
|
errorString = ""
|
||||||
try:
|
try:
|
||||||
self.nodes[0].z_sendmany(myzaddr, recipients, 1, Decimal('21000000.00000001'))
|
self.nodes[0].z_sendmany(myzaddr, recipients, 1, float(Decimal('21000000.00000001')))
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Amount out of range" in errorString, True)
|
assert_equal("Amount out of range" in errorString, True)
|
||||||
|
|
||||||
# Send will fail because fee is larger than sum of outputs
|
# Send will fail because fee is larger than sum of outputs
|
||||||
|
errorString = ""
|
||||||
try:
|
try:
|
||||||
self.nodes[0].z_sendmany(myzaddr, recipients, 1, (amount_per_recipient * num_t_recipients) + Decimal('0.00000001'))
|
self.nodes[0].z_sendmany(myzaddr, recipients, 1, float((amount_per_recipient * num_t_recipients) + Decimal('0.00000001')))
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("is greater than the sum of outputs" in errorString, True)
|
assert_equal("is greater than the sum of outputs" in errorString, True)
|
||||||
|
|
||||||
@@ -334,10 +352,10 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
|||||||
send_amount = num_recipients * amount_per_recipient
|
send_amount = num_recipients * amount_per_recipient
|
||||||
custom_fee = Decimal('0.00012345')
|
custom_fee = Decimal('0.00012345')
|
||||||
zbalance = self.nodes[0].z_getbalance(myzaddr)
|
zbalance = self.nodes[0].z_getbalance(myzaddr)
|
||||||
for i in xrange(0,num_recipients):
|
for i in range(0,num_recipients):
|
||||||
newzaddr = self.nodes[2].z_getnewaddress()
|
newzaddr = self.nodes[2].z_getnewaddress()
|
||||||
recipients.append({"address":newzaddr, "amount":amount_per_recipient})
|
recipients.append({"address":newzaddr, "amount":amount_per_recipient})
|
||||||
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, minconf, custom_fee)
|
myopid = self.nodes[0].z_sendmany(myzaddr, recipients, minconf, float(custom_fee))
|
||||||
wait_and_assert_operationid_status(self.nodes[0], myopid)
|
wait_and_assert_operationid_status(self.nodes[0], myopid)
|
||||||
self.sync_all()
|
self.sync_all()
|
||||||
self.nodes[1].generate(1)
|
self.nodes[1].generate(1)
|
||||||
@@ -353,8 +371,8 @@ class WalletProtectCoinbaseTest (BitcoinTestFramework):
|
|||||||
|
|
||||||
resp = self.nodes[0].z_getbalance(myzaddr)
|
resp = self.nodes[0].z_getbalance(myzaddr)
|
||||||
assert_equal(Decimal(resp), zbalance - custom_fee - send_amount)
|
assert_equal(Decimal(resp), zbalance - custom_fee - send_amount)
|
||||||
sproutvalue -= custom_fee
|
shieldedvalue -= custom_fee
|
||||||
check_value_pool(self.nodes[0], 'sprout', sproutvalue)
|
check_value_pool(self.nodes[0], SHIELDED_POOL, shieldedvalue)
|
||||||
|
|
||||||
notes = self.nodes[0].z_listunspent(1, 99999, False, [myzaddr])
|
notes = self.nodes[0].z_listunspent(1, 99999, False, [myzaddr])
|
||||||
sum_of_notes = sum([note["amount"] for note in notes])
|
sum_of_notes = sum([note["amount"] for note in notes])
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2018 The Zcash developers
|
# Copyright (c) 2018 The Zcash developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
@@ -8,21 +8,99 @@ from test_framework.test_framework import BitcoinTestFramework
|
|||||||
from test_framework.authproxy import JSONRPCException
|
from test_framework.authproxy import JSONRPCException
|
||||||
from test_framework.util import (
|
from test_framework.util import (
|
||||||
assert_equal,
|
assert_equal,
|
||||||
start_nodes,
|
initialize_chain_clean,
|
||||||
|
p2p_port,
|
||||||
|
set_node_times,
|
||||||
|
start_node,
|
||||||
|
sync_blocks,
|
||||||
wait_and_assert_operationid_status,
|
wait_and_assert_operationid_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
# Test wallet behaviour with Sapling addresses
|
# Test wallet behaviour with Sapling addresses
|
||||||
class WalletSaplingTest(BitcoinTestFramework):
|
class WalletSaplingTest(BitcoinTestFramework):
|
||||||
|
|
||||||
|
def setup_chain(self):
|
||||||
|
# The shared initialize_chain() cache builder in test_framework/util.py has not been
|
||||||
|
# repaired for DragonX (it spawns the cache nodes by bare name off PATH, drives the
|
||||||
|
# CLI without -regtest so it dials the assetchain RPC port, and deletes debug.log
|
||||||
|
# from the non-net-specific datadir). Build the same starting state here instead --
|
||||||
|
# see _generate_starting_chain() -- so this test does not depend on it.
|
||||||
|
print("Initializing test directory " + self.options.tmpdir)
|
||||||
|
initialize_chain_clean(self.options.tmpdir, 4)
|
||||||
|
|
||||||
|
# !!! test_framework/util.py:start_node() hardcodes "-connect=0" into every regtest
|
||||||
|
# node's argv. Modern Bitcoin Core special-cases that value to mean "make no automatic
|
||||||
|
# connections", but THIS codebase does not (net.cpp ThreadOpenConnections just iterates
|
||||||
|
# mapMultiArgs["-connect"]), so "0" is dialled as a hostname: it resolves to 0.0.0.0,
|
||||||
|
# which on Linux connects to localhost on Params().GetDefaultPort() -- 21768, the live
|
||||||
|
# DRAGONX p2p port. Observed directly: a regtest node started by the unmodified
|
||||||
|
# framework peered with the production dragonxd on this host and with seven public
|
||||||
|
# mainnet nodes (heights ~3.25M) and began ingesting mainnet headers. "-connect" also
|
||||||
|
# soft-sets "-listen=0", so the framework's own connect_nodes_bi() can never establish
|
||||||
|
# the local links a multi-node test needs.
|
||||||
|
#
|
||||||
|
# Both problems are in the shared framework, which this port is not allowed to touch, so
|
||||||
|
# they are worked around per-node here: the daemon is launched through a tiny wrapper
|
||||||
|
# that strips the "-connect=0" argument, and the real topology/listening flags are passed
|
||||||
|
# as extra_args (which start_node appends after its own).
|
||||||
|
def _daemon_wrapper(self):
|
||||||
|
srcdir = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "..", "..", "src", "dragonxd")
|
||||||
|
path = os.path.join(self.options.tmpdir, "dragonxd-no-connect0")
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write("#!/usr/bin/env bash\n")
|
||||||
|
f.write("args=()\n")
|
||||||
|
f.write('for a in "$@"; do\n')
|
||||||
|
f.write(' if [ "$a" = "-connect=0" ]; then continue; fi\n')
|
||||||
|
f.write(' args+=("$a")\n')
|
||||||
|
f.write("done\n")
|
||||||
|
f.write('exec %s "${args[@]}"\n' % os.path.realpath(srcdir))
|
||||||
|
os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR)
|
||||||
|
return path
|
||||||
|
|
||||||
def setup_nodes(self):
|
def setup_nodes(self):
|
||||||
return start_nodes(4, self.options.tmpdir, [[
|
binary = self._daemon_wrapper()
|
||||||
#'-nuparams=5ba81b19:201', # Overwinter
|
nodes = []
|
||||||
#'-nuparams=76b809bb:203', # Sapling
|
for i in range(4):
|
||||||
#'-experimentalfeatures', '-zmergetoaddress',
|
extra_args = [
|
||||||
]] * 4)
|
#'-nuparams=5ba81b19:201', # Overwinter
|
||||||
|
#'-nuparams=76b809bb:203', # Sapling
|
||||||
|
#'-experimentalfeatures', '-zmergetoaddress',
|
||||||
|
# Listen on this test's PID-keyed port so the nodes can actually peer with
|
||||||
|
# each other, and only ever dial each other -- never the public network.
|
||||||
|
'-listen=1',
|
||||||
|
'-bind=127.0.0.1',
|
||||||
|
'-port=%d' % p2p_port(i),
|
||||||
|
] + ['-connect=127.0.0.1:%d' % p2p_port(j) for j in range(4) if j != i]
|
||||||
|
nodes.append(start_node(i, self.options.tmpdir, extra_args, binary=binary))
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
def setup_network(self, split=False):
|
||||||
|
super(WalletSaplingTest, self).setup_network(split)
|
||||||
|
self._generate_starting_chain()
|
||||||
|
|
||||||
|
def _generate_starting_chain(self):
|
||||||
|
# Equivalent of test_framework.util.initialize_chain(): a 200-block chain where each
|
||||||
|
# of the 4 nodes mined 25 blocks twice, so every node holds 25 mature and 25 immature
|
||||||
|
# coinbases. Block timestamps are 10 minutes apart starting 1 Jan 2014, as there.
|
||||||
|
block_time = 1388534400
|
||||||
|
for _round in range(2):
|
||||||
|
for peer in range(4):
|
||||||
|
for _j in range(25):
|
||||||
|
set_node_times(self.nodes, block_time)
|
||||||
|
self.nodes[peer].generate(1)
|
||||||
|
block_time += 10 * 75
|
||||||
|
# Must sync before next peer starts generating blocks
|
||||||
|
sync_blocks(self.nodes)
|
||||||
|
# Drop back to wall-clock time: initialize_chain() stops the cache nodes and the test
|
||||||
|
# then runs against freshly started nodes that have no mocktime set.
|
||||||
|
set_node_times(self.nodes, 0)
|
||||||
|
self.sync_all()
|
||||||
|
|
||||||
def run_test(self):
|
def run_test(self):
|
||||||
# Sanity-check the test harness
|
# Sanity-check the test harness
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2017 The Zcash developers
|
# Copyright (c) 2017 The Zcash developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
@@ -22,8 +22,24 @@ class WalletShieldCoinbaseTest (BitcoinTestFramework):
|
|||||||
initialize_chain_clean(self.options.tmpdir, 4)
|
initialize_chain_clean(self.options.tmpdir, 4)
|
||||||
|
|
||||||
def setup_network(self, split=False):
|
def setup_network(self, split=False):
|
||||||
args = ['-regtestprotectcoinbase', '-debug=zrpcunsafe']
|
# DragonX-specific environment flags. None of these change what the test
|
||||||
args2 = ['-regtestprotectcoinbase', '-debug=zrpcunsafe', "-mempooltxinputlimit=7"]
|
# asserts; without them the test cannot run at all on this daemon:
|
||||||
|
# -listen=1/-bind=127.0.0.1: the framework's start_node() always passes
|
||||||
|
# -connect=0, and AppInit2 soft-sets -listen=0 whenever -connect is
|
||||||
|
# present, so the nodes never listen and connect_nodes_bi() silently
|
||||||
|
# builds an empty topology (sync_all() then spins forever). An explicit
|
||||||
|
# -listen=1 beats the SoftSetBoolArg.
|
||||||
|
# -dnsseed=0: chainparams_commandline() keeps DRAGONX's DNS seeds and
|
||||||
|
# overwrites pchMessageStart with the DRAGONX chain magic on *every*
|
||||||
|
# network including regtest, so a regtest node otherwise dials and
|
||||||
|
# handshakes with live mainnet peers (observed: 8 mainnet peers,
|
||||||
|
# blocks=3254237, feeding mainnet headers into the regtest node).
|
||||||
|
# -autoshield=0: DragonX auto-shields matured coinbase every 25 blocks
|
||||||
|
# by default, which would race the manual z_shieldcoinbase calls under
|
||||||
|
# test and move the balances this test checks.
|
||||||
|
isolate = ['-listen=1', '-bind=127.0.0.1', '-dnsseed=0', '-autoshield=0']
|
||||||
|
args = ['-regtestprotectcoinbase', '-debug=zrpcunsafe'] + isolate
|
||||||
|
args2 = ['-regtestprotectcoinbase', '-debug=zrpcunsafe', "-mempooltxinputlimit=7"] + isolate
|
||||||
if self.addr_type != 'sprout':
|
if self.addr_type != 'sprout':
|
||||||
nu = [
|
nu = [
|
||||||
'-nuparams=5ba81b19:0', # Overwinter
|
'-nuparams=5ba81b19:0', # Overwinter
|
||||||
@@ -42,7 +58,7 @@ class WalletShieldCoinbaseTest (BitcoinTestFramework):
|
|||||||
self.sync_all()
|
self.sync_all()
|
||||||
|
|
||||||
def run_test (self):
|
def run_test (self):
|
||||||
print "Mining blocks..."
|
print("Mining blocks...")
|
||||||
|
|
||||||
self.nodes[0].generate(1)
|
self.nodes[0].generate(1)
|
||||||
self.sync_all()
|
self.sync_all()
|
||||||
@@ -73,42 +89,42 @@ class WalletShieldCoinbaseTest (BitcoinTestFramework):
|
|||||||
self.nodes[2].importaddress(mytaddr)
|
self.nodes[2].importaddress(mytaddr)
|
||||||
try:
|
try:
|
||||||
self.nodes[2].z_shieldcoinbase(mytaddr, myzaddr)
|
self.nodes[2].z_shieldcoinbase(mytaddr, myzaddr)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Could not find any coinbase funds to shield" in errorString, True)
|
assert_equal("Could not find any coinbase funds to shield" in errorString, True)
|
||||||
|
|
||||||
# Shielding will fail because fee is negative
|
# Shielding will fail because fee is negative
|
||||||
try:
|
try:
|
||||||
self.nodes[0].z_shieldcoinbase("*", myzaddr, -1)
|
self.nodes[0].z_shieldcoinbase("*", myzaddr, -1)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Amount out of range" in errorString, True)
|
assert_equal("Amount out of range" in errorString, True)
|
||||||
|
|
||||||
# Shielding will fail because fee is larger than MAX_MONEY
|
# Shielding will fail because fee is larger than MAX_MONEY
|
||||||
try:
|
try:
|
||||||
self.nodes[0].z_shieldcoinbase("*", myzaddr, Decimal('21000000.00000001'))
|
self.nodes[0].z_shieldcoinbase("*", myzaddr, Decimal('21000000.00000001'))
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Amount out of range" in errorString, True)
|
assert_equal("Amount out of range" in errorString, True)
|
||||||
|
|
||||||
# Shielding will fail because fee is larger than sum of utxos
|
# Shielding will fail because fee is larger than sum of utxos
|
||||||
try:
|
try:
|
||||||
self.nodes[0].z_shieldcoinbase("*", myzaddr, 999)
|
self.nodes[0].z_shieldcoinbase("*", myzaddr, 999)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Insufficient coinbase funds" in errorString, True)
|
assert_equal("Insufficient coinbase funds" in errorString, True)
|
||||||
|
|
||||||
# Shielding will fail because limit parameter must be at least 0
|
# Shielding will fail because limit parameter must be at least 0
|
||||||
try:
|
try:
|
||||||
self.nodes[0].z_shieldcoinbase("*", myzaddr, Decimal('0.001'), -1)
|
self.nodes[0].z_shieldcoinbase("*", myzaddr, Decimal('0.001'), -1)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("Limit on maximum number of utxos cannot be negative" in errorString, True)
|
assert_equal("Limit on maximum number of utxos cannot be negative" in errorString, True)
|
||||||
|
|
||||||
# Shielding will fail because limit parameter is absurdly large
|
# Shielding will fail because limit parameter is absurdly large
|
||||||
try:
|
try:
|
||||||
self.nodes[0].z_shieldcoinbase("*", myzaddr, Decimal('0.001'), 99999999999999)
|
self.nodes[0].z_shieldcoinbase("*", myzaddr, Decimal('0.001'), 99999999999999)
|
||||||
except JSONRPCException,e:
|
except JSONRPCException as e:
|
||||||
errorString = e.error['message']
|
errorString = e.error['message']
|
||||||
assert_equal("JSON integer out of range" in errorString, True)
|
assert_equal("JSON integer out of range" in errorString, True)
|
||||||
|
|
||||||
@@ -214,3 +230,13 @@ class WalletShieldCoinbaseTest (BitcoinTestFramework):
|
|||||||
sync_mempools(self.nodes[:2])
|
sync_mempools(self.nodes[:2])
|
||||||
self.nodes[1].generate(1)
|
self.nodes[1].generate(1)
|
||||||
self.sync_all()
|
self.sync_all()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Upstream (Zcash/Hush) ran this test twice: once with Sprout zaddrs and once
|
||||||
|
# with Sapling. DragonX has no Sprout support at all -- z_getnewaddress only
|
||||||
|
# accepts "sapling" or "amnesia" (src/wallet/rpcwallet.cpp z_getnewaddress),
|
||||||
|
# so WalletShieldCoinbaseTest('sprout') cannot even allocate its target
|
||||||
|
# address. The sprout-only branches inside run_test() are kept intact for
|
||||||
|
# reference but only the sapling variant is executed.
|
||||||
|
print("Running for sapling...")
|
||||||
|
WalletShieldCoinbaseTest('sapling').main()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2016 The Zcash developers
|
# Copyright (c) 2016 The Zcash developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
@@ -20,7 +20,16 @@ class WalletTreeStateTest (BitcoinTestFramework):
|
|||||||
|
|
||||||
# Start nodes with -regtestprotectcoinbase to set fCoinbaseMustBeProtected to true.
|
# Start nodes with -regtestprotectcoinbase to set fCoinbaseMustBeProtected to true.
|
||||||
def setup_network(self, split=False):
|
def setup_network(self, split=False):
|
||||||
self.nodes = start_nodes(3, self.options.tmpdir, extra_args=[['-regtestprotectcoinbase','-debug=zrpc']] * 3 )
|
# -listen=1 and -dns=0 are DragonX-specific harness requirements, not part of the
|
||||||
|
# original test: start_node() passes -connect=0, which trips the "-connect set ->
|
||||||
|
# setting -listen=0" parameter interaction, so without -listen=1 the three nodes
|
||||||
|
# cannot open the p2p links connect_nodes_bi() asks for (node1/node2 stay at height
|
||||||
|
# 0 forever and sync_all() can never converge). -dns=0 blocks the unconditional
|
||||||
|
# node1..node10.dragonx.is -addnode injection in hush_args(), which otherwise dials
|
||||||
|
# the real DragonX seed nodes from regtest and floods these nodes with mainnet headers.
|
||||||
|
self.nodes = start_nodes(3, self.options.tmpdir,
|
||||||
|
extra_args=[['-regtestprotectcoinbase','-debug=zrpc',
|
||||||
|
'-listen=1','-dns=0']] * 3 )
|
||||||
connect_nodes_bi(self.nodes,0,1)
|
connect_nodes_bi(self.nodes,0,1)
|
||||||
connect_nodes_bi(self.nodes,1,2)
|
connect_nodes_bi(self.nodes,1,2)
|
||||||
connect_nodes_bi(self.nodes,0,2)
|
connect_nodes_bi(self.nodes,0,2)
|
||||||
@@ -28,7 +37,7 @@ class WalletTreeStateTest (BitcoinTestFramework):
|
|||||||
self.sync_all()
|
self.sync_all()
|
||||||
|
|
||||||
def run_test (self):
|
def run_test (self):
|
||||||
print "Mining blocks..."
|
print("Mining blocks...")
|
||||||
|
|
||||||
self.nodes[0].generate(100)
|
self.nodes[0].generate(100)
|
||||||
self.sync_all()
|
self.sync_all()
|
||||||
@@ -79,7 +88,7 @@ class WalletTreeStateTest (BitcoinTestFramework):
|
|||||||
myopid = self.nodes[0].z_sendmany(myzaddr, recipients)
|
myopid = self.nodes[0].z_sendmany(myzaddr, recipients)
|
||||||
|
|
||||||
# Wait for Tx 2 to begin executing...
|
# Wait for Tx 2 to begin executing...
|
||||||
for x in xrange(1, 60):
|
for x in range(1, 60):
|
||||||
results = self.nodes[0].z_getoperationstatus([myopid])
|
results = self.nodes[0].z_getoperationstatus([myopid])
|
||||||
status = results[0]["status"]
|
status = results[0]["status"]
|
||||||
if status == "executing":
|
if status == "executing":
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python2
|
#!/usr/bin/env python3
|
||||||
# Copyright (c) 2016-2024 The Hush developers
|
# Copyright (c) 2016-2024 The Hush developers
|
||||||
# Copyright (c) 2014 The Bitcoin Core developers
|
# Copyright (c) 2014 The Bitcoin Core developers
|
||||||
# Distributed under the GPLv3 software license, see the accompanying
|
# Distributed under the GPLv3 software license, see the accompanying
|
||||||
@@ -37,8 +37,8 @@ and confirm again balances are correct.
|
|||||||
from test_framework.test_framework import BitcoinTestFramework
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
from test_framework.authproxy import JSONRPCException
|
from test_framework.authproxy import JSONRPCException
|
||||||
from test_framework.util import assert_equal, initialize_chain_clean, \
|
from test_framework.util import assert_equal, initialize_chain_clean, \
|
||||||
start_nodes, start_node, connect_nodes, stop_node, \
|
start_nodes, start_node, connect_nodes, \
|
||||||
sync_blocks, sync_mempools
|
sync_blocks, sync_mempools, bitcoind_processes
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@@ -48,6 +48,46 @@ import logging
|
|||||||
|
|
||||||
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
|
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
|
||||||
|
|
||||||
|
# Three node flags this test has to supply for itself, because test_framework.util.start_node()
|
||||||
|
# cannot give it a usable isolated regtest network on DragonX:
|
||||||
|
#
|
||||||
|
# -listen=1: start_node() passes "-connect=0" to every node. In DragonX (as in Bitcoin) the
|
||||||
|
# presence of a -connect argument SoftSetBoolArg()s -listen to false, so no test node ever
|
||||||
|
# binds its p2p port and connect_nodes() cannot build the loopback topology this test needs.
|
||||||
|
# Verified on a live run: only the RPC ports were listening and getpeerinfo showed zero
|
||||||
|
# 127.0.0.1 peers on all four nodes. SoftSetBoolArg does not override an explicit value.
|
||||||
|
#
|
||||||
|
# -dns=0: hush_args() unconditionally appends node1..node10.dragonx.is to -addnode whenever the
|
||||||
|
# chain name is DRAGONX, regardless of network, and -connect=0 does not suppress it. On a live
|
||||||
|
# run every "regtest" node ended up with 7-8 established connections to production mainnet
|
||||||
|
# nodes on port 21768, was flooded with mainnet headers ("AcceptBlockHeader: hashPrevBlock ...
|
||||||
|
# not found"), and one of them aborted on CheckBlockIndex(). -dns=0 stops the hostnames from
|
||||||
|
# resolving while leaving the literal 127.0.0.1:PORT addnodes connect_nodes() uses intact.
|
||||||
|
#
|
||||||
|
# -allowlist=127.0.0.1: "-connect=0" does not mean "no connections". The daemon resolves the
|
||||||
|
# literal "0" to 0.0.0.0 and dials it on the chain's default p2p port, i.e. 127.0.0.1:21768 --
|
||||||
|
# which on a machine that also runs a real node is the PRODUCTION daemon (observed:
|
||||||
|
# ESTAB 127.0.0.1:41282 -> 127.0.0.1:21768 from every test node, peer subver /DragonX:1.0.3/,
|
||||||
|
# startingheight 3254254). That peer is outbound, so it is the node's only preferred-download
|
||||||
|
# peer; main.cpp:8398 then computes fFetch=false for every inbound peer, and node3 -- which in
|
||||||
|
# this test's topology is dialed by everyone and dials no one -- never downloads an announced
|
||||||
|
# block, so sync_blocks() hangs forever. Allowlisting loopback makes inbound test peers
|
||||||
|
# preferred-download too (main.cpp:381), which restores block propagation.
|
||||||
|
LISTEN = "-listen=1"
|
||||||
|
NODNS = "-dns=0"
|
||||||
|
ALLOWLIST = "-allowlist=127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
def stop_node_and_reap(node, i):
|
||||||
|
# Equivalent to test_framework.util.stop_node(), which cannot be called: it does
|
||||||
|
# print("Stopping node " + i) with the int index that every caller passes, which raises
|
||||||
|
# TypeError. Reimplemented here rather than editing shared framework code other tests use.
|
||||||
|
print("Stopping node %d" % i)
|
||||||
|
node.stop()
|
||||||
|
bitcoind_processes[i].wait()
|
||||||
|
del bitcoind_processes[i]
|
||||||
|
|
||||||
|
|
||||||
class WalletBackupTest(BitcoinTestFramework):
|
class WalletBackupTest(BitcoinTestFramework):
|
||||||
|
|
||||||
def setup_chain(self):
|
def setup_chain(self):
|
||||||
@@ -62,7 +102,10 @@ class WalletBackupTest(BitcoinTestFramework):
|
|||||||
ed2 = "-exportdir=" + self.options.tmpdir + "/node2"
|
ed2 = "-exportdir=" + self.options.tmpdir + "/node2"
|
||||||
|
|
||||||
# nodes 1, 2,3 are spenders, let's give them a keypool=100
|
# nodes 1, 2,3 are spenders, let's give them a keypool=100
|
||||||
extra_args = [["-keypool=100", ed0], ["-keypool=100", ed1], ["-keypool=100", ed2], []]
|
extra_args = [["-keypool=100", ed0, LISTEN, NODNS, ALLOWLIST],
|
||||||
|
["-keypool=100", ed1, LISTEN, NODNS, ALLOWLIST],
|
||||||
|
["-keypool=100", ed2, LISTEN, NODNS, ALLOWLIST],
|
||||||
|
[LISTEN, NODNS, ALLOWLIST]]
|
||||||
self.nodes = start_nodes(4, self.options.tmpdir, extra_args)
|
self.nodes = start_nodes(4, self.options.tmpdir, extra_args)
|
||||||
connect_nodes(self.nodes[0], 3)
|
connect_nodes(self.nodes[0], 3)
|
||||||
connect_nodes(self.nodes[1], 3)
|
connect_nodes(self.nodes[1], 3)
|
||||||
@@ -95,18 +138,18 @@ class WalletBackupTest(BitcoinTestFramework):
|
|||||||
|
|
||||||
# As above, this mirrors the original bash test.
|
# As above, this mirrors the original bash test.
|
||||||
def start_three(self):
|
def start_three(self):
|
||||||
self.nodes[0] = start_node(0, self.options.tmpdir)
|
self.nodes[0] = start_node(0, self.options.tmpdir, [LISTEN, NODNS, ALLOWLIST])
|
||||||
self.nodes[1] = start_node(1, self.options.tmpdir)
|
self.nodes[1] = start_node(1, self.options.tmpdir, [LISTEN, NODNS, ALLOWLIST])
|
||||||
self.nodes[2] = start_node(2, self.options.tmpdir)
|
self.nodes[2] = start_node(2, self.options.tmpdir, [LISTEN, NODNS, ALLOWLIST])
|
||||||
connect_nodes(self.nodes[0], 3)
|
connect_nodes(self.nodes[0], 3)
|
||||||
connect_nodes(self.nodes[1], 3)
|
connect_nodes(self.nodes[1], 3)
|
||||||
connect_nodes(self.nodes[2], 3)
|
connect_nodes(self.nodes[2], 3)
|
||||||
connect_nodes(self.nodes[2], 0)
|
connect_nodes(self.nodes[2], 0)
|
||||||
|
|
||||||
def stop_three(self):
|
def stop_three(self):
|
||||||
stop_node(self.nodes[0], 0)
|
stop_node_and_reap(self.nodes[0], 0)
|
||||||
stop_node(self.nodes[1], 1)
|
stop_node_and_reap(self.nodes[1], 1)
|
||||||
stop_node(self.nodes[2], 2)
|
stop_node_and_reap(self.nodes[2], 2)
|
||||||
|
|
||||||
def erase_three(self):
|
def erase_three(self):
|
||||||
os.remove(self.options.tmpdir + "/node0/regtest/wallet.dat")
|
os.remove(self.options.tmpdir + "/node0/regtest/wallet.dat")
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ hush_gtest_SOURCES = \
|
|||||||
gtest/utils.cpp \
|
gtest/utils.cpp \
|
||||||
gtest/test_randomx_preverify.cpp \
|
gtest/test_randomx_preverify.cpp \
|
||||||
gtest/test_hdtransparent.cpp \
|
gtest/test_hdtransparent.cpp \
|
||||||
gtest/test_mnemonic_compat.cpp
|
gtest/test_mnemonic_compat.cpp \
|
||||||
|
gtest/test_stratum_jobid.cpp
|
||||||
|
|
||||||
hush_gtest_CPPFLAGS = $(AM_CPPFLAGS) -DMULTICORE -fopenmp -DBINARY_OUTPUT -DCURVE_ALT_BN128 -DSTATIC $(BITCOIN_INCLUDES)
|
hush_gtest_CPPFLAGS = $(AM_CPPFLAGS) -DMULTICORE -fopenmp -DBINARY_OUTPUT -DCURVE_ALT_BN128 -DSTATIC $(BITCOIN_INCLUDES)
|
||||||
hush_gtest_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
|
hush_gtest_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
|
||||||
|
|||||||
@@ -257,14 +257,18 @@ UniValue CallRPC(const std::string& strMethod, const UniValue& params)
|
|||||||
event_base_dispatch(base.get());
|
event_base_dispatch(base.get());
|
||||||
|
|
||||||
if (response.status == 0) {
|
if (response.status == 0) {
|
||||||
|
// Report the port we ACTUALLY dialled. ASSETCHAINS_RPCPORT is a separate global that is
|
||||||
|
// initialised to the mainnet default at the top of this file and never assigned here, so
|
||||||
|
// using it made every failure claim port 21769 no matter what -rpcport was given -- which
|
||||||
|
// reads as "your -rpcport was ignored" and sends you chasing a config bug that isn't there.
|
||||||
throw CConnectionFailed(strprintf("couldn't connect to server at port %d : %s (code %d)\n(make sure server is running and you are connecting to the correct RPC port)",
|
throw CConnectionFailed(strprintf("couldn't connect to server at port %d : %s (code %d)\n(make sure server is running and you are connecting to the correct RPC port)",
|
||||||
ASSETCHAINS_RPCPORT, http_errorstring(response.error), response.error));
|
port, http_errorstring(response.error), response.error));
|
||||||
} else if (response.status == HTTP_UNAUTHORIZED) {
|
} else if (response.status == HTTP_UNAUTHORIZED) {
|
||||||
throw std::runtime_error("incorrect rpcuser or rpcpassword (authorization failed)");
|
throw std::runtime_error("incorrect rpcuser or rpcpassword (authorization failed)");
|
||||||
} else if (response.status >= 400 && response.status != HTTP_BAD_REQUEST && response.status != HTTP_NOT_FOUND && response.status != HTTP_INTERNAL_SERVER_ERROR) {
|
} else if (response.status >= 400 && response.status != HTTP_BAD_REQUEST && response.status != HTTP_NOT_FOUND && response.status != HTTP_INTERNAL_SERVER_ERROR) {
|
||||||
throw std::runtime_error(strprintf("server returned HTTP error %d", response.status));
|
throw std::runtime_error(strprintf("server returned HTTP error %d", response.status));
|
||||||
} else if (response.body.empty()) {
|
} else if (response.body.empty()) {
|
||||||
throw std::runtime_error(strprintf("no response from server at port %d", ASSETCHAINS_RPCPORT ));
|
throw std::runtime_error(strprintf("no response from server at port %d", port));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse reply
|
// Parse reply
|
||||||
|
|||||||
29
src/chain.h
29
src/chain.h
@@ -35,7 +35,7 @@ extern bool fZindex;
|
|||||||
// These version thresholds control whether nSproutValue/nSaplingValue are
|
// These version thresholds control whether nSproutValue/nSaplingValue are
|
||||||
// serialized in the block index. They must be <= CLIENT_VERSION or the
|
// serialized in the block index. They must be <= CLIENT_VERSION or the
|
||||||
// values will never be persisted, causing nChainSaplingValue to reset
|
// values will never be persisted, causing nChainSaplingValue to reset
|
||||||
// to 0 after node restart. DragonX CLIENT_VERSION is 1010050 (v1.1.0.50).
|
// to 0 after node restart. DragonX CLIENT_VERSION is 1030050 (v1.3.0.50).
|
||||||
static const int SPROUT_VALUE_VERSION = 1000000;
|
static const int SPROUT_VALUE_VERSION = 1000000;
|
||||||
static const int SAPLING_VALUE_VERSION = 1000000;
|
static const int SAPLING_VALUE_VERSION = 1000000;
|
||||||
// Block-index records written at >= this version store nSaplingValue as a boost::optional
|
// Block-index records written at >= this version store nSaplingValue as a boost::optional
|
||||||
@@ -113,10 +113,9 @@ enum BlockStatus: uint32_t {
|
|||||||
//! Scripts & signatures ok. Implies all parents are also at least SCRIPTS.
|
//! Scripts & signatures ok. Implies all parents are also at least SCRIPTS.
|
||||||
BLOCK_VALID_SCRIPTS = 5,
|
BLOCK_VALID_SCRIPTS = 5,
|
||||||
|
|
||||||
// flag to check if contextual check block has passed in Accept block, if it has not check at connect block.
|
|
||||||
BLOCK_VALID_CONTEXT = 6,
|
|
||||||
|
|
||||||
//! All validity bits.
|
//! All validity bits.
|
||||||
|
//! NOTE: the levels above are sequential VALUES occupying this 3-bit field, not independent
|
||||||
|
//! bits, so any flag stored in nStatus must live entirely outside this mask.
|
||||||
BLOCK_VALID_MASK = BLOCK_VALID_HEADER | BLOCK_VALID_TREE | BLOCK_VALID_TRANSACTIONS |
|
BLOCK_VALID_MASK = BLOCK_VALID_HEADER | BLOCK_VALID_TREE | BLOCK_VALID_TRANSACTIONS |
|
||||||
BLOCK_VALID_CHAIN | BLOCK_VALID_SCRIPTS,
|
BLOCK_VALID_CHAIN | BLOCK_VALID_SCRIPTS,
|
||||||
|
|
||||||
@@ -129,9 +128,29 @@ enum BlockStatus: uint32_t {
|
|||||||
BLOCK_FAILED_MASK = BLOCK_FAILED_VALID | BLOCK_FAILED_CHILD,
|
BLOCK_FAILED_MASK = BLOCK_FAILED_VALID | BLOCK_FAILED_CHILD,
|
||||||
|
|
||||||
BLOCK_ACTIVATES_UPGRADE = 128, //! block activates a network upgrade
|
BLOCK_ACTIVATES_UPGRADE = 128, //! block activates a network upgrade
|
||||||
BLOCK_IN_TMPFILE = 256
|
BLOCK_IN_TMPFILE = 256,
|
||||||
|
|
||||||
|
//! ContextualCheckBlock already passed in AcceptBlock, so ConnectBlock may skip re-running it.
|
||||||
|
//! Was 6 until v1.3.0, which put it INSIDE BLOCK_VALID_MASK (1|2|3|4|5 == 7): `nStatus |=
|
||||||
|
//! BLOCK_VALID_CONTEXT` then overwrote the validity level rather than setting a flag, so a
|
||||||
|
//! block that was only written to disk read back as BLOCK_VALID_SCRIPTS and every later
|
||||||
|
//! RaiseValidity() silently no-opped. Detected by CheckBlockIndex's "CHAIN valid implies all
|
||||||
|
//! parents are CHAIN valid" assert, which aborts any node doing out-of-order block download
|
||||||
|
//! (regtest only, where fDefaultConsistencyChecks is true). Validity was only ever inflated,
|
||||||
|
//! never deflated, so ConnectBlock's full validation was never skipped -- see git history.
|
||||||
|
//! Legacy block indexes still carry the polluted low bits; they resolve on reindex, and until
|
||||||
|
//! then simply re-run the contextual check they used to skip.
|
||||||
|
BLOCK_VALID_CONTEXT = 512
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//! The validity level is a small integer packed into BLOCK_VALID_MASK, so every other nStatus flag
|
||||||
|
//! must be disjoint from it. Enforced here so this class of bug cannot be reintroduced silently.
|
||||||
|
static_assert((BLOCK_VALID_CONTEXT & BLOCK_VALID_MASK) == 0, "BLOCK_VALID_CONTEXT overlaps the validity-level field");
|
||||||
|
static_assert((BLOCK_HAVE_MASK & BLOCK_VALID_MASK) == 0, "BLOCK_HAVE_MASK overlaps the validity-level field");
|
||||||
|
static_assert((BLOCK_FAILED_MASK & BLOCK_VALID_MASK) == 0, "BLOCK_FAILED_MASK overlaps the validity-level field");
|
||||||
|
static_assert((BLOCK_ACTIVATES_UPGRADE & BLOCK_VALID_MASK) == 0, "BLOCK_ACTIVATES_UPGRADE overlaps the validity-level field");
|
||||||
|
static_assert((BLOCK_IN_TMPFILE & BLOCK_VALID_MASK) == 0, "BLOCK_IN_TMPFILE overlaps the validity-level field");
|
||||||
|
|
||||||
//! Short-hand for the highest consensus validity we implement.
|
//! Short-hand for the highest consensus validity we implement.
|
||||||
//! Blocks with this validity are assumed to satisfy all consensus rules.
|
//! Blocks with this validity are assumed to satisfy all consensus rules.
|
||||||
static const BlockStatus BLOCK_VALID_CONSENSUS = BLOCK_VALID_SCRIPTS;
|
static const BlockStatus BLOCK_VALID_CONSENSUS = BLOCK_VALID_SCRIPTS;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
//! These need to be macros, as clientversion.cpp's and bitcoin*-res.rc's voodoo requires it
|
//! 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!
|
// Must be kept in sync with configure.ac , ugh!
|
||||||
#define CLIENT_VERSION_MAJOR 1
|
#define CLIENT_VERSION_MAJOR 1
|
||||||
#define CLIENT_VERSION_MINOR 2
|
#define CLIENT_VERSION_MINOR 3
|
||||||
#define CLIENT_VERSION_REVISION 0
|
#define CLIENT_VERSION_REVISION 0
|
||||||
#define CLIENT_VERSION_BUILD 50
|
#define CLIENT_VERSION_BUILD 50
|
||||||
|
|
||||||
|
|||||||
98
src/gtest/test_stratum_jobid.cpp
Normal file
98
src/gtest/test_stratum_jobid.cpp
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (c) 2016-2026 The Hush developers
|
||||||
|
// Distributed under the GPLv3 software license, see the accompanying
|
||||||
|
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||||
|
//
|
||||||
|
// Regression coverage for the b3e81f1ed fix:
|
||||||
|
// "stratum: do not abort the daemon on a malformed 63-character job_id"
|
||||||
|
//
|
||||||
|
// The EWBF "31 bytes job_id" path in stratum_mining_submit() completes a
|
||||||
|
// 63-character job_id with each hex digit in turn and feeds the result to
|
||||||
|
// uint256(). ParseHex() stops at the first non-hex character and returns a
|
||||||
|
// SHORT vector without signalling an error, and base_blob's vector ctor
|
||||||
|
// asserts vch.size() == 32. asserts are live in release builds here, and the
|
||||||
|
// job_id arrives from an unauthenticated client -- so a single mining.submit
|
||||||
|
// whose 63-character job_id contains any non-hex byte (63 spaces will do)
|
||||||
|
// used to abort the node.
|
||||||
|
//
|
||||||
|
// The fix size-checks each candidate before constructing uint256. These tests
|
||||||
|
// pin the exact invariant that guard relies on, using the real ParseHex and
|
||||||
|
// uint256 primitives, without ever constructing a uint256 from a short vector
|
||||||
|
// (which would still abort under the guard we are protecting).
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "uint256.h"
|
||||||
|
#include "utilstrencodings.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const std::string HEXDIGITS = "0123456789abcdef";
|
||||||
|
|
||||||
|
// Mirrors the guarded completion loop in stratum_mining_submit(): try every
|
||||||
|
// single-hex-digit completion and report whether ANY of them parses to a
|
||||||
|
// whole 32-byte value. Only then is uint256() construction reached.
|
||||||
|
bool AnyCompletionParsesTo32(const std::string& jobid) {
|
||||||
|
for (char d : HEXDIGITS) {
|
||||||
|
std::vector<unsigned char> vch = ParseHex(jobid + d);
|
||||||
|
if (vch.size() == 32) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// The exact example from the fix commit: a 63-character job_id of spaces.
|
||||||
|
// No completion may reach a 32-byte vector, so the daemon never constructs
|
||||||
|
// uint256() and never aborts.
|
||||||
|
TEST(StratumJobId, AllSpacesNeverYields32Bytes) {
|
||||||
|
std::string spaces(63, ' ');
|
||||||
|
ASSERT_EQ(spaces.size(), 63u);
|
||||||
|
for (char d : HEXDIGITS) {
|
||||||
|
EXPECT_NE(ParseHex(spaces + d).size(), 32u)
|
||||||
|
<< "completion '" << d << "' unexpectedly produced 32 bytes";
|
||||||
|
}
|
||||||
|
EXPECT_FALSE(AnyCompletionParsesTo32(spaces));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single non-hex byte embedded in an otherwise-hex 63-char job_id is enough:
|
||||||
|
// ParseHex stops at it, so every completion is short.
|
||||||
|
TEST(StratumJobId, SingleNonHexByteInMiddleIsRejected) {
|
||||||
|
std::string jobid(63, 'a');
|
||||||
|
jobid[30] = 'g'; // 'g' is not a hex digit
|
||||||
|
ASSERT_EQ(jobid.size(), 63u);
|
||||||
|
EXPECT_FALSE(AnyCompletionParsesTo32(jobid));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A non-hex byte at the very end (position 62) is likewise rejected: the last
|
||||||
|
// hex pair can never complete to a whole byte.
|
||||||
|
TEST(StratumJobId, NonHexByteAtEndIsRejected) {
|
||||||
|
std::string jobid(62, 'a');
|
||||||
|
jobid.push_back('z'); // length 63, last char non-hex
|
||||||
|
ASSERT_EQ(jobid.size(), 63u);
|
||||||
|
EXPECT_FALSE(AnyCompletionParsesTo32(jobid));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A genuine truncated EWBF job_id -- 63 real hex characters -- must complete
|
||||||
|
// to exactly 32 bytes for every digit, so uint256() construction is safe.
|
||||||
|
TEST(StratumJobId, ValidSixtyThreeHexCompletesToExactly32Bytes) {
|
||||||
|
std::string jobid(63, 'a');
|
||||||
|
ASSERT_EQ(jobid.size(), 63u);
|
||||||
|
for (char d : HEXDIGITS) {
|
||||||
|
std::vector<unsigned char> vch = ParseHex(jobid + d);
|
||||||
|
ASSERT_EQ(vch.size(), 32u) << "completion '" << d << "'";
|
||||||
|
// 32-byte vector: construction must not trip the size assertion.
|
||||||
|
uint256 h(vch);
|
||||||
|
EXPECT_EQ(h.size(), 32u);
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(AnyCompletionParsesTo32(jobid));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity anchors for ParseHex's odd/even handling that the loop depends on:
|
||||||
|
// an odd hex length drops the trailing nibble (63 hex -> 31 bytes), and one
|
||||||
|
// more hex char fills the 32nd byte (64 hex -> 32 bytes).
|
||||||
|
TEST(StratumJobId, ParseHexOddLengthDropsTrailingNibble) {
|
||||||
|
EXPECT_EQ(ParseHex(std::string(63, 'a')).size(), 31u);
|
||||||
|
EXPECT_EQ(ParseHex(std::string(64, 'a')).size(), 32u);
|
||||||
|
}
|
||||||
@@ -420,6 +420,10 @@ static void libevent_log_cb(int severity, const char *msg)
|
|||||||
LogPrint("libevent", "libevent: %s\n", msg);
|
LogPrint("libevent", "libevent: %s\n", msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Cap on the combined size of an HTTP request line + headers. libevent's default is EV_SIZE_MAX,
|
||||||
|
* i.e. unbounded, and it buffers before any ACL or auth check runs. */
|
||||||
|
static const size_t MAX_HEADERS_SIZE = 8192;
|
||||||
|
|
||||||
bool InitHTTPServer()
|
bool InitHTTPServer()
|
||||||
{
|
{
|
||||||
struct evhttp* http = 0;
|
struct evhttp* http = 0;
|
||||||
@@ -467,6 +471,10 @@ bool InitHTTPServer()
|
|||||||
|
|
||||||
evhttp_set_timeout(http, GetArg("-rpcservertimeout", DEFAULT_HTTP_SERVER_TIMEOUT));
|
evhttp_set_timeout(http, GetArg("-rpcservertimeout", DEFAULT_HTTP_SERVER_TIMEOUT));
|
||||||
evhttp_set_max_body_size(http, MAX_SIZE);
|
evhttp_set_max_body_size(http, MAX_SIZE);
|
||||||
|
// libevent defaults max_headers_size to EV_SIZE_MAX, so without this a single connection can
|
||||||
|
// stream an unbounded request line / header block and grow RSS ~1:1 with bytes sent, BEFORE the
|
||||||
|
// -rpcallowip ACL or auth check runs (both happen after libevent has parsed the request).
|
||||||
|
evhttp_set_max_headers_size(http, MAX_HEADERS_SIZE);
|
||||||
evhttp_set_gencb(http, http_request_cb, NULL);
|
evhttp_set_gencb(http, http_request_cb, NULL);
|
||||||
|
|
||||||
if (!HTTPBindAddresses(http)) {
|
if (!HTTPBindAddresses(http)) {
|
||||||
|
|||||||
@@ -660,7 +660,7 @@ void hush_nSPVreq(CNode *pfrom,std::vector<uint8_t> request) // received a reque
|
|||||||
struct NSPV_utxosresp U;
|
struct NSPV_utxosresp U;
|
||||||
if ( len >= 2 && len < 64+5 && request[1] < 64 && (request[1] == len-3 || request[1] == len-7 || request[1] == len-11) )
|
if ( len >= 2 && len < 64+5 && request[1] < 64 && (request[1] == len-3 || request[1] == len-7 || request[1] == len-11) )
|
||||||
{
|
{
|
||||||
int32_t skipcount = 0; char coinaddr[64]; uint32_t filter; uint8_t isCC = 0;
|
int32_t skipcount = 0; char coinaddr[64]; uint32_t filter = 0; uint8_t isCC = 0; // only assigned on the len-11 form; the other two passed it on uninitialized
|
||||||
memcpy(coinaddr,&request[2],request[1]); // request[1] < 64 bounds the copy + the terminator write
|
memcpy(coinaddr,&request[2],request[1]); // request[1] < 64 bounds the copy + the terminator write
|
||||||
coinaddr[request[1]] = 0;
|
coinaddr[request[1]] = 0;
|
||||||
if ( request[1] == len-3 )
|
if ( request[1] == len-3 )
|
||||||
@@ -698,7 +698,7 @@ void hush_nSPVreq(CNode *pfrom,std::vector<uint8_t> request) // received a reque
|
|||||||
struct NSPV_txidsresp T;
|
struct NSPV_txidsresp T;
|
||||||
if ( len >= 2 && len < 64+5 && request[1] < 64 && (request[1] == len-3 || request[1] == len-7 || request[1] == len-11) )
|
if ( len >= 2 && len < 64+5 && request[1] < 64 && (request[1] == len-3 || request[1] == len-7 || request[1] == len-11) )
|
||||||
{
|
{
|
||||||
int32_t skipcount = 0; char coinaddr[64]; uint32_t filter; uint8_t isCC = 0;
|
int32_t skipcount = 0; char coinaddr[64]; uint32_t filter = 0; uint8_t isCC = 0; // only assigned on the len-11 form; the other two passed it on uninitialized
|
||||||
memcpy(coinaddr,&request[2],request[1]); // request[1] < 64 bounds the copy + the terminator write
|
memcpy(coinaddr,&request[2],request[1]); // request[1] < 64 bounds the copy + the terminator write
|
||||||
coinaddr[request[1]] = 0;
|
coinaddr[request[1]] = 0;
|
||||||
if ( request[1] == len-3 )
|
if ( request[1] == len-3 )
|
||||||
|
|||||||
@@ -1383,8 +1383,14 @@ void hush_configfile(char *symbol,uint16_t rpcport)
|
|||||||
#endif
|
#endif
|
||||||
} else {
|
} else {
|
||||||
_hush_userpass(myusername,mypassword,fp);
|
_hush_userpass(myusername,mypassword,fp);
|
||||||
mapArgs["-rpcpassword"] = mypassword;
|
// Feed the credentials read by InitRPCAuthentication (httprpc.cpp) and the
|
||||||
mapArgs["-rpcusername"] = myusername;
|
// CLI (bitcoin-cli.cpp) -- both read "-rpcuser"/"-rpcpassword". Use SoftSetArg
|
||||||
|
// so a value passed on the command line (or an explicit -rpcuser/-rpcpassword)
|
||||||
|
// still wins: the old direct assignment silently overwrote a command-line
|
||||||
|
// -rpcpassword on every restart once this conf existed, and the username was
|
||||||
|
// written to a misspelled "-rpcusername" key that nothing ever reads.
|
||||||
|
SoftSetArg("-rpcpassword", mypassword);
|
||||||
|
SoftSetArg("-rpcuser", myusername);
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1777,11 +1783,21 @@ void hush_args(char *argv0)
|
|||||||
fprintf(stderr,".oO Starting %s Full Node (Extreme Privacy!) with genproc=%d notary=%d\n",name.c_str(),HUSH_MININGTHREADS, IS_HUSH_NOTARY);
|
fprintf(stderr,".oO Starting %s Full Node (Extreme Privacy!) with genproc=%d notary=%d\n",name.c_str(),HUSH_MININGTHREADS, IS_HUSH_NOTARY);
|
||||||
|
|
||||||
vector<string> DRAGONX_nodes = {};
|
vector<string> DRAGONX_nodes = {};
|
||||||
// Only DRAGONX connects to these by default, other chains must opt-in via -connect/-addnode
|
// Only DRAGONX connects to these by default, other chains must opt-in via -connect/-addnode.
|
||||||
|
// Never on regtest or testnet: regtest reuses mainnet's network magic, so injecting the mainnet
|
||||||
|
// seeds here makes a supposedly isolated node handshake production peers and pull their headers
|
||||||
|
// into its own index -- which is exactly what it did, and why multi-node rpc-tests were talking
|
||||||
|
// to the live chain. NOTE: hush_args() runs between ParseParameters() and ReadConfigFile()
|
||||||
|
// (bitcoind.cpp:115/144/158), so this sees a command-line -regtest (how qa/rpc-tests starts
|
||||||
|
// nodes) but NOT a bare "regtest=1" in the config file.
|
||||||
const bool isdragonx = strncmp(name.c_str(), "DRAGONX",7) == 0 ? true : false;
|
const bool isdragonx = strncmp(name.c_str(), "DRAGONX",7) == 0 ? true : false;
|
||||||
|
|
||||||
LogPrint("net", "%s: isdragonx=%d\n", __func__, isdragonx);
|
// Seed injection only -- isdragonx itself must stay true here, because it also selects
|
||||||
if (isdragonx) {
|
// ac_private, ac_algo, blocktime and the reward/halving schedule below.
|
||||||
|
const bool isnotmainnet = GetBoolArg("-regtest", false) || GetBoolArg("-testnet", false);
|
||||||
|
|
||||||
|
LogPrint("net", "%s: isdragonx=%d isnotmainnet=%d\n", __func__, isdragonx, isnotmainnet);
|
||||||
|
if (isdragonx && !isnotmainnet) {
|
||||||
// node8-node10 are PLACEHOLDERS with no DNS records yet. A hostname that
|
// node8-node10 are PLACEHOLDERS with no DNS records yet. A hostname that
|
||||||
// does not resolve is harmless here: ThreadOpenAddedConnections just fails
|
// does not resolve is harmless here: ThreadOpenAddedConnections just fails
|
||||||
// to open the connection and retries on its 2-minute cycle. Reserving the
|
// to open the connection and retries on its 2-minute cycle. Reserving the
|
||||||
|
|||||||
43
src/init.cpp
43
src/init.cpp
@@ -1432,7 +1432,13 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
|||||||
mempool.setSanityCheck(1.0 / ratio);
|
mempool.setSanityCheck(1.0 / ratio);
|
||||||
}
|
}
|
||||||
fCheckBlockIndex = GetBoolArg("-checkblockindex", chainparams.DefaultConsistencyChecks());
|
fCheckBlockIndex = GetBoolArg("-checkblockindex", chainparams.DefaultConsistencyChecks());
|
||||||
fCheckpointsEnabled = GetBoolArg("-checkpoints", true);
|
// Regtest inherits the DRAGONX *mainnet* checkpoint set (chainparams_commandline applies it
|
||||||
|
// for every SMART_CHAIN_SYMBOL=="DRAGONX" network, regardless of -regtest), whose top height
|
||||||
|
// ~3.2M would otherwise pin an isolated regtest chain in IsInitialBlockDownload() forever --
|
||||||
|
// disabling the ChainTip auto-ops (autoshield/sweep/consolidation) and the below-checkpoint
|
||||||
|
// script-check skip. Default checkpoints OFF on regtest so a fresh regtest node leaves IBD
|
||||||
|
// normally; still overridable with -checkpoints=1.
|
||||||
|
fCheckpointsEnabled = GetBoolArg("-checkpoints", chainparams.NetworkIDString() != "regtest");
|
||||||
|
|
||||||
// -par=0 means autodetect, but nScriptCheckThreads==0 means no concurrency
|
// -par=0 means autodetect, but nScriptCheckThreads==0 means no concurrency
|
||||||
nScriptCheckThreads = GetArg("-par", DEFAULT_SCRIPTCHECK_THREADS);
|
nScriptCheckThreads = GetArg("-par", DEFAULT_SCRIPTCHECK_THREADS);
|
||||||
@@ -2254,10 +2260,22 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
|||||||
if (nLoadWalletRet != DB_LOAD_OK)
|
if (nLoadWalletRet != DB_LOAD_OK)
|
||||||
{
|
{
|
||||||
if (nLoadWalletRet == DB_CORRUPT)
|
if (nLoadWalletRet == DB_CORRUPT)
|
||||||
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 "
|
// Abort HERE, as the DB_NEED_REWRITE branch below already does. Falling through
|
||||||
"phrase with -mnemonic=\"<your seed phrase>\" -rescan (see debug.log for "
|
// runs several hundred more lines of initialisation against a wallet we have just
|
||||||
"the specific record at fault).") << "\n";
|
// declared corrupt -- including SetHDSeedOrigin(), which WRITES to it, and the
|
||||||
|
// rescan and SetBestChain that follow.
|
||||||
|
//
|
||||||
|
// The old text advised restoring with -mnemonic. That is wrong twice over:
|
||||||
|
// -usemnemonic defaulted to 0 in v1.0.3 so many such wallets never had a phrase,
|
||||||
|
// and SetHDSeedFromMnemonic refuses a non-empty wallet, so "move wallet.dat aside"
|
||||||
|
// would discard every non-HD key the salvage preserved.
|
||||||
|
strErrors << _("Error loading wallet.dat: the wallet database is corrupt. Your keys may "
|
||||||
|
"still be intact -- do NOT delete or replace wallet.dat. Back it up now, "
|
||||||
|
"and see debug.log for the specific record at fault.") << "\n";
|
||||||
|
LogPrintf("%s", strErrors.str());
|
||||||
|
return InitError(strErrors.str());
|
||||||
|
}
|
||||||
else if (nLoadWalletRet == DB_NONCRITICAL_ERROR)
|
else if (nLoadWalletRet == DB_NONCRITICAL_ERROR)
|
||||||
{
|
{
|
||||||
string msg(_("Warning: error reading wallet.dat! All keys read correctly, but transaction data"
|
string msg(_("Warning: error reading wallet.dat! All keys read correctly, but transaction data"
|
||||||
@@ -2636,8 +2654,19 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
|||||||
nStart = GetTimeMillis();
|
nStart = GetTimeMillis();
|
||||||
pwalletMain->ScanForWalletTransactions(pindexRescan, true);
|
pwalletMain->ScanForWalletTransactions(pindexRescan, true);
|
||||||
LogPrintf(" rescan %15dms\n", GetTimeMillis() - nStart);
|
LogPrintf(" rescan %15dms\n", GetTimeMillis() - nStart);
|
||||||
pwalletMain->SetBestChain(chainActive.GetLocator());
|
// ONLY record "scanned to the tip" if the scan actually reached it. An aborted or
|
||||||
nWalletDBUpdated++;
|
// shutdown-interrupted scan that stamps a tip locator tells the next startup there is
|
||||||
|
// nothing left to scan (the chainActive.Tip() != pindexRescan guard above then skips
|
||||||
|
// the rescan entirely), so every transaction in the unscanned range stays out of
|
||||||
|
// mapWallet permanently: invisible to getbalance and unspendable. The scan writes its
|
||||||
|
// own locator at the interrupt point instead.
|
||||||
|
if (pwalletMain->fLastRescanCompleted) {
|
||||||
|
pwalletMain->SetBestChain(chainActive.GetLocator());
|
||||||
|
nWalletDBUpdated++;
|
||||||
|
} else {
|
||||||
|
LogPrintf("Rescan did not complete; leaving the best-block locator at the scan's own "
|
||||||
|
"checkpoint so the remaining range is rescanned on the next start\n");
|
||||||
|
}
|
||||||
|
|
||||||
// Restore wallet transaction metadata after -zapwallettxes=1
|
// Restore wallet transaction metadata after -zapwallettxes=1
|
||||||
if (GetBoolArg("-zapwallettxes", false) && GetArg("-zapwallettxes", "1") != "2")
|
if (GetBoolArg("-zapwallettxes", false) && GetArg("-zapwallettxes", "1") != "2")
|
||||||
|
|||||||
38
src/main.cpp
38
src/main.cpp
@@ -8336,6 +8336,24 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
|
|||||||
// Message: addr
|
// Message: addr
|
||||||
if (fSendTrickle)
|
if (fSendTrickle)
|
||||||
{
|
{
|
||||||
|
// Accumulate into vAddr and send it ONCE (or in MAX_ADDR_TO_SEND-sized batches).
|
||||||
|
// This loop previously pushed the ENTIRE pto->vAddrToSend on every accepted address,
|
||||||
|
// so a single 24-byte getaddr produced N messages of N addresses each instead of one
|
||||||
|
// message of N -- ~500x the intended bandwidth on a typical addrman, all serialized
|
||||||
|
// (with per-message double-SHA256 checksums) while cs_main is held. The locally built
|
||||||
|
// vAddr was accumulated and then discarded, and the vAddr.resize(MAX_ADDR_TO_SEND) was
|
||||||
|
// a no-op standing where upstream has vAddr.clear().
|
||||||
|
const char* msg_type;
|
||||||
|
int make_flags;
|
||||||
|
if (pto->m_wants_addrv2) {
|
||||||
|
msg_type = NetMsgType::ADDRV2;
|
||||||
|
make_flags = ADDRV2_FORMAT;
|
||||||
|
} else {
|
||||||
|
msg_type = NetMsgType::ADDR;
|
||||||
|
make_flags = 0;
|
||||||
|
}
|
||||||
|
const CNetMsgMaker msgMaker(std::min(pto->nVersion, PROTOCOL_VERSION));
|
||||||
|
|
||||||
vector<CAddress> vAddr;
|
vector<CAddress> vAddr;
|
||||||
vAddr.reserve(pto->vAddrToSend.size());
|
vAddr.reserve(pto->vAddrToSend.size());
|
||||||
BOOST_FOREACH(const CAddress& addr, pto->vAddrToSend)
|
BOOST_FOREACH(const CAddress& addr, pto->vAddrToSend)
|
||||||
@@ -8343,29 +8361,17 @@ bool SendMessages(CNode* pto, bool fSendTrickle)
|
|||||||
if (pto->AddAddressIfNotAlreadyKnown(addr))
|
if (pto->AddAddressIfNotAlreadyKnown(addr))
|
||||||
{
|
{
|
||||||
vAddr.push_back(addr);
|
vAddr.push_back(addr);
|
||||||
|
|
||||||
if (vAddr.size() >= MAX_ADDR_TO_SEND)
|
if (vAddr.size() >= MAX_ADDR_TO_SEND)
|
||||||
{
|
{
|
||||||
// Should be impossible since we always check size before adding to
|
pto->PushAddrMessage(msgMaker.Make(make_flags, msg_type, vAddr));
|
||||||
// vAddrToSend. Recover by trimming the vector.
|
vAddr.clear();
|
||||||
vAddr.resize(MAX_ADDR_TO_SEND);
|
|
||||||
}
|
}
|
||||||
const char* msg_type;
|
|
||||||
int make_flags;
|
|
||||||
if (pto->m_wants_addrv2) {
|
|
||||||
msg_type = NetMsgType::ADDRV2;
|
|
||||||
make_flags = ADDRV2_FORMAT;
|
|
||||||
} else {
|
|
||||||
msg_type = NetMsgType::ADDR;
|
|
||||||
make_flags = 0;
|
|
||||||
}
|
|
||||||
pto->PushAddrMessage(CNetMsgMaker(std::min(pto->nVersion, PROTOCOL_VERSION)).Make(make_flags, msg_type, pto->vAddrToSend));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pto->vAddrToSend.clear();
|
pto->vAddrToSend.clear();
|
||||||
vAddr.clear();
|
if (!vAddr.empty())
|
||||||
|
pto->PushAddrMessage(msgMaker.Make(make_flags, msg_type, vAddr));
|
||||||
}
|
}
|
||||||
|
|
||||||
CNodeState &state = *State(pto->GetId());
|
CNodeState &state = *State(pto->GetId());
|
||||||
|
|||||||
@@ -607,7 +607,11 @@ public:
|
|||||||
// Known checking here is only to save space from duplicates.
|
// Known checking here is only to save space from duplicates.
|
||||||
// SendMessages will filter it again for knowns that were added
|
// SendMessages will filter it again for knowns that were added
|
||||||
// after addresses were pushed.
|
// after addresses were pushed.
|
||||||
if (_addr.IsValid() && !IsAddressKnown(addr) && addr_format_supported) {
|
// NOTE: _addr (the address being queued), NOT addr (this peer's own address, net.h ~416).
|
||||||
|
// Testing the member made the filter constant for the connection's lifetime: once the peer's
|
||||||
|
// own address entered its addrKnown -- routine, via the remote's AdvertizeLocal -- every
|
||||||
|
// relay path to it silently no-opped until the daily addrKnown.reset().
|
||||||
|
if (_addr.IsValid() && !IsAddressKnown(_addr) && addr_format_supported) {
|
||||||
|
|
||||||
if (vAddrToSend.size() >= MAX_ADDR_TO_SEND) {
|
if (vAddrToSend.size() >= MAX_ADDR_TO_SEND) {
|
||||||
vAddrToSend[insecure_rand() % vAddrToSend.size()] = _addr;
|
vAddrToSend[insecure_rand() % vAddrToSend.size()] = _addr;
|
||||||
|
|||||||
@@ -534,6 +534,10 @@ UniValue getblockdeltas(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
|||||||
if (fHelp || params.size() != 1)
|
if (fHelp || params.size() != 1)
|
||||||
throw runtime_error("");
|
throw runtime_error("");
|
||||||
|
|
||||||
|
// Reads mapBlockIndex / chainActive (and, below, pcoinsTip's mutable anchor cache),
|
||||||
|
// all of which are cs_main-guarded. Every sibling RPC in this file locks; this one did not.
|
||||||
|
LOCK(cs_main);
|
||||||
|
|
||||||
std::string strHash = params[0].get_str();
|
std::string strHash = params[0].get_str();
|
||||||
uint256 hash(uint256S(strHash));
|
uint256 hash(uint256S(strHash));
|
||||||
|
|
||||||
@@ -602,11 +606,19 @@ UniValue getblockhashes(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
|||||||
|
|
||||||
std::vector<std::pair<uint256, unsigned int> > blockHashes;
|
std::vector<std::pair<uint256, unsigned int> > blockHashes;
|
||||||
|
|
||||||
if (fActiveOnly)
|
{
|
||||||
|
// The lock must SPAN GetTimestampIndex: with fActiveOnly it calls blockOnchainActive() for
|
||||||
|
// every row, which reads mapBlockIndex and chainActive. The previous form was
|
||||||
|
// if (fActiveOnly)
|
||||||
|
// LOCK(cs_main);
|
||||||
|
// and LOCK() declares a scoped object, so as an unbraced substatement it was constructed
|
||||||
|
// and destroyed on that line -- the walk then ran completely unsynchronised. Taken
|
||||||
|
// unconditionally here: this RPC is explorer-only and not hot, and a conditional lock is
|
||||||
|
// exactly the shape that produced the bug.
|
||||||
LOCK(cs_main);
|
LOCK(cs_main);
|
||||||
|
if (!GetTimestampIndex(high, low, fActiveOnly, blockHashes)) {
|
||||||
if (!GetTimestampIndex(high, low, fActiveOnly, blockHashes)) {
|
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "No information available for block hashes");
|
||||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "No information available for block hashes");
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UniValue result(UniValue::VARR);
|
UniValue result(UniValue::VARR);
|
||||||
@@ -877,6 +889,10 @@ UniValue getblockmerkletree(const UniValue& params, bool fHelp, const CPubKey& m
|
|||||||
+ HelpExampleRpc("getblockmerkletree", "290000")
|
+ HelpExampleRpc("getblockmerkletree", "290000")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reads mapBlockIndex / chainActive (and, below, pcoinsTip's mutable anchor cache),
|
||||||
|
// all of which are cs_main-guarded. Every sibling RPC in this file locks; this one did not.
|
||||||
|
LOCK(cs_main);
|
||||||
|
|
||||||
CBlockIndex* phushblockindex;
|
CBlockIndex* phushblockindex;
|
||||||
uint256 blockRoot;
|
uint256 blockRoot;
|
||||||
SaplingMerkleTree tree;
|
SaplingMerkleTree tree;
|
||||||
|
|||||||
@@ -759,9 +759,25 @@ UniValue getblocktemplate(const UniValue& params, bool fHelp, const CPubKey& myp
|
|||||||
#ifdef ENABLE_WALLET
|
#ifdef ENABLE_WALLET
|
||||||
CReserveKey reservekey(pwalletMain);
|
CReserveKey reservekey(pwalletMain);
|
||||||
LEAVE_CRITICAL_SECTION(cs_main);
|
LEAVE_CRITICAL_SECTION(cs_main);
|
||||||
pblocktemplate = CreateNewBlockWithKey(reservekey,pindexPrevNew->GetHeight()+1,HUSH_MAXGPUCOUNT,false);
|
// MUST re-enter cs_main before letting an exception escape. The enclosing LOCK(cs_main) is
|
||||||
|
// a scoped CMutexLock whose owns_lock is still true, so if CreateNewBlockWithKey throws
|
||||||
|
// (any wallet/BDB fault: disk full, EMFILE, a corrupt wallet.dat) its destructor unlocks an
|
||||||
|
// already-unlocked mutex during unwinding -> BOOST_VERIFY -> SIGABRT. Asserts cannot be
|
||||||
|
// compiled out here (main.cpp #errors on NDEBUG), so this aborts the daemon instead of
|
||||||
|
// returning the actionable error, and the abort happens inside unwinding so nothing is logged.
|
||||||
|
try {
|
||||||
|
pblocktemplate = CreateNewBlockWithKey(reservekey,pindexPrevNew->GetHeight()+1,HUSH_MAXGPUCOUNT,false);
|
||||||
|
} catch (...) {
|
||||||
|
ENTER_CRITICAL_SECTION(cs_main);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
pblocktemplate = CreateNewBlockWithKey();
|
try {
|
||||||
|
pblocktemplate = CreateNewBlockWithKey();
|
||||||
|
} catch (...) {
|
||||||
|
ENTER_CRITICAL_SECTION(cs_main);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
ENTER_CRITICAL_SECTION(cs_main);
|
ENTER_CRITICAL_SECTION(cs_main);
|
||||||
if (!pblocktemplate)
|
if (!pblocktemplate)
|
||||||
@@ -1112,6 +1128,27 @@ static bool StratumMinerRecvLines(int fd, std::string& buf, int timeout_ms, std:
|
|||||||
// authorize, receive work + the per-height RandomX key, then vary the block nNonce, hash with
|
// authorize, receive work + the per-height RandomX key, then vary the block nNonce, hash with
|
||||||
// RandomX (byte-identical to CheckRandomXSolution via GetRandomXInput), and submit a 32-byte
|
// RandomX (byte-identical to CheckRandomXSolution via GetRandomXInput), and submit a 32-byte
|
||||||
// solution when the block hash meets target. Exists to validate the -stratum RandomX pool path.
|
// solution when the block hash meets target. Exists to validate the -stratum RandomX pool path.
|
||||||
|
//! Upper bound on how long stratummine will hold an RPC worker thread (and a 256 MB RandomX
|
||||||
|
//! cache). An unbounded caller-supplied deadline pins both indefinitely.
|
||||||
|
static const int64_t MAX_STRATUMMINE_TIMEOUT = 3600;
|
||||||
|
|
||||||
|
//! Parse a 64-char hex field from mining.notify into a uint256.
|
||||||
|
//! These fields come from whatever host the operator pointed us at, and both primitives below are
|
||||||
|
//! unforgiving: uint256's vector constructor asserts on a wrong-size input (uint256.cpp:30, and
|
||||||
|
//! NDEBUG is never defined for this build so the assert is live in release), while ParseHex
|
||||||
|
//! silently truncates at the first non-hex character. A short or garbled field would therefore
|
||||||
|
//! abort the daemon rather than be rejected. Returns false instead.
|
||||||
|
static bool StratumHex256(const std::string& hex, uint256& out)
|
||||||
|
{
|
||||||
|
if (hex.size() != 64 || !IsHex(hex))
|
||||||
|
return false;
|
||||||
|
std::vector<unsigned char> v = ParseHex(hex);
|
||||||
|
if (v.size() != 32)
|
||||||
|
return false;
|
||||||
|
out = uint256(v);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
UniValue stratummine(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
UniValue stratummine(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
||||||
{
|
{
|
||||||
if (fHelp || params.size() < 2 || params.size() > 4)
|
if (fHelp || params.size() < 2 || params.size() > 4)
|
||||||
@@ -1129,10 +1166,24 @@ UniValue stratummine(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
|||||||
if (ASSETCHAINS_ALGO != ASSETCHAINS_RANDOMX)
|
if (ASSETCHAINS_ALGO != ASSETCHAINS_RANDOMX)
|
||||||
throw JSONRPCError(RPC_MISC_ERROR, "stratummine only supports RandomX chains");
|
throw JSONRPCError(RPC_MISC_ERROR, "stratummine only supports RandomX chains");
|
||||||
|
|
||||||
|
// This is a reference miner for exercising -stratum, not a production facility: it dials an
|
||||||
|
// operator-supplied host, blocks an RPC worker for the whole run, and parses whatever that host
|
||||||
|
// chooses to send back. Require an explicit opt-in, and exempt regtest so the test suite can
|
||||||
|
// still drive it. NOTE: do NOT gate this on fExperimentalMode -- that defaults to TRUE
|
||||||
|
// (init.cpp:1195), so it would leave the RPC exposed on every node and the gate would be a no-op.
|
||||||
|
if (!GetBoolArg("-stratummine", false) && Params().NetworkIDString() != "regtest")
|
||||||
|
throw JSONRPCError(RPC_MISC_ERROR,
|
||||||
|
"stratummine is a test-only reference miner and is disabled by default; "
|
||||||
|
"restart with -stratummine to enable it");
|
||||||
|
|
||||||
const std::string host = params[0].get_str();
|
const std::string host = params[0].get_str();
|
||||||
const int port = params[1].get_int();
|
const int port = params[1].get_int();
|
||||||
const std::string addr = params.size() > 2 ? params[2].get_str() : "x";
|
const std::string addr = params.size() > 2 ? params[2].get_str() : "x";
|
||||||
const int64_t timeout = params.size() > 3 ? params[3].get_int64() : 120;
|
int64_t timeout = params.size() > 3 ? params[3].get_int64() : 120;
|
||||||
|
if (timeout < 1)
|
||||||
|
timeout = 1;
|
||||||
|
if (timeout > MAX_STRATUMMINE_TIMEOUT)
|
||||||
|
timeout = MAX_STRATUMMINE_TIMEOUT;
|
||||||
const int64_t deadline = GetTime() + timeout;
|
const int64_t deadline = GetTime() + timeout;
|
||||||
|
|
||||||
// connect (blocking TCP)
|
// connect (blocking TCP)
|
||||||
@@ -1166,6 +1217,11 @@ UniValue stratummine(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
|||||||
uint256 hashPrevBlock, hashMerkleRoot, hashReserved;
|
uint256 hashPrevBlock, hashMerkleRoot, hashReserved;
|
||||||
|
|
||||||
auto processLine = [&](const std::string& line) {
|
auto processLine = [&](const std::string& line) {
|
||||||
|
// Every get_str()/get_int() below throws on a type mismatch, and this lambda runs inside the
|
||||||
|
// window where the RandomX cache and VM are allocated and the socket is open -- all of which
|
||||||
|
// are released only on the normal path. A malformed server message must therefore never
|
||||||
|
// escape from here, or it leaks 256 MB and the fd on its way out.
|
||||||
|
try {
|
||||||
UniValue v;
|
UniValue v;
|
||||||
if (!v.read(line)) return;
|
if (!v.read(line)) return;
|
||||||
const UniValue& id = find_value(v, "id");
|
const UniValue& id = find_value(v, "id");
|
||||||
@@ -1185,16 +1241,26 @@ UniValue stratummine(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
|||||||
poolTarget = UintToArith256(uint256S(p[0].get_str()));
|
poolTarget = UintToArith256(uint256S(p[0].get_str()));
|
||||||
haveTarget = true;
|
haveTarget = true;
|
||||||
} else if (m == "mining.notify" && p.size() >= 7) {
|
} else if (m == "mining.notify" && p.size() >= 7) {
|
||||||
|
// Validate every fixed-width field before committing any of it, so a malformed job is
|
||||||
|
// ignored outright rather than half-applied over the previous one.
|
||||||
|
uint256 prev, merkle, reserved;
|
||||||
|
if (!StratumHex256(p[2].get_str(), prev) ||
|
||||||
|
!StratumHex256(p[3].get_str(), merkle) ||
|
||||||
|
!StratumHex256(p[4].get_str(), reserved))
|
||||||
|
return;
|
||||||
jobId = p[0].get_str();
|
jobId = p[0].get_str();
|
||||||
nVersion = bswap_32((uint32_t)strtoul(p[1].get_str().c_str(), NULL, 16));
|
nVersion = bswap_32((uint32_t)strtoul(p[1].get_str().c_str(), NULL, 16));
|
||||||
hashPrevBlock = uint256(ParseHex(p[2].get_str()));
|
hashPrevBlock = prev;
|
||||||
hashMerkleRoot = uint256(ParseHex(p[3].get_str()));
|
hashMerkleRoot = merkle;
|
||||||
hashReserved = uint256(ParseHex(p[4].get_str()));
|
hashReserved = reserved;
|
||||||
timeHex = p[5].get_str();
|
timeHex = p[5].get_str();
|
||||||
nTime = bswap_32((uint32_t)strtoul(timeHex.c_str(), NULL, 16));
|
nTime = bswap_32((uint32_t)strtoul(timeHex.c_str(), NULL, 16));
|
||||||
nBits = bswap_32((uint32_t)strtoul(p[6].get_str().c_str(), NULL, 16));
|
nBits = bswap_32((uint32_t)strtoul(p[6].get_str().c_str(), NULL, 16));
|
||||||
haveJob = true;
|
haveJob = true;
|
||||||
}
|
}
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
// Malformed message from the server: ignore the line and keep mining.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string buf;
|
std::string buf;
|
||||||
@@ -1302,7 +1368,7 @@ static const CRPCCommand commands[] =
|
|||||||
{ // category name actor (function) okSafeMode
|
{ // category name actor (function) okSafeMode
|
||||||
// --------------------- ------------------------ ----------------------- ----------
|
// --------------------- ------------------------ ----------------------- ----------
|
||||||
#ifndef WIN32
|
#ifndef WIN32
|
||||||
{ "mining", "stratummine", &stratummine, true },
|
{ "mining", "stratummine", &stratummine, false },
|
||||||
#endif
|
#endif
|
||||||
{ "mining", "getlocalsolps", &getlocalsolps, true },
|
{ "mining", "getlocalsolps", &getlocalsolps, true },
|
||||||
{ "mining", "getnetworksolps", &getnetworksolps, true },
|
{ "mining", "getnetworksolps", &getnetworksolps, true },
|
||||||
|
|||||||
@@ -656,9 +656,18 @@ void CustomizeWork(const StratumClient& client, const StratumWork& current_work,
|
|||||||
LogPrint("stratum", "%s\n", msg);
|
LogPrint("stratum", "%s\n", msg);
|
||||||
throw std::runtime_error(msg);
|
throw std::runtime_error(msg);
|
||||||
}
|
}
|
||||||
if (cb.vout[0].scriptPubKey == (CScript() << OP_FALSE)) {
|
// Unconditional. This used to be guarded on the coinbase still carrying the OP_FALSE
|
||||||
cb.vout[0].scriptPubKey = GetScriptForDestination(addr.Get());
|
// placeholder, which made it a no-op for every client after the first once a customized
|
||||||
|
// coinbase had been written back into the shared template -- so those miners silently
|
||||||
|
// mined the first miner's payout address. The template is now left pristine (see
|
||||||
|
// GetWorkUnit), and stamping unconditionally means a coinbase that somehow arrives
|
||||||
|
// already customized can never be inherited by a different miner.
|
||||||
|
if (!addr.IsValid()) {
|
||||||
|
const std::string msg = strprintf("%s: no valid payout address for this client; unable to customize work", __func__);
|
||||||
|
LogPrint("stratum", "%s\n", msg);
|
||||||
|
throw std::runtime_error(msg);
|
||||||
}
|
}
|
||||||
|
cb.vout[0].scriptPubKey = GetScriptForDestination(addr.Get());
|
||||||
}
|
}
|
||||||
|
|
||||||
// cb_branch = current_work.m_cb_branch;
|
// cb_branch = current_work.m_cb_branch;
|
||||||
@@ -864,20 +873,24 @@ std::string GetWorkUnit(StratumClient& client)
|
|||||||
static const std::vector<unsigned char> dummy(32-extranonce1.size(), 0x00); // extranonce2
|
static const std::vector<unsigned char> dummy(32-extranonce1.size(), 0x00); // extranonce2
|
||||||
CustomizeWork(client, current_work, client.m_addr, extranonce1, dummy, cb, bf, cb_branch);
|
CustomizeWork(client, current_work, client.m_addr, extranonce1, dummy, cb, bf, cb_branch);
|
||||||
|
|
||||||
// without 2 lines below equihash solutinon on SubmitWork will be incorrect, bcz we should
|
|
||||||
// change vtx[0] in current work and re-calc hashMerkleRoot
|
|
||||||
// TODO: refactor all of these ... may be change this in current_work directly is bad idea,
|
|
||||||
// and we should do all checks and hashMerkleRoot at SubmitBlock(...)
|
|
||||||
|
|
||||||
current_work.GetBlock().vtx[0] = cb;
|
|
||||||
current_work.GetBlock().hashMerkleRoot = current_work.GetBlock().BuildMerkleTree();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CBlockHeader blkhdr;
|
CBlockHeader blkhdr;
|
||||||
// Setup native proof-of-work
|
// Setup native proof-of-work
|
||||||
|
|
||||||
blkhdr = current_work.GetBlock().GetBlockHeader(); // copy entire blockheader created with CreateNewBlock to blkhdr
|
// The shared template MUST keep its pristine OP_FALSE coinbase. This previously did
|
||||||
|
// current_work.GetBlock().vtx[0] = cb;
|
||||||
|
// current_work.GetBlock().hashMerkleRoot = current_work.GetBlock().BuildMerkleTree();
|
||||||
|
// which published one client's coinbase to every other client on the same job: the merkle
|
||||||
|
// root they were told to mine, and the block they eventually submitted, both committed to
|
||||||
|
// the first client's payout address. Derive this client's header from a local copy instead,
|
||||||
|
// which is what the TODO that used to sit here was asking for.
|
||||||
|
{
|
||||||
|
CBlock tmp(current_work.GetBlock());
|
||||||
|
tmp.vtx[0] = cb;
|
||||||
|
blkhdr = tmp.GetBlockHeader();
|
||||||
|
blkhdr.hashMerkleRoot = tmp.BuildMerkleTree();
|
||||||
|
}
|
||||||
// CDataStream ds(SER_GETHASH, SERIALIZE_TRANSACTION_NO_WITNESS);
|
// CDataStream ds(SER_GETHASH, SERIALIZE_TRANSACTION_NO_WITNESS);
|
||||||
CDataStream ds(SER_GETHASH, PROTOCOL_VERSION);
|
CDataStream ds(SER_GETHASH, PROTOCOL_VERSION);
|
||||||
ds << cb;
|
ds << cb;
|
||||||
@@ -1012,9 +1025,32 @@ bool SubmitBlock(StratumClient& client, const uint256& job_id, const StratumWork
|
|||||||
: std::vector<unsigned char>(sol.begin() + 3, sol.end());
|
: std::vector<unsigned char>(sol.begin() + 3, sol.end());
|
||||||
|
|
||||||
blkhdr.hashFinalSaplingRoot = current_work.GetBlock().hashFinalSaplingRoot;
|
blkhdr.hashFinalSaplingRoot = current_work.GetBlock().hashFinalSaplingRoot;
|
||||||
blkhdr.hashMerkleRoot = current_work.GetBlock().hashMerkleRoot;
|
// Recompute from the coinbase CustomizeWork() just derived for THIS client. Reading the
|
||||||
|
// shared template's root would be wrong now that the template is left pristine, and was
|
||||||
|
// wrong before too -- it carried whichever client happened to request work first.
|
||||||
|
{
|
||||||
|
CBlock tmp(current_work.GetBlock());
|
||||||
|
tmp.vtx[0] = cb;
|
||||||
|
blkhdr.hashMerkleRoot = tmp.BuildMerkleTree();
|
||||||
|
}
|
||||||
blkhdr.nNonce = (uint256) nonce;
|
blkhdr.nNonce = (uint256) nonce;
|
||||||
|
|
||||||
|
// Cheap SHA256d filter first. This test used to sit *below* the RandomX verify, so 32
|
||||||
|
// arbitrary bytes from any peer bought a full ~65ms randomx_calculate_hash before anything
|
||||||
|
// rejected them -- on the shared HTTP/RPC libevent thread, and holding the global
|
||||||
|
// cs_randomx_validator that block validation also takes. GetHash() is SerializeHash over the
|
||||||
|
// header including nSolution, so passing this costs real grinding. Semantics are unchanged:
|
||||||
|
// an empty local_diff still parses to zero and still rejects, exactly as before.
|
||||||
|
if (!instance_of_cstratumparams.fAllowLowDiffShares &&
|
||||||
|
UintToArith256(blkhdr.GetHash()) > arith_uint256(current_work.local_diff)) {
|
||||||
|
CBlockIndex diff_index;
|
||||||
|
diff_index.nBits = UintToArith256(blkhdr.GetHash()).GetCompact();
|
||||||
|
const double share_diff = GetDifficulty(&diff_index);
|
||||||
|
diff_index.nBits = arith_uint256(current_work.local_diff).GetCompact();
|
||||||
|
const double target_diff = GetDifficulty(&diff_index);
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Low diff share (diff %g, local %g)", share_diff, target_diff));
|
||||||
|
}
|
||||||
|
|
||||||
// block is constructed, now it's time to VerifyEH
|
// block is constructed, now it's time to VerifyEH
|
||||||
|
|
||||||
if (StratumIsRandomX()) {
|
if (StratumIsRandomX()) {
|
||||||
@@ -1072,10 +1108,8 @@ bool SubmitBlock(StratumClient& client, const uint256& job_id, const StratumWork
|
|||||||
std::chrono::duration<double, std::milli> elapsed;
|
std::chrono::duration<double, std::milli> elapsed;
|
||||||
uint64_t shares_accepted_since_last;
|
uint64_t shares_accepted_since_last;
|
||||||
|
|
||||||
// TODO: we need to check hash > local port diff, and if it's true -> throw an exception -> diff too low (!)
|
// (the low-diff share check moved above the RandomX verify -- see SubmitBlock's cheap
|
||||||
if (!instance_of_cstratumparams.fAllowLowDiffShares)
|
// SHA256d filter -- so that attacker-controlled bytes cannot buy a RandomX hash)
|
||||||
if (UintToArith256(blkhdr.GetHash()) > arith_uint256(current_work.local_diff))
|
|
||||||
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Low diff share (diff %g, local %g)", hush_real_diff, hush_local_diff));
|
|
||||||
|
|
||||||
if (finish > start)
|
if (finish > start)
|
||||||
{
|
{
|
||||||
@@ -1323,6 +1357,18 @@ UniValue stratum_mining_submit(StratumClient& client, const UniValue& params)
|
|||||||
|
|
||||||
const std::string method("mining.submit");
|
const std::string method("mining.submit");
|
||||||
BoundParams(method, params, 5,5);
|
BoundParams(method, params, 5,5);
|
||||||
|
|
||||||
|
// Parity with every other handler (GetWorkUnit, mining.aux.*, mining.extranonce.*), which all
|
||||||
|
// refuse an unauthorized client. NOTE this is not authentication: mining.authorize validates no
|
||||||
|
// credential, so it only costs an attacker one extra line. It is here so the submit path cannot
|
||||||
|
// be reached without at least completing the handshake; the cheap-target check below is what
|
||||||
|
// actually bounds the work an unknown peer can force.
|
||||||
|
if (!client.m_authorized && client.m_aux_addr.empty()) {
|
||||||
|
const std::string msg = strprintf("%s: share submitted by an unauthorized client", __func__);
|
||||||
|
LogPrint("stratum", "%s\n", msg);
|
||||||
|
throw JSONRPCError(RPC_INVALID_REQUEST, "Stratum client not authorized. Use mining.authorize first, with a DragonX R.. address as the username or 'x' to mine to the default address.");
|
||||||
|
}
|
||||||
|
|
||||||
// First parameter is the client username, which is ignored.
|
// First parameter is the client username, which is ignored.
|
||||||
|
|
||||||
/* EWBF 31 bytes job_id fix */
|
/* EWBF 31 bytes job_id fix */
|
||||||
@@ -1334,7 +1380,16 @@ UniValue stratum_mining_submit(StratumClient& client, const UniValue& params)
|
|||||||
if (job_id_str.length() == 63) {
|
if (job_id_str.length() == 63) {
|
||||||
fEWBFJobIDFixNeeded = true;
|
fEWBFJobIDFixNeeded = true;
|
||||||
for(const auto& hexDigit : hexDigits) {
|
for(const auto& hexDigit : hexDigits) {
|
||||||
ret = uint256(ParseHex(job_id_str + hexDigit));
|
// ParseHex() stops at the first non-hex character and returns a SHORT vector
|
||||||
|
// without signalling an error, and base_blob(const std::vector<unsigned char>&)
|
||||||
|
// asserts vch.size() == 32. Constructing without checking therefore lets any
|
||||||
|
// 63-character job_id containing a non-hex byte abort the daemon -- from an
|
||||||
|
// unauthenticated client, before any other validation. Skip bad candidates
|
||||||
|
// instead; if none of the 16 completions parse, ret stays null, misses
|
||||||
|
// work_templates below, and the handler returns false cleanly.
|
||||||
|
std::vector<unsigned char> vch = ParseHex(job_id_str + hexDigit);
|
||||||
|
if (vch.size() != 32) continue;
|
||||||
|
ret = uint256(vch);
|
||||||
if (work_templates.count(ret)) break;
|
if (work_templates.count(ret)) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,7 +299,10 @@ UniValue importprivkey(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
|||||||
bool fRescan = true;
|
bool fRescan = true;
|
||||||
if (params.size() > 2)
|
if (params.size() > 2)
|
||||||
fRescan = params[2].get_bool();
|
fRescan = params[2].get_bool();
|
||||||
if ( fRescan && params.size() == 4 )
|
// '> 3', not '== 4': with the optional 5th (secret_key) argument present the equality test
|
||||||
|
// failed and height silently stayed 0, rescanning from genesis. Every sibling RPC in this file
|
||||||
|
// already uses the '>' form.
|
||||||
|
if ( fRescan && params.size() > 3 )
|
||||||
height = params[3].get_int();
|
height = params[3].get_int();
|
||||||
|
|
||||||
|
|
||||||
@@ -746,10 +749,17 @@ UniValue dumpwallet_impl(const UniValue& params, bool fHelp, bool fDumpZKeys)
|
|||||||
HDSeed hdSeed;
|
HDSeed hdSeed;
|
||||||
// Dump the 64-byte derivation seed (for mnemonic wallets this is the
|
// Dump the 64-byte derivation seed (for mnemonic wallets this is the
|
||||||
// expanded BIP39 seed), so re-importing the hex reproduces the same keys.
|
// expanded BIP39 seed), so re-importing the hex reproduces the same keys.
|
||||||
pwalletMain->GetHDSeedForDerivation(hdSeed);
|
// The return MUST be checked: on failure hdSeed is default-constructed, and emitting it
|
||||||
auto rawSeed = hdSeed.RawSeed();
|
// anyway writes a blank seed next to a legitimate-looking BLAKE2b-of-empty fingerprint --
|
||||||
file << strprintf("# HDSeed=%s fingerprint=%s", HexStr(rawSeed.begin(), rawSeed.end()), hdSeed.Fingerprint().GetHex());
|
// a backup that looks valid and restores nothing. The per-key dump below is still a
|
||||||
file << "\n";
|
// complete backup without this line.
|
||||||
|
if (pwalletMain->GetHDSeedForDerivation(hdSeed)) {
|
||||||
|
auto rawSeed = hdSeed.RawSeed();
|
||||||
|
file << strprintf("# HDSeed=%s fingerprint=%s", HexStr(rawSeed.begin(), rawSeed.end()), hdSeed.Fingerprint().GetHex());
|
||||||
|
file << "\n";
|
||||||
|
} else {
|
||||||
|
file << "# HDSeed unavailable (wallet locked, no HD seed, or hdchain unproven)\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
file << "\n";
|
file << "\n";
|
||||||
for (std::vector<std::pair<int64_t, CKeyID> >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) {
|
for (std::vector<std::pair<int64_t, CKeyID> >::const_iterator it = vKeyBirth.begin(); it != vKeyBirth.end(); it++) {
|
||||||
|
|||||||
@@ -801,10 +801,18 @@ bool CWallet::CommitAutomatedTx(const CTransaction& tx) {
|
|||||||
|
|
||||||
void CWallet::SetBestChain(const CBlockLocator& loc)
|
void CWallet::SetBestChain(const CBlockLocator& loc)
|
||||||
{
|
{
|
||||||
|
// Default ctor => fFlushOnClose=true => ~CDB runs a full BDB txn_checkpoint over the entire
|
||||||
|
// cache. Fine for the hourly/shutdown callers; ruinous inside a scan (see SetBestChainNoFlush).
|
||||||
CWalletDB walletdb(strWalletFile);
|
CWalletDB walletdb(strWalletFile);
|
||||||
SetBestChainINTERNAL(walletdb, loc);
|
SetBestChainINTERNAL(walletdb, loc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CWallet::SetBestChainNoFlush(const CBlockLocator& loc)
|
||||||
|
{
|
||||||
|
CWalletDB walletdb(strWalletFile, "r+", false);
|
||||||
|
return SetBestChainINTERNAL(walletdb, loc);
|
||||||
|
}
|
||||||
|
|
||||||
std::set<std::pair<libzcash::PaymentAddress, uint256>> CWallet::GetNullifiersForAddresses(
|
std::set<std::pair<libzcash::PaymentAddress, uint256>> CWallet::GetNullifiersForAddresses(
|
||||||
const std::set<libzcash::PaymentAddress> & addresses)
|
const std::set<libzcash::PaymentAddress> & addresses)
|
||||||
{
|
{
|
||||||
@@ -1232,6 +1240,16 @@ int CWallet::SaplingWitnessMinimumHeight(const uint256& nullifier, int nWitnessH
|
|||||||
return nMinimumHeight;
|
return nMinimumHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int CWallet::SaplingWitnessMinimumHeight(const boost::optional<uint256>& nullifier, int nWitnessHeight, int nMinimumHeight)
|
||||||
|
{
|
||||||
|
// No nullifier => an incoming-viewing-key-only note (z_importviewingkey without the full
|
||||||
|
// viewing key). Spend depth is unknowable, so treat it as unspent and keep its witness.
|
||||||
|
if (!nullifier) {
|
||||||
|
return min(nWitnessHeight, nMinimumHeight);
|
||||||
|
}
|
||||||
|
return SaplingWitnessMinimumHeight(*nullifier, nWitnessHeight, nMinimumHeight);
|
||||||
|
}
|
||||||
|
|
||||||
int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessOnly)
|
int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessOnly)
|
||||||
{
|
{
|
||||||
LOCK2(cs_main, cs_wallet);
|
LOCK2(cs_main, cs_wallet);
|
||||||
@@ -1269,7 +1287,7 @@ int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessO
|
|||||||
|
|
||||||
//Skip Validation when witness root has been validated
|
//Skip Validation when witness root has been validated
|
||||||
if (nd->witnessRootValidated) {
|
if (nd->witnessRootValidated) {
|
||||||
nMinimumHeight = SaplingWitnessMinimumHeight(*item.second.nullifier, nd->witnessHeight, nMinimumHeight);
|
nMinimumHeight = SaplingWitnessMinimumHeight(item.second.nullifier, nd->witnessHeight, nMinimumHeight);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1281,12 +1299,12 @@ int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessO
|
|||||||
CBlockIndex* whIndex = chainActive[nd->witnessHeight];
|
CBlockIndex* whIndex = chainActive[nd->witnessHeight];
|
||||||
if (whIndex == NULL) {
|
if (whIndex == NULL) {
|
||||||
//witnessHeight strictly above the active chain (transient catch-up): cannot validate yet
|
//witnessHeight strictly above the active chain (transient catch-up): cannot validate yet
|
||||||
nMinimumHeight = SaplingWitnessMinimumHeight(*item.second.nullifier, nd->witnessHeight, nMinimumHeight);
|
nMinimumHeight = SaplingWitnessMinimumHeight(item.second.nullifier, nd->witnessHeight, nMinimumHeight);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (nd->witnesses.front().root() == whIndex->hashFinalSaplingRoot) {
|
if (nd->witnesses.front().root() == whIndex->hashFinalSaplingRoot) {
|
||||||
nd->witnessRootValidated = true;
|
nd->witnessRootValidated = true;
|
||||||
nMinimumHeight = SaplingWitnessMinimumHeight(*item.second.nullifier, nd->witnessHeight, nMinimumHeight);
|
nMinimumHeight = SaplingWitnessMinimumHeight(item.second.nullifier, nd->witnessHeight, nMinimumHeight);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
//root mismatch on the active chain -> desynced; fall through to rebuild below
|
//root mismatch on the active chain -> desynced; fall through to rebuild below
|
||||||
@@ -1298,7 +1316,7 @@ int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessO
|
|||||||
blockRoot = pblockindex->hashFinalSaplingRoot;
|
blockRoot = pblockindex->hashFinalSaplingRoot;
|
||||||
if (witnessRoot == blockRoot) {
|
if (witnessRoot == blockRoot) {
|
||||||
nd->witnessRootValidated = true;
|
nd->witnessRootValidated = true;
|
||||||
nMinimumHeight = SaplingWitnessMinimumHeight(*item.second.nullifier, nd->witnessHeight, nMinimumHeight);
|
nMinimumHeight = SaplingWitnessMinimumHeight(item.second.nullifier, nd->witnessHeight, nMinimumHeight);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1350,7 +1368,7 @@ int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessO
|
|||||||
}
|
}
|
||||||
nd->witnessHeight = pblockindex->GetHeight();
|
nd->witnessHeight = pblockindex->GetHeight();
|
||||||
UpdateSaplingNullifierNoteMapWithTx(wtxItem.second);
|
UpdateSaplingNullifierNoteMapWithTx(wtxItem.second);
|
||||||
nMinimumHeight = SaplingWitnessMinimumHeight(*item.second.nullifier, nd->witnessHeight, nMinimumHeight);
|
nMinimumHeight = SaplingWitnessMinimumHeight(item.second.nullifier, nd->witnessHeight, nMinimumHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1436,8 +1454,9 @@ void CWallet::BuildWitnessCache(const CBlockIndex* pindex, bool witnessOnly)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pwalletMain->fAbortRescan) {
|
if (pwalletMain->fAbortRescan) {
|
||||||
|
// Do NOT clear fRescanning here: a witness rebuild is not a rescan, and clearing it from
|
||||||
|
// this path desynchronises the flag from ScanForWalletTransactions, which owns it.
|
||||||
LogPrintf("%s: rescan aborted during witness rebuild\n", __func__);
|
LogPrintf("%s: rescan aborted during witness rebuild\n", __func__);
|
||||||
pwalletMain->fRescanning = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int h = pbi->GetHeight();
|
int h = pbi->GetHeight();
|
||||||
@@ -2813,6 +2832,15 @@ bool CWallet::SetHDSeedFromMnemonic(const std::string& phrase)
|
|||||||
|
|
||||||
bool CWallet::GetHDSeedForDerivation(HDSeed& seedOut) const
|
bool CWallet::GetHDSeedForDerivation(HDSeed& seedOut) const
|
||||||
{
|
{
|
||||||
|
// Single choke point for every HD derivation in the wallet. When the hdchain record could not
|
||||||
|
// be trusted at load time we do not know whether fMnemonicSeed should be true, and guessing
|
||||||
|
// wrong derives into an entirely different key tree -- so refuse rather than guess. Callers
|
||||||
|
// surface this as a clean error (z_getnewaddress, sendmany, shieldcoinbase) or skip
|
||||||
|
// (autoshield). Transparent address generation falls back to the legacy random-key path,
|
||||||
|
// because hdChain.seedFp stays null and so IsHDTransparentEnabled() is false.
|
||||||
|
if (fHDChainUnproven)
|
||||||
|
return false;
|
||||||
|
|
||||||
HDSeed stored;
|
HDSeed stored;
|
||||||
if (!GetHDSeed(stored))
|
if (!GetHDSeed(stored))
|
||||||
return false;
|
return false;
|
||||||
@@ -3425,6 +3453,12 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
|
|||||||
if(fZdebug)
|
if(fZdebug)
|
||||||
LogPrintf("%s: fUpdate=%d now=%li\n",__func__,fUpdate,nNow);
|
LogPrintf("%s: fUpdate=%d now=%li\n",__func__,fUpdate,nNow);
|
||||||
|
|
||||||
|
// fAbortRescan is sticky: nothing else in the tree ever clears it, so without this a single
|
||||||
|
// `abortrescan` RPC would disable every later scan AND every later BuildWitnessCache for the
|
||||||
|
// lifetime of the process (BuildWitnessCache bails on the same flag), freezing witness heights
|
||||||
|
// while the chain advances and progressively rendering notes unspendable.
|
||||||
|
pwalletMain->fAbortRescan = false;
|
||||||
|
pwalletMain->fLastRescanCompleted = false;
|
||||||
pwalletMain->fRescanning = true;
|
pwalletMain->fRescanning = true;
|
||||||
CBlockIndex* pindex = pindexStart;
|
CBlockIndex* pindex = pindexStart;
|
||||||
pwalletMain->rescanStartHeight = pindex->GetHeight();
|
pwalletMain->rescanStartHeight = pindex->GetHeight();
|
||||||
@@ -3439,6 +3473,51 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
|
|||||||
pwalletMain->rescanHeight = pindex ? pindex->GetHeight() : 0;
|
pwalletMain->rescanHeight = pindex ? pindex->GetHeight() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- interrupt checkpoint setup -------------------------------------------------------
|
||||||
|
// Where does the wallet currently believe it has scanned to? A checkpoint may only ever
|
||||||
|
// ADVANCE that point, and only if this scan is contiguous with it. The RPC entry points
|
||||||
|
// (rescan / importprivkey / z_importkey / z_importviewingkey) take a caller-supplied start
|
||||||
|
// height validated only against chainActive.Height(), so a scan can legitimately begin far
|
||||||
|
// ABOVE the persisted locator -- writing a checkpoint from such a scan would mark the
|
||||||
|
// skipped range as scanned and hide any funds in it.
|
||||||
|
CBlockIndex* pindexPersisted = NULL;
|
||||||
|
{
|
||||||
|
CWalletDB walletdb(strWalletFile, "r+", false);
|
||||||
|
CBlockLocator locPersisted;
|
||||||
|
if (walletdb.ReadBestBlock(locPersisted))
|
||||||
|
pindexPersisted = FindForkInGlobalIndex(chainActive, locPersisted);
|
||||||
|
}
|
||||||
|
const bool fMayCheckpoint = pindexPersisted != NULL &&
|
||||||
|
pindexStart->GetHeight() <= pindexPersisted->GetHeight() + 1;
|
||||||
|
if (!fMayCheckpoint) {
|
||||||
|
LogPrintf("%s: scan starts at %d but the wallet is persisted at %d; progress will NOT be "
|
||||||
|
"checkpointed on interrupt (a non-contiguous scan cannot safely advance the locator)\n",
|
||||||
|
__func__, pindexStart->GetHeight(),
|
||||||
|
pindexPersisted ? pindexPersisted->GetHeight() : -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist progress when the scan is cut short. `pindexStopped` is the block we were ABOUT to
|
||||||
|
// scan, so the last fully-processed block is its parent. Resume restarts AT the locator's own
|
||||||
|
// block (CChain::GetLocator pushes it first; FindForkInGlobalIndex returns it), giving one
|
||||||
|
// block of deliberate overlap -- idempotent, because AddToWallet only merges when the tx is
|
||||||
|
// already present. No witness work is needed: witnesses are re-derived from each note's own
|
||||||
|
// witnessHeight, and witnessRootValidated is in-memory-only so every note is revalidated
|
||||||
|
// against hashFinalSaplingRoot on the next start.
|
||||||
|
auto checkpointProgress = [&](const CBlockIndex* pindexStopped) {
|
||||||
|
if (!fMayCheckpoint || !pindexStopped || !pindexStopped->pprev)
|
||||||
|
return;
|
||||||
|
const CBlockIndex* pindexDone = pindexStopped->pprev;
|
||||||
|
if (pindexDone->GetHeight() <= pindexPersisted->GetHeight())
|
||||||
|
return; // never move the locator backwards
|
||||||
|
if (SetBestChainNoFlush(chainActive.GetLocator(pindexDone))) {
|
||||||
|
LogPrintf("%s: checkpointed scan progress at height %d\n", __func__, pindexDone->GetHeight());
|
||||||
|
} else {
|
||||||
|
LogPrintf("%s: FAILED to checkpoint scan progress at height %d; the scan will replay "
|
||||||
|
"from height %d on the next start\n", __func__, pindexDone->GetHeight(),
|
||||||
|
pindexPersisted->GetHeight());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
ShowProgress(_("Rescanning..."), 0); // show rescan progress in GUI as dialog or on splashscreen, if -rescan on startup
|
ShowProgress(_("Rescanning..."), 0); // show rescan progress in GUI as dialog or on splashscreen, if -rescan on startup
|
||||||
double dProgressStart = Checkpoints::GuessVerificationProgress(chainParams.Checkpoints(), pindex, false);
|
double dProgressStart = Checkpoints::GuessVerificationProgress(chainParams.Checkpoints(), pindex, false);
|
||||||
double dProgressTip = Checkpoints::GuessVerificationProgress(chainParams.Checkpoints(), chainActive.LastTip(), false);
|
double dProgressTip = Checkpoints::GuessVerificationProgress(chainParams.Checkpoints(), chainActive.LastTip(), false);
|
||||||
@@ -3447,15 +3526,20 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
|
|||||||
{
|
{
|
||||||
pwalletMain->rescanHeight = pindex->GetHeight();
|
pwalletMain->rescanHeight = pindex->GetHeight();
|
||||||
if(pwalletMain->fAbortRescan) {
|
if(pwalletMain->fAbortRescan) {
|
||||||
//TODO: should we update witness caches?
|
// The witness caches do NOT need updating here: on resume each note's witnesses are
|
||||||
LogPrintf("%s: Rescan aborted at block %d\n", pwalletMain->rescanHeight);
|
// re-derived from its own witnessHeight, independently of the locator, and
|
||||||
pwalletMain->fRescanning = false;
|
// witnessRootValidated is in-memory-only so a full validation pass runs next boot.
|
||||||
|
// What DOES need saving is the locator -- see the checkpoint below.
|
||||||
|
pwalletMain->fRescanning = false;
|
||||||
|
pwalletMain->fAbortRescan = false; // consume it; see the note at scan entry
|
||||||
|
LogPrintf("%s: Rescan aborted at block %d\n", __func__, pwalletMain->rescanHeight);
|
||||||
|
checkpointProgress(pindex);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
if (ShutdownRequested()) {
|
if (ShutdownRequested()) {
|
||||||
//TODO: should we update witness caches?
|
|
||||||
LogPrintf("%s: Rescan interrupted by shutdown request at block %d\n", pwalletMain->rescanHeight);
|
|
||||||
pwalletMain->fRescanning = false;
|
pwalletMain->fRescanning = false;
|
||||||
|
LogPrintf("%s: Rescan interrupted by shutdown request at block %d\n", __func__, pwalletMain->rescanHeight);
|
||||||
|
checkpointProgress(pindex);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3516,6 +3600,9 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
|
|||||||
|
|
||||||
// we are no longer rescanning
|
// we are no longer rescanning
|
||||||
pwalletMain->fRescanning = false;
|
pwalletMain->fRescanning = false;
|
||||||
|
// Reached only by running the loop to the end of the active chain. Callers use this to decide
|
||||||
|
// whether it is honest to record the wallet as scanned up to the tip.
|
||||||
|
pwalletMain->fLastRescanCompleted = true;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -833,6 +833,10 @@ public:
|
|||||||
bool fAutoShieldRunning = false;
|
bool fAutoShieldRunning = false;
|
||||||
|
|
||||||
std::atomic<bool> fAbortRescan{false};
|
std::atomic<bool> fAbortRescan{false};
|
||||||
|
//! True only when the last ScanForWalletTransactions ran to completion. An aborted or
|
||||||
|
//! shutdown-interrupted scan leaves this false, so callers must not record the wallet as
|
||||||
|
//! scanned up to the chain tip -- doing so makes the unscanned range permanently invisible.
|
||||||
|
bool fLastRescanCompleted = false;
|
||||||
// abort current rescan
|
// abort current rescan
|
||||||
void AbortRescan() { fAbortRescan = true; }
|
void AbortRescan() { fAbortRescan = true; }
|
||||||
// Are we currently aborting a rescan?
|
// Are we currently aborting a rescan?
|
||||||
@@ -892,6 +896,12 @@ public:
|
|||||||
protected:
|
protected:
|
||||||
|
|
||||||
int SaplingWitnessMinimumHeight(const uint256& nullifier, int nWitnessHeight, int nMinimumHeight);
|
int SaplingWitnessMinimumHeight(const uint256& nullifier, int nWitnessHeight, int nMinimumHeight);
|
||||||
|
//! Overload for a note whose nullifier may be unset. A note discovered through an imported
|
||||||
|
//! INCOMING viewing key has no nullifier (computing one needs the full viewing key), so
|
||||||
|
//! dereferencing the optional aborts the daemon. Treats such a note as unspent, which is the
|
||||||
|
//! conservative direction: it keeps the witness alive rather than pruning a note we cannot
|
||||||
|
//! prove spent.
|
||||||
|
int SaplingWitnessMinimumHeight(const boost::optional<uint256>& nullifier, int nWitnessHeight, int nMinimumHeight);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* pindex is the new tip being connected.
|
* pindex is the new tip being connected.
|
||||||
@@ -904,17 +914,22 @@ protected:
|
|||||||
*/
|
*/
|
||||||
void DecrementNoteWitnesses(const CBlockIndex* pindex);
|
void DecrementNoteWitnesses(const CBlockIndex* pindex);
|
||||||
|
|
||||||
|
//! Returns true only if the atomic write actually committed. Callers that checkpoint during a
|
||||||
|
//! long scan need to know: a silently-failing checkpoint would otherwise be retried forever at
|
||||||
|
//! full cost while never making progress.
|
||||||
template <typename WalletDB>
|
template <typename WalletDB>
|
||||||
void SetBestChainINTERNAL(WalletDB& walletdb, const CBlockLocator& loc) {
|
bool SetBestChainINTERNAL(WalletDB& walletdb, const CBlockLocator& loc) {
|
||||||
if (!walletdb.TxnBegin()) {
|
if (!walletdb.TxnBegin()) {
|
||||||
// This needs to be done atomically, so don't do it at all
|
// This needs to be done atomically, so don't do it at all
|
||||||
LogPrintf("SetBestChain(): Couldn't start atomic write\n");
|
LogPrintf("SetBestChain(): Couldn't start atomic write\n");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
LOCK(cs_wallet);
|
LOCK(cs_wallet);
|
||||||
for (std::pair<const uint256, CWalletTx>& wtxItem : mapWallet) {
|
for (std::pair<const uint256, CWalletTx>& wtxItem : mapWallet) {
|
||||||
auto wtx = wtxItem.second;
|
// By reference: a copy here deep-copies every note's witness deque
|
||||||
|
// (WITNESS_CACHE_SIZE entries) for every transaction, on every call.
|
||||||
|
const CWalletTx& wtx = wtxItem.second;
|
||||||
// We skip transactions for which mapSaplingNoteData
|
// We skip transactions for which mapSaplingNoteData
|
||||||
// is empty. This covers transactions that have no Sapling data
|
// is empty. This covers transactions that have no Sapling data
|
||||||
// (i.e. are purely transparent), as well as shielding and unshielding
|
// (i.e. are purely transparent), as well as shielding and unshielding
|
||||||
@@ -923,32 +938,33 @@ protected:
|
|||||||
if (!walletdb.WriteTx(wtxItem.first, wtx)) {
|
if (!walletdb.WriteTx(wtxItem.first, wtx)) {
|
||||||
LogPrintf("SetBestChain(): Failed to write CWalletTx, aborting atomic write\n");
|
LogPrintf("SetBestChain(): Failed to write CWalletTx, aborting atomic write\n");
|
||||||
walletdb.TxnAbort();
|
walletdb.TxnAbort();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!walletdb.WriteWitnessCacheSize(nWitnessCacheSize)) {
|
if (!walletdb.WriteWitnessCacheSize(nWitnessCacheSize)) {
|
||||||
LogPrintf("SetBestChain(): Failed to write nWitnessCacheSize, aborting atomic write\n");
|
LogPrintf("SetBestChain(): Failed to write nWitnessCacheSize, aborting atomic write\n");
|
||||||
walletdb.TxnAbort();
|
walletdb.TxnAbort();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (!walletdb.WriteBestBlock(loc)) {
|
if (!walletdb.WriteBestBlock(loc)) {
|
||||||
LogPrintf("SetBestChain(): Failed to write best block, aborting atomic write\n");
|
LogPrintf("SetBestChain(): Failed to write best block, aborting atomic write\n");
|
||||||
walletdb.TxnAbort();
|
walletdb.TxnAbort();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (const std::exception &exc) {
|
} catch (const std::exception &exc) {
|
||||||
// Unexpected failure
|
// Unexpected failure
|
||||||
LogPrintf("SetBestChain(): Unexpected error during atomic write:\n");
|
LogPrintf("SetBestChain(): Unexpected error during atomic write:\n");
|
||||||
LogPrintf("%s\n", exc.what());
|
LogPrintf("%s\n", exc.what());
|
||||||
walletdb.TxnAbort();
|
walletdb.TxnAbort();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
if (!walletdb.TxnCommit()) {
|
if (!walletdb.TxnCommit()) {
|
||||||
// Couldn't commit all to db, but in-memory state is fine
|
// Couldn't commit all to db, but in-memory state is fine
|
||||||
LogPrintf("SetBestChain(): Couldn't commit atomic write\n");
|
LogPrintf("SetBestChain(): Couldn't commit atomic write\n");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -961,6 +977,11 @@ protected:
|
|||||||
|
|
||||||
/* the hd chain data model (chain counters) */
|
/* the hd chain data model (chain counters) */
|
||||||
CHDChain hdChain;
|
CHDChain hdChain;
|
||||||
|
//! Set at load time when hdChain cannot be trusted to describe this wallet's HD seed -- the
|
||||||
|
//! record was missing, or its seedFp does not match the seed actually loaded. While true the
|
||||||
|
//! wallet is fully usable for existing keys but refuses to DERIVE new ones, because it cannot
|
||||||
|
//! tell which key tree it belongs to. Never serialized; recomputed on every load.
|
||||||
|
bool fHDChainUnproven = false;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/*
|
/*
|
||||||
@@ -1277,8 +1298,12 @@ public:
|
|||||||
void RunSaplingConsolidation(int blockHeight);
|
void RunSaplingConsolidation(int blockHeight);
|
||||||
void RunAutoShieldCoinbase(int blockHeight);
|
void RunAutoShieldCoinbase(int blockHeight);
|
||||||
bool CommitAutomatedTx(const CTransaction& tx);
|
bool CommitAutomatedTx(const CTransaction& tx);
|
||||||
/** Saves witness caches and best block locator to disk. */
|
/** Saves witness caches and best block locator to disk. Overrides CValidationInterface. */
|
||||||
void SetBestChain(const CBlockLocator& loc);
|
void SetBestChain(const CBlockLocator& loc);
|
||||||
|
/** As SetBestChain, but for use INSIDE a long scan: opens the wallet DB with fFlushOnClose=false
|
||||||
|
* so the call does not trigger a full BDB txn_checkpoint over the whole cache (see the comment
|
||||||
|
* at CWallet::SetBestChain), and reports whether the write actually committed. */
|
||||||
|
bool SetBestChainNoFlush(const CBlockLocator& loc);
|
||||||
std::set<std::pair<libzcash::PaymentAddress, uint256>> GetNullifiersForAddresses(const std::set<libzcash::PaymentAddress> & addresses);
|
std::set<std::pair<libzcash::PaymentAddress, uint256>> GetNullifiersForAddresses(const std::set<libzcash::PaymentAddress> & addresses);
|
||||||
bool IsNoteSaplingChange(const std::set<std::pair<libzcash::PaymentAddress, uint256>> & nullifierSet, const libzcash::PaymentAddress & address, const SaplingOutPoint & entry);
|
bool IsNoteSaplingChange(const std::set<std::pair<libzcash::PaymentAddress, uint256>> & nullifierSet, const libzcash::PaymentAddress & address, const SaplingOutPoint & entry);
|
||||||
|
|
||||||
@@ -1415,6 +1440,12 @@ public:
|
|||||||
void SetHDChain(const CHDChain& chain, bool memonly);
|
void SetHDChain(const CHDChain& chain, bool memonly);
|
||||||
const CHDChain& GetHDChain() const { return hdChain; }
|
const CHDChain& GetHDChain() const { return hdChain; }
|
||||||
|
|
||||||
|
//! Mark hdChain as untrustworthy for derivation (see the member's declaration). Set only by
|
||||||
|
//! CWalletDB::LoadWallet; there is deliberately no way to clear it short of reloading, so a
|
||||||
|
//! degraded wallet cannot be talked back into deriving without a real repair.
|
||||||
|
void SetHDChainUnproven() { fHDChainUnproven = true; }
|
||||||
|
bool IsHDChainUnproven() const { return fHDChainUnproven; }
|
||||||
|
|
||||||
/* Record (in memory and in wallet.dat) how this wallet's HD seed came to
|
/* Record (in memory and in wallet.dat) how this wallet's HD seed came to
|
||||||
exist. Best-effort: a failed write is logged, not fatal — the next start
|
exist. Best-effort: a failed write is logged, not fatal — the next start
|
||||||
simply re-classifies, and re-classification always errs toward
|
simply re-classifies, and re-classification always errs toward
|
||||||
|
|||||||
@@ -421,6 +421,15 @@ public:
|
|||||||
// True when that record had to be repaired on read (see the "hdchain" case
|
// True when that record had to be repaired on read (see the "hdchain" case
|
||||||
// in ReadKeyValue); LoadWallet rewrites it in full form afterwards.
|
// in ReadKeyValue); LoadWallet rewrites it in full form afterwards.
|
||||||
bool fHDChainRepaired;
|
bool fHDChainRepaired;
|
||||||
|
// True once an "hdchain" record was ENCOUNTERED, whether or not it parsed. This is what
|
||||||
|
// separates the two failure shapes: a v1.0.3-or-earlier -salvagewallet drops the record
|
||||||
|
// entirely (its IsKeyType has no "hdchain" case), so absent == salvaged and recoverable,
|
||||||
|
// while present-but-unreadable means wider file damage.
|
||||||
|
bool fHDChainSeen;
|
||||||
|
// Fingerprint taken from the KEY of the hdseed/chdseed record. Available even for an
|
||||||
|
// encrypted wallet, where the seed itself cannot be read at load time.
|
||||||
|
bool fHDSeedSeen;
|
||||||
|
uint256 hdSeedFpSeen;
|
||||||
|
|
||||||
CWalletScanState() {
|
CWalletScanState() {
|
||||||
nKeys = nCKeys = nKeyMeta = nZKeys = nCZKeys = nZKeyMeta = nSapZAddrs = 0;
|
nKeys = nCKeys = nKeyMeta = nZKeys = nCZKeys = nZKeyMeta = nSapZAddrs = 0;
|
||||||
@@ -429,6 +438,8 @@ public:
|
|||||||
nFileVersion = 0;
|
nFileVersion = 0;
|
||||||
fHDChainRead = false;
|
fHDChainRead = false;
|
||||||
fHDChainRepaired = false;
|
fHDChainRepaired = false;
|
||||||
|
fHDChainSeen = false;
|
||||||
|
fHDSeedSeen = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -836,6 +847,8 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
|||||||
strErr = "Error reading wallet database: LoadHDSeed failed";
|
strErr = "Error reading wallet database: LoadHDSeed failed";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
wss.fHDSeedSeen = true;
|
||||||
|
wss.hdSeedFpSeen = seedFp;
|
||||||
}
|
}
|
||||||
else if (strType == "chdseed")
|
else if (strType == "chdseed")
|
||||||
{
|
{
|
||||||
@@ -849,9 +862,15 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
wss.fIsEncrypted = true;
|
wss.fIsEncrypted = true;
|
||||||
|
// The fingerprint is the plaintext KEY of the chdseed record, so this works while
|
||||||
|
// the wallet is locked and the seed itself is unreadable.
|
||||||
|
wss.fHDSeedSeen = true;
|
||||||
|
wss.hdSeedFpSeen = seedFp;
|
||||||
}
|
}
|
||||||
else if (strType == "hdchain")
|
else if (strType == "hdchain")
|
||||||
{
|
{
|
||||||
|
// Record the ENCOUNTER before any parsing can fail.
|
||||||
|
wss.fHDChainSeen = true;
|
||||||
CHDChain chain;
|
CHDChain chain;
|
||||||
// Keep an untouched copy: a failed >> has already consumed part of ssValue.
|
// Keep an untouched copy: a failed >> has already consumed part of ssValue.
|
||||||
CDataStream ssRetry(ssValue.begin(), ssValue.end(), ssValue.GetType(), ssValue.GetVersion());
|
CDataStream ssRetry(ssValue.begin(), ssValue.end(), ssValue.GetType(), ssValue.GetVersion());
|
||||||
@@ -1046,23 +1065,60 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A wallet that holds an HD seed but whose hdchain record is missing or
|
// A wallet that holds an HD seed but no usable hdchain record cannot safely DERIVE: hdChain
|
||||||
// unreadable is NOT safe to run. hdChain would fall back to its SetNull
|
// falls back to its SetNull defaults, clearing fMnemonicSeed and so switching derivation from
|
||||||
// defaults (walletdb.h:105-113), which (a) clears fMnemonicSeed, switching
|
// the 64-byte BIP39 seed to the raw 32-byte entropy -- an entirely different key tree.
|
||||||
// HD derivation from the 64-byte BIP39 seed to the raw 32-byte entropy
|
//
|
||||||
// (CWallet::GetHDSeedForDerivation, wallet.cpp:2615-2633) -> an entirely
|
// saplingAccountCounter was previously listed here as a second hazard. It is not one:
|
||||||
// different key tree, and (b) resets saplingAccountCounter to 0, so the
|
// GenerateNewSaplingZKey loops `do {...} while (HaveSaplingSpendingKey(...))` and
|
||||||
// next GenerateNewSaplingZKey walks back over accounts that already exist.
|
// DeriveNewChildKey loops `while (HaveKey(...))`, so a counter that starts low walks forward
|
||||||
// Both are silent today (a bad hdchain read is only DB_NONCRITICAL_ERROR).
|
// past accounts that already exist rather than colliding with them.
|
||||||
// Fail loud instead of quietly deriving into the wrong tree.
|
//
|
||||||
if (pwallet->HaveHDSeed() && !wss.fHDChainRead)
|
// Two shapes reach here and only one is real corruption:
|
||||||
|
// (a) the record is ABSENT -- the signature of a -salvagewallet run by v1.0.3 or earlier,
|
||||||
|
// whose IsKeyType has no "hdchain" case, so salvage dropped it. The keys are intact.
|
||||||
|
// Refusing strands the wallet with no way back: the node aborts before the RPC server
|
||||||
|
// exists, and -mnemonic refuses a non-empty wallet, so there is no user-executable
|
||||||
|
// recovery path at all.
|
||||||
|
// (b) the record is PRESENT but unreadable -- wider file damage. Keep refusing.
|
||||||
|
if (pwallet->HaveHDSeed() && !wss.fHDChainRead && wss.fHDChainSeen)
|
||||||
{
|
{
|
||||||
LogPrintf("Error loading wallet.dat: HD seed present but the hdchain record is missing or corrupt. "
|
LogPrintf("Error loading wallet.dat: the hdchain record is present but unreadable. Your keys "
|
||||||
"Recover by restoring from the seed phrase: move wallet.dat aside and start with "
|
"are intact -- do NOT delete or replace wallet.dat. Back it up and see debug.log.\n");
|
||||||
"-mnemonic=\"<your seed phrase>\" -rescan\n");
|
|
||||||
return DB_CORRUPT;
|
return DB_CORRUPT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const char* pszDegraded = NULL;
|
||||||
|
if (pwallet->HaveHDSeed() && !wss.fHDChainRead)
|
||||||
|
{
|
||||||
|
pszDegraded = "the hdchain record is missing (an older -salvagewallet drops it)";
|
||||||
|
}
|
||||||
|
// A chain WAS read, but it does not belong to the seed we loaded. No legitimate writer can
|
||||||
|
// produce that -- InstallHDSeed always stores seedFp = seed.Fingerprint(). It is the mark
|
||||||
|
// of a wallet that lost its hdchain to an old salvage and was then USED on that old build,
|
||||||
|
// which persists a SetNull-derived chain carrying a null seedFp. Such a wallet otherwise
|
||||||
|
// starts up perfectly clean and derives into the WRONG TREE forever, silently -- strictly
|
||||||
|
// worse than failing to open, which is why it is worth detecting here.
|
||||||
|
else if (pwallet->HaveHDSeed() && wss.fHDSeedSeen &&
|
||||||
|
pwallet->GetHDChain().seedFp != wss.hdSeedFpSeen)
|
||||||
|
{
|
||||||
|
pszDegraded = "the hdchain record does not belong to this wallet's HD seed";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pszDegraded != NULL)
|
||||||
|
{
|
||||||
|
pwallet->SetHDChainUnproven();
|
||||||
|
LogPrintf("Wallet opened in DEGRADED mode: %s. Existing keys are intact, spendable and "
|
||||||
|
"receivable, but no NEW HD-derived key can be generated and new transparent "
|
||||||
|
"addresses will not be recoverable from a seed phrase. Back up wallet.dat now "
|
||||||
|
"and do NOT delete or replace it.\n", pszDegraded);
|
||||||
|
// An old salvage also dropped defaultkey and bestblock, which would make this look like
|
||||||
|
// a first run and skip the rescan, leaving a permanently zero balance.
|
||||||
|
SoftSetBoolArg("-rescan", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Any wallet corruption at all: skip any rewriting or
|
// Any wallet corruption at all: skip any rewriting or
|
||||||
// upgrading, we don't want to make it worse.
|
// upgrading, we don't want to make it worse.
|
||||||
if (result != DB_LOAD_OK)
|
if (result != DB_LOAD_OK)
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ fi
|
|||||||
|
|
||||||
DESC=""
|
DESC=""
|
||||||
SUFFIX=""
|
SUFFIX=""
|
||||||
if [ -e "$(which git 2>/dev/null)" -a "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]; then
|
# Allow the build system to supply the version when git metadata is unavailable:
|
||||||
|
# container builds exclude .git, and a linked worktree's .git file points outside
|
||||||
|
# the build context. Without this such builds are stamped "-unk".
|
||||||
|
if [ -n "${DRAGONX_BUILD_DESC:-}" ]; then
|
||||||
|
DESC="$DRAGONX_BUILD_DESC"
|
||||||
|
elif [ -e "$(which git 2>/dev/null)" -a "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]; then
|
||||||
# clean 'dirty' status of touched files that haven't been modified
|
# clean 'dirty' status of touched files that haven't been modified
|
||||||
git diff >/dev/null 2>/dev/null
|
git diff >/dev/null 2>/dev/null
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user