5 Commits

Author SHA1 Message Date
53b1fe332b Rebrand cleanups: getpeerinfo help example + 1.0.3 debian changelog entry
net.cpp: getpeerinfo help address example 18030->21768 and 'Hush server'->'DragonX server'. debian/changelog: prepend 1.0.3 release entry summarizing IBD speedups, witness fix, bulk streaming, seed phrases, assumeutxo removal. NOTE net.cpp change needs a daemon rebuild to surface in runtime RPC help. Staged on 176; not pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 06:35:46 +02:00
9c6ccbb726 Fix dragonx-cli -rpcport help default: 18030 (hush) -> 21769 (DragonX)
The -rpcport help string in bitcoin-cli.cpp hardcoded hush's 18030; the actual default (BaseParams().RPCPort()) is DragonX's 21769, so this was misleading help text only (the CLI already connects to 21769). Set to 21769 and regenerated doc/man/dragonx-cli.1 from the rebuilt binary. NOTE: a separate hush 18030 leftover remains in src/rpc/net.cpp:357 (getpeerinfo help example address) - daemon RPC help, out of scope here. Staged on 176; not pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 06:35:46 +02:00
34432e5848 Harvest DragonX packaging + legal artifacts from compliant-rebrand
Legal: correct GPLv3 LICENSE (fixes garbled 'GENERAL GENERAL'), AUTHORS DragonX attribution, COPYING. Packaging: man pages REGENERATED from the 1.0.3 binaries via help2man (dragonxd/dragonx-cli/dragonx-tx.1 -> v1.0.3, correct dates), wired into doc/man/Makefile.am (dist_man1_MANS), orphaned hush*.1 removed. Init/openrc/systemd scripts, Debian packaging (control/changelog/copyright rebranded hush->dragonx + install stubs), example confs taken from origin/compliant-rebrand (c05134e77). REMAINING follow-ups: (1) debian/changelog still tops at 1.0.0 - add a 1.0.3 entry; (2) dragonx-cli --help hardcodes rpcport default 18030 (hush) - fix the HelpMessage string in source then regen. Staged on 176 for review; not pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 06:35:46 +02:00
4caf2fc68f Add BIP39 seed phrases (SilentDragonXLite-compatible) and HD transparent keys
Derive transparent (t-addr) keys from the HD seed and add BIP39 mnemonic seed
phrases that are byte-for-byte compatible with SilentDragonXLite, so the same
24 words recover the same shielded and transparent addresses in either wallet.

HD transparent keys:
- Derive t-keys from the seed at m/44'/coin'/0'/0/i (were random CKeys).
- CHDChain gains a version-gated transparent counter; existing wallets load
  unchanged. GenerateNewKey routes through DeriveNewChildKey when enabled
  (-hdtransparent, default on).
- Restore from a seed hex via -hdseed with gap-limit pre-derivation; birthday
  pinned to genesis so the rescan is not clipped.

BIP39 seed phrases:
- Wire the vendored trezor BIP39 lib (src/crypto/bip39) into the build, fix its
  BIP39_WORDS guard, and disable the insecure mnemonic cache.
- Match SDXLite exactly: English wordlist, empty passphrase, PBKDF2 64-byte
  seed, coin type 141, ZIP-32 m/32'/141'/i' and BIP44 m/44'/141'/0'/0/i. Store
  the 32-byte entropy and expand to the 64-byte seed on demand.
- Restore via -mnemonic, create via -usemnemonic, reveal via z_exportmnemonic.

Verified by gtests including a known-answer BIP39 seed vector and z/t address
derivation checks (src/gtest/test_hdtransparent.cpp, test_mnemonic_compat.cpp).
Docs in doc/hd-transparent-keys.md and doc/seed-phrase.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 01:57:18 -05:00
84aefb5475 Remove assumeutxo / UTXO-snapshot feature
Removes the dumptxoutset RPC, -loadutxosnapshot / -loadutxosnapshotunsafe,
the CCoinsViewDB Dump/LoadSnapshot machinery + CUTXOSnapshotHeader, the
AssumeutxoData chainparams anchor, the LoadSnapshotChainstate activation +
reorg-below-H guard, the persisted assumeutxo-height flag, and the gtest.

Rationale: it duplicated the existing bootstrap (same skip-the-genesis-grind
fast-sync, no speed advantage), its only real edge was a trust model we don't
need for this chain, and it was inert anyway (no published snapshot hash in
chainparams). The -loadutxosnapshot load path adopted an external UTXO set and
bypassed genesis validation, so removing it also drops that attack surface.
Builds clean (no dangling references); the kept IBD speedups (RandomX
pre-verify, adaptive dbcache, tlsmanager) are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:42:37 -05:00
54 changed files with 1740 additions and 828 deletions

View File

@@ -1,3 +1,7 @@
# The DragonX Developers
Dan S https://git.dragonx.is/dan
# The Hush Developers # The Hush Developers
Duke Leto https://git.hush.is/duke https://github.com/leto Duke Leto https://git.hush.is/duke https://github.com/leto

View File

@@ -1,3 +1,4 @@
Copyright (c) 2024-2026 The DragonX developers
Copyright (c) 2018-2025 The Hush developers Copyright (c) 2018-2025 The Hush developers
Copyright (c) 2009-2017 The Bitcoin Core developers Copyright (c) 2009-2017 The Bitcoin Core developers
Copyright (c) 2009-2018 Bitcoin Developers Copyright (c) 2009-2018 Bitcoin Developers

26
LICENSE
View File

@@ -1,4 +1,4 @@
GENERAL GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
@@ -7,15 +7,15 @@
Preamble Preamble
The GENERAL General Public License is a free, copyleft license for The GNU General Public License is a free, copyleft license for
software and other kinds of works. software and other kinds of works.
The licenses for most software and other practical works are designed The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast, to take away your freedom to share and change the works. By contrast,
the GENERAL General Public License is intended to guarantee your freedom to the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the software for all its users. We, the Free Software Foundation, use the
GENERAL General Public License for most of our software; it applies also to GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to any other work released this way by its authors. You can apply it to
your programs, too. your programs, too.
@@ -37,7 +37,7 @@ freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they or can get the source code. And you must show them these terms so they
know their rights. know their rights.
Developers that use the GENERAL GPL protect your rights with two steps: Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License (1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it. giving you legal permission to copy, distribute and/or modify it.
@@ -72,7 +72,7 @@ modification follow.
0. Definitions. 0. Definitions.
"This License" refers to version 3 of the GENERAL General Public License. "This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks. works, such as semiconductor masks.
@@ -549,35 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program. License would be to refrain entirely from conveying the Program.
13. Use with the GENERAL Affero General Public License. 13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed permission to link or combine any covered work with a work licensed
under version 3 of the GENERAL Affero General Public License into a single under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work, License will continue to apply to the part which is the covered work,
but the special requirements of the GENERAL Affero General Public License, but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the section 13, concerning interaction through a network will apply to the
combination as such. combination as such.
14. Revised Versions of this License. 14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of The Free Software Foundation may publish revised and/or new versions of
the GENERAL General Public License from time to time. Such new versions will the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to be similar in spirit to the present version, but may differ in detail to
address new problems or concerns. address new problems or concerns.
Each version is given a distinguishing version number. If the Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GENERAL General Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the Foundation. If the Program does not specify a version number of the
GENERAL General Public License, you may choose any version ever published GNU General Public License, you may choose any version ever published
by the Free Software Foundation. by the Free Software Foundation.
If the Program specifies that a proxy can decide which future If the Program specifies that a proxy can decide which future
versions of the GENERAL General Public License can be used, that proxy's versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you public statement of acceptance of a version permanently authorizes you
to choose that version for the Program. to choose that version for the Program.

View File

@@ -1,3 +1,22 @@
dragonx (1.0.3) stable; urgency=medium
* IBD/sync speedups: parallel RandomX pre-verification, adaptive -dbcache, P2P download fixes
* Fix Sapling witness desync and parallelize witness cache rebuild
* Opt-in bulk block streaming (-bulkblocksync) for faster initial sync
* BIP39 seed phrases (SilentDragonXLite-compatible) and HD transparent keys
* Remove assumeutxo / UTXO-snapshot feature
-- DragonX <dan-s-dev@proton.me> Tue, 07 Jul 2026 05:49:59 +0200
dragonx (1.0.0) stable; urgency=medium
* Initial release of DragonX, forked from Hush Full Node
* Full legal-compliant rebrand: binaries, config, documentation
* RandomX proof-of-work, 36-second block time, fully shielded transactions
* New binary names: dragonxd, dragonx-cli, dragonx-tx
-- DragonX <dan-s-dev@proton.me> Mon, 03 Mar 2026 00:00:00 +0000
hush (3.10.5) stable; urgency=medium hush (3.10.5) stable; urgency=medium
* DragonX is no longer supported by this codebase * DragonX is no longer supported by this codebase

View File

@@ -1,18 +1,18 @@
Source: hush Source: dragonx
Section: utils Section: utils
Priority: optional Priority: optional
Maintainer: Hush <myhushteam@gmail.com> Maintainer: DragonX <dan-s-dev@proton.me>
Homepage: https://hush.is Homepage: https://dragonx.is
Build-Depends: autoconf, automake, bsdmainutils, build-essential, Build-Depends: autoconf, automake, bsdmainutils, build-essential,
cmake, curl, git, g++-multilib, libc6-dev, libsodium-dev, cmake, curl, git, g++-multilib, libc6-dev, libsodium-dev,
libtool, m4, ncurses-dev, pkg-config, python, libtool, m4, ncurses-dev, pkg-config, python,
unzip, wget, zlib1g-dev unzip, wget, zlib1g-dev
Vcs-Git: https://git.hush.is/hush/hush3.git Vcs-Git: https://git.dragonx.is/DragonX/dragonx.git
Vcs-Browser: https://git.hush.is/hush/hush3 Vcs-Browser: https://git.dragonx.is/DragonX/dragonx
Package: hush Package: dragonx
Architecture: amd64 arm64 Architecture: amd64 arm64
Depends: ${shlibs:Depends} Depends: ${shlibs:Depends}
Description: Cryptocoin full node for Hush Description: Privacy-focused cryptocurrency full node for DragonX
Speak And Transact Freely with Hush, which inherits from Bitcoin Protocol and DragonX is a privacy-focused cryptocurrency using RandomX proof-of-work.
Zcash Protocol and is focused on private communications. All transactions are shielded by default. Fork of the Hush Full Node.

View File

@@ -1,8 +1,9 @@
Files: * Files: *
Copyright: 2016-2026, The Hush developers Copyright: 2024-2026, The DragonX developers
2016-2026, The Hush developers
2009-2016, Bitcoin Core developers 2009-2016, Bitcoin Core developers
License: GPLv3 License: GPLv3
Comment: https://hush.is Comment: https://dragonx.is
Files: depends/sources/libsodium-*.tar.gz Files: depends/sources/libsodium-*.tar.gz
Copyright: 2013-2016 Frank Denis Copyright: 2013-2016 Frank Denis

View File

@@ -0,0 +1 @@
DEBIAN/examples/DRAGONX.conf

View File

@@ -0,0 +1,3 @@
usr/bin/dragonxd
usr/bin/dragonx-cli
usr/bin/dragonx-tx

View File

@@ -0,0 +1,3 @@
DEBIAN/manpages/dragonx-cli.1
DEBIAN/manpages/dragonx-tx.1
DEBIAN/manpages/dragonxd.1

View File

@@ -0,0 +1,209 @@
## DRAGONX.conf configuration file. Lines beginning with # are comments.
# Network-related settings:
# Run a regression test network
#regtest=0
# Run a test node (which means you can mine with no peers)
#testnode=1
#set a custom client name/user agent
#clientName=GoldenSandtrout
# Rescan from block height
#rescan=123
# Connect via a SOCKS5 proxy
#proxy=127.0.0.1:9050
# Automatically create Tor hidden service
#listenonion=1
#Use separate SOCKS5 proxy to reach peers via Tor hidden services
#onion=1.2.3.4:9050
# Only connect to nodes in network <net> (ipv4, ipv6, onion or i2p)"));
#onlynet=<net>
#Tor control port to use if onion listening enabled
#torcontrol=127.0.0.1:9051
# Bind to given address and always listen on it. Use [host]:port notation for IPv6
#bind=<addr>
# Bind to given address and allowlist peers connecting to it. Use [host]:port notation for IPv6
#allowbind=<addr>
##############################################################
## Quick Primer on addnode vs connect ##
## Let's say for instance you use addnode=4.2.2.4 ##
## addnode will connect you to and tell you about the ##
## nodes connected to 4.2.2.4. In addition it will tell ##
## the other nodes connected to it that you exist so ##
## they can connect to you. ##
## connect will not do the above when you 'connect' to it. ##
## It will *only* connect you to 4.2.2.4 and no one else.##
## ##
## So if you're behind a firewall, or have other problems ##
## finding nodes, add some using 'addnode'. ##
## ##
## If you want to stay private, use 'connect' to only ##
## connect to "trusted" nodes. ##
## ##
## If you run multiple nodes on a LAN, there's no need for ##
## all of them to open lots of connections. Instead ##
## 'connect' them all to one node that is port forwarded ##
## and has lots of connections. ##
## Thanks goes to [Noodle] on Freenode. ##
##############################################################
# Use as many addnode= settings as you like to connect to specific peers
#addnode=69.164.218.197
#addnode=10.0.0.2:8233
# Alternatively use as many connect= settings as you like to connect ONLY to specific peers
#connect=69.164.218.197
#connect=10.0.0.1:8233
# Listening mode, enabled by default except when 'connect' is being used
#listen=1
# Maximum number of inbound+outbound connections.
#maxconnections=
#
# JSON-RPC options (for controlling a running dragonxd process)
#
# server=1 tells node to accept JSON-RPC commands (set as default if not specified)
#server=1
# Bind to given address to listen for JSON-RPC connections. Use [host]:port notation for IPv6.
# This option can be specified multiple times (default: bind to all interfaces)
#rpcbind=<addr>
# You must set rpcuser and rpcpassword to secure the JSON-RPC api
# These will automatically be created for you
#rpcuser=user
#rpcpassword=supersecretpassword
# How many seconds node will wait for a complete RPC HTTP request.
# after the HTTP connection is established.
#rpcclienttimeout=30
# By default, only RPC connections from localhost are allowed.
# Specify as many rpcallowip= settings as you like to allow connections from other hosts,
# either as a single IPv4/IPv6 or with a subnet specification.
# NOTE: opening up the RPC port to hosts outside your local trusted network is NOT RECOMMENDED,
# because the rpcpassword is transmitted over the network unencrypted and also because anyone
# that can authenticate on the RPC port can steal your keys + take over the account running dragonxd
#rpcallowip=10.1.1.34/255.255.255.0
#rpcallowip=1.2.3.4/24
#rpcallowip=2001:db8:85a3:0:0:8a2e:370:7334/96
# Listen for RPC connections on this TCP port:
#rpcport=1234
# You can use dragonxd to send commands to dragonxd
# running on another host using this option:
#rpcconnect=127.0.0.1
# Transaction Fee
# Send transactions as zero-fee transactions if possible (default: 0)
#sendfreetransactions=0
# Create transactions that have enough fees (or priority) so they are likely to # begin confirmation within n blocks (default: 1).
# This setting is overridden by the -paytxfee option.
#txconfirmtarget=n
# Miscellaneous options
# Enable mining at startup
#gen=1
# Set the number of threads to be used for mining (-1 = all cores).
#genproclimit=1
# Specify a different Equihash solver (e.g. "tromp") to try to mine
# faster when gen=1.
#equihashsolver=default
# Pre-generate this many public/private key pairs, so wallet backups will be valid for
# both prior transactions and several dozen future transactions.
#keypool=100
# Pay an optional transaction fee every time you send a tx. Transactions with fees
# are more likely than free transactions to be included in generated blocks, so may
# be validated sooner. This setting does not affect private transactions created with
# 'z_sendmany'.
#paytxfee=0.00
#Rewind the chain to specific block height. This is useful for creating snapshots at a given block height.
#rewind=555
#Stop the chain a specific block height. This is useful for creating snapshots at a given block height.
#stopat=1000000
#Set an address to use as change address for all transactions. This value must be set to a 33 byte pubkey. All mined coins will also be sent to this address.
#pubkey=027dc7b5cfb5efca96674b45e9fda18df069d040b9fd9ff32c35df56005e330392
# Disable clearnet (ipv4 and ipv6) connections to this node
#clearnet=0
# Disable ipv4
#disableipv4=1
# Disable ipv6
#disableipv6=1
# Enable transaction index
#txindex=1
# Enable address index
#addressindex=1
# Enable timestamp index
#timestampindex=1
# Enable spent index
#spentindex=1
# Enable shielded stats index
#zindex=1
# Attempt to salvage a corrupt wallet
# salvagewallet=1
# Mine all blocks to this address (not good for your privacy and not recommended!)
# Disallowed if clearnet=0
# mineraddress=XXX
# Disable wallet
#disablewallet=1
# Allow mining to an address that is not in the current wallet
#minetolocalwallet=0
# Delete all wallet transactions
#zapwallettxes=1
# Enable sapling consolidation
# consolidation=1
# Enable stratum server
# stratum=1
# Run a command each time a new block is seen
# %s in command is replaced by block hash
#blocknotify=/my/awesome/script.sh %s
# Run a command when wallet gets a new tx
# %s in command is replaced with txid
#walletnotify=/my/cool/script.sh %s
# Run a command when tx expires
# %s in command is replaced with txid
#txexpirynotify=/my/elite/script.sh %s
# Execute this commend to send a tx
# %s is replaced with tx hex
#txsend=/send/it.sh %s

View File

@@ -0,0 +1,59 @@
description "Hush Daemon"
start on runlevel [2345]
stop on starting rc RUNLEVEL=[016]
env HUSHD_BIN="/usr/bin/dragonxd"
env HUSHD_USER="hush"
env HUSHD_GROUP="hush"
env HUSHD_PIDDIR="/var/run/dragonxd"
# upstart can't handle variables constructed with other variables
env HUSHD_PIDFILE="/var/run/dragonxd/dragonxd.pid"
env HUSHD_CONFIGFILE="/etc/hush/hush.conf"
env HUSHD_DATADIR="/var/lib/dragonxd"
expect fork
respawn
respawn limit 5 120
kill timeout 60
pre-start script
# this will catch non-existent config files
# dragonxd will check and exit with this very warning, but it can do so
# long after forking, leaving upstart to think everything started fine.
# since this is a commonly encountered case on install, just check and
# warn here.
if ! grep -qs '^rpcpassword=' "$HUSHD_CONFIGFILE" ; then
echo "ERROR: You must set a secure rpcpassword to run dragonxd."
echo "The setting must appear in $HUSHD_CONFIGFILE"
echo
echo "This password is security critical to securing wallets "
echo "and must not be the same as the rpcuser setting."
echo "You can generate a suitable random password using the following"
echo "command from the shell:"
echo
echo "bash -c 'tr -dc a-zA-Z0-9 < /dev/urandom | head -c32 && echo'"
echo
exit 1
fi
mkdir -p "$HUSHD_PIDDIR"
chmod 0755 "$HUSHD_PIDDIR"
chown $HUSHD_USER:$HUSHD_GROUP "$HUSHD_PIDDIR"
chown $HUSHD_USER:$HUSHD_GROUP "$HUSHD_CONFIGFILE"
chmod 0660 "$HUSHD_CONFIGFILE"
end script
exec start-stop-daemon \
--start \
--pidfile "$HUSHD_PIDFILE" \
--chuid $HUSHD_USER:$HUSHD_GROUP \
--exec "$HUSHD_BIN" \
-- \
-pid="$HUSHD_PIDFILE" \
-conf="$HUSHD_CONFIGFILE" \
-datadir="$HUSHD_DATADIR" \
-disablewallet \
-daemon

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
#
# dragonxd The hush core server.
#
#
# chkconfig: 345 80 20
# description: dragonxd
# processname: dragonxd
#
# Source function library.
. /etc/init.d/functions
# you can override defaults in /etc/sysconfig/dragonxd, see below
if [ -f /etc/sysconfig/dragonxd ]; then
. /etc/sysconfig/dragonxd
fi
RETVAL=0
prog=dragonxd
# you can override the lockfile via HUSHD_LOCKFILE in /etc/sysconfig/dragonxd
lockfile=${HUSHD_LOCKFILE-/var/lock/subsys/dragonxd}
# dragonxd defaults to /usr/bin/dragonxd, override with HUSHD_BIN
dragonxd=${HUSHD_BIN-/usr/bin/dragonxd}
# dragonxd opts default to -disablewallet, override with HUSHD_OPTS
dragonxd_opts=${HUSHD_OPTS--disablewallet}
start() {
echo -n $"Starting $prog: "
daemon $DAEMONOPTS $dragonxd $dragonxd_opts
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && touch $lockfile
return $RETVAL
}
stop() {
echo -n $"Stopping $prog: "
killproc $prog
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && rm -f $lockfile
return $RETVAL
}
case "$1" in
start)
start
;;
stop)
stop
;;
status)
status $prog
;;
restart)
stop
start
;;
*)
echo "Usage: service $prog {start|stop|status|restart}"
exit 1
;;
esac

View File

@@ -0,0 +1,87 @@
#!/sbin/runscript
# backward compatibility for existing gentoo layout
#
if [ -d "/var/lib/hush/.hush" ]; then
HUSHD_DEFAULT_DATADIR="/var/lib/hush/.hush"
else
HUSHD_DEFAULT_DATADIR="/var/lib/dragonxd"
fi
HUSHD_CONFIGFILE=${HUSHD_CONFIGFILE:-/etc/hush/hush.conf}
HUSHD_PIDDIR=${HUSHD_PIDDIR:-/var/run/dragonxd}
HUSHD_PIDFILE=${HUSHD_PIDFILE:-${HUSHD_PIDDIR}/dragonxd.pid}
HUSHD_DATADIR=${HUSHD_DATADIR:-${HUSHD_DEFAULT_DATADIR}}
HUSHD_USER=${HUSHD_USER:-${HUSH_USER:-hush}}
HUSHD_GROUP=${HUSHD_GROUP:-hush}
HUSHD_BIN=${HUSHD_BIN:-/usr/bin/dragonxd}
HUSHD_NICE=${HUSHD_NICE:-${NICELEVEL:-0}}
HUSHD_OPTS="${HUSHD_OPTS:-${HUSH_OPTS}}"
name="Hush Full Node Daemon"
description="Hush cryptocurrency P2P network daemon"
command="/usr/bin/dragonxd"
command_args="-pid=\"${HUSHD_PIDFILE}\" \
-conf=\"${HUSHD_CONFIGFILE}\" \
-datadir=\"${HUSHD_DATADIR}\" \
-daemon \
${HUSHD_OPTS}"
required_files="${HUSHD_CONFIGFILE}"
start_stop_daemon_args="-u ${HUSHD_USER} \
-N ${HUSHD_NICE} -w 2000"
pidfile="${HUSHD_PIDFILE}"
# The retry schedule to use when stopping the daemon. Could be either
# a timeout in seconds or multiple signal/timeout pairs (like
# "SIGKILL/180 SIGTERM/300")
retry="${HUSHD_SIGTERM_TIMEOUT}"
depend() {
need localmount net
}
# verify
# 1) that the datadir exists and is writable (or create it)
# 2) that a directory for the pid exists and is writable
# 3) ownership and permissions on the config file
start_pre() {
checkpath \
-d \
--mode 0750 \
--owner "${HUSHD_USER}:${HUSHD_GROUP}" \
"${HUSHD_DATADIR}"
checkpath \
-d \
--mode 0755 \
--owner "${HUSHD_USER}:${HUSHD_GROUP}" \
"${HUSHD_PIDDIR}"
checkpath -f \
-o ${HUSHD_USER}:${HUSHD_GROUP} \
-m 0660 \
${HUSHD_CONFIGFILE}
checkconfig || return 1
}
checkconfig()
{
if ! grep -qs '^rpcpassword=' "${HUSHD_CONFIGFILE}" ; then
eerror ""
eerror "ERROR: You must set a secure rpcpassword to run dragonxd."
eerror "The setting must appear in ${HUSHD_CONFIGFILE}"
eerror ""
eerror "This password is security critical to securing wallets "
eerror "and must not be the same as the rpcuser setting."
eerror "You can generate a suitable random password using the following"
eerror "command from the shell:"
eerror ""
eerror "bash -c 'tr -dc a-zA-Z0-9 < /dev/urandom | head -c32 && echo'"
eerror ""
eerror ""
return 1
fi
}

View File

@@ -0,0 +1,33 @@
# /etc/conf.d/dragonxd: config file for /etc/init.d/dragonxd
# Config file location
#HUSHD_CONFIGFILE="/etc/hush/hush.conf"
# What directory to write pidfile to? (created and owned by $HUSHD_USER)
#HUSHD_PIDDIR="/var/run/dragonxd"
# What filename to give the pidfile
#HUSHD_PIDFILE="${HUSHD_PIDDIR}/dragonxd.pid"
# Where to write dragonxd data (be mindful that the blockchain is large)
#HUSHD_DATADIR="/var/lib/dragonxd"
# User and group to own dragonxd process
#HUSHD_USER="hush"
#HUSHD_GROUP="hush"
# Path to dragonxd executable
#HUSHD_BIN="/usr/bin/dragonxd"
# Nice value to run dragonxd under
#HUSHD_NICE=0
# Additional options (avoid -conf and -datadir, use flags above)
HUSHD_OPTS="-disablewallet"
# The timeout in seconds OpenRC will wait for dragonxd to terminate
# after a SIGTERM has been raised.
# Note that this will be mapped as argument to start-stop-daemon's
# '--retry' option, which means you can specify a retry schedule
# here. For more information see man 8 start-stop-daemon.
HUSHD_SIGTERM_TIMEOUT=60

View File

@@ -0,0 +1,22 @@
[Unit]
Description=Hush: Speak And Transact Freely
After=network.target
[Service]
User=hush
Group=hush
Type=forking
PIDFile=/var/lib/dragonxd/dragonxd.pid
ExecStart=/usr/bin/dragonxd -daemon -pid=/var/lib/dragonxd/dragonxd.pid \
-conf=/etc/hush/hush.conf -datadir=/var/lib/dragonxd -disablewallet
Restart=always
PrivateTmp=true
TimeoutStopSec=60s
TimeoutStartSec=2s
StartLimitInterval=120s
StartLimitBurst=5
[Install]
WantedBy=multi-user.target

7
doc/beefy-DRAGONX.conf Normal file
View File

@@ -0,0 +1,7 @@
rpcuser=dontuseweakusernameoryougetrobbed
rpcpassword=dontuseweakpasswordoryougetrobbed
txindex=1
server=1
rpcworkqueue=64
addnode=1.2.3.4
addnode=5.6.7.8

29
doc/dragonxd-systemd.md Normal file
View File

@@ -0,0 +1,29 @@
# Systemd script for the DragonX daemon
## Set it up
First set it up as follows:
* Copy dragonxd.service to the systemd user directory, which is /usr/lib/systemd/user directory
## Basic Usage
How to start the script:
`systemctl start --user dragonxd.service`
How to stop the script:
`systemctl stop --user dragonxd.service`
How to restart the script:
`systemctl restart --user dragonxd.service`
## How to watch it as it starts
Use the following on most Linux distros:
`watch systemctl status --user dragonxd.service`
Or watch the log directly:
`tail -f ~/.hush/DRAGONX/debug.log`
## Troubleshooting
* Don't run it with sudo or root, or it won't work with the wallet.

9
doc/dragonxd.service Normal file
View File

@@ -0,0 +1,9 @@
[Unit]
Description=DragonX daemon
After=network.target
[Service]
ExecStart=/usr/bin/dragonxd
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,87 @@
# HD transparent keys
DragonX derives **transparent** (t-address) keys deterministically from the
wallet's HD seed, so they can be recovered from the seed alone — the same way
Sapling (shielded) keys already are.
## Derivation
Transparent keys are derived over secp256k1 using BIP32/BIP44:
```
m / 44' / coin_type' / 0' / 0 / i
```
* `coin_type` is `Params().BIP44CoinType()`**141** on mainnet, **1** on
test/regtest.
* Account is fixed at `0'` and the chain at `0` (external). The internal/change
chain (`1`) is **not** used: on this `ac_private=1` chain a non-coinbase
transparent output is consensus-invalid, so transparent change can never carry
value.
* `i` is `CHDChain.transparentChildCounter`, a monotonic index persisted in the
wallet so the same addresses regenerate after a seed-only restore.
Each derived key records its `hdKeypath` and the seed fingerprint (`seedFp`) in
its `CKeyMetadata`, matching the Sapling scheme.
## Why this matters on a private chain
On DragonX (`ac_private=1` from genesis) a normal user can never *receive* to a
transparent address — inbound t-payments are rejected by consensus. The only
thing that legitimately lands spendable value on a t-address is a **mining
coinbase** (plus notary/burn special cases). There is no "coinbase must be
shielded" rule, so mature coinbase is directly spendable.
So HD transparent keys exist to let a **miner recover coinbase rewards** that
were paid to wallet-derived t-addresses, using only the seed.
## Enabling / disabling
Controlled by `-hdtransparent` (default **on**). When on and the wallet has an
HD seed, every newly generated transparent key (receive address, change,
coinbase payout drawn from the keypool) is HD-derived.
```
-hdtransparent=0 # keep the legacy behaviour (random transparent keys)
```
## Backing up and restoring
* **Back up the seed.** `z_exportwallet <file>` writes the 32-byte HD seed as a
`# HDSeed=<hex>` line. Guard this value like a private key.
* **Restore into a fresh/empty wallet** by starting the node with:
```
-hdseed=<64-hex-character seed>
-hdtransparentgaplimit=<n> # HD transparent keys to pre-derive (default 1000)
```
On restore the node injects the seed, pre-derives `n` transparent keys with a
genesis birthday, and the normal startup rescan finds any coinbase paid to
them. Raise `-hdtransparentgaplimit` if the wallet minted more than `n`
distinct coinbase addresses.
> **Warning:** passing `-hdseed` on the command line exposes the seed to your
> shell history and the process list. Prefer putting it in `DRAGONX.conf` with
> tight file permissions, and remove it after the restore completes.
## Limitations (read before relying on recovery)
* **Legacy random keys are not recoverable.** Any transparent key created before
this feature (or with `-hdtransparent=0`) came from the CSPRNG, not the seed,
and the phrase/seed will **not** regenerate it. Keep `wallet.dat` /
`dumpwallet` backups for those. A wallet that predates the feature and then
enables it becomes a *mix* of random (old) and HD (new) keys.
* **Gap limit.** A rescan only discovers keys already present in the wallet.
Restore pre-derives `-hdtransparentgaplimit` keys; coinbase paid to an index
beyond that window is not found until you derive further and rescan again.
* **Scope.** Recovers transparent **coinbase** value only, per the consensus
rules above. Shielded funds are recovered separately via the Sapling HD keys.
## On-disk compatibility
The transparent counter is stored in `CHDChain` under a new serialization
version (`VERSION_HD_TRANSPARENT = 2`). Existing v1 `wallet.dat` records load
unchanged (the counter defaults to 0); the record is rewritten as v2 the first
time an HD transparent key is derived. Downgrading a v2 wallet to an older
binary is not supported.

View File

@@ -1 +1 @@
dist_man1_MANS=hushd.1 hush-cli.1 hush-tx.1 dist_man1_MANS=dragonxd.1 dragonx-cli.1 dragonx-tx.1

View File

@@ -1,21 +1,21 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1.
.TH HUSH-CLI "1" "March 2026" "hush-cli v3.10.5" "User Commands" .TH DRAGONX "1" "July 2026" "DragonX RPC client version v1.0.3-95aeaed0c-dirty" "User Commands"
.SH NAME .SH NAME
hush-cli \- manual page for hush-cli v3.10.5 DragonX \- manual page for DragonX RPC client version v1.0.3-95aeaed0c-dirty
.SH DESCRIPTION .SH DESCRIPTION
Hush RPC client version v3.10.5\-04916cdf5 DragonX RPC client version v1.0.3\-95aeaed0c\-dirty
.PP .PP
In order to ensure you are adequately protecting your privacy when using Hush, In order to ensure you are adequately protecting your privacy when using
please see <https://hush.is/security/>. DragonX, please see <https://dragonx.is/security/>.
.SS "Usage:" .SS "Usage:"
.TP .TP
hush\-cli [options] <command> [params] dragonx\-cli [options] <command> [params]
Send command to Hush Send command to DragonX
.TP .TP
hush\-cli [options] help dragonx\-cli [options] help
List commands List commands
.TP .TP
hush\-cli [options] help <command> dragonx\-cli [options] help <command>
Get help for a command Get help for a command
.SH OPTIONS .SH OPTIONS
.HP .HP
@@ -25,7 +25,7 @@ This help message
.HP .HP
\fB\-conf=\fR<file> \fB\-conf=\fR<file>
.IP .IP
Specify configuration file (default: HUSH3.conf) Specify configuration file (default: DRAGONX.conf)
.HP .HP
\fB\-datadir=\fR<dir> \fB\-datadir=\fR<dir>
.IP .IP
@@ -47,7 +47,7 @@ Send commands to node running on <ip> (default: 127.0.0.1)
.HP .HP
\fB\-rpcport=\fR<port> \fB\-rpcport=\fR<port>
.IP .IP
Connect to JSON\-RPC on <port> (default: 18030 ) Connect to JSON\-RPC on <port> (default: 21769 )
.HP .HP
\fB\-rpcwait\fR \fB\-rpcwait\fR
.IP .IP
@@ -70,20 +70,25 @@ Timeout in seconds during HTTP requests, or 0 for no timeout. (default:
.IP .IP
Read extra arguments from standard input, one per line until EOF/Ctrl\-D Read extra arguments from standard input, one per line until EOF/Ctrl\-D
(recommended for sensitive information such as passphrases) (recommended for sensitive information such as passphrases)
.PP
In order to ensure you are adequately protecting your privacy when using
DragonX, please see <https://dragonx.is/security/>.
.SH COPYRIGHT .SH COPYRIGHT
Copyright \(co 2024\-2026 The DragonX Developers
In order to ensure you are adequately protecting your privacy when using Hush, .PP
please see <https://hush.is/security/>. .br
Copyright \(co 2016\-2024 Duke Leto and The Hush Developers
Copyright (C) 2016-2026 Duke Leto and The Hush Developers .PP
.br
Copyright (C) 2016-2020 jl777 and SuperNET developers Copyright \(co 2016\-2020 jl777 and SuperNET developers
.PP
Copyright (C) 2016-2018 The Zcash developers .br
Copyright \(co 2016\-2018 The Zcash developers
Copyright (C) 2009-2014 The Bitcoin Core developers .PP
.br
Copyright \(co 2009\-2014 The Bitcoin Core developers
.PP
This is experimental Free Software! Fuck Yeah!!!!! This is experimental Free Software! Fuck Yeah!!!!!
.PP
Distributed under the GPLv3 software license, see the accompanying file COPYING Distributed under the GPLv3 software license, see the accompanying file COPYING
or <https://www.gnu.org/licenses/gpl-3.0.en.html>. or <https://www.gnu.org/licenses/gpl\-3.0.en.html>.

View File

@@ -1,9 +1,9 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1.
.TH HUSH-TX "1" "March 2026" "hush-tx v3.10.5" "User Commands" .TH DRAGONX-TX "1" "July 2026" "dragonx-tx v1.0.3-4caf2fc68" "User Commands"
.SH NAME .SH NAME
hush-tx \- manual page for hush-tx v3.10.5 dragonx-tx \- DragonX transaction utility
.SH DESCRIPTION .SH DESCRIPTION
hush\-tx utility version v3.10.5\-04916cdf5 hush\-tx utility version v1.0.3\-4caf2fc68
.SS "Usage:" .SS "Usage:"
.TP .TP
hush\-tx [options] <hex\-tx> [commands] hush\-tx [options] <hex\-tx> [commands]
@@ -84,20 +84,3 @@ Load JSON file FILENAME into register NAME
set=NAME:JSON\-STRING set=NAME:JSON\-STRING
.IP .IP
Set register NAME to given JSON\-STRING Set register NAME to given JSON\-STRING
.SH COPYRIGHT
In order to ensure you are adequately protecting your privacy when using Hush,
please see <https://hush.is/security/>.
Copyright (C) 2016-2026 Duke Leto and The Hush Developers
Copyright (C) 2016-2020 jl777 and SuperNET developers
Copyright (C) 2016-2018 The Zcash developers
Copyright (C) 2009-2014 The Bitcoin Core developers
This is experimental Free Software! Fuck Yeah!!!!!
Distributed under the GPLv3 software license, see the accompanying file COPYING
or <https://www.gnu.org/licenses/gpl-3.0.en.html>.

View File

@@ -1,16 +1,16 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1.
.TH HUSHD "1" "March 2026" "hushd v3.10.5" "User Commands" .TH DRAGONX "1" "July 2026" "DragonX Daemon version v1.0.3-4caf2fc68" "User Commands"
.SH NAME .SH NAME
hushd \- manual page for hushd v3.10.5 DragonX \- manual page for DragonX Daemon version v1.0.3-4caf2fc68
.SH DESCRIPTION .SH DESCRIPTION
Hush Daemon version v3.10.5\-04916cdf5 DragonX Daemon version v1.0.3\-4caf2fc68
.PP .PP
In order to ensure you are adequately protecting your privacy when using Hush, In order to ensure you are adequately protecting your privacy when using
please see <https://hush.is/security/>. DragonX, please see <https://dragonx.is/security/>.
.SS "Usage:" .SS "Usage:"
.TP .TP
hushd [options] dragonxd [options]
Start a Hush Daemon Start DragonX Daemon
.SH OPTIONS .SH OPTIONS
.HP .HP
\-? \-?
@@ -32,11 +32,11 @@ How thorough the block verification of \fB\-checkblocks\fR is (0\-4, default: 3)
.HP .HP
\fB\-clientname=\fR<SomeName> \fB\-clientname=\fR<SomeName>
.IP .IP
Full node client name, default 'GoldenSandtrout' Full node client name, default 'DragonX'
.HP .HP
\fB\-conf=\fR<file> \fB\-conf=\fR<file>
.IP .IP
Specify configuration file (default: HUSH3.conf) Specify configuration file (default: DRAGONX.conf)
.HP .HP
\fB\-daemon\fR \fB\-daemon\fR
.IP .IP
@@ -52,7 +52,11 @@ Specify directory to be used when exporting data
.HP .HP
\fB\-dbcache=\fR<n> \fB\-dbcache=\fR<n>
.IP .IP
Set database cache size in megabytes (4 to 16384, default: 512) Set database cache size in megabytes (4 to 16384). Default: adaptive \-
uses most free RAM to speed up initial block download (far fewer
UTXO flushes to disk) and automatically shrinks if other
applications need memory, always leaving a reserve free. Setting
a fixed value disables adaptive sizing.
.HP .HP
\fB\-loadblock=\fR<file> \fB\-loadblock=\fR<file>
.IP .IP
@@ -78,9 +82,15 @@ applied)
.HP .HP
\fB\-par=\fR<n> \fB\-par=\fR<n>
.IP .IP
Set the number of script verification threads (\fB\-32\fR to 16, 0 = auto, <0 = Set the number of script verification threads (\fB\-4\fR to 16, 0 = auto, <0 =
leave that many cores free, default: 0) leave that many cores free, default: 0)
.HP .HP
\fB\-randomxverifythreads=\fR<n>
.IP
Number of threads for parallel RandomX PoW pre\-verification of
post\-checkpoint blocks during sync (0 = inline only, max 16,
default: same as \fB\-par\fR)
.HP
\fB\-pid=\fR<file> \fB\-pid=\fR<file>
.IP .IP
Specify pid file (default: hushd.pid) Specify pid file (default: hushd.pid)
@@ -337,6 +347,40 @@ Do not load the wallet and disable wallet RPC calls
.IP .IP
Set key pool size to <n> (default: 100) Set key pool size to <n> (default: 100)
.HP .HP
\fB\-hdtransparent\fR
.IP
Derive transparent addresses from the HD seed so they can be recovered
from it (default: 1)
.HP
\fB\-hdseed=\fR<hex>
.IP
Restore a fresh/empty wallet from a 32\- or 64\-byte HD seed hex (the
value shown in z_exportwallet's '# HDSeed=' line). WARNING:
exposes the seed to your shell history and process list.
.HP
\fB\-mnemonic=\fR<words>
.IP
Restore/create a fresh/empty wallet from a BIP39 seed phrase, compatible
with SilentDragonXLite (English, no passphrase). WARNING: exposes
the phrase to your shell history and process list; prefer
DRAGONX.conf with tight permissions.
.HP
\fB\-usemnemonic\fR
.IP
Create new wallets from a fresh BIP39 seed phrase so the 24 words can be
exported (z_exportmnemonic) and used in SilentDragonXLite
(default: 0)
.HP
\fB\-hdtransparentgaplimit=\fR<n>
.IP
On \fB\-mnemonic\fR/\-hdseed restore, pre\-derive this many HD transparent keys
so a rescan can find coinbase paid to them (default: 1000)
.HP
\fB\-mnemonicsaplinggap=\fR<n>
.IP
On \fB\-mnemonic\fR/\-hdseed restore, pre\-derive this many shielded (Sapling)
addresses so a rescan can find notes sent to them (default: 100)
.HP
\fB\-consolidation\fR \fB\-consolidation\fR
.IP .IP
Enable auto Sapling note consolidation (default: false) Enable auto Sapling note consolidation (default: false)
@@ -649,8 +693,8 @@ multiple times (default: bind to all interfaces)
.HP .HP
\fB\-stratumport=\fR<port> \fB\-stratumport=\fR<port>
.IP .IP
Listen for Stratum work requests on <port> (default: 19031 or testnet: Listen for Stratum work requests on <port> (default: 22769 or testnet:
19031) 22769)
.HP .HP
\fB\-stratumallowip=\fR<ip> \fB\-stratumallowip=\fR<ip>
.IP .IP
@@ -659,7 +703,7 @@ single IP (e.g. 1.2.3.4), a network/netmask (e.g.
1.2.3.4/255.255.255.0) or a network/CIDR (e.g. 1.2.3.4/24). This 1.2.3.4/255.255.255.0) or a network/CIDR (e.g. 1.2.3.4/24). This
option can be specified multiple times option can be specified multiple times
.PP .PP
Hush Arrakis Chain options: DragonX Chain options:
.HP .HP
\fB\-ac_algo\fR \fB\-ac_algo\fR
.IP .IP
@@ -760,20 +804,25 @@ Starting supply, default is 10
\fB\-ac_txpow\fR \fB\-ac_txpow\fR
.IP .IP
Enforce transaction\-rate limit, default 0 Enforce transaction\-rate limit, default 0
.PP
In order to ensure you are adequately protecting your privacy when using
DragonX, please see <https://dragonx.is/security/>.
.SH COPYRIGHT .SH COPYRIGHT
Copyright \(co 2024\-2026 The DragonX Developers
In order to ensure you are adequately protecting your privacy when using Hush, .PP
please see <https://hush.is/security/>. .br
Copyright \(co 2016\-2024 Duke Leto and The Hush Developers
Copyright (C) 2016-2026 Duke Leto and The Hush Developers .PP
.br
Copyright (C) 2016-2020 jl777 and SuperNET developers Copyright \(co 2016\-2020 jl777 and SuperNET developers
.PP
Copyright (C) 2016-2018 The Zcash developers .br
Copyright \(co 2016\-2018 The Zcash developers
Copyright (C) 2009-2014 The Bitcoin Core developers .PP
.br
Copyright \(co 2009\-2014 The Bitcoin Core developers
.PP
This is experimental Free Software! Fuck Yeah!!!!! This is experimental Free Software! Fuck Yeah!!!!!
.PP
Distributed under the GPLv3 software license, see the accompanying file COPYING Distributed under the GPLv3 software license, see the accompanying file COPYING
or <https://www.gnu.org/licenses/gpl-3.0.en.html>. or <https://www.gnu.org/licenses/gpl\-3.0.en.html>.

90
doc/seed-phrase.md Normal file
View File

@@ -0,0 +1,90 @@
# BIP39 seed phrases (SilentDragonXLite-compatible)
DragonX full-node wallets can be created from and restored to a **BIP39 24-word
seed phrase** that is **byte-for-byte compatible with SilentDragonXLite**: the
same words produce the same transparent (t-) and shielded (z-) addresses in
either wallet, so funds move between the light wallet and the full node with one
backup.
## What makes them compatible
Compatibility requires the mnemonic, the seed derivation, and every HD path to
match exactly. They do:
| Detail | Value (both wallets) |
|---|---|
| Word list | BIP39 English, 2048 words |
| Passphrase | empty (no "25th word") |
| Mnemonic → seed | PBKDF2-HMAC-SHA512, 2048 rounds, salt `"mnemonic"`, 64-byte output |
| Coin type | 141 (KMD SLIP-0044) |
| Shielded path | `m/32'/141'/i'` (ZIP-32) |
| Transparent path | `m/44'/141'/0'/0/i` (BIP44) |
The node stores the 32-byte BIP39 **entropy** (SilentDragonXLite's on-disk
convention) and expands it to the 64-byte seed on demand for derivation. The
node's vendored BIP39 library (`src/crypto/bip39`) is byte-identical to
SilentDragonXLite's `tiny-bip39` 0.6.2, and the derivation is anchored by a
known-answer test (`src/gtest/test_mnemonic_compat.cpp`).
## Restore from a phrase
Start the node once, on a **fresh/empty datadir**, with the phrase:
```
dragonxd -mnemonic="word1 word2 ... word24"
```
or, preferably (keeps the phrase out of your shell history and process list),
put it in `DRAGONX.conf` with tight permissions:
```
mnemonic=word1 word2 ... word24
```
On restore the node pre-derives keys and rescans from genesis to recover funds:
* `-hdtransparentgaplimit=<n>` — HD transparent keys to pre-derive (default 1000)
* `-mnemonicsaplinggap=<n>` — shielded addresses to pre-derive (default 100)
Raise these if the wallet used many addresses. Restore only works on a wallet
with no seed yet (a brand-new datadir); it refuses to overwrite an existing seed.
## Create a new phrase on the node
By default new node wallets use a random (non-mnemonic) seed. To create a new
wallet from a fresh 24-word phrase instead — so you can export it and use it in
SilentDragonXLite — start with:
```
dragonxd -usemnemonic
```
## Show / back up the phrase
For a mnemonic wallet (created with `-usemnemonic` or restored with `-mnemonic`):
```
dragonx-cli z_exportmnemonic
```
returns the 24 words and the seed fingerprint. The wallet must be unlocked.
Guard the phrase like a private key.
## Limitations
* **English + empty passphrase only.** Any other word list or a BIP39 passphrase
would break compatibility, so they are not accepted.
* **Legacy / random-seed wallets have no phrase.** A wallet created before this
feature (or without `-usemnemonic`) has a random seed; `z_exportmnemonic`
returns an error for it — use `z_exportwallet` to back up the raw seed. Such
wallets are not SilentDragonXLite-compatible.
* **Scope.** Recovers HD-derived shielded funds and transparent coinbase (see
[hd-transparent-keys.md](hd-transparent-keys.md) for why only coinbase lands on
t-addresses on this `ac_private=1` chain). Keys imported with `z_importkey` are
not seed-derived and are not recovered by the phrase.
## On-disk compatibility
Mnemonic wallets set `CHDChain` version 3 (`VERSION_HD_MNEMONIC`). Older wallet
records load unchanged. Downgrading a mnemonic wallet to an older binary is not
supported.

View File

@@ -325,6 +325,7 @@ libbitcoin_wallet_a_SOURCES = \
wallet/asyncrpcoperation_shieldcoinbase.cpp \ wallet/asyncrpcoperation_shieldcoinbase.cpp \
wallet/crypter.cpp \ wallet/crypter.cpp \
wallet/db.cpp \ wallet/db.cpp \
wallet/mnemonic.cpp \
zcash/Note.cpp \ zcash/Note.cpp \
transaction_builder.cpp \ transaction_builder.cpp \
wallet/rpcdump.cpp \ wallet/rpcdump.cpp \
@@ -361,6 +362,23 @@ crypto_libbitcoin_crypto_a_SOURCES = \
crypto/sha512.cpp \ crypto/sha512.cpp \
crypto/sha512.h crypto/sha512.h
# Vendored trezor-crypto BIP39 (mnemonic seed phrases). Kept self-contained so
# the same 24 words are compatible with SilentDragonXLite (tiny-bip39 0.6.2).
crypto_libbitcoin_crypto_a_SOURCES += \
crypto/bip39/bip39.c \
crypto/bip39/bip39.h \
crypto/bip39/bip39_english.h \
crypto/bip39/pbkdf2.c \
crypto/bip39/pbkdf2.h \
crypto/bip39/hmac.c \
crypto/bip39/hmac.h \
crypto/bip39/sha2.c \
crypto/bip39/sha2.h \
crypto/bip39/memzero.c \
crypto/bip39/memzero.h \
crypto/bip39/options.h \
crypto/bip39/rand.h
if EXPERIMENTAL_ASM if EXPERIMENTAL_ASM
crypto_libbitcoin_crypto_a_SOURCES += crypto/sha256_sse4.cpp crypto_libbitcoin_crypto_a_SOURCES += crypto/sha256_sse4.cpp
endif endif

View File

@@ -7,12 +7,13 @@ bin_PROGRAMS += hush-gtest
# NOTE: the original test list used an invalid automake form (comment after a trailing # NOTE: the original test list used an invalid automake form (comment after a trailing
# backslash, and `zcash_gtest_SOURCES +=` with no prior `=`), which is why the whole # backslash, and `zcash_gtest_SOURCES +=` with no prior `=`), which is why the whole
# gtest harness was disabled via a `#include`. Minimal valid set: the harness + the # gtest harness was disabled via a `#include`. Minimal valid set: the harness + the
# UTXO-snapshot round-trip test. Re-add other gtest sources here as they are revived. # Re-add other gtest sources here as they are revived.
hush_gtest_SOURCES = \ hush_gtest_SOURCES = \
gtest/main.cpp \ gtest/main.cpp \
gtest/utils.cpp \ gtest/utils.cpp \
gtest/test_utxosnapshot.cpp \ gtest/test_randomx_preverify.cpp \
gtest/test_randomx_preverify.cpp gtest/test_hdtransparent.cpp \
gtest/test_mnemonic_compat.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)

View File

@@ -53,7 +53,7 @@ std::string HelpMessageCli()
strUsage += HelpMessageOpt("-regtest", _("Enter regression test mode, which uses a special chain in which blocks can be " strUsage += HelpMessageOpt("-regtest", _("Enter regression test mode, which uses a special chain in which blocks can be "
"solved instantly. This is intended for regression testing tools and app development.")); "solved instantly. This is intended for regression testing tools and app development."));
strUsage += HelpMessageOpt("-rpcconnect=<ip>", strprintf(_("Send commands to node running on <ip> (default: %s)"), "127.0.0.1")); strUsage += HelpMessageOpt("-rpcconnect=<ip>", strprintf(_("Send commands to node running on <ip> (default: %s)"), "127.0.0.1"));
strUsage += HelpMessageOpt("-rpcport=<port>", strprintf(_("Connect to JSON-RPC on <port> (default: %u )"), 18030)); strUsage += HelpMessageOpt("-rpcport=<port>", strprintf(_("Connect to JSON-RPC on <port> (default: %u )"), 21769));
strUsage += HelpMessageOpt("-rpcwait", _("Wait for RPC server to start")); strUsage += HelpMessageOpt("-rpcwait", _("Wait for RPC server to start"));
strUsage += HelpMessageOpt("-rpcuser=<user>", _("Username for JSON-RPC connections")); strUsage += HelpMessageOpt("-rpcuser=<user>", _("Username for JSON-RPC connections"));
strUsage += HelpMessageOpt("-rpcpassword=<pw>", _("Password for JSON-RPC connections")); strUsage += HelpMessageOpt("-rpcpassword=<pw>", _("Password for JSON-RPC connections"));

View File

@@ -69,17 +69,6 @@ public:
double fTransactionsPerDay; double fTransactionsPerDay;
}; };
/** Trusted UTXO-snapshot (assumeutxo-style) anchor. When `hash` is set, a node loading a
* snapshot via -loadutxosnapshot must produce exactly this content hash at this height,
* otherwise the snapshot is refused. Null hash = not configured (loading requires the
* explicit -loadutxosnapshotunsafe override, e.g. for regtest/testing). Mirrors the
* hardcoded-checkpoint trust model. */
struct AssumeutxoData {
int height;
uint256 hash;
bool IsNull() const { return hash.IsNull(); }
};
enum Bech32Type { enum Bech32Type {
SAPLING_PAYMENT_ADDRESS, SAPLING_PAYMENT_ADDRESS,
SAPLING_FULL_VIEWING_KEY, SAPLING_FULL_VIEWING_KEY,
@@ -116,7 +105,6 @@ public:
const std::string& Bech32HRP(Bech32Type type) const { return bech32HRPs[type]; } const std::string& Bech32HRP(Bech32Type type) const { return bech32HRPs[type]; }
const std::vector<uint8_t>& FixedSeeds() const { return vFixedSeeds; } const std::vector<uint8_t>& FixedSeeds() const { return vFixedSeeds; }
const CCheckpointData& Checkpoints() const { return checkpointData; } const CCheckpointData& Checkpoints() const { return checkpointData; }
const AssumeutxoData& Assumeutxo() const { return assumeutxoData; }
/** Return the founder's reward address and script for a given block height */ /** Return the founder's reward address and script for a given block height */
std::string GetFoundersRewardAddressAtHeight(int height) const; std::string GetFoundersRewardAddressAtHeight(int height) const;
CScript GetFoundersRewardScriptAtHeight(int height) const; CScript GetFoundersRewardScriptAtHeight(int height) const;
@@ -156,7 +144,6 @@ protected:
bool fMineBlocksOnDemand = false; bool fMineBlocksOnDemand = false;
bool fTestnetToBeDeprecatedFieldRPC = false; bool fTestnetToBeDeprecatedFieldRPC = false;
CCheckpointData checkpointData; CCheckpointData checkpointData;
AssumeutxoData assumeutxoData; // null by default; set per-network in chainparams.cpp once a snapshot hash is published
std::vector<std::string> vFoundersRewardAddress; std::vector<std::string> vFoundersRewardAddress;
}; };

View File

@@ -33,10 +33,13 @@
#include "rand.h" #include "rand.h"
#include "sha2.h" #include "sha2.h"
#if USE_BIP39_CACHE // BIP39_WORDS is used unconditionally by the wordlist helpers below, so it must
// be defined even when the BIP39 cache is disabled (upstream places it inside
// the cache block by mistake).
int BIP39_WORDS = 2048; int BIP39_WORDS = 2048;
#if USE_BIP39_CACHE
static int bip39_cache_index = 0; static int bip39_cache_index = 0;
static CONFIDENTIAL struct { static CONFIDENTIAL struct {

View File

@@ -56,8 +56,10 @@
#endif #endif
// implement BIP39 caching // implement BIP39 caching
// Disabled: caching keeps the plaintext mnemonic/passphrase/seed in a static
// process-lifetime buffer, which we do not want in a wallet daemon.
#ifndef USE_BIP39_CACHE #ifndef USE_BIP39_CACHE
#define USE_BIP39_CACHE 1 #define USE_BIP39_CACHE 0
#define BIP39_CACHE_SIZE 4 #define BIP39_CACHE_SIZE 4
#endif #endif

View File

@@ -0,0 +1,171 @@
// Copyright (c) 2016-2024 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
//
// Tests for HD-derived transparent keys (m/44'/coin'/0'/0/i) and the
// version-gated CHDChain serialization used to persist the transparent counter.
#include <gtest/gtest.h>
#include "key.h"
#include "chainparams.h"
#include "streams.h"
#include "uint256.h"
#include "util.h"
#include "version.h"
#include "wallet/wallet.h"
#include "wallet/walletdb.h"
#include "zcash/zip32.h"
// Build an in-memory wallet with a known seed + hdChain so that the
// HD-transparent path (IsHDTransparentEnabled) is active.
static void LoadSeedForTest(CWallet& wallet, const HDSeed& seed)
{
wallet.LoadHDSeed(seed);
CHDChain chain;
chain.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
chain.seedFp = seed.Fingerprint();
chain.nCreateTime = 1;
wallet.SetHDChain(chain, true /* memonly */);
}
// Same seed must reproduce the same transparent addresses in the same order:
// this is the recovery guarantee that lets a seed-only restore find coinbase.
TEST(hdtransparent_tests, DeterministicFromSeed)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed rawSeed(HD_WALLET_SEED_LENGTH, 0x42);
HDSeed seed(rawSeed);
std::vector<CKeyID> keysA;
{
CWallet wallet;
LoadSeedForTest(wallet, seed);
ASSERT_TRUE(wallet.IsHDTransparentEnabled());
LOCK(wallet.cs_wallet);
for (int i = 0; i < 5; i++) {
CPubKey pk = wallet.GenerateNewKey();
keysA.push_back(pk.GetID());
const CKeyMetadata& md = wallet.mapKeyMetadata[pk.GetID()];
EXPECT_EQ(md.seedFp, seed.Fingerprint());
EXPECT_EQ(md.hdKeypath, std::string("m/44'/141'/0'/0/") + std::to_string(i));
}
}
// Fresh wallet, same seed -> identical keys.
{
CWallet wallet;
LoadSeedForTest(wallet, seed);
LOCK(wallet.cs_wallet);
for (int i = 0; i < 5; i++) {
CPubKey pk = wallet.GenerateNewKey();
EXPECT_EQ(pk.GetID(), keysA[i]);
}
}
}
// Pin the exact derivation path so it can never silently change.
TEST(hdtransparent_tests, KnownDerivationPath)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed rawSeed(HD_WALLET_SEED_LENGTH, 0x42);
HDSeed seed(rawSeed);
// Independently derive m/44'/141'/0'/0/0.
RawHDSeed raw = seed.RawSeed();
CExtKey m, purpose, coinType, account, external, child;
m.SetMaster(raw.data(), raw.size());
m.Derive(purpose, 44 | BIP32_HARDENED_KEY_LIMIT);
purpose.Derive(coinType, 141 | BIP32_HARDENED_KEY_LIMIT);
coinType.Derive(account, 0 | BIP32_HARDENED_KEY_LIMIT);
account.Derive(external, 0);
external.Derive(child, 0);
CKeyID expected = child.key.GetPubKey().GetID();
CWallet wallet;
LoadSeedForTest(wallet, seed);
LOCK(wallet.cs_wallet);
CPubKey pk = wallet.GenerateNewKey();
EXPECT_EQ(pk.GetID(), expected);
}
// A pre-existing v1 CHDChain record (no transparent counter) must still
// deserialize under v2 code, leaving transparentChildCounter at 0; and a v2
// record must round-trip the counter.
TEST(hdtransparent_tests, HDChainVersionCompat)
{
CHDChain v1;
v1.nVersion = CHDChain::VERSION_HD_BASE; // 1: transparentChildCounter not serialized
v1.seedFp = uint256S("0000000000000000000000000000000000000000000000000000000000000001");
v1.nCreateTime = 12345;
v1.saplingAccountCounter = 7;
CDataStream ss(SER_DISK, PROTOCOL_VERSION);
ss << v1;
CHDChain out; // default-constructed: SetNull() zeroes transparentChildCounter
ss >> out;
EXPECT_EQ(out.nVersion, +CHDChain::VERSION_HD_BASE); // unary + -> rvalue, avoid ODR-use of static const
EXPECT_EQ(out.seedFp, v1.seedFp);
EXPECT_EQ(out.nCreateTime, (int64_t)12345);
EXPECT_EQ(out.saplingAccountCounter, (uint32_t)7);
EXPECT_EQ(out.transparentChildCounter, (uint32_t)0);
CHDChain v2;
v2.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
v2.saplingAccountCounter = 3;
v2.transparentChildCounter = 42;
CDataStream ss2(SER_DISK, PROTOCOL_VERSION);
ss2 << v2;
CHDChain out2;
ss2 >> out2;
EXPECT_EQ(out2.nVersion, +CHDChain::VERSION_HD_TRANSPARENT);
EXPECT_EQ(out2.saplingAccountCounter, (uint32_t)3);
EXPECT_EQ(out2.transparentChildCounter, (uint32_t)42);
}
// Restoring from a 32-byte seed hex reproduces the same keys as the source
// wallet, and refuses to run when a seed already exists.
TEST(hdtransparent_tests, RestoreFromSeedHex)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed rawSeed(HD_WALLET_SEED_LENGTH, 0x7a);
HDSeed seed(rawSeed);
std::string seedHex = HexStr(seed.RawSeed());
// Source wallet: derive some keys.
std::vector<CKeyID> expected;
{
CWallet wallet;
LoadSeedForTest(wallet, seed);
LOCK(wallet.cs_wallet);
for (int i = 0; i < 3; i++)
expected.push_back(wallet.GenerateNewKey().GetID());
}
// Restored wallet: inject the seed hex, pre-derive, and compare.
{
CWallet wallet;
ASSERT_TRUE(wallet.SetHDSeedFromHex(seedHex));
// Second attempt must fail: a seed already exists.
EXPECT_FALSE(wallet.SetHDSeedFromHex(seedHex));
wallet.TopUpHDTransparentKeys(3, 1);
LOCK(wallet.cs_wallet);
for (int i = 0; i < 3; i++)
EXPECT_TRUE(wallet.HaveKey(expected[i]));
}
// Bad input is rejected.
{
CWallet wallet;
EXPECT_FALSE(wallet.SetHDSeedFromHex("nothex"));
EXPECT_FALSE(wallet.SetHDSeedFromHex("abcd")); // too short
}
}

View File

@@ -0,0 +1,141 @@
// Copyright (c) 2016-2024 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
//
// Proves that a BIP39 seed phrase produces the SAME transparent and shielded
// addresses on the DragonX full node as in SilentDragonXLite. The proof chain:
// phrase -> entropy (round-trip) -> 64-byte BIP39 seed (known-answer)
// -> z/t addresses (wallet path == direct ZIP-32/BIP44 derivation).
// The 64-byte seed is anchored to the well-known BIP39 value for the all-zero
// "abandon...art" entropy with an EMPTY passphrase, which is exactly what
// SilentDragonXLite's tiny-bip39 0.6.2 feeds into the same coin_type=141 paths.
#include <gtest/gtest.h>
#include "chainparams.h"
#include "key.h"
#include "key_io.h"
#include "util.h"
#include "wallet/mnemonic.h"
#include "wallet/wallet.h"
#include "zcash/Address.hpp"
#include "zcash/zip32.h"
// The canonical 24-word phrase for 32 bytes of all-zero entropy.
static const char* ABANDON_ART =
"abandon abandon abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon abandon abandon art";
// The standard BIP39 seed for that phrase with an EMPTY passphrase
// (PBKDF2-HMAC-SHA512, 2048 rounds, salt "mnemonic"). Matches tiny-bip39.
static const char* SEED64_HEX =
"408b285c123836004f4b8842c89324c1f01382450c0d439af345ba7fc49acf70"
"5489c6fc77dbd4e3dc1dd8cc6bc9f043db8ada1e243c4a0eafb290d399480840";
// First shielded address for a 64-byte seed: m/32'/141'/0' default address.
static std::string DeriveZAddrFromSeed64(RawHDSeed seed64)
{
HDSeed s(seed64);
auto m = libzcash::SaplingExtendedSpendingKey::Master(s);
auto xsk = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT)
.Derive(141 | ZIP32_HARDENED_KEY_LIMIT)
.Derive(0 | ZIP32_HARDENED_KEY_LIMIT);
return EncodePaymentAddress(xsk.DefaultAddress());
}
// First transparent address for a BIP32 master over `seedBytes`:
// m/44'/141'/0'/0/0.
static std::string DeriveTAddrFromSeedBytes(RawHDSeed seedBytes)
{
CExtKey master, purpose, coinType, account, external, child;
master.SetMaster(seedBytes.data(), seedBytes.size());
master.Derive(purpose, 44 | BIP32_HARDENED_KEY_LIMIT);
purpose.Derive(coinType, 141 | BIP32_HARDENED_KEY_LIMIT);
coinType.Derive(account, 0 | BIP32_HARDENED_KEY_LIMIT);
account.Derive(external, 0);
external.Derive(child, 0);
return EncodeDestination(child.key.GetPubKey().GetID());
}
// The 64-byte seed derived from the mnemonic must equal the known BIP39 value.
// This is the cross-wallet anchor: SilentDragonXLite feeds the identical seed.
TEST(mnemonic_compat, Bip39SeedKnownAnswer)
{
RawHDSeed entropy(32, 0);
RawHDSeed seed64;
ASSERT_TRUE(Bip39SeedFromEntropy(entropy, seed64));
ASSERT_EQ(seed64.size(), (size_t)64);
EXPECT_EQ(HexStr(seed64.begin(), seed64.end()), std::string(SEED64_HEX));
}
TEST(mnemonic_compat, EntropyPhraseRoundTrip)
{
RawHDSeed zeros(32, 0);
std::string phrase;
ASSERT_TRUE(EntropyToMnemonic(zeros, phrase));
EXPECT_EQ(phrase, std::string(ABANDON_ART));
EXPECT_TRUE(MnemonicIsValid(ABANDON_ART));
RawHDSeed entropy;
ASSERT_TRUE(MnemonicToEntropy(ABANDON_ART, entropy));
EXPECT_EQ(entropy.size(), (size_t)32);
EXPECT_EQ(HexStr(entropy.begin(), entropy.end()), std::string(64, '0'));
// Bad checksum / unknown words are rejected.
EXPECT_FALSE(MnemonicIsValid("abandon abandon abandon"));
EXPECT_FALSE(MnemonicIsValid("clearly not valid bip39 words at all here"));
RawHDSeed junk;
EXPECT_FALSE(MnemonicToEntropy("clearly not valid bip39 words at all here", junk));
}
// The wallet's mnemonic derivation must reproduce the exact addresses obtained
// by driving ZIP-32 / BIP44 directly from the known 64-byte seed, and must be
// deterministic across wallets.
TEST(mnemonic_compat, WalletDerivesSdxliteAddresses)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed zeros(32, 0), seed64;
ASSERT_TRUE(Bip39SeedFromEntropy(zeros, seed64));
const std::string expZ = DeriveZAddrFromSeed64(seed64);
const std::string expT = DeriveTAddrFromSeedBytes(seed64);
EXPECT_EQ(expZ.substr(0, 2), "zs"); // sapling HRP for mainnet
CWallet wallet;
ASSERT_TRUE(wallet.SetHDSeedFromMnemonic(ABANDON_ART));
ASSERT_TRUE(wallet.IsMnemonicSeed());
{
LOCK(wallet.cs_wallet);
EXPECT_EQ(EncodePaymentAddress(wallet.GenerateNewSaplingZKey()), expZ);
EXPECT_EQ(EncodeDestination(wallet.GenerateNewKey().GetID()), expT);
}
// Same phrase, fresh wallet -> identical first addresses.
CWallet wallet2;
ASSERT_TRUE(wallet2.SetHDSeedFromMnemonic(ABANDON_ART));
{
LOCK(wallet2.cs_wallet);
EXPECT_EQ(EncodePaymentAddress(wallet2.GenerateNewSaplingZKey()), expZ);
EXPECT_EQ(EncodeDestination(wallet2.GenerateNewKey().GetID()), expT);
}
// The phrase round-trips out of the wallet.
std::string exported;
ASSERT_TRUE(wallet.GetMnemonicPhrase(exported));
EXPECT_EQ(exported, std::string(ABANDON_ART));
}
// Negative: feeding the 32-byte entropy DIRECTLY as the seed (the classic
// interop bug) must produce a different address than the 64-byte BIP39 seed.
TEST(mnemonic_compat, RawEntropyDiffersFromMnemonicSeed)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed zeros(32, 0), seed64;
ASSERT_TRUE(Bip39SeedFromEntropy(zeros, seed64));
const std::string seedT = DeriveTAddrFromSeedBytes(seed64); // correct (64-byte)
const std::string entropyT = DeriveTAddrFromSeedBytes(zeros); // wrong (32-byte)
EXPECT_NE(seedT, entropyT);
}

View File

@@ -1,203 +0,0 @@
// Copyright (c) 2024-2026 The DragonX developers
// Distributed under the GPLv3 software license, see the accompanying
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
//
// Round-trip tests for the trusted UTXO snapshot (assumeutxo-style) dump/load core
// (CCoinsViewDB::DumpSnapshot / LoadSnapshot). This exercises the highest-risk part of
// the feature in isolation: that coins, Sapling commitment trees, the nullifier set, the
// best block and the best Sapling anchor survive a serialize -> hash -> deserialize cycle
// exactly, and that integrity/trust verification rejects tampered or wrong-hash snapshots.
#include <gtest/gtest.h>
#include <boost/filesystem.hpp>
#include "chainparams.h"
#include "coins.h"
#include "txdb.h"
#include "script/script.h"
#include "uint256.h"
#include "zcash/IncrementalMerkleTree.hpp"
namespace {
// Populate an in-memory chainstate DB directly via BatchWrite (mirrors how blocks persist
// coins/anchors/nullifiers), so DumpSnapshot has a realistic mixed state to serialize.
void PopulateChainstate(CCoinsViewDB &db, const uint256 &bestBlock,
uint256 &anchorRootOut, const uint256 &nullifierIn)
{
// One unspent transparent output.
CCoinsMap mapCoins;
{
uint256 txid = uint256S("0xaa00000000000000000000000000000000000000000000000000000000000001");
CCoinsCacheEntry &e = mapCoins[txid];
e.coins.fCoinBase = false;
e.coins.nVersion = 1;
e.coins.nHeight = 100;
e.coins.vout.resize(1);
e.coins.vout[0].nValue = 12345;
e.coins.vout[0].scriptPubKey = CScript() << OP_TRUE;
e.flags = CCoinsCacheEntry::DIRTY;
}
// One Sapling commitment tree (anchor), keyed by its root.
SaplingMerkleTree tree;
tree.append(uint256S("0xbb00000000000000000000000000000000000000000000000000000000000002"));
anchorRootOut = tree.root();
CAnchorsSaplingMap mapSaplingAnchors;
{
CAnchorsSaplingCacheEntry &e = mapSaplingAnchors[anchorRootOut];
e.entered = true;
e.tree = tree;
e.flags = CAnchorsSaplingCacheEntry::DIRTY;
}
// One spent Sapling nullifier.
CNullifiersMap mapSaplingNullifiers;
{
CNullifiersCacheEntry &e = mapSaplingNullifiers[nullifierIn];
e.entered = true;
e.flags = CNullifiersCacheEntry::DIRTY;
}
CAnchorsSproutMap mapSproutAnchors; // empty
CNullifiersMap mapSproutNullifiers; // empty
ASSERT_TRUE(db.BatchWrite(mapCoins, bestBlock, uint256(), anchorRootOut,
mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers));
}
CUTXOSnapshotHeader MakeHeader(const uint256 &bestBlock, const uint256 &bestAnchor)
{
CUTXOSnapshotHeader h;
h.nMagic = UTXO_SNAPSHOT_MAGIC;
h.nVersion = UTXO_SNAPSHOT_VERSION;
memcpy(&h.nNetworkMagic, Params().MessageStart(), 4);
h.baseBlockHash = bestBlock;
h.nHeight = 100;
h.nChainTx = 1;
h.fHasChainSaplingValue = 1;
h.nChainSaplingValue = 999;
h.bestSaplingAnchor = bestAnchor;
return h;
}
} // namespace
TEST(UTXOSnapshot, RoundTripPreservesChainstate)
{
SelectParams(CBaseChainParams::REGTEST);
const uint256 bestBlock = uint256S("0xff00000000000000000000000000000000000000000000000000000000000009");
const uint256 nullifier = uint256S("0xcc00000000000000000000000000000000000000000000000000000000000003");
CCoinsViewDB src(1 << 20, true); // in-memory
uint256 anchorRoot;
PopulateChainstate(src, bestBlock, anchorRoot, nullifier);
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
CUTXOSnapshotHeader header = MakeHeader(bestBlock, anchorRoot);
uint256 dumpHash; std::string err;
ASSERT_TRUE(src.DumpSnapshot(path.string(), header, dumpHash, err)) << err;
EXPECT_EQ(header.nCoins, 1u);
EXPECT_EQ(header.nSaplingAnchors, 1u);
EXPECT_EQ(header.nSaplingNullifiers, 1u);
// Load into a fresh in-memory DB (integrity check only, no trust hash).
CCoinsViewDB dst(1 << 20, true);
CUTXOSnapshotHeader loadedHeader; uint256 loadHash;
ASSERT_TRUE(dst.LoadSnapshot(path.string(), uint256(), /*fRequireExpected=*/false, loadedHeader, loadHash, err)) << err;
// Hash is deterministic across dump and load.
EXPECT_EQ(dumpHash, loadHash);
EXPECT_EQ(loadedHeader.nHeight, 100);
EXPECT_EQ(loadedHeader.baseBlockHash, bestBlock);
// Best block round-trips.
EXPECT_EQ(dst.GetBestBlock(), bestBlock);
// Coins round-trip: the stored UTXO must come back intact. (We check the specific coin
// directly rather than via GetStats(), which dereferences mapBlockIndex for the best block
// — not populated in this pure unit test.) The full-content equivalence is already proven
// by dumpHash == loadHash above.
const uint256 txid = uint256S("0xaa00000000000000000000000000000000000000000000000000000000000001");
CCoins c1, c2;
ASSERT_TRUE(src.GetCoins(txid, c1));
ASSERT_TRUE(dst.GetCoins(txid, c2));
ASSERT_EQ(c2.vout.size(), 1u);
EXPECT_EQ(c2.vout[0].nValue, c1.vout[0].nValue);
EXPECT_TRUE(c2.vout[0].scriptPubKey == c1.vout[0].scriptPubKey);
// Sapling anchor (commitment tree) round-trips byte-exactly: the recovered tree's root
// must equal the key it was stored under (this is the invariant ConnectBlock relies on).
SaplingMerkleTree recovered;
ASSERT_TRUE(dst.GetSaplingAnchorAt(anchorRoot, recovered));
EXPECT_EQ(recovered.root(), anchorRoot);
EXPECT_EQ(dst.GetBestAnchor(SAPLING), anchorRoot);
// Nullifier set round-trips.
EXPECT_TRUE(dst.GetNullifier(nullifier, SAPLING));
EXPECT_FALSE(dst.GetNullifier(uint256S("0xdead"), SAPLING));
boost::filesystem::remove(path);
}
TEST(UTXOSnapshot, RejectsTrustHashMismatch)
{
SelectParams(CBaseChainParams::REGTEST);
const uint256 bestBlock = uint256S("0xff0000000000000000000000000000000000000000000000000000000000000a");
const uint256 nullifier = uint256S("0xcc0000000000000000000000000000000000000000000000000000000000000b");
CCoinsViewDB src(1 << 20, true);
uint256 anchorRoot;
PopulateChainstate(src, bestBlock, anchorRoot, nullifier);
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
CUTXOSnapshotHeader header = MakeHeader(bestBlock, anchorRoot);
uint256 dumpHash; std::string err;
ASSERT_TRUE(src.DumpSnapshot(path.string(), header, dumpHash, err)) << err;
// A wrong "trusted" hash must be refused.
CCoinsViewDB dst(1 << 20, true);
CUTXOSnapshotHeader h2; uint256 hh;
uint256 wrong = uint256S("0x1234");
EXPECT_FALSE(dst.LoadSnapshot(path.string(), wrong, /*fRequireExpected=*/true, h2, hh, err));
// The correct hash must pass.
EXPECT_TRUE(dst.LoadSnapshot(path.string(), dumpHash, /*fRequireExpected=*/true, h2, hh, err)) << err;
boost::filesystem::remove(path);
}
TEST(UTXOSnapshot, RejectsCorruptedFile)
{
SelectParams(CBaseChainParams::REGTEST);
const uint256 bestBlock = uint256S("0xff0000000000000000000000000000000000000000000000000000000000000c");
const uint256 nullifier = uint256S("0xcc0000000000000000000000000000000000000000000000000000000000000d");
CCoinsViewDB src(1 << 20, true);
uint256 anchorRoot;
PopulateChainstate(src, bestBlock, anchorRoot, nullifier);
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
CUTXOSnapshotHeader header = MakeHeader(bestBlock, anchorRoot);
uint256 dumpHash; std::string err;
ASSERT_TRUE(src.DumpSnapshot(path.string(), header, dumpHash, err)) << err;
// Flip a byte near the end (inside the coins/anchor payload, before the trailing hash).
{
boost::filesystem::fstream f(path, std::ios::in | std::ios::out | std::ios::binary);
f.seekg(0, std::ios::end);
std::streamoff sz = f.tellg();
ASSERT_GT(sz, 40);
f.seekg(sz - 40);
char c; f.read(&c, 1);
f.seekp(sz - 40);
c = (char)(c ^ 0xff);
f.write(&c, 1);
}
CCoinsViewDB dst(1 << 20, true);
CUTXOSnapshotHeader h2; uint256 hh;
EXPECT_FALSE(dst.LoadSnapshot(path.string(), uint256(), /*fRequireExpected=*/false, h2, hh, err));
boost::filesystem::remove(path);
}

View File

@@ -390,8 +390,6 @@ std::string HelpMessage(HelpMessageMode mode)
strUsage += HelpMessageOpt("-exportdir=<dir>", _("Specify directory to be used when exporting data")); strUsage += HelpMessageOpt("-exportdir=<dir>", _("Specify directory to be used when exporting data"));
strUsage += HelpMessageOpt("-dbcache=<n>", strprintf(_("Set database cache size in megabytes (%d to %d). Default: adaptive - uses most free RAM to speed up initial block download (far fewer UTXO flushes to disk) and automatically shrinks if other applications need memory, always leaving a reserve free. Setting a fixed value disables adaptive sizing."), nMinDbCache, nMaxDbCache)); strUsage += HelpMessageOpt("-dbcache=<n>", strprintf(_("Set database cache size in megabytes (%d to %d). Default: adaptive - uses most free RAM to speed up initial block download (far fewer UTXO flushes to disk) and automatically shrinks if other applications need memory, always leaving a reserve free. Setting a fixed value disables adaptive sizing."), nMinDbCache, nMaxDbCache));
strUsage += HelpMessageOpt("-loadblock=<file>", _("Imports blocks from external blk000??.dat file") + " " + _("on startup")); strUsage += HelpMessageOpt("-loadblock=<file>", _("Imports blocks from external blk000??.dat file") + " " + _("on startup"));
strUsage += HelpMessageOpt("-loadutxosnapshot=<file>", _("On a fresh node (empty chainstate), load a trusted UTXO snapshot produced by 'dumptxoutset' and fast-forward the tip to its height, skipping replay of earlier blocks. Block headers up to that height must already be present (e.g. via header sync or bootstrap). Blocks above the snapshot are still fully validated."));
strUsage += HelpMessageOpt("-loadutxosnapshotunsafe", _("Allow -loadutxosnapshot even when no trusted snapshot hash is hardcoded for this network (verifies file integrity only, not authenticity). Testing/regtest only."));
strUsage += HelpMessageOpt("-maxdebugfilesize=<n>", strprintf(_("Set the max size of the debug.log file (default: %u)"), 15)); strUsage += HelpMessageOpt("-maxdebugfilesize=<n>", strprintf(_("Set the max size of the debug.log file (default: %u)"), 15));
strUsage += HelpMessageOpt("-maxorphantx=<n>", strprintf(_("Keep at most <n> unconnectable transactions in memory (default: %u)"), DEFAULT_MAX_ORPHAN_TRANSACTIONS)); strUsage += HelpMessageOpt("-maxorphantx=<n>", strprintf(_("Keep at most <n> unconnectable transactions in memory (default: %u)"), DEFAULT_MAX_ORPHAN_TRANSACTIONS));
strUsage += HelpMessageOpt("-maxreorg=<n>", _("Specify the maximum length of a blockchain re-organization")); strUsage += HelpMessageOpt("-maxreorg=<n>", _("Specify the maximum length of a blockchain re-organization"));
@@ -469,6 +467,12 @@ std::string HelpMessage(HelpMessageMode mode)
strUsage += HelpMessageGroup(_("Wallet options:")); strUsage += HelpMessageGroup(_("Wallet options:"));
strUsage += HelpMessageOpt("-disablewallet", _("Do not load the wallet and disable wallet RPC calls")); strUsage += HelpMessageOpt("-disablewallet", _("Do not load the wallet and disable wallet RPC calls"));
strUsage += HelpMessageOpt("-keypool=<n>", strprintf(_("Set key pool size to <n> (default: %u)"), 100)); strUsage += HelpMessageOpt("-keypool=<n>", strprintf(_("Set key pool size to <n> (default: %u)"), 100));
strUsage += HelpMessageOpt("-hdtransparent", strprintf(_("Derive transparent addresses from the HD seed so they can be recovered from it (default: %u)"), 1));
strUsage += HelpMessageOpt("-hdseed=<hex>", _("Restore a fresh/empty wallet from a 32- or 64-byte HD seed hex (the value shown in z_exportwallet's '# HDSeed=' line). WARNING: exposes the seed to your shell history and process list."));
strUsage += HelpMessageOpt("-mnemonic=<words>", _("Restore/create a fresh/empty wallet from a BIP39 seed phrase, compatible with SilentDragonXLite (English, no passphrase). WARNING: exposes the phrase to your shell history and process list; prefer DRAGONX.conf with tight permissions."));
strUsage += HelpMessageOpt("-usemnemonic", strprintf(_("Create new wallets from a fresh BIP39 seed phrase so the 24 words can be exported (z_exportmnemonic) and used in SilentDragonXLite (default: %u)"), 0));
strUsage += HelpMessageOpt("-hdtransparentgaplimit=<n>", strprintf(_("On -mnemonic/-hdseed restore, pre-derive this many HD transparent keys so a rescan can find coinbase paid to them (default: %u)"), 1000));
strUsage += HelpMessageOpt("-mnemonicsaplinggap=<n>", strprintf(_("On -mnemonic/-hdseed restore, pre-derive this many shielded (Sapling) addresses so a rescan can find notes sent to them (default: %u)"), 100));
strUsage += HelpMessageOpt("-consolidation", _("Enable auto Sapling note consolidation (default: false)")); strUsage += HelpMessageOpt("-consolidation", _("Enable auto Sapling note consolidation (default: false)"));
strUsage += HelpMessageOpt("-consolidationinterval", _("Block interval between consolidations (default: 25)")); strUsage += HelpMessageOpt("-consolidationinterval", _("Block interval between consolidations (default: 25)"));
strUsage += HelpMessageOpt("-consolidatesaplingaddress=<zaddr>", _("Specify Sapling Address to Consolidate. (default: all)")); strUsage += HelpMessageOpt("-consolidatesaplingaddress=<zaddr>", _("Specify Sapling Address to Consolidate. (default: all)"));
@@ -2099,44 +2103,6 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
break; break;
} }
// Trusted UTXO snapshot fast-sync (assumeutxo-style). If -loadutxosnapshot is given
// and the chainstate is still empty, load the verified snapshot and fast-forward the
// tip to height H; blocks above H then sync with full PoW/script/Sapling validation.
{
std::string snapPath = GetArg("-loadutxosnapshot", "");
if (!snapPath.empty()) {
if (!pcoinsdbview->GetBestBlock().IsNull()) {
LogPrintf("%s: -loadutxosnapshot ignored, chainstate is not empty\n", __func__);
} else {
const CChainParams::AssumeutxoData& au = chainparams.Assumeutxo();
bool unsafe = GetBoolArg("-loadutxosnapshotunsafe", false);
if (au.IsNull() && !unsafe) {
strLoadError = _("-loadutxosnapshot: no trusted snapshot hash is configured for this network; refusing (use -loadutxosnapshotunsafe for testing only)");
break;
}
CUTXOSnapshotHeader hdr; uint256 gotHash; std::string snapErr;
bool requireExpected = !au.IsNull() && !unsafe;
if (!pcoinsdbview->LoadSnapshot(snapPath, au.hash, requireExpected, hdr, gotHash, snapErr)) {
strLoadError = strprintf(_("Failed to load UTXO snapshot: %s"), snapErr);
break;
}
if (!au.IsNull() && hdr.nHeight != au.height) {
strLoadError = _("UTXO snapshot height does not match the trusted value for this network");
break;
}
pcoinsTip->SetBestBlock(hdr.baseBlockHash); // refresh cache view of the freshly-written chainstate
std::string fixErr;
if (!LoadSnapshotChainstate(hdr, fixErr)) {
strLoadError = strprintf(_("Failed to activate UTXO snapshot tip: %s"), fixErr);
break;
}
pblocktree->WriteAssumeutxoHeight(hdr.nHeight); // persist reorg-below-H guard across restarts
LogPrintf("%s: loaded trusted UTXO snapshot at height %d (hash %s); syncing forward with full validation\n",
__func__, hdr.nHeight, gotHash.GetHex());
}
}
}
HUSH_LOADINGBLOCKS = 0; HUSH_LOADINGBLOCKS = 0;
// Check for changed -txindex state // Check for changed -txindex state
if (fTxIndex != GetBoolArg("-txindex", true)) { if (fTxIndex != GetBoolArg("-txindex", true)) {
@@ -2305,11 +2271,57 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
} }
if (!pwalletMain->HaveHDSeed()) if (!pwalletMain->HaveHDSeed())
{
std::string mnemonic = GetArg("-mnemonic", "");
std::string hdSeedHex = GetArg("-hdseed", "");
bool restoring = false;
if (!mnemonic.empty() && !hdSeedHex.empty())
return InitError(_("Specify only one of -mnemonic or -hdseed, not both"));
if (!mnemonic.empty())
{
// Restore/create a wallet from a BIP39 seed phrase, byte-compatible
// with SilentDragonXLite. Must be a fresh/empty wallet.
if (!pwalletMain->SetHDSeedFromMnemonic(mnemonic))
return InitError(_("Invalid -mnemonic: expected a valid BIP39 English phrase on a fresh/empty wallet"));
LogPrintf("%s: restoring wallet from -mnemonic seed phrase\n", __func__);
restoring = true;
}
else if (!hdSeedHex.empty())
{
// Restore from a previously exported HD seed hex (z_exportwallet's
// "# HDSeed=" line): 32 bytes (raw) or 64 bytes (BIP39-derived).
if (!pwalletMain->SetHDSeedFromHex(hdSeedHex))
return InitError(_("Invalid -hdseed: expected a 32- or 64-hex-character seed on a fresh/empty wallet"));
LogPrintf("%s: restoring wallet from -hdseed\n", __func__);
restoring = true;
}
else
{ {
// generate a new HD seed // generate a new HD seed
pwalletMain->GenerateNewSeed(); pwalletMain->GenerateNewSeed();
} }
if (restoring)
{
// Pre-derive keys (birthday = genesis) so the startup rescan finds
// funds paid to them: transparent coinbase + shielded notes.
int64_t tGap = GetArg("-hdtransparentgaplimit", 1000);
if (tGap < 0) tGap = 0;
pwalletMain->TopUpHDTransparentKeys((unsigned int)tGap, 1);
int64_t zGap = GetArg("-mnemonicsaplinggap", 100);
if (zGap < 0) zGap = 0;
{
LOCK(pwalletMain->cs_wallet);
for (int i = 0; i < (int)zGap; i++)
pwalletMain->GenerateNewSaplingZKey();
}
LogPrintf("%s: pre-derived %d transparent and %d sapling keys for restore rescan\n", __func__, (int)tGap, (int)zGap);
}
}
//Set Sapling Consolidation //Set Sapling Consolidation
pwalletMain->fSaplingConsolidationEnabled = GetBoolArg("-consolidation", false); pwalletMain->fSaplingConsolidationEnabled = GetBoolArg("-consolidation", false);
if(pwalletMain->fSaplingConsolidationEnabled) { if(pwalletMain->fSaplingConsolidationEnabled) {

View File

@@ -39,6 +39,9 @@
*/ */
typedef std::vector<unsigned char, secure_allocator<unsigned char> > CPrivKey; typedef std::vector<unsigned char, secure_allocator<unsigned char> > CPrivKey;
/** BIP32: child indices at or above this are hardened. */
const unsigned int BIP32_HARDENED_KEY_LIMIT = 0x80000000;
/** An encapsulated private key. */ /** An encapsulated private key. */
class CKey class CKey
{ {

View File

@@ -85,7 +85,6 @@ void hush_pricesupdate(int32_t height,CBlock *pblock);
BlockMap mapBlockIndex; BlockMap mapBlockIndex;
CChain chainActive; CChain chainActive;
CBlockIndex *pindexBestHeader = NULL; CBlockIndex *pindexBestHeader = NULL;
int nAssumeutxoSnapshotHeight = -1; // height H of a loaded UTXO snapshot; reorgs below H are refused (-1 = none)
static int64_t nTimeBestReceived = 0; static int64_t nTimeBestReceived = 0;
CWaitableCriticalSection csBestBlock; CWaitableCriticalSection csBestBlock;
CConditionVariable cvBlockChange; CConditionVariable cvBlockChange;
@@ -4201,45 +4200,6 @@ static void PruneBlockIndexCandidates() {
assert(!setBlockIndexCandidates.empty()); assert(!setBlockIndexCandidates.empty());
} }
// Activate a trusted UTXO snapshot (assumeutxo-style) as the chain tip WITHOUT replaying blocks
// 0..H. The chainstate has already been populated by CCoinsViewDB::LoadSnapshot(); here we mark the
// snapshot's base block (height H) as fully validated and set it as the active tip. Blocks above H
// then connect normally with full PoW + script + Sapling-proof validation. Requires that the block
// HEADERS for height H are already present in mapBlockIndex (from prior header sync or bootstrap).
// NOTE: below-H blocks have no body/undo data, so reorgs below H are impossible (see Stage D guard).
bool LoadSnapshotChainstate(const CUTXOSnapshotHeader& header, std::string& strError)
{
LOCK(cs_main);
BlockMap::iterator it = mapBlockIndex.find(header.baseBlockHash);
if (it == mapBlockIndex.end() || it->second == NULL) {
strError = "block header for the snapshot height is not present; sync headers (or use the bootstrap) before loading a UTXO snapshot";
return false;
}
CBlockIndex* pindexH = it->second;
if (pindexH->GetHeight() != header.nHeight) {
strError = "snapshot base block height does not match its header index";
return false;
}
// Only nChainTx is consensus-relevant for tip selection; nTx must merely be non-zero so the
// (nChainTx != 0) candidate-eligibility checks hold. Ancestors legitimately have nTx==0 here
// because we never received their bodies — this is the assumeutxo trust assumption.
if (pindexH->nTx == 0)
pindexH->nTx = (header.nChainTx > 0 ? (unsigned int)header.nChainTx : 1);
pindexH->nChainTx = (unsigned int)header.nChainTx;
if (header.fHasChainSaplingValue)
pindexH->nChainSaplingValue = header.nChainSaplingValue;
pindexH->RaiseValidity(BLOCK_VALID_SCRIPTS);
nAssumeutxoSnapshotHeight = pindexH->GetHeight(); // arm the reorg-below-H guard (Stage D)
setBlockIndexCandidates.insert(pindexH);
chainActive.SetTip(pindexH);
if (pindexBestHeader == NULL || pindexBestHeader->GetHeight() < pindexH->GetHeight())
pindexBestHeader = pindexH;
PruneBlockIndexCandidates();
return true;
}
/** /**
* Try to make some progress towards making pindexMostWork the active block. * Try to make some progress towards making pindexMostWork the active block.
* pblock is either NULL or a pointer to a CBlock corresponding to pindexMostWork. * pblock is either NULL or a pointer to a CBlock corresponding to pindexMostWork.
@@ -4288,15 +4248,6 @@ static bool ActivateBestChainStep(bool fSkipdpow, CValidationState &state, CBloc
return state.DoS(100, error("ActivateBestChainStep(): pindexOldTip->GetHeight().%d > notarizedht %d && pindexFork->GetHeight().%d is < notarizedht %d, so ignore it",(int32_t)pindexOldTip->GetHeight(),notarizedht,(int32_t)pindexFork->GetHeight(),notarizedht), return state.DoS(100, error("ActivateBestChainStep(): pindexOldTip->GetHeight().%d > notarizedht %d && pindexFork->GetHeight().%d is < notarizedht %d, so ignore it",(int32_t)pindexOldTip->GetHeight(),notarizedht,(int32_t)pindexFork->GetHeight(),notarizedht),
REJECT_INVALID, "past-notarized-height"); REJECT_INVALID, "past-notarized-height");
} }
// Refuse reorgs whose fork point is below a loaded UTXO snapshot height (Stage D): the node has
// no block/undo data for 0..H, so disconnecting below H is impossible. Belt-and-suspenders on top
// of checkpoint fork-rejection (H sits at/below the last hardcoded checkpoint).
if ( nAssumeutxoSnapshotHeight >= 0 && pindexFork != 0 && pindexFork->GetHeight() < nAssumeutxoSnapshotHeight )
{
return state.DoS(100, error("ActivateBestChainStep(): reorg fork height %d is below the loaded UTXO snapshot height %d; refusing",
(int32_t)pindexFork->GetHeight(), nAssumeutxoSnapshotHeight),
REJECT_INVALID, "below-assumeutxo-snapshot");
}
// - On ChainDB initialization, pindexOldTip will be null, so there are no removable blocks. // - On ChainDB initialization, pindexOldTip will be null, so there are no removable blocks.
// - If pindexMostWork is in a chain that doesn't have the same genesis block as our chain, // - If pindexMostWork is in a chain that doesn't have the same genesis block as our chain,
@@ -6101,15 +6052,6 @@ bool static LoadBlockIndexDB()
pblocktree->ReadReindexing(fReindexing); pblocktree->ReadReindexing(fReindexing);
fReindex |= fReindexing; fReindex |= fReindexing;
// Restore the loaded-UTXO-snapshot height so the reorg-below-H guard survives restarts.
{
int snapHeight = -1;
if (pblocktree->ReadAssumeutxoHeight(snapHeight) && snapHeight >= 0) {
nAssumeutxoSnapshotHeight = snapHeight;
LogPrintf("%s: loaded-from-UTXO-snapshot height is %d; reorgs below it are refused\n", __func__, snapHeight);
}
}
// Check whether we have a transaction index // Check whether we have a transaction index
pblocktree->ReadFlag("txindex", fTxIndex); pblocktree->ReadFlag("txindex", fTxIndex);
LogPrintf("%s: transaction index %s\n", __func__, fTxIndex ? "enabled" : "disabled"); LogPrintf("%s: transaction index %s\n", __func__, fTxIndex ? "enabled" : "disabled");

View File

@@ -956,19 +956,10 @@ extern CChain chainActive;
/** Global variable that points to the active CCoinsView (protected by cs_main) */ /** Global variable that points to the active CCoinsView (protected by cs_main) */
extern CCoinsViewCache *pcoinsTip; extern CCoinsViewCache *pcoinsTip;
/** Global variable that points to the coins database (chainstate/, protected by cs_main). /** Global variable that points to the coins database (chainstate/, protected by cs_main). */
* Exposed for the UTXO-snapshot (assumeutxo-style) dump/load paths. */
class CCoinsViewDB; class CCoinsViewDB;
extern CCoinsViewDB *pcoinsdbview; extern CCoinsViewDB *pcoinsdbview;
/** Activate a trusted UTXO snapshot (already written to the chainstate DB by LoadSnapshot) as the
* chain tip at its height H, without replaying blocks 0..H. Headers for H must already exist. */
struct CUTXOSnapshotHeader;
bool LoadSnapshotChainstate(const CUTXOSnapshotHeader& header, std::string& strError);
/** Height H of a loaded UTXO snapshot (assumeutxo). Reorgs whose fork point is below H are refused
* because the node has no block/undo data for 0..H. -1 means no snapshot is in effect. */
extern int nAssumeutxoSnapshotHeight;
/** Global variable that points to the active block tree (protected by cs_main) */ /** Global variable that points to the active block tree (protected by cs_main) */
extern CBlockTreeDB *pblocktree; extern CBlockTreeDB *pblocktree;

View File

@@ -862,76 +862,6 @@ UniValue gettxoutsetinfo(const UniValue& params, bool fHelp, const CPubKey& mypk
return ret; return ret;
} }
UniValue dumptxoutset(const UniValue& params, bool fHelp, const CPubKey& mypk)
{
if (fHelp || params.size() != 1)
throw runtime_error(
"dumptxoutset \"path\"\n"
"\nWrite a trusted snapshot of the current chainstate (UTXO set + Sapling commitment\n"
"trees, nullifier set and pool value) to disk. The snapshot can be loaded by a fresh\n"
"node with -loadutxosnapshot=<file> to skip replaying the chain from genesis.\n"
"\nThis is intended to be run at a final/checkpoint height; the node must be fully synced.\n"
"\nArguments:\n"
"1. \"path\" (string, required) path to write the snapshot file (must not already exist)\n"
"\nResult:\n"
"{\n"
" \"height\": n, (numeric) snapshot height H\n"
" \"base_hash\": \"hex\", (string) block hash at height H\n"
" \"snapshot_hash\": \"hex\", (string) content hash to hardcode for verification\n"
" \"coins\": n, (numeric) number of UTXO records\n"
" \"sapling_anchors\": n, (numeric) number of Sapling anchor records\n"
" \"sapling_nullifiers\": n, (numeric) number of Sapling nullifier records\n"
" \"path\": \"...\" (string) the file written\n"
"}\n"
"\nExamples:\n"
+ HelpExampleCli("dumptxoutset", "/path/to/dragonx-utxo.dat")
+ HelpExampleRpc("dumptxoutset", "\"/path/to/dragonx-utxo.dat\"")
);
boost::filesystem::path path = boost::filesystem::absolute(params[0].get_str());
if (boost::filesystem::exists(path))
throw JSONRPCError(RPC_INVALID_PARAMETER, "path already exists, refusing to overwrite: " + path.string());
LOCK(cs_main);
if (pcoinsdbview == nullptr || pcoinsTip == nullptr)
throw JSONRPCError(RPC_INTERNAL_ERROR, "chainstate not available");
// Flush so the on-disk chainstate matches the in-memory tip before we iterate it.
FlushStateToDisk();
CBlockIndex *tip = chainActive.Tip();
if (tip == nullptr)
throw JSONRPCError(RPC_INTERNAL_ERROR, "no chain tip");
CUTXOSnapshotHeader header;
header.nMagic = UTXO_SNAPSHOT_MAGIC;
header.nVersion = UTXO_SNAPSHOT_VERSION;
memcpy(&header.nNetworkMagic, Params().MessageStart(), 4);
header.baseBlockHash = tip->GetBlockHash();
header.nHeight = tip->GetHeight();
header.nChainTx = tip->nChainTx;
if (tip->nChainSaplingValue) {
header.fHasChainSaplingValue = 1;
header.nChainSaplingValue = *tip->nChainSaplingValue;
}
header.bestSaplingAnchor = pcoinsdbview->GetBestAnchor(SAPLING);
uint256 snapshotHash;
std::string strError;
if (!pcoinsdbview->DumpSnapshot(path.string(), header, snapshotHash, strError))
throw JSONRPCError(RPC_INTERNAL_ERROR, "dumptxoutset failed: " + strError);
UniValue ret(UniValue::VOBJ);
ret.push_back(Pair("height", (int64_t)header.nHeight));
ret.push_back(Pair("base_hash", header.baseBlockHash.GetHex()));
ret.push_back(Pair("snapshot_hash", snapshotHash.GetHex()));
ret.push_back(Pair("coins", (int64_t)header.nCoins));
ret.push_back(Pair("sapling_anchors", (int64_t)header.nSaplingAnchors));
ret.push_back(Pair("sapling_nullifiers", (int64_t)header.nSaplingNullifiers));
ret.push_back(Pair("path", path.string()));
return ret;
}
UniValue getblockmerkletree(const UniValue& params, bool fHelp, const CPubKey& mypk) UniValue getblockmerkletree(const UniValue& params, bool fHelp, const CPubKey& mypk)
{ {
@@ -1924,7 +1854,6 @@ static const CRPCCommand commands[] =
{ "blockchain", "getrawmempool", &getrawmempool, true }, { "blockchain", "getrawmempool", &getrawmempool, true },
{ "blockchain", "gettxout", &gettxout, true }, { "blockchain", "gettxout", &gettxout, true },
{ "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, true }, { "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, true },
{ "blockchain", "dumptxoutset", &dumptxoutset, true },
{ "blockchain", "verifychain", &verifychain, true }, { "blockchain", "verifychain", &verifychain, true },
/* Not shown in help */ /* Not shown in help */

View File

@@ -354,7 +354,7 @@ UniValue getaddednodeinfo(const UniValue& params, bool fHelp, const CPubKey& myp
" \"connected\" : true|false, (boolean) If connected\n" " \"connected\" : true|false, (boolean) If connected\n"
" \"addresses\" : [\n" " \"addresses\" : [\n"
" {\n" " {\n"
" \"address\" : \"192.168.0.201:18030\", (string) The Hush server host and port\n" " \"address\" : \"192.168.0.201:21768\", (string) The DragonX server host and port\n"
" \"connected\" : \"outbound\" (string) connection, inbound or outbound\n" " \"connected\" : \"outbound\" (string) connection, inbound or outbound\n"
" }\n" " }\n"
" ,...\n" " ,...\n"

View File

@@ -474,6 +474,7 @@ static const CRPCCommand vRPCCommands[] =
{ "wallet", "z_listaddresses", &z_listaddresses, true }, { "wallet", "z_listaddresses", &z_listaddresses, true },
{ "wallet", "z_listnullifiers", &z_listnullifiers, true }, { "wallet", "z_listnullifiers", &z_listnullifiers, true },
{ "wallet", "z_exportkey", &z_exportkey, true }, { "wallet", "z_exportkey", &z_exportkey, true },
{ "wallet", "z_exportmnemonic", &z_exportmnemonic, true },
{ "wallet", "z_importkey", &z_importkey, true }, { "wallet", "z_importkey", &z_importkey, true },
{ "wallet", "z_exportviewingkey", &z_exportviewingkey, true }, { "wallet", "z_exportviewingkey", &z_exportviewingkey, true },
{ "wallet", "z_importviewingkey", &z_importviewingkey, true }, { "wallet", "z_importviewingkey", &z_importviewingkey, true },

View File

@@ -353,6 +353,7 @@ extern UniValue nspv_listccmoduleunspent(const UniValue& params, bool fHelp, con
extern UniValue getblocksubsidy(const UniValue& params, bool fHelp, const CPubKey& mypk); extern UniValue getblocksubsidy(const UniValue& params, bool fHelp, const CPubKey& mypk);
extern UniValue z_exportkey(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp extern UniValue z_exportkey(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp
extern UniValue z_exportmnemonic(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp
extern UniValue z_importkey(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp extern UniValue z_importkey(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp
extern UniValue z_exportviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp extern UniValue z_exportviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp
extern UniValue z_importviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp extern UniValue z_importviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp

View File

@@ -271,233 +271,6 @@ bool CCoinsViewDB::GetStats(CCoinsStats &stats) const {
return true; return true;
} }
// Helper: count entries in the coins DB whose key prefix matches `prefix`.
// LevelDB returns keys in sorted order, so iteration is deterministic across nodes.
static uint64_t CountByPrefix(CDBWrapper &db, char prefix)
{
boost::scoped_ptr<CDBIterator> pcursor(db.NewIterator());
uint64_t n = 0;
for (pcursor->Seek(prefix); pcursor->Valid(); pcursor->Next()) {
boost::this_thread::interruption_point();
std::pair<char, uint256> key;
if (pcursor->GetKey(key) && key.first == prefix) n++;
else break;
}
return n;
}
bool CCoinsViewDB::DumpSnapshot(const std::string &path, CUTXOSnapshotHeader &header, uint256 &hashRet, std::string &strError) const
{
CDBWrapper *pdb = const_cast<CDBWrapper*>(&db);
// Counting pass (caller holds cs_main and has flushed, so the set is stable).
header.nCoins = CountByPrefix(*pdb, DB_COINS);
header.nSaplingAnchors = CountByPrefix(*pdb, DB_SAPLING_ANCHOR);
header.nSaplingNullifiers = CountByPrefix(*pdb, DB_SAPLING_NULLIFIER);
FILE *f = fopen(path.c_str(), "wb");
if (f == nullptr) { strError = "cannot open snapshot file for writing: " + path; return false; }
CAutoFile fileout(f, SER_DISK, CLIENT_VERSION);
// The content hash is computed over the same logical object stream the loader will
// reconstruct, so producer and consumer agree regardless of on-disk encoding.
CHashWriter hasher(SER_GETHASH, PROTOCOL_VERSION);
fileout << header;
hasher << header;
// Coins ('c')
{
boost::scoped_ptr<CDBIterator> pcursor(pdb->NewIterator());
uint64_t n = 0;
for (pcursor->Seek(DB_COINS); pcursor->Valid(); pcursor->Next()) {
boost::this_thread::interruption_point();
std::pair<char, uint256> key;
CCoins coins;
if (pcursor->GetKey(key) && key.first == DB_COINS) {
if (!pcursor->GetValue(coins)) { strError = "failed reading coins record"; return false; }
fileout << key.second; hasher << key.second;
fileout << coins; hasher << coins;
n++;
} else break;
}
if (n != header.nCoins) { strError = "coin count changed during dump"; return false; }
}
// Sapling anchors ('Z') — the commitment trees referenced by spends above H.
{
boost::scoped_ptr<CDBIterator> pcursor(pdb->NewIterator());
uint64_t n = 0;
for (pcursor->Seek(DB_SAPLING_ANCHOR); pcursor->Valid(); pcursor->Next()) {
boost::this_thread::interruption_point();
std::pair<char, uint256> key;
SaplingMerkleTree tree;
if (pcursor->GetKey(key) && key.first == DB_SAPLING_ANCHOR) {
if (!pcursor->GetValue(tree)) { strError = "failed reading sapling anchor"; return false; }
fileout << key.second; hasher << key.second;
fileout << tree; hasher << tree;
n++;
} else break;
}
if (n != header.nSaplingAnchors) { strError = "sapling anchor count changed during dump"; return false; }
}
// Sapling nullifiers ('S') — spent markers; value is always true, so only the key matters.
{
boost::scoped_ptr<CDBIterator> pcursor(pdb->NewIterator());
uint64_t n = 0;
for (pcursor->Seek(DB_SAPLING_NULLIFIER); pcursor->Valid(); pcursor->Next()) {
boost::this_thread::interruption_point();
std::pair<char, uint256> key;
if (pcursor->GetKey(key) && key.first == DB_SAPLING_NULLIFIER) {
fileout << key.second; hasher << key.second;
n++;
} else break;
}
if (n != header.nSaplingNullifiers) { strError = "sapling nullifier count changed during dump"; return false; }
}
hashRet = hasher.GetHash();
fileout << hashRet; // trailing content hash (not fed into the hasher)
return true;
}
bool CCoinsViewDB::LoadSnapshot(const std::string &path, const uint256 &expectedHash, bool fRequireExpected,
CUTXOSnapshotHeader &headerRet, uint256 &hashRet, std::string &strError)
{
uint32_t netmagic = 0;
memcpy(&netmagic, Params().MessageStart(), 4);
// ---- Pass 1: read + verify integrity (and the trusted hash) WITHOUT writing to the DB ----
CUTXOSnapshotHeader header;
uint256 computed;
{
FILE *f = fopen(path.c_str(), "rb");
if (f == nullptr) { strError = "cannot open snapshot file: " + path; return false; }
CAutoFile filein(f, SER_DISK, CLIENT_VERSION);
CHashWriter hasher(SER_GETHASH, PROTOCOL_VERSION);
try {
filein >> header; hasher << header;
if (header.nMagic != UTXO_SNAPSHOT_MAGIC) { strError = "not a DragonX UTXO snapshot (bad magic)"; return false; }
if (header.nVersion != UTXO_SNAPSHOT_VERSION) { strError = "unsupported snapshot version"; return false; }
if (header.nNetworkMagic != netmagic) { strError = "snapshot is for a different network"; return false; }
for (uint64_t i = 0; i < header.nCoins; i++) {
boost::this_thread::interruption_point();
uint256 txid; CCoins coins;
filein >> txid; filein >> coins;
hasher << txid; hasher << coins;
}
for (uint64_t i = 0; i < header.nSaplingAnchors; i++) {
boost::this_thread::interruption_point();
uint256 root; SaplingMerkleTree tree;
filein >> root; filein >> tree;
hasher << root; hasher << tree;
}
for (uint64_t i = 0; i < header.nSaplingNullifiers; i++) {
boost::this_thread::interruption_point();
uint256 nf;
filein >> nf;
hasher << nf;
}
uint256 stored;
filein >> stored;
computed = hasher.GetHash();
if (computed != stored) { strError = "snapshot content hash mismatch (corrupt or truncated)"; return false; }
} catch (const std::exception &e) {
strError = std::string("error reading snapshot: ") + e.what();
return false;
}
}
if (fRequireExpected && computed != expectedHash) {
strError = "snapshot hash does not match the trusted value hardcoded for this network";
return false;
}
hashRet = computed;
headerRet = header;
// ---- Pass 2: apply to the (empty) chainstate DB in bounded batches ----
const size_t CHUNK = 100000;
CCoinsMap mapCoins;
CAnchorsSproutMap mapSproutAnchors; // unused on this chain, always empty
CAnchorsSaplingMap mapSaplingAnchors;
CNullifiersMap mapSproutNullifiers; // unused, always empty
CNullifiersMap mapSaplingNullifiers;
{
FILE *f = fopen(path.c_str(), "rb");
if (f == nullptr) { strError = "cannot reopen snapshot file: " + path; return false; }
CAutoFile filein(f, SER_DISK, CLIENT_VERSION);
try {
CUTXOSnapshotHeader hdr2;
filein >> hdr2; // header already validated in pass 1
for (uint64_t i = 0; i < header.nCoins; i++) {
boost::this_thread::interruption_point();
uint256 txid; CCoins coins;
filein >> txid; filein >> coins;
CCoinsCacheEntry &e = mapCoins[txid];
e.coins = coins;
e.flags = CCoinsCacheEntry::DIRTY;
if (mapCoins.size() >= CHUNK) {
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
{ strError = "batch write failed (coins)"; return false; }
mapCoins.clear();
}
}
if (!mapCoins.empty()) {
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
{ strError = "batch write failed (coins remainder)"; return false; }
mapCoins.clear();
}
for (uint64_t i = 0; i < header.nSaplingAnchors; i++) {
boost::this_thread::interruption_point();
uint256 root; SaplingMerkleTree tree;
filein >> root; filein >> tree;
CAnchorsSaplingCacheEntry &e = mapSaplingAnchors[root];
e.entered = true;
e.tree = tree;
e.flags = CAnchorsSaplingCacheEntry::DIRTY;
if (mapSaplingAnchors.size() >= CHUNK) {
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
{ strError = "batch write failed (anchors)"; return false; }
mapSaplingAnchors.clear();
}
}
if (!mapSaplingAnchors.empty()) {
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
{ strError = "batch write failed (anchors remainder)"; return false; }
mapSaplingAnchors.clear();
}
for (uint64_t i = 0; i < header.nSaplingNullifiers; i++) {
boost::this_thread::interruption_point();
uint256 nf;
filein >> nf;
CNullifiersCacheEntry &e = mapSaplingNullifiers[nf];
e.entered = true;
e.flags = CNullifiersCacheEntry::DIRTY;
if (mapSaplingNullifiers.size() >= CHUNK) {
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
{ strError = "batch write failed (nullifiers)"; return false; }
mapSaplingNullifiers.clear();
}
}
} catch (const std::exception &e) {
strError = std::string("error applying snapshot: ") + e.what();
return false;
}
}
// Final write: flush any remaining nullifiers AND set the best-block / best-sapling-anchor
// pointers, so GetBestBlock()==H and GetBestAnchor(SAPLING) resolve after load.
if (!BatchWrite(mapCoins, header.baseBlockHash, uint256(), header.bestSaplingAnchor,
mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
{ strError = "final batch write failed"; return false; }
return true;
}
bool CBlockTreeDB::WriteBatchSync(const std::vector<std::pair<int, const CBlockFileInfo*> >& fileInfo, int nLastFile, const std::vector<CBlockIndex*>& blockinfo) { bool CBlockTreeDB::WriteBatchSync(const std::vector<std::pair<int, const CBlockFileInfo*> >& fileInfo, int nLastFile, const std::vector<CBlockIndex*>& blockinfo) {
CDBBatch batch(*this); CDBBatch batch(*this);
if (fDebug) if (fDebug)
@@ -884,14 +657,6 @@ bool CBlockTreeDB::ReadFlag(const std::string &name, bool &fValue) const {
return true; return true;
} }
bool CBlockTreeDB::WriteAssumeutxoHeight(int nHeight) {
return Write(std::make_pair(DB_FLAG, std::string("assumeutxoheight")), nHeight);
}
bool CBlockTreeDB::ReadAssumeutxoHeight(int &nHeight) const {
return Read(std::make_pair(DB_FLAG, std::string("assumeutxoheight")), nHeight);
}
void hush_index2pubkey33(uint8_t *pubkey33,CBlockIndex *pindex,int32_t height); void hush_index2pubkey33(uint8_t *pubkey33,CBlockIndex *pindex,int32_t height);
bool CBlockTreeDB::blockOnchainActive(const uint256 &hash) { bool CBlockTreeDB::blockOnchainActive(const uint256 &hash) {

View File

@@ -56,61 +56,6 @@ static const int64_t nMaxDbCache = sizeof(void*) > 4 ? 16384 : 1024;
//! min. -dbcache in (MiB) //! min. -dbcache in (MiB)
static const int64_t nMinDbCache = 4; static const int64_t nMinDbCache = 4;
/** Magic + version for the trusted UTXO-snapshot (assumeutxo-style) file format. */
static const uint32_t UTXO_SNAPSHOT_MAGIC = 0x58535844; // 'DXSX'
static const uint8_t UTXO_SNAPSHOT_VERSION = 1;
/**
* Header of a trusted chainstate snapshot taken at a final height H. On this private
* chain the chainstate is more than transparent UTXOs, so the snapshot also carries the
* Sapling commitment trees, the nullifier set, the best Sapling anchor and the pool value.
*
* File layout: [CUTXOSnapshotHeader]
* nCoins × (uint256 txid, CCoins)
* nSaplingAnchors × (uint256 root, SaplingMerkleTree)
* nSaplingNullifiers × (uint256 nullifier)
* uint256 contentHash // hash over everything above (NOT itself)
*/
struct CUTXOSnapshotHeader
{
uint32_t nMagic;
uint8_t nVersion;
uint32_t nNetworkMagic; // Params().MessageStart() as uint32 — prevents cross-network use
uint256 baseBlockHash; // hash of block H (the snapshot tip)
int32_t nHeight; // H
uint64_t nChainTx; // cumulative tx count at H (needed for tip fix-up)
uint8_t fHasChainSaplingValue;
int64_t nChainSaplingValue; // cumulative Sapling pool value at H (valid iff fHasChainSaplingValue)
uint256 bestSaplingAnchor; // best Sapling anchor root at H
uint64_t nCoins;
uint64_t nSaplingAnchors;
uint64_t nSaplingNullifiers;
CUTXOSnapshotHeader() { SetNull(); }
void SetNull() {
nMagic = 0; nVersion = 0; nNetworkMagic = 0; baseBlockHash.SetNull();
nHeight = 0; nChainTx = 0; fHasChainSaplingValue = 0; nChainSaplingValue = 0;
bestSaplingAnchor.SetNull(); nCoins = 0; nSaplingAnchors = 0; nSaplingNullifiers = 0;
}
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action) {
READWRITE(nMagic);
READWRITE(nVersion);
READWRITE(nNetworkMagic);
READWRITE(baseBlockHash);
READWRITE(nHeight);
READWRITE(nChainTx);
READWRITE(fHasChainSaplingValue);
READWRITE(nChainSaplingValue);
READWRITE(bestSaplingAnchor);
READWRITE(nCoins);
READWRITE(nSaplingAnchors);
READWRITE(nSaplingNullifiers);
}
};
/** CCoinsView backed by the coin database (chainstate/) */ /** CCoinsView backed by the coin database (chainstate/) */
class CCoinsViewDB : public CCoinsView class CCoinsViewDB : public CCoinsView
{ {
@@ -136,19 +81,6 @@ public:
CNullifiersMap &mapSproutNullifiers, CNullifiersMap &mapSproutNullifiers,
CNullifiersMap &mapSaplingNullifiers); CNullifiersMap &mapSaplingNullifiers);
bool GetStats(CCoinsStats &stats) const; bool GetStats(CCoinsStats &stats) const;
//! Stream the full chainstate at the current tip into a snapshot file (assumeutxo-style
//! producer). Caller fills the metadata fields of `header` (height, baseBlockHash, nChainTx,
//! pool value, bestSaplingAnchor); this fills the counts, writes the file, and returns the
//! content hash. Caller must hold cs_main and have flushed the cache to disk first.
bool DumpSnapshot(const std::string &path, CUTXOSnapshotHeader &header, uint256 &hashRet, std::string &strError) const;
//! Load a snapshot file produced by DumpSnapshot into the (empty) chainstate DB. Two passes:
//! pass 1 reads everything and verifies the internal content hash (and, if fRequireExpected,
//! that it equals expectedHash) WITHOUT touching the DB; pass 2 writes coins/anchors/nullifiers
//! plus the best-block / best-sapling-anchor pointers. Returns the header + computed hash.
bool LoadSnapshot(const std::string &path, const uint256 &expectedHash, bool fRequireExpected,
CUTXOSnapshotHeader &headerRet, uint256 &hashRet, std::string &strError);
}; };
/** Access to the block database (blocks/index/) */ /** Access to the block database (blocks/index/) */
@@ -185,9 +117,6 @@ public:
bool ReadTimestampBlockIndex(const uint256 &hash, unsigned int &logicalTS) const; bool ReadTimestampBlockIndex(const uint256 &hash, unsigned int &logicalTS) const;
bool WriteFlag(const std::string &name, bool fValue); bool WriteFlag(const std::string &name, bool fValue);
bool ReadFlag(const std::string &name, bool &fValue) const; bool ReadFlag(const std::string &name, bool &fValue) const;
//! Persist/restore the height of a loaded UTXO snapshot so the reorg-below-H guard survives restarts.
bool WriteAssumeutxoHeight(int nHeight);
bool ReadAssumeutxoHeight(int &nHeight) const;
bool LoadBlockIndexGuts(); bool LoadBlockIndexGuts();
bool blockOnchainActive(const uint256 &hash); bool blockOnchainActive(const uint256 &hash);
UniValue Snapshot(int top); UniValue Snapshot(int top);

View File

@@ -312,7 +312,7 @@ bool AsyncRPCOperation_mergetoaddress::main_impl()
// recoverable, while keeping it logically separate from the ZIP 32 // recoverable, while keeping it logically separate from the ZIP 32
// Sapling key hierarchy, which the user might not be using. // Sapling key hierarchy, which the user might not be using.
HDSeed seed; HDSeed seed;
if (!pwalletMain->GetHDSeed(seed)) { if (!pwalletMain->GetHDSeedForDerivation(seed)) {
throw JSONRPCError( throw JSONRPCError(
RPC_WALLET_ERROR, RPC_WALLET_ERROR,
"AsyncRPCOperation_sendmany: HD seed not found"); "AsyncRPCOperation_sendmany: HD seed not found");

View File

@@ -377,7 +377,7 @@ bool AsyncRPCOperation_sendmany::main_impl() {
// recoverable, while keeping it logically separate from the ZIP 32 // recoverable, while keeping it logically separate from the ZIP 32
// Sapling key hierarchy, which the user might not be using. // Sapling key hierarchy, which the user might not be using.
HDSeed seed; HDSeed seed;
if (!pwalletMain->GetHDSeed(seed)) { if (!pwalletMain->GetHDSeedForDerivation(seed)) {
throw JSONRPCError( throw JSONRPCError(
RPC_WALLET_ERROR, RPC_WALLET_ERROR,
"AsyncRPCOperation_sendmany::main_impl(): HD seed not found"); "AsyncRPCOperation_sendmany::main_impl(): HD seed not found");

View File

@@ -197,7 +197,7 @@ bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) c
// recoverable, while keeping it logically separate from the ZIP 32 // recoverable, while keeping it logically separate from the ZIP 32
// Sapling key hierarchy, which the user might not be using. // Sapling key hierarchy, which the user might not be using.
HDSeed seed; HDSeed seed;
if (!pwalletMain->GetHDSeed(seed)) { if (!pwalletMain->GetHDSeedForDerivation(seed)) {
throw JSONRPCError( throw JSONRPCError(
RPC_WALLET_ERROR, RPC_WALLET_ERROR,
"CWallet::GenerateNewSaplingZKey(): HD seed not found"); "CWallet::GenerateNewSaplingZKey(): HD seed not found");

99
src/wallet/mnemonic.cpp Normal file
View File

@@ -0,0 +1,99 @@
// Copyright (c) 2016-2024 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
#include "wallet/mnemonic.h"
#include "random.h"
#include "support/cleanse.h"
#include <mutex>
#include <cstring>
extern "C" {
#include "crypto/bip39/bip39.h"
}
// The vendored BIP39 library references random_buffer() (used by its
// mnemonic_generate()). We do not compile trezor's insecure rand.c; instead we
// route it to the node CSPRNG so any BIP39 randomness is cryptographically
// sound. random_buffer is declared weak in rand.c, so this strong definition
// is the one that links.
extern "C" void random_buffer(uint8_t* buf, size_t len)
{
GetRandBytes(buf, (int)len);
}
// mnemonic_from_data()/mnemonic_to_seed() use process-static scratch buffers,
// so serialize all access behind one lock and copy results out immediately.
static std::mutex cs_bip39;
bool MnemonicIsValid(const std::string& phrase)
{
std::lock_guard<std::mutex> lock(cs_bip39);
return mnemonic_check(phrase.c_str()) != 0;
}
bool MnemonicToEntropy(const std::string& phrase, RawHDSeed& entropyOut)
{
std::lock_guard<std::mutex> lock(cs_bip39);
// Reject bad checksum / unknown words first.
if (mnemonic_check(phrase.c_str()) == 0) {
return false;
}
// mnemonic_to_entropy() writes 33 bytes (entropy || 1 checksum byte) and
// returns the total bit count (words * 11).
uint8_t buf[33];
int totalBits = mnemonic_to_entropy(phrase.c_str(), buf);
if (totalBits <= 0) {
return false;
}
int words = totalBits / 11;
if (words != 12 && words != 18 && words != 24) {
memory_cleanse(buf, sizeof(buf));
return false;
}
int entropyBytes = words * 4 / 3; // 12->16, 18->24, 24->32
entropyOut.assign(buf, buf + entropyBytes);
memory_cleanse(buf, sizeof(buf));
return true;
}
bool EntropyToMnemonic(const RawHDSeed& entropy, std::string& phraseOut)
{
std::lock_guard<std::mutex> lock(cs_bip39);
const char* phrase = mnemonic_from_data(entropy.data(), (int)entropy.size());
if (phrase == nullptr) {
return false;
}
phraseOut.assign(phrase);
mnemonic_clear(); // wipe the static buffer
return true;
}
bool Bip39SeedFromEntropy(const RawHDSeed& entropy, RawHDSeed& seed64Out)
{
std::lock_guard<std::mutex> lock(cs_bip39);
// Regenerate the canonical phrase from entropy (matches SDXLite's
// Mnemonic::from_entropy(entropy).phrase()), then PBKDF2 with an EMPTY
// passphrase to get the standard 64-byte BIP39 seed.
const char* phrase = mnemonic_from_data(entropy.data(), (int)entropy.size());
if (phrase == nullptr) {
return false;
}
uint8_t seed[64];
mnemonic_to_seed(phrase, "", seed, nullptr);
mnemonic_clear();
seed64Out.assign(seed, seed + 64);
memory_cleanse(seed, sizeof(seed));
return true;
}
bool GenerateMnemonicEntropy(int bits, RawHDSeed& entropyOut)
{
if (bits != 128 && bits != 160 && bits != 192 && bits != 224 && bits != 256) {
return false;
}
entropyOut.resize(bits / 8);
GetRandBytes(entropyOut.data(), (int)entropyOut.size());
return true;
}

39
src/wallet/mnemonic.h Normal file
View File

@@ -0,0 +1,39 @@
// Copyright (c) 2016-2024 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
#ifndef HUSH_WALLET_MNEMONIC_H
#define HUSH_WALLET_MNEMONIC_H
#include <string>
#include "zcash/zip32.h" // RawHDSeed
// Thin, thread-safe C++ wrapper over the vendored BIP39 (trezor-crypto) library.
// It reproduces SilentDragonXLite's tiny-bip39 0.6.2 conventions EXACTLY so the
// same 24 words yield the same addresses in both wallets:
// - English wordlist only (byte-identical to tiny-bip39's english.txt)
// - empty BIP39 passphrase (no "25th word")
// - PBKDF2-HMAC-SHA512, 2048 rounds, 64-byte seed
// - the seed is derived from the CANONICAL phrase regenerated from entropy,
// matching SDXLite's Mnemonic::from_entropy(entropy).phrase() round-trip.
//! True if `phrase` is a valid BIP39 mnemonic (word list + checksum).
bool MnemonicIsValid(const std::string& phrase);
//! Parse `phrase` into its BIP39 entropy (16/20/24/28/32 bytes). Validates the
//! checksum first. Returns false on any invalid input.
bool MnemonicToEntropy(const std::string& phrase, RawHDSeed& entropyOut);
//! Regenerate the canonical English mnemonic phrase from `entropy`.
bool EntropyToMnemonic(const RawHDSeed& entropy, std::string& phraseOut);
//! Derive the 64-byte BIP39 seed used for HD derivation from `entropy`, exactly
//! as SilentDragonXLite does: canonical phrase from entropy, then PBKDF2 with an
//! empty passphrase.
bool Bip39SeedFromEntropy(const RawHDSeed& entropy, RawHDSeed& seed64Out);
//! Generate fresh BIP39 entropy of `bits` (128/160/192/224/256) from the node
//! CSPRNG, for creating a new mnemonic wallet.
bool GenerateMnemonicEntropy(int bits, RawHDSeed& entropyOut);
#endif // HUSH_WALLET_MNEMONIC_H

View File

@@ -742,7 +742,9 @@ UniValue dumpwallet_impl(const UniValue& params, bool fHelp, bool fDumpZKeys)
file << strprintf("# mined on %s\n", EncodeDumpTime(chainActive.Tip()->GetBlockTime())); file << strprintf("# mined on %s\n", EncodeDumpTime(chainActive.Tip()->GetBlockTime()));
{ {
HDSeed hdSeed; HDSeed hdSeed;
pwalletMain->GetHDSeed(hdSeed); // Dump the 64-byte derivation seed (for mnemonic wallets this is the
// expanded BIP39 seed), so re-importing the hex reproduces the same keys.
pwalletMain->GetHDSeedForDerivation(hdSeed);
auto rawSeed = hdSeed.RawSeed(); auto rawSeed = hdSeed.RawSeed();
file << strprintf("# HDSeed=%s fingerprint=%s", HexStr(rawSeed.begin(), rawSeed.end()), hdSeed.Fingerprint().GetHex()); file << strprintf("# HDSeed=%s fingerprint=%s", HexStr(rawSeed.begin(), rawSeed.end()), hdSeed.Fingerprint().GetHex());
file << "\n"; file << "\n";
@@ -1026,6 +1028,50 @@ UniValue z_exportkey(const UniValue& params, bool fHelp, const CPubKey& mypk)
return EncodeSpendingKey(sk.get()); return EncodeSpendingKey(sk.get());
} }
UniValue z_exportmnemonic(const UniValue& params, bool fHelp, const CPubKey& mypk)
{
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() != 0)
throw runtime_error(
"z_exportmnemonic\n"
"\nReveal the wallet's BIP39 seed phrase (24 words).\n"
"The phrase is byte-compatible with SilentDragonXLite: the same words\n"
"restore the same transparent and shielded addresses in either wallet.\n"
"Only works for wallets created or restored from a mnemonic (see the\n"
"-mnemonic and -usemnemonic options). Requires the wallet be unlocked.\n"
"\nResult:\n"
"{\n"
" \"mnemonic\" : \"word1 ... word24\", (string) the BIP39 seed phrase\n"
" \"seedfp\" : \"hex\" (string) the HD seed fingerprint\n"
"}\n"
"\nExamples:\n"
+ HelpExampleCli("z_exportmnemonic", "")
+ HelpExampleRpc("z_exportmnemonic", "")
);
LOCK2(cs_main, pwalletMain->cs_wallet);
EnsureWalletIsUnlocked();
if (!pwalletMain->IsMnemonicSeed()) {
throw JSONRPCError(RPC_WALLET_ERROR,
"This wallet's seed was not derived from a mnemonic, so no seed phrase is available. "
"Use z_exportwallet to back up the raw HD seed instead.");
}
std::string phrase;
if (!pwalletMain->GetMnemonicPhrase(phrase)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Could not retrieve the seed phrase (is the wallet unlocked?)");
}
UniValue ret(UniValue::VOBJ);
ret.push_back(Pair("mnemonic", phrase));
ret.push_back(Pair("seedfp", pwalletMain->GetHDChain().seedFp.GetHex()));
return ret;
}
UniValue z_exportviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk) UniValue z_exportviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk)
{ {
if (!EnsureWalletIsAvailable(fHelp)) if (!EnsureWalletIsAvailable(fHelp))

View File

@@ -305,7 +305,7 @@ void zsTxSendsToJSON(const CWalletTx& wtx, UniValue& sends, CAmount& totalSends,
//Decrypt sapling outgoing t to z transaction using HDseed //Decrypt sapling outgoing t to z transaction using HDseed
if (wtx.vShieldedSpend.size()==0) { if (wtx.vShieldedSpend.size()==0) {
HDSeed seed; HDSeed seed;
if (pwalletMain->GetHDSeed(seed)) { if (pwalletMain->GetHDSeedForDerivation(seed)) {
auto opt = libzcash::SaplingOutgoingPlaintext::decrypt( auto opt = libzcash::SaplingOutgoingPlaintext::decrypt(
outputDesc.outCiphertext,ovkForShieldingFromTaddr(seed),outputDesc.cv,outputDesc.cm,outputDesc.ephemeralKey); outputDesc.outCiphertext,ovkForShieldingFromTaddr(seed),outputDesc.cv,outputDesc.cm,outputDesc.ephemeralKey);

View File

@@ -6272,6 +6272,7 @@ extern UniValue importaddress(const UniValue& params, bool fHelp, const CPubKey&
extern UniValue dumpwallet(const UniValue& params, bool fHelp, const CPubKey& mypk); extern UniValue dumpwallet(const UniValue& params, bool fHelp, const CPubKey& mypk);
extern UniValue importwallet(const UniValue& params, bool fHelp, const CPubKey& mypk); extern UniValue importwallet(const UniValue& params, bool fHelp, const CPubKey& mypk);
extern UniValue z_exportkey(const UniValue& params, bool fHelp, const CPubKey& mypk); extern UniValue z_exportkey(const UniValue& params, bool fHelp, const CPubKey& mypk);
extern UniValue z_exportmnemonic(const UniValue& params, bool fHelp, const CPubKey& mypk);
extern UniValue z_importkey(const UniValue& params, bool fHelp, const CPubKey& mypk); extern UniValue z_importkey(const UniValue& params, bool fHelp, const CPubKey& mypk);
extern UniValue z_exportviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk); extern UniValue z_exportviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk);
extern UniValue z_importviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk); extern UniValue z_importviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk);
@@ -6351,6 +6352,7 @@ static const CRPCCommand commands[] =
{ "wallet", "z_getnewaddress", &z_getnewaddress, true }, { "wallet", "z_getnewaddress", &z_getnewaddress, true },
{ "wallet", "z_listaddresses", &z_listaddresses, true }, { "wallet", "z_listaddresses", &z_listaddresses, true },
{ "wallet", "z_exportkey", &z_exportkey, true }, { "wallet", "z_exportkey", &z_exportkey, true },
{ "wallet", "z_exportmnemonic", &z_exportmnemonic, true },
{ "wallet", "z_importkey", &z_importkey, true }, { "wallet", "z_importkey", &z_importkey, true },
{ "wallet", "z_exportviewingkey", &z_exportviewingkey, true }, { "wallet", "z_exportviewingkey", &z_exportviewingkey, true },
{ "wallet", "z_importviewingkey", &z_importviewingkey, true }, { "wallet", "z_importviewingkey", &z_importviewingkey, true },

View File

@@ -36,6 +36,7 @@
#include "utilmoneystr.h" #include "utilmoneystr.h"
#include "zcash/Note.hpp" #include "zcash/Note.hpp"
#include "crypter.h" #include "crypter.h"
#include "wallet/mnemonic.h"
#include "coins.h" #include "coins.h"
#include "wallet/asyncrpcoperation_saplingconsolidation.h" #include "wallet/asyncrpcoperation_saplingconsolidation.h"
#include "wallet/asyncrpcoperation_sweep.h" #include "wallet/asyncrpcoperation_sweep.h"
@@ -131,7 +132,7 @@ SaplingPaymentAddress CWallet::GenerateNewSaplingZKey(bool addToWallet)
// Try to get the seed // Try to get the seed
HDSeed seed; HDSeed seed;
if (!GetHDSeed(seed)) if (!GetHDSeedForDerivation(seed))
throw std::runtime_error("CWallet::GenerateNewSaplingZKey(): HD seed not found"); throw std::runtime_error("CWallet::GenerateNewSaplingZKey(): HD seed not found");
auto m = libzcash::SaplingExtendedSpendingKey::Master(seed); auto m = libzcash::SaplingExtendedSpendingKey::Master(seed);
@@ -222,7 +223,20 @@ CPubKey CWallet::GenerateNewKey()
bool fCompressed = CanSupportFeature(FEATURE_COMPRPUBKEY); // default to compressed public keys if we want 0.6.0 wallets bool fCompressed = CanSupportFeature(FEATURE_COMPRPUBKEY); // default to compressed public keys if we want 0.6.0 wallets
CKey secret; CKey secret;
// Create new metadata
int64_t nCreationTime = GetTime();
CKeyMetadata metadata(nCreationTime);
// Derive the transparent key deterministically from the HD seed when the
// feature is enabled, so it can be recovered from the seed alone. Otherwise
// fall back to a random key (e.g. legacy wallets that have no HD seed).
if (IsHDTransparentEnabled()) {
DeriveNewChildKey(metadata, secret);
fCompressed = true; // BIP32-derived keys are always compressed
} else {
secret.MakeNewKey(fCompressed); secret.MakeNewKey(fCompressed);
}
// Compressed public keys were introduced in version 0.6.0 // Compressed public keys were introduced in version 0.6.0
if (fCompressed) if (fCompressed)
@@ -231,9 +245,7 @@ CPubKey CWallet::GenerateNewKey()
CPubKey pubkey = secret.GetPubKey(); CPubKey pubkey = secret.GetPubKey();
assert(secret.VerifyPubKey(pubkey)); assert(secret.VerifyPubKey(pubkey));
// Create new metadata mapKeyMetadata[pubkey.GetID()] = metadata;
int64_t nCreationTime = GetTime();
mapKeyMetadata[pubkey.GetID()] = CKeyMetadata(nCreationTime);
if (!nTimeFirstKey || nCreationTime < nTimeFirstKey) if (!nTimeFirstKey || nCreationTime < nTimeFirstKey)
nTimeFirstKey = nCreationTime; nTimeFirstKey = nCreationTime;
@@ -242,6 +254,57 @@ CPubKey CWallet::GenerateNewKey()
return pubkey; return pubkey;
} }
// Derive a new transparent key from the HD seed along the BIP44 external chain
// m/44'/coin_type'/0'/0/i. The child index is taken from (and advances)
// hdChain.transparentChildCounter, which is persisted so the same keys can be
// regenerated after a seed-only restore. Mirrors GenerateNewSaplingZKey.
void CWallet::DeriveNewChildKey(CKeyMetadata& metadata, CKey& secretRet)
{
AssertLockHeld(cs_wallet); // mapKeyMetadata / hdChain
HDSeed seed;
if (!GetHDSeedForDerivation(seed))
throw std::runtime_error("CWallet::DeriveNewChildKey(): HD seed not found");
RawHDSeed rawSeed = seed.RawSeed();
CExtKey masterKey; // m
CExtKey purposeKey; // m/44'
CExtKey coinTypeKey; // m/44'/coin_type'
CExtKey accountKey; // m/44'/coin_type'/0'
CExtKey externalChainKey; // m/44'/coin_type'/0'/0
CExtKey childKey; // m/44'/coin_type'/0'/0/i
masterKey.SetMaster(rawSeed.data(), rawSeed.size());
uint32_t bip44CoinType = Params().BIP44CoinType();
// BIP44 path, single account (0'), external chain (0). On this ac_private=1
// chain the internal/change chain can never hold value, so it is unused.
masterKey.Derive(purposeKey, 44 | BIP32_HARDENED_KEY_LIMIT);
purposeKey.Derive(coinTypeKey, bip44CoinType | BIP32_HARDENED_KEY_LIMIT);
coinTypeKey.Derive(accountKey, 0 | BIP32_HARDENED_KEY_LIMIT);
accountKey.Derive(externalChainKey, 0);
// Derive the next child index, skipping any key already in the wallet.
do {
externalChainKey.Derive(childKey, hdChain.transparentChildCounter);
metadata.hdKeypath = "m/44'/" + std::to_string(bip44CoinType) + "'/0'/0/" + std::to_string(hdChain.transparentChildCounter);
metadata.seedFp = hdChain.seedFp;
hdChain.transparentChildCounter++;
} while (HaveKey(childKey.key.GetPubKey().GetID()));
secretRet = childKey.key;
// Bump a legacy v1 chain to v2 so the transparent counter gets persisted.
if (hdChain.nVersion < CHDChain::VERSION_HD_TRANSPARENT)
hdChain.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
// Persist the advanced counter so restarts / restores don't reuse indices.
if (fFileBacked && !CWalletDB(strWalletFile).WriteHDChain(hdChain))
throw std::runtime_error("CWallet::DeriveNewChildKey(): Writing HD chain model failed");
}
bool CWallet::AddKeyPubKey(const CKey& secret, const CPubKey &pubkey) bool CWallet::AddKeyPubKey(const CKey& secret, const CPubKey &pubkey)
{ {
AssertLockHeld(cs_wallet); // mapKeyMetadata AssertLockHeld(cs_wallet); // mapKeyMetadata
@@ -2290,18 +2353,39 @@ CAmount CWallet::GetChange(const CTransaction& tx) const
bool CWallet::IsHDFullyEnabled() const bool CWallet::IsHDFullyEnabled() const
{ {
// Only Sapling addresses are HD for now // Both Sapling and transparent addresses are HD when transparent HD is on.
return false; return IsHDTransparentEnabled();
}
bool CWallet::IsHDTransparentEnabled() const
{
// Transparent keys are HD-derived when the wallet has an HD seed and the
// feature is enabled (default on). Legacy wallets keep any pre-existing
// random t-keys; only newly generated keys become HD (and those old random
// keys are NOT seed-recoverable, so wallet.dat backups remain necessary).
return !hdChain.seedFp.IsNull() && GetBoolArg("-hdtransparent", true);
} }
void CWallet::GenerateNewSeed() void CWallet::GenerateNewSeed()
{ {
LOCK(cs_wallet); LOCK(cs_wallet);
auto seed = HDSeed::Random(HD_WALLET_SEED_LENGTH);
int64_t nCreationTime = GetTime(); int64_t nCreationTime = GetTime();
// Opt-in: create the wallet from a fresh BIP39 mnemonic so its 24 words can
// be exported (z_exportmnemonic) and used in SilentDragonXLite.
if (GetBoolArg("-usemnemonic", false)) {
RawHDSeed entropy;
if (GenerateMnemonicEntropy(256, entropy)) {
HDSeed seed(entropy);
if (InstallHDSeed(seed, true, nCreationTime))
return;
}
LogPrintf("%s: -usemnemonic seed generation failed, falling back to a random seed\n", __func__);
}
auto seed = HDSeed::Random(HD_WALLET_SEED_LENGTH);
// If the wallet is encrypted and locked, this will fail. // If the wallet is encrypted and locked, this will fail.
if (!SetHDSeed(seed)) if (!SetHDSeed(seed))
throw std::runtime_error(std::string(__func__) + ": SetHDSeed failed"); throw std::runtime_error(std::string(__func__) + ": SetHDSeed failed");
@@ -2310,7 +2394,7 @@ void CWallet::GenerateNewSeed()
// the child index counter in the database // the child index counter in the database
// as a hdchain object // as a hdchain object
CHDChain newHdChain; CHDChain newHdChain;
newHdChain.nVersion = CHDChain::VERSION_HD_BASE; newHdChain.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
newHdChain.seedFp = seed.Fingerprint(); newHdChain.seedFp = seed.Fingerprint();
newHdChain.nCreateTime = nCreationTime; newHdChain.nCreateTime = nCreationTime;
SetHDChain(newHdChain, false); SetHDChain(newHdChain, false);
@@ -2374,6 +2458,122 @@ bool CWallet::LoadCryptedHDSeed(const uint256& seedFp, const std::vector<unsigne
return CCryptoKeyStore::SetCryptedHDSeed(seedFp, seed); return CCryptoKeyStore::SetCryptedHDSeed(seedFp, seed);
} }
bool CWallet::InstallHDSeed(const HDSeed& seed, bool fMnemonic, int64_t nCreateTime)
{
AssertLockHeld(cs_wallet);
if (!SetHDSeed(seed))
return false;
CHDChain newHdChain;
newHdChain.nVersion = fMnemonic ? CHDChain::VERSION_HD_MNEMONIC
: CHDChain::VERSION_HD_TRANSPARENT;
newHdChain.seedFp = seed.Fingerprint();
newHdChain.nCreateTime = nCreateTime;
newHdChain.fMnemonicSeed = fMnemonic;
SetHDChain(newHdChain, false);
return true;
}
bool CWallet::SetHDSeedFromHex(const std::string& seedHex)
{
LOCK(cs_wallet);
// Refuse to clobber an existing seed (the keystore refuses too); restore
// must run on a fresh/empty wallet.
if (HaveHDSeed())
return false;
if (!IsHex(seedHex))
return false;
std::vector<unsigned char> raw = ParseHex(seedHex);
// 32 = legacy raw seed; 64 = BIP39-derived seed (as exported by a mnemonic
// wallet). Either is used directly for derivation (fMnemonicSeed = false).
if (raw.size() != 32 && raw.size() != 64)
return false;
RawHDSeed rawSeed(raw.begin(), raw.end());
HDSeed seed(rawSeed);
return InstallHDSeed(seed, false, 1); // birthday = genesis for a restore
}
bool CWallet::SetHDSeedFromMnemonic(const std::string& phrase)
{
LOCK(cs_wallet);
if (HaveHDSeed())
return false;
RawHDSeed entropy;
if (!MnemonicToEntropy(phrase, entropy))
return false;
// Store the BIP39 entropy as the HDSeed (SilentDragonXLite's on-disk
// convention); the 64-byte seed is expanded from it on demand.
HDSeed seed(entropy);
return InstallHDSeed(seed, true, 1); // birthday = genesis for a restore
}
bool CWallet::GetHDSeedForDerivation(HDSeed& seedOut) const
{
HDSeed stored;
if (!GetHDSeed(stored))
return false;
if (!hdChain.fMnemonicSeed) {
seedOut = stored; // legacy / hex seed: fed to derivation directly
return true;
}
// Mnemonic wallet: the stored seed is the 32-byte BIP39 entropy. Expand it
// to the 64-byte BIP39 seed exactly as SilentDragonXLite does.
RawHDSeed seed64;
if (!Bip39SeedFromEntropy(stored.RawSeed(), seed64))
return false;
seedOut = HDSeed(seed64);
return true;
}
bool CWallet::GetMnemonicPhrase(std::string& phraseOut) const
{
if (!hdChain.fMnemonicSeed)
return false;
HDSeed stored;
if (!GetHDSeed(stored)) // fails on an encrypted+locked wallet
return false;
return EntropyToMnemonic(stored.RawSeed(), phraseOut);
}
void CWallet::TopUpHDTransparentKeys(unsigned int count, int64_t nBirthday)
{
LOCK(cs_wallet);
if (!IsHDTransparentEnabled())
return;
for (unsigned int i = 0; i < count; i++) {
CKey secret;
CKeyMetadata metadata(nBirthday);
DeriveNewChildKey(metadata, secret);
CPubKey pubkey = secret.GetPubKey();
assert(secret.VerifyPubKey(pubkey));
mapKeyMetadata[pubkey.GetID()] = metadata;
// Keep the birthday floor at nBirthday so the rescan is not clipped
// (derived keys are stamped nBirthday, not "now", precisely for this).
if (!nTimeFirstKey || nBirthday < nTimeFirstKey)
nTimeFirstKey = nBirthday;
if (!AddKeyPubKey(secret, pubkey))
throw std::runtime_error("CWallet::TopUpHDTransparentKeys(): AddKeyPubKey failed");
}
}
void CWalletTx::SetSaplingNoteData(mapSaplingNoteData_t &noteData) void CWalletTx::SetSaplingNoteData(mapSaplingNoteData_t &noteData)
{ {
mapSaplingNoteData.clear(); mapSaplingNoteData.clear();

View File

@@ -1063,6 +1063,9 @@ public:
* Generate a new key * Generate a new key
*/ */
CPubKey GenerateNewKey(); CPubKey GenerateNewKey();
//! Derive a new transparent key from the HD seed along the BIP44 external
//! chain m/44'/coin_type'/0'/0/i, advancing hdChain.transparentChildCounter.
void DeriveNewChildKey(CKeyMetadata& metadata, CKey& secretRet);
//! Adds a key to the store, and saves it to disk. //! Adds a key to the store, and saves it to disk.
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey); bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);
//! Adds a key to the store, without saving it to disk (used by LoadWallet) //! Adds a key to the store, without saving it to disk (used by LoadWallet)
@@ -1294,6 +1297,10 @@ public:
/* Returns true if HD is enabled for all address types, false if only for Sapling */ /* Returns true if HD is enabled for all address types, false if only for Sapling */
bool IsHDFullyEnabled() const; bool IsHDFullyEnabled() const;
/* Returns true if transparent keys should be HD-derived from the seed.
Requires an HD seed and the -hdtransparent option (default on). */
bool IsHDTransparentEnabled() const;
/* Generates a new HD seed (will reset the chain child index counters) /* Generates a new HD seed (will reset the chain child index counters)
Sets the seed's version based on the current wallet version (so the Sets the seed's version based on the current wallet version (so the
caller must ensure the current wallet version is correct before calling caller must ensure the current wallet version is correct before calling
@@ -1303,6 +1310,41 @@ public:
bool SetHDSeed(const HDSeed& seed); bool SetHDSeed(const HDSeed& seed);
bool SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret); bool SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret);
/* Restore a wallet's HD seed from a hex string (as exported in the
z_exportwallet "# HDSeed=" comment): 32 bytes for a legacy raw seed, or
64 bytes for a BIP39-derived seed. Only succeeds on a wallet that has no
seed yet. Sets the chain birthday to genesis so a rescan finds all
historical (coinbase) funds. Returns false on bad input or existing seed. */
bool SetHDSeedFromHex(const std::string& seedHex);
/* Restore/create a wallet from a BIP39 mnemonic phrase, byte-compatible with
SilentDragonXLite: stores the 32-byte entropy, marks the chain mnemonic,
and derives the 64-byte BIP39 seed on demand. Only succeeds on a wallet
with no seed yet. Returns false on an invalid phrase or existing seed. */
bool SetHDSeedFromMnemonic(const std::string& phrase);
/* Return the wallet's 24-word BIP39 recovery phrase, if this is a mnemonic
wallet and the seed is available (unlocked). Returns false otherwise. */
bool GetMnemonicPhrase(std::string& phraseOut) const;
/* True if the HD seed was derived from a BIP39 mnemonic (stored as entropy). */
bool IsMnemonicSeed() const { return hdChain.fMnemonicSeed; }
/* Return the seed to feed into HD derivation. For mnemonic wallets this
expands the stored 32-byte entropy into the 64-byte BIP39 seed; for legacy
wallets it is the stored seed unchanged. Use this everywhere keys/OVKs are
derived so behaviour matches SilentDragonXLite. */
bool GetHDSeedForDerivation(HDSeed& seedOut) const;
/* Shared tail of the seed-install paths: stores `seed` and a fresh CHDChain
(mnemonic vs raw) with the given birthday. Caller must hold cs_wallet. */
bool InstallHDSeed(const HDSeed& seed, bool fMnemonic, int64_t nCreateTime);
/* Pre-derive `count` HD transparent keys (external chain) into the keystore,
stamped with creation time `nBirthday`, so a subsequent rescan can find
funds paid to them after a seed-only restore. */
void TopUpHDTransparentKeys(unsigned int count, int64_t nBirthday);
/* Set the HD chain model (chain child index counters) */ /* Set the HD chain model (chain child index counters) */
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; }

View File

@@ -62,11 +62,24 @@ class CHDChain
{ {
public: public:
static const int VERSION_HD_BASE = 1; static const int VERSION_HD_BASE = 1;
static const int CURRENT_VERSION = VERSION_HD_BASE; // Version 2 adds the transparent (secp256k1/BIP44) external-chain counter.
static const int VERSION_HD_TRANSPARENT = 2;
// Version 3 marks a seed derived from a BIP39 mnemonic: the stored HDSeed is
// the 32-byte BIP39 entropy, expanded to the 64-byte seed for derivation
// (matches SilentDragonXLite's on-disk convention).
static const int VERSION_HD_MNEMONIC = 3;
static const int CURRENT_VERSION = VERSION_HD_MNEMONIC;
int nVersion; int nVersion;
uint256 seedFp; uint256 seedFp;
int64_t nCreateTime; // 0 means unknown int64_t nCreateTime; // 0 means unknown
uint32_t saplingAccountCounter; uint32_t saplingAccountCounter;
// Next index on the HD transparent external chain m/44'/coin'/0'/0/i.
// Only serialized/consulted when nVersion >= VERSION_HD_TRANSPARENT.
uint32_t transparentChildCounter;
// True when the stored HDSeed is BIP39 entropy that must be expanded to the
// 64-byte BIP39 seed before HD derivation. Only serialized when
// nVersion >= VERSION_HD_MNEMONIC (false for all pre-existing wallets).
bool fMnemonicSeed;
CHDChain() { SetNull(); } CHDChain() { SetNull(); }
@@ -79,6 +92,14 @@ public:
READWRITE(seedFp); READWRITE(seedFp);
READWRITE(nCreateTime); READWRITE(nCreateTime);
READWRITE(saplingAccountCounter); READWRITE(saplingAccountCounter);
// Version-gated so pre-existing v1 wallet.dat records still deserialize
// (they simply leave the newer fields at their SetNull defaults).
if (this->nVersion >= VERSION_HD_TRANSPARENT) {
READWRITE(transparentChildCounter);
}
if (this->nVersion >= VERSION_HD_MNEMONIC) {
READWRITE(fMnemonicSeed);
}
} }
void SetNull() void SetNull()
@@ -87,6 +108,8 @@ public:
seedFp.SetNull(); seedFp.SetNull();
nCreateTime = 0; nCreateTime = 0;
saplingAccountCounter = 0; saplingAccountCounter = 0;
transparentChildCounter = 0;
fMnemonicSeed = false;
} }
}; };