24 Commits
v1.1.0 ... dev

Author SHA1 Message Date
4dc57e80b1 build: bump version to 1.2.0
dev and the v1.1.0 tag were version-indistinguishable: both reported
CLIENT_VERSION 1010050, subversion "/DragonX:1.1.0/" and IS_RELEASE=true, because
660678f9b was the last commit to touch a version file and it predates the tag. The
twenty commits since were therefore invisible to every channel a client can query,
and the in-app updater compares exactly those. Only git-describe distinguished
them, and that degrades to "-unk" on a tarball build.

A minor bump rather than a patch: since v1.1.0 the tree gained auto-shield-coinbase
(a new feature, on by default where the seed is known-recoverable), BIP39 seed
phrases as the default for new wallets, the z_autoshieldstatus RPC, and three new
wallet.dat record types. Understating that as 1.1.1 would hide an on-disk format
change from the one place users look.

The published v1.1.0 tag is left where it is. Re-pointing a tag that is already on
the remote breaks anyone who fetched it.

The wallet feature version deliberately stays at FEATURE_LATEST = 60000. The new
records are additive and older binaries skip unknown types harmlessly, while
bumping it would make them refuse the wallet outright with DB_TOO_NEW. The one real
incompatibility, a truncated hdchain record, is self-healing as of a0ccb4be1, so
refusing to load would be strictly worse for the user than what happens today.

Verified: configure.ac and clientversion.h agree; CLIENT_VERSION 1010050 -> 1020050;
build.sh derives 1.2.0; bitcoin-config.h carries CLIENT_VERSION_MINOR 2 after a
reconfigure run with the depends CONFIG_SITE; a full rebuild of src succeeds with 0
errors and both binaries report v1.2.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 21:52:51 +02:00
04ac7c1186 doc: how to build release binaries in containers, for several glibc floors
Binaries built on Ubuntu 22.04 require GLIBC_2.34 and GLIBCXX_3.4.30 and will not
start on Ubuntu 20.04 -- which is four of our five seeds, and an unknown share of
users. The binary the fleet actually runs today needs only GLIBC_2.29, so it was
built somewhere older; seed 176 has since been upgraded to 22.04 and now produces
binaries it is the only seed able to run.

--linux-compat and Dockerfile.compat already solved this (6d56ad854) but were
undocumented outside the build script and pinned to one base image. Parameterise
the base via ARG BASE_IMAGE (default unchanged, so --linux-compat behaves exactly
as before) and document the whole path.

doc/build-containers.md is written to be executed by a person or an agent starting
from a machine with nothing installed: why the glibc direction matters, with the
measured numbers; what already exists in the repo so nobody writes a second build
system; prerequisites and honest cost (~15GB, 4GB RAM, 1-2h per base because
depends/ builds boost, BDB, wolfssl and rust from source); one-target and
multi-target recipes; which base to pick and why 20.04 is the recommended floor
while 18.04 needs verifying (GCC 7 against -std=c++17); a mandatory verification
step with the exact objdump/readelf commands and the expected ceilings; and the
traps.

The traps are the part worth having written down: ETXTBSY when installing over a
running daemon (cp fails even after the process exits -- stage and rename, then
sha256-verify before starting); never touching configure.ac in a configured tree,
because the mtime alone triggers a reconfigure that dies on libdb_cxx; never
blind-touching a path that may not exist, which silently creates stray empty files;
RandomX needing ARCH=default or it emits AVX-512 that SIGILLs the fleet; build-win.sh
silently discarding every argument; and macOS being uncontainerisable because
depends/ has no darwin cross path at all.

Also records that full static linking is NOT the answer here: the daemon resolves
node1..node5.dragonx.is via getaddrinfo, and static glibc pushes that through NSS,
which dlopens libnss_dns at runtime and reintroduces the dependency it was meant to
remove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 21:28:12 +02:00
65130c3120 async/wallet: close the loose ends around automated operations
Six small defects, all found by an audit of the automated-operation path and all
verified present before changing anything.

AsyncRPCQueue::addOperation returned void and silently dropped the operation when
the queue was closed or finishing. Every caller assumed success: the three
schedulers left their running flag set with nothing in flight (which then blocks
every later round), and z_sendmany / z_shieldcoinbase / z_mergetoaddress returned
an opid for work that would never run -- z_shieldcoinbase and z_mergetoaddress
having already locked their selected coins in the constructor. It now returns
bool; the schedulers release the flag and log, and the three RPCs raise an error
instead of handing back an opid. Coin locks are memory-only, so a refusal at
shutdown reclaims them with the process; the lie about success was the defect.

Nothing ever removed finished automated operations from the queue's map.
popOperationForId is reached only from z_getoperationresult, so on a node running
autoshield every 25 blocks the map grew by one entry per round forever. The
schedulers now pop the operation they just cancelled. The worker already handles
a missing id ("cannot find operation in map, may have been removed",
asyncrpcqueue.cpp), and it releases lock_ before calling main(), so popping under
cs_wallet introduces no lock cycle.

The autoshield operation built its transaction against targetHeight_, the
enqueue-time height, while SetExpiryHeight and the network-upgrade straddle guard
both used tipHeight. Since the builder's height selects the consensus branch id,
the guard was checking a height the transaction was not signed against -- it could
not prevent the failure it exists to prevent. Now tipHeight throughout.

cancel() in the sweep, consolidation and autoshield operations set CANCELLED
unconditionally, dropping the base class's guard entirely. The schedulers cancel
the previous operation when they enqueue the next, so a round that had already
SUCCEEDED got its result relabelled as cancelled. Restored a narrower guard: still
cancellable while READY or EXECUTING (the base class refuses the latter, which
would defeat cancellation here), but a terminal state is left alone.

CommitAutomatedTx dumped the whole transaction to stderr on every commit,
duplicating the LogPrintf that CommitTransaction does one call later. ToString()
emits a line per input, so with the 400-input autoshield cap that was tens of KB
of stderr per round. Removed.

Also corrected a comment that credited the immature-coinbase exclusion to
fOnlySpendable (the argument is fOnlyConfirmed; the exclusion is unconditional in
AvailableCoins), and noted that AUTOSHIELD_CTXIN_P2SH_SIZE is a byte size that
merely happens to share the value 400 with the input cap.

Verified on an isolated regtest chain, 8/8: nine consecutive autoshield rounds
succeed after the builder-height change; the operation map stays at 1 entry across
all nine (it grew one per round before); stderr totals 1608 bytes for the whole
run with zero CommitAutomatedTx dumps, while debug.log still records all nine
commits via CommitTransaction; no round is relabelled cancelled; funds shield
correctly and no coin locks leak.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 19:18:30 +02:00
698bcf9574 build: derive the release version from configure.ac
build.sh hardcoded VERSION="1.0.3" while configure.ac had been at 1.1.0 since
660678f9b. package_release() uses it to name the output directory, so
`./build.sh --all-release` from dev would have emitted
release/dragonx-1.0.3-<platform>/ containing binaries that report 1.1.0 --
mislabelled artifacts, from the one place where the label is what users see.

Read the four _CLIENT_VERSION_* defines out of configure.ac instead, applying the
same suffix rule its _CLIENT_VERSION_SUFFIX m4 uses (build < 25 -> beta, < 50 ->
rc, == 50 -> plain, > 50 -> point release), and abort if any of them cannot be
parsed rather than naming a release directory after an empty string.

SCRIPT_DIR moves above the version block because the lookup needs it.

Verified: derives 1.1.0 from the current tree, and a deliberately unparseable
configure.ac makes it exit 1 with a message instead of guessing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:56:18 +02:00
a0ccb4be1d wallet: repair a truncated hdchain record instead of refusing the wallet
A build predating VERSION_HD_TRANSPARENT rewrites the hdchain record with only
its four base fields while leaving nVersion at whatever it read. Our version-gated
reads then run off the end of the stream, ReadKeyValue turns the throw into
fHDChainRead=false, and LoadWallet escalates that to DB_CORRUPT. Verified against
the real v1.0.2-2b011d6ee release binary: a dev wallet, opened once by v1.0.2 and
given a single new sapling address, came back to

  Error reading wallet database: hdchain record is corrupt
  Error loading wallet.dat: HD seed present but the hdchain record is missing or corrupt
  Error loading wallet.dat: Wallet corrupted

Nothing is actually lost there -- the same test showed the seed phrase restoring
the full balance, the autoshield destination, and even the address v1.0.2 had
generated -- but the user is shown "Wallet corrupted" with no hint of that.

Recover instead, for a v1 or v2 record. Everything derivation depends on is either
in the four-field prefix or in the separate hdseed record, and the trailing
counters are self-healing: DeriveNewChildKey and GenerateNewSaplingZKey both skip
indices whose key the wallet already holds, so restarting a counter at 0 re-walks
past existing keys rather than reissuing them. Read the prefix from an untouched
copy of the stream, default the missing tail, log it, and rewrite the record in
full form so the next load is clean.

A record claiming nVersion >= VERSION_HD_MNEMONIC still fails loud: that flag
selects the derivation input, so guessing it wrong yields a different key tree in
silence. No wallet this code has written can be in that state -- every
InstallHDSeed call site passes fMnemonic=false -- so the branch is defensive only.

Also say what to do about it. Both the log line and the init error now name the
remedy (move wallet.dat aside, restart with -mnemonic and -rescan) rather than
stopping at "Wallet corrupted".

Verified on regtest against the real v1.0.2 binary, 14/14: the round-trip that
previously ended in "Wallet corrupted" now loads, logs the repair and the rewrite,
keeps the balance, the autoshield destination and v1.0.2's own address, still
issues distinct fresh t-addresses after the counter reset, needs no repair on the
second load, and remains fully recoverable from the seed phrase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 18:18:38 +02:00
358011bd54 wallet: resolve the autoshield destination at startup
pwalletMain->autoShieldAddress was only ever written by a round running in the
current process, so on any node without an explicit -autoshieldaddress,
z_autoshieldstatus reported an empty destination from startup until the first
round fired. disabled_reason was empty on that path too (rpcwallet.cpp), so the
RPC showed autoshield true, running false, no address and no explanation -- the
exact silent state z_autoshieldstatus was added to eliminate. On a restored
wallet, which pre-derives the whole -mnemonicsaplinggap window and therefore
always has an in-gap account to pick, the answer was known at startup and simply
not computed.

Split the read-only half of resolveDestination into a free function shared with
init: the configured override if set, else the lowest in-gap account
m/32'/coin'/i' the wallet already holds. init calls it once when autoshield is
enabled and no explicit address was given.

It deliberately does not generate a key. Deriving a fresh sapling account as a
side effect of populating a status field would mutate the wallet to make an RPC
prettier, so step 3 of resolveDestination -- the generation path, which must stay
inside the operation where an unlocked wallet is already established -- is left
where it was. A brand-new wallet holds nothing in-gap, so the field stays empty
there and disabled_reason now says why instead of being blank.

Behaviour is otherwise unchanged: same derivation, same lowest-index-wins rule,
same refusal to trust CKeyMetadata, same caching for the life of the process.

Verified on an isolated regtest chain, 12/12:
  fresh wallet      -> address "", reason "no destination resolved yet; one will
                       be derived from the HD seed on the first round"
  after one round   -> address set, reason empty
  after RESTART     -> address visible with NO block mined since (height 12 both
                       sides), and z_listaddresses still holds exactly 1 address,
                       so init derived nothing
  -autoshieldaddress-> still overrides the derived destination, and the round
                       shields into it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 17:41:32 +02:00
7a62fc4877 wallet: break the sweep/consolidation/autoshield deadlock
A successful-but-incomplete sweep round deliberately returns with fSweepRunning
still set and nextSweep unadvanced, as a "keep draining next block" baton. Every
early return in RunSaplingSweep, though, leaves that baton set without
re-dispatching -- and RunSaplingConsolidation, which is gated on fSweepRunning,
then returns without advancing nextConsolidation. So the "consolidation is
within 5 blocks" blackout at the top of RunSaplingSweep never lifts: sweep waits
on consolidation, consolidation waits on sweep, and neither runs again.

That much is pre-existing. What is new is that autoshield now shares the gate --
RunAutoShieldCoinbase returns early on fSweepRunning || fConsolidationRunning --
so a wedged sweep silently disables coinbase shielding too, with
z_autoshieldstatus reporting autoshield true, running false, and no reason.

Only honour the baton while a sweep operation is genuinely in flight: if the
operation for saplingSweepOperationId is absent or has reached a terminal state,
drop the stale flag and let the checks below decide afresh. The drain model is
unchanged -- nextSweep is still unadvanced, so the next block re-dispatches.

Also report the deferral in z_autoshieldstatus, so mutual exclusion with sweep
or consolidation reads as a deferral rather than an unexplained idle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 07:59:13 +02:00
9a8f17b2c8 wallet: bound autoshield rounds and lock their inputs
Two defects in a single autoshield round, both of the quiet kind.

The size estimate reserved a flat 2000 bytes for "header + sietch outputs", but
every autoshield tx carries three Sapling OutputDescriptions -- the change note
plus the two Sietch dummies -- and the real fixed cost is ~2937 bytes. Measured
across seven live mainnet coinbase shields: 113.5 bytes per input and 2936.6
+/- 0.8 bytes fixed, of which 3 * 948 = 2844 is the output descriptions. The
estimate was therefore short by ~937 bytes before a single input was counted.

Inputs are charged AUTOSHIELD_CTXIN_DUST_SIZE = 148, which is conservative for
the default P2PK coinbase but exact for P2PKH, so with a P2PKH coinbase a
backlog of 1332..1337 utxos passed the estimate and built a tx over
MAX_TX_SIZE_AFTER_SAPLING. CommitTransaction calls AddToWallet before
AcceptToMemoryPool, so a rejected oversize tx leaves its inputs reading as spent.

z_shieldcoinbase caps a manual shield at SHIELD_COINBASE_DEFAULT_LIMIT = 50
utxos; autoshield dropped that cap and relied on the byte estimate alone.
Restore one -- AUTOSHIELD_MAX_INPUTS = 400 -- so the byte arithmetic is no longer
the only thing between a large backlog and an oversize transaction. The
remainder is shielded on the next round.

Second, the proof build deliberately runs without cs_wallet so wallet RPCs are
not stalled, which leaves a multi-second window in which a concurrent
z_shieldcoinbase or z_sendmany can re-select the same coinbase outputs.
AvailableCoins already honours IsLockedCoin and z_shieldcoinbase already
brackets its selection with LockCoin/UnlockCoin; autoshield made zero LockCoin
calls. Take the locks under cs_wallet at selection time and release them via
RAII, since several early returns sit between selection and commit and a leaked
lock would exclude those coins from every future round.

Verified on an isolated regtest chain with a 540-utxo backlog:
  round 1 logged "reached per-round input cap (400)" and committed exactly 400
    inputs in a 48351-byte tx (estimate 62300, limit 200000)
  round 2 took the remaining 151; backlog drained 540 -> 0
  listlockunspent showed 400 coins locked mid-round and 0 afterwards
  48351 bytes for 400 inputs implies 2937 bytes of fixed overhead, agreeing
    with the mainnet measurement to 14 bytes

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 07:59:13 +02:00
1ec6590fb7 wallet: add z_autoshieldstatus
Auto-shielding could silently decline to run with no way to ask why. The
destination it resolves was equally invisible: the only evidence was a LogPrintf
emitted once per round, so an operator wanting to know where their mined coinbase
was going had to grep debug.log.

z_autoshieldstatus reports the enable state, whether a round is in flight, the
next height, interval, fee, minimum utxos, and the resolved destination -- plus
the HD seed provenance in both numeric and readable form, whether the seed is
phrase-recoverable, and a disabled_reason explaining why it is off when it is.

That last field is the point. "autoshield": false on its own does not distinguish
an operator who passed -autoshield=0 from a wallet whose seed provenance is not
known-recoverable, and those need different responses.

Mirrors z_sweepstatus in shape and registration.

Verified on all three branches:
  fresh wallet    -> autoshield true, origin 1 "created on an empty wallet",
                     seed_recoverable true, disabled_reason ""
  -autoshield=0   -> disabled_reason "disabled by -autoshield=0"
  upgraded wallet -> autoshield false, origin 4 "predates provenance recording",
                     seed_recoverable false, disabled_reason "HD seed origin is
                     not known-recoverable; back the seed up and pass -autoshield=1"

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 06:04:43 +02:00
92e6c7008d wallet: define CHDChain's static constants out of line
hush-gtest failed to link with "undefined reference to
CHDChain::VERSION_HD_MNEMONIC". The version constants are static const int with
in-class initialisers and no definition anywhere, so any ODR use needs one --
and gtest's EXPECT_*/ASSERT_* macros take their arguments by const reference,
which is exactly that. test_mnemonic_compat.cpp:161 passes VERSION_HD_MNEMONIC
to EXPECT_LT.

dragonxd links either way, because nothing in the daemon binds these to a
reference; only the test target exposed it, and the test target was never built
on the branch that introduced the test.

Define all four rather than only the one that failed: VERSION_HD_BASE,
VERSION_HD_TRANSPARENT and CURRENT_VERSION carry the identical latent fault, and
the next EXPECT_EQ against any of them would hit the same wall. Fixing the test
instead would have hidden the problem rather than removed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 18:58:24 +02:00
733df964ec Merge autoshield-safety into dev: auto-shield coinbase + BIP39 by default
Brings the auto-shield-coinbase feature and the wallet seed work onto the release
line so both are exercised from dev rather than a side branch.

From upstream: auto-shield of matured coinbase into a wallet z-address, plus
fixes to two pre-existing scheduler wedges (consolidation dispatching every block
instead of once per interval, and a failing sweep stranding its running flag).

On top of that:
  * the auto-shield destination is chosen by re-deriving m/32'/coin'/i' from the
    seed and taking the lowest in-gap account the wallet holds a key for, rather
    than the first entry in std::set order. Key metadata is NOT evidence of
    provenance -- z_importwallet copies hdKeypath and seedFp verbatim from the
    import file -- so a crafted import could otherwise claim account 0 and
    capture every shielded reward;
  * HD seed/chain persistence is hardened: the chain record is written before the
    seed, a corrupt hdchain is loud rather than silently reverting derivation to
    the raw entropy, hdchain survives -salvagewallet, and the silent BIP39 ->
    random seed fallback is gone;
  * seed provenance is recorded, and -autoshield defaults ON only where that
    provenance says the seed is recoverable;
  * mnemonic wallets now store the EXPANDED 64-byte BIP39 seed with the 32-byte
    entropy in a separate display-only record. Derivation reads stored bytes
    directly on every binary, so key trees are identical and no CHDChain version
    bump or minversion fence is needed -- the format stays readable by earlier
    releases;
  * new wallets are created from a BIP39 phrase by default;
  * the plaintext hdseed record is erased when a wallet is encrypted. It was
    previously left behind, and CDB::Rewrite copies surviving records verbatim,
    so the unencrypted seed persisted on disk forever.

TWO DEFAULTS CHANGE for new wallets: auto-shielding (where the seed provenance is
known) and BIP39 seed generation. Existing wallets are untouched -- seed
generation is reachable only when a wallet has none, and all three key stores
refuse to replace an existing seed.

Testing state, stated plainly: every commit built clean and the branch was
verified on an isolated chain -- destination selection provably ignores an
imported foreign key, both provenance branches behave, a new-format wallet
reopened by a pre-change binary lists identical addresses, and restoring only the
24 words recovers the wallet. NOT yet tested: any of this against a funded wallet
on mainnet, or a soak of these defaults on a real node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 11:53:32 -05:00
9734402d7b wallet: create new wallets from a BIP39 seed phrase by default
New wallets now get an exportable 24-word phrase instead of a random seed that
no phrase can ever reproduce. Only wallets with no seed yet are affected;
GenerateNewSeed is reachable from one place, under !HaveHDSeed(), and all three
key stores refuse to replace an existing seed.

This is safe to default on now that the storage form is backwards compatible: the
expanded 64-byte BIP39 seed is what gets stored, so a binary predating any of
this reads it and derives the same keys.

Verified on an isolated chain before flipping:
  - a new-format wallet reopened with the tagged v1.1.0 binary, which has no
    knowledge of the entropy record, listed identical addresses;
  - restoring only the 24 words into a fresh datadir recovered every address.

Note this changes what a new wallet is, not what an existing one is: the same
entropy yields a different key tree depending on which side of this commit
created the wallet. Nothing migrates, and nothing needs to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 07:01:22 +02:00
20b2cbe830 wallet: store the expanded BIP39 seed for mnemonic wallets
Mnemonic wallets stored the 32-byte BIP39 entropy as the HD seed and relied on
CHDChain.fMnemonicSeed to tell the deriver to expand it first. That flag is
version-gated in the CHDChain serialisation, so a binary that predates it reads
the record, never consumes the trailing byte, and derives from the raw entropy --
a different key tree, silently, with no error.

Store the expanded 64-byte BIP39 seed instead, with fMnemonic = false, and keep
the entropy in its own display-only record. Derivation then reads the stored
bytes directly on every binary, old or new, so key trees are identical and no
CHDChain version bump or minversion fence is needed. The wallet format stays
readable by earlier releases rather than becoming one-way.

Addresses are unchanged: the previous format expanded the entropy on every read
and fed the same 64 bytes to Master(). This is also the form the tree already
round-trips through -- z_exportwallet dumps the expanded seed, and restoring that
hex via -hdseed installs it with fMnemonic = false.

Write order is load-bearing: seed first, entropy second. A crash between them
leaves a wallet with a seed and no phrase, which is merely inconvenient. The
reverse would leave an entropy record with no seed, and the next start would mint
a different seed while the wallet still held a phrase for the old one.

-usemnemonic still defaults to false; only explicit opt-in and -mnemonic restores
take this path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 06:39:38 +02:00
2d7dd90c55 wallet: plumb the mnemonic entropy through CWallet
Wires the key store secret to the database records: load and store paths on
CWallet, the two ReadKeyValue arms, and the export path.

GetMnemonicPhrase now prefers the entropy record and verifies it before printing:
a phrase is only returned if expanding it reproduces the seed derivation actually
uses. It falls back to the existing fMnemonicSeed path, so wallets that store the
entropy AS the seed keep working unchanged.

IsMnemonicSeed() now means "a phrase is available" rather than "the seed is the
entropy", which is what every caller actually wants.

Still a no-op on every existing wallet: nothing creates an entropy record yet, so
GetMnemonicEntropy returns false and the old code path is taken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 06:37:43 +02:00
3caec548ae wallet: persist the mnemonic entropy in wallet.dat
Adds the record pair for the entropy, mirroring "hdseed"/"chdseed": a plaintext
form, an encrypted form that erases its plaintext counterpart the way
WriteCryptedKey does, and an erase.

Both new types are registered in IsKeyType so -salvagewallet preserves them.
Without that, salvage would silently drop the phrase while keeping the wallet
otherwise intact.

The records are defined but nothing writes them yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 06:30:18 +02:00
2a7fdcc1db wallet: add an optional mnemonic-entropy secret to the key stores
Additive plumbing for storing a BIP39 entropy alongside the HD seed, mirroring
how the seed itself is handled through both key store layers: a plaintext member
on CBasicKeyStore, an encrypted pair on CCryptoKeyStore, encryption during the
unencrypted-to-encrypted conversion in EncryptKeys with the plaintext cleared,
and decryption on unlock.

RawHDSeed and CKeyingMaterial are the same secure_allocator vector type, so the
entropy passes through EncryptSecret/DecryptSecret with no adaptation.

Nothing calls this yet; there is no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 06:23:09 +02:00
143c33de48 wallet: erase the plaintext HD seed record when the wallet is encrypted
CWalletDB::WriteCryptedHDSeed wrote the "chdseed" record and left "hdseed" in
place, unlike WriteCryptedKey which erases "key"/"wkey" after writing "ckey".
No erase of "hdseed" existed anywhere in src/wallet/.

CDB::Rewrite does not save us: EncryptWallet calls it with pszSkip defaulted, so
it copies every surviving record verbatim into the new file. The result is that a
wallet created unencrypted and later encrypted keeps its raw HD seed in cleartext
on disk permanently, and reloads it into memory on every start.

Add CWalletDB::EraseHDSeed and call it from CWallet::SetCryptedHDSeed after the
encrypted record is written, through the same CWalletDB so it shares
EncryptWallet's transaction. Erase returns true on DB_NOTFOUND, so a wallet that
was never written in plaintext is unaffected.

The erase is deliberately best-effort and only logs on failure. A hard failure
here propagates into CCryptoKeyStore::EncryptKeys, which EncryptWallet turns into
assert(false) with half the keys encrypted in memory; a warning is strictly
better than that.

Note this path is only reachable with -developerencryptwallet, which is
experimental and off by default on this chain, so this is a latent fix rather
than a live one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 06:14:31 +02:00
e2f88175ab wallet: default -autoshield ON only when the HD seed is known-recoverable
Autoshield moves mined coinbase into a z-address that only this wallet's HD seed
can re-derive. Defaulting that ON is only defensible where the user can actually
restore that seed.

Two cases fail that test. A seedless legacy wallet has a seed minted onto it
silently at first start, so no backup the user already holds contains it. And a
wallet seeded by an earlier build predates provenance recording, so we cannot
tell which case it was. Both are now classified as not-known-recoverable and
autoshield stays off there until the operator backs the seed up and passes
-autoshield=1. An explicit -autoshield=0/1 still wins in either direction.

Wallets this software created on an empty datadir, or restored from a
user-supplied -mnemonic/-hdseed, keep the ON default: in both cases the user
either has the phrase or supplied the seed themselves.

Verified on real wallets: a wallet carrying no origin record is classified
unknown and logs "autoshield left OFF by default: HD seed origin 4"; a wallet
created by the previous commit logs "autoshield enabled" with no
re-classification, confirming the record persists rather than being recomputed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 03:22:55 +02:00
a494eabdce wallet: harden HD seed/chain persistence and record seed provenance
Autoshield sends mined coinbase to a seed-derived z-address, so the records that
decide how the seed derives keys become fund-safety critical. Three of them were
not treated that way.

InstallHDSeed wrote the seed record before the chain record, non-transactionally.
A crash between the two left a wallet holding a seed with no hdchain: on the next
load hdChain silently reverts to defaults, clearing fMnemonicSeed -- which
switches the derivation input from the expanded BIP39 seed to the raw entropy --
and resetting saplingAccountCounter. Write the chain first; the opposite torn
state is harmless and self-heals, because HaveHDSeed() is then false and init
installs again.

The hdchain record was read as a bare deserialise inside a catch-all with strErr
never set, and it is not a key type, so a corrupt record was downgraded to a
non-critical error and the node booted into the wrong key tree. Report it, track
whether it was read, and refuse to load a wallet that holds a seed but no
readable hdchain. Also preserve hdchain through a keys-only salvage, which would
otherwise drop it and produce exactly the state we now refuse.

GenerateNewSeed silently fell back to a random seed when BIP39 generation failed,
producing a wallet that looks mnemonic-capable but whose words can never be
exported and which no seed phrase can restore. A user who asked for -usemnemonic
now gets that or a hard failure.

Finally, record how the seed came to exist -- created on an empty wallet,
restored from -mnemonic/-hdseed, retrofitted onto a pre-existing seedless wallet,
or predating this record. A retrofitted seed is in no backup the user already
holds, so a feature that moves funds into addresses only that seed can re-derive
must not enable itself there by default. Nothing consumes this yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 03:18:35 +02:00
dad162a89a wallet: pick the autoshield destination by seed re-derivation, in-gap only
resolveDestination() took the first spendable address in std::set order, which
orders on the raw Sapling diversifier (zcash/Address.hpp:95-98) -- uncorrelated
with anything the operator can see, and unstable across restarts as addresses
are added.

Worse, GetSaplingPaymentAddresses() also returns z_importkey/z_importwallet
addresses, and CKeyMetadata is NOT evidence of provenance: both hdKeypath and
seedFp are copied verbatim out of the import source (rpcdump.cpp:511-516 ->
wallet.cpp:5440-5441) with no verification. A crafted import can therefore claim
this wallet's seedFp and keypath m/32'/coin'/0' and capture every shielded
mining reward into a key no seed restore can reproduce. Filtering on metadata
would not have caught that.

Derive instead. A bare -mnemonic/-hdseed restore pre-derives exactly
-mnemonicsaplinggap sapling accounts from index 0 with saplingAccountCounter
reset (init.cpp:2349-2355), so the only self-recoverable destinations are the
default addresses of m/32'/<coin>'/i' for i below the gap. Walk that window from
the seed and take the lowest index the wallet holds a spending key for. Deriving
is the only authoritative test and cannot be spoofed.

When nothing in the window is held yet, derive the next account -- but only if it
will land inside the window. GenerateNewSaplingZKey does not derive at
saplingAccountCounter: its do/while skips indices already held
(wallet.cpp:150-157), so a bare counter-below-gap test is unsound. Predict the
lowest free index at or above the counter and post-verify the returned address.
If no free account remains below the gap, refuse the round and leave the coinbase
transparent -- transparent funds are still recoverable through the 1000-key
transparent gap, an unfindable note is not.

Refusing is safe: main_impl turns a false return into a clean skip, and main()
always advances nextAutoShield and clears fAutoShieldRunning, so a refused round
cannot latch the feature off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 02:52:34 +02:00
bb292a89cc test: merge feature/autoshield-coinbase into v1.1.0 for isolated testing
Not for release. Built solely to exercise auto-shield-coinbase on an isolated
mining chain, since the feature is a no-op on a non-mining node (the canary
seed has no balance, no z-address, no coinbase, and does not mine).

Merge is clean: util/build-win.sh auto-merged, keeping both our ARCH=default
and their -Wa,-mbig-obj. Checkpoints, verify-once guard and the version bump
all survive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 19:24:20 -05:00
4d72e5fc30 wallet: fix sweep-scheduler wedge and unsynchronized driver mutation
The zaddr-sweep op cleared fSweepRunning/nextSweep only on the sweepComplete
success path (inside main_impl), so a cancelled or throwing sweep left
fSweepRunning stuck true. Since fSweepRunning now also gates consolidation and
the default-on autoshield, a persistently failing sweep (e.g. a corrupt-witness
note) would wedge all three background ops for the session.

Move the scheduler bookkeeping into main() so it runs on every terminal state
(success/failure/exception/cancel), guarded by op id. Preserve the intended
"keep draining every block until swept" model: on a successful-but-incomplete
round the flag stays set and nextSweep is not advanced; on completion OR on
failure/exception the flag is released and nextSweep backs off one interval, so
a failing sweep no longer retries every block or wedges the other ops.

Also fix RunSaplingSweep to take cs_wallet itself (was AssertLockHeld, a no-op
in release builds) since ChainTip does not hold it there and the driver mutates
scheduler state + enqueues -- matching RunSaplingConsolidation and
RunAutoShieldCoinbase.

sweepComplete is recorded via a new member; saplingSweepOperationId made public.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-21 18:55:16 -05:00
ca730a5d98 wallet: fix consolidation-scheduler wedge (ran every block; dead mutual-exclusion)
The Sapling auto-consolidation scheduler never advanced nextConsolidation after
init and never set fConsolidationRunning, so once the tip passed the init
threshold `-consolidation` dispatched a consolidation op every block instead of
once per -consolidationinterval, and every guard that reads fConsolidationRunning
(in RunSaplingSweep, and in the new autoshield driver) was dead.

Mirror the intended scheduler model:
- RunSaplingConsolidation sets fConsolidationRunning=true before dispatch and
  self-guards with `if (fConsolidationRunning) return;`.
- The consolidation op advances nextConsolidation = consolidationInterval +
  tipHeight and clears fConsolidationRunning on every terminal state
  (success/failure/exception/cancel), guarded by op id so only the current op
  mutates scheduler state.
- saplingConsolidationOperationId moved to public so the op can read it.

Restores the documented once-per-interval cadence and makes the
sweep/consolidation/autoshield mutual-exclusion guards effective.

Note: the sweep op has the same latent wedge (fSweepRunning/nextSweep are
cleared only on the sweepComplete success path, so a cancelled or throwing
sweep leaves fSweepRunning stuck true) - left for a follow-up; this commit is
the template to port.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-21 18:28:17 -05:00
2ebbbc777c wallet: add default-on auto-shield-coinbase; drain miner coinbase into a z-addr
On this ac_private=1 chain miners accumulate one transparent coinbase UTXO per
block (the only transparent output the chain permits). It had to be shielded
manually via z_shieldcoinbase, and left unshielded it is the sole persistent
metadata leak on the chain and the source of miner UTXO-fragmentation send
failures.

Add AsyncRPCOperation_autoshieldcoinbase: a periodic, default-on wallet op
driven from CWallet::ChainTip alongside sweep/consolidation, draining matured
coinbase into a wallet-owned Sapling z-address in size-bounded batches.

- Enqueue-only driver (RunAutoShieldCoinbase): takes only cs_wallet in the
  notify context; all gathering runs on the async worker under LOCK2(cs_main,
  cs_wallet).
- Dedicated op (not a reuse of z_shieldcoinbase) so it never toggles mining.
- Default-ON but conditional: silent no-op on -disablewallet, external
  -mineraddress, non-mining, or locked wallets (explicit IsLocked() guard).
- Destination is reuse-then-create; -autoshieldaddress override is
  spend-key-validated at init so funds cannot be stranded.
- Sweep-model bookkeeping: advances nextAutoShield and clears the running flag
  on every terminal state; an op-id guard stops a stale op clobbering scheduler
  state; a self-guard stops cancel/re-enqueue churn.
- Sietch-padded output shape matches manual z_shieldcoinbase txns.

Config: -autoshield (default true), -autoshieldinterval, -autoshieldaddress,
-autoshieldfee (range-validated), -autoshieldminutxos.

Incorporates fixes from an 8-angle code review: CAmount fee type with init-time
range validation, op-id-guarded flag bookkeeping, driver self-guard, and a
tipHeight-consistent NU-activation guard.

Note: the fConsolidationRunning / nextConsolidation consolidation-scheduler
wedge is a pre-existing bug, left for a separate change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-21 02:06:09 -05:00
27 changed files with 2121 additions and 62 deletions

View File

@@ -1,4 +1,8 @@
FROM ubuntu:20.04
# Base image is parameterised so one Dockerfile can produce binaries for several
# glibc floors: docker build --build-arg BASE_IMAGE=ubuntu:18.04 ...
# The default is unchanged, so `./build.sh --linux-compat` behaves exactly as before.
ARG BASE_IMAGE=ubuntu:20.04
FROM ${BASE_IMAGE}
ENV DEBIAN_FRONTEND=noninteractive

View File

@@ -6,10 +6,34 @@
set -eu -o pipefail
VERSION="1.0.3"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RELEASE_DIR="$SCRIPT_DIR/release"
# Derive the release version from configure.ac instead of hardcoding it here.
# A stale literal names the release directories after the wrong version while the
# binaries inside report the real one: this said 1.0.3 while the tree was already
# 1.1.0, so `./build.sh --all-release` would have produced
# release/dragonx-1.0.3-<platform>/ full of binaries announcing 1.1.0.
# Mirrors configure.ac's _CLIENT_VERSION_SUFFIX m4 exactly:
# build < 25 -> beta(build+1) build < 50 -> rc(build-24)
# build == 50 -> plain release build > 50 -> point release (build-50)
_acdef() { sed -n "s/^define(_CLIENT_VERSION_$1, *\([0-9]\{1,\}\))/\1/p" "$SCRIPT_DIR/configure.ac"; }
_V_MAJOR="$(_acdef MAJOR)"
_V_MINOR="$(_acdef MINOR)"
_V_REVISION="$(_acdef REVISION)"
_V_BUILD="$(_acdef BUILD)"
if [ -z "$_V_MAJOR" ] || [ -z "$_V_MINOR" ] || [ -z "$_V_REVISION" ] || [ -z "$_V_BUILD" ]; then
echo "ERROR: could not read the version from $SCRIPT_DIR/configure.ac" >&2
echo " refusing to build a release whose directory name would be wrong." >&2
exit 1
fi
if [ "$_V_BUILD" -lt 25 ]; then _V_SUFFIX="$_V_REVISION-beta$((_V_BUILD + 1))"
elif [ "$_V_BUILD" -lt 50 ]; then _V_SUFFIX="$_V_REVISION-rc$((_V_BUILD - 24))"
elif [ "$_V_BUILD" -eq 50 ]; then _V_SUFFIX="$_V_REVISION"
else _V_SUFFIX="$_V_REVISION-$((_V_BUILD - 50))"
fi
VERSION="$_V_MAJOR.$_V_MINOR.$_V_SUFFIX"
# Parse release flags
BUILD_LINUX_RELEASE=0
BUILD_WIN_RELEASE=0

View File

@@ -2,7 +2,7 @@ dnl require autoconf 2.60 (AS_ECHO/AS_ECHO_N)
AC_PREREQ([2.60])
define(_CLIENT_VERSION_MAJOR, 1)
dnl Must be kept in sync with src/clientversion.h , ugh!
define(_CLIENT_VERSION_MINOR, 1)
define(_CLIENT_VERSION_MINOR, 2)
define(_CLIENT_VERSION_REVISION, 0)
define(_CLIENT_VERSION_BUILD, 50)
define(_ZC_BUILD_VAL, m4_if(m4_eval(_CLIENT_VERSION_BUILD < 25), 1, m4_incr(_CLIENT_VERSION_BUILD), m4_eval(_CLIENT_VERSION_BUILD < 50), 1, m4_eval(_CLIENT_VERSION_BUILD - 24), m4_eval(_CLIENT_VERSION_BUILD == 50), 1, , m4_eval(_CLIENT_VERSION_BUILD - 50)))

View File

@@ -1,3 +1,35 @@
dragonx (1.2.0) stable; urgency=medium
* Auto-shield matured coinbase into a wallet-owned Sapling address on a block
interval. The destination is derived from the HD seed at m/32'/coin'/i' and
is the lowest index inside -mnemonicsaplinggap, so a bare seed-phrase restore
re-derives it; auto-shielding refuses to run rather than send anywhere a
restore would not find. Enabled only when the seed's provenance is known to be
recoverable, so upgraded wallets stay opted out until the operator says
otherwise.
* Create new wallets from a BIP39 seed phrase by default, byte-compatible with
SilentDragonXLite. z_exportmnemonic returns the phrase; -mnemonic restores
from it.
* New RPC z_autoshieldstatus reports whether auto-shielding is on, the resolved
destination, the HD seed's provenance, and why it is off when it is off.
* Bound each auto-shield round to 400 inputs and correct the transaction size
estimate to account for all three Sapling output descriptions, and lock the
selected coins for the duration of proof building so a concurrent
z_shieldcoinbase or z_sendmany cannot select them too.
* Repair, rather than reject, an hdchain record truncated by an older wallet
build. Previously one address generated under a pre-1.1.0 binary left the
wallet unopenable with "Wallet corrupted"; the record is now completed and
rewritten, and the error text names the seed-phrase remedy when it genuinely
cannot be recovered.
* Clear a stale sweep flag that could otherwise leave sweeping, consolidation
and auto-shielding permanently disabled together, and stop the async queue
silently discarding operations at shutdown while reporting success.
* Derive the release version from configure.ac in build.sh instead of a
hardcoded literal, and document container-based release builds in
doc/build-containers.md.
-- DragonX Developers <dev@dragonx.is> Tue, 25 Aug 2026 19:45:00 +0000
dragonx (1.1.0) stable; urgency=medium
* Extend DRAGONX checkpoints to height 3,226,000, enabling the existing

223
doc/build-containers.md Normal file
View File

@@ -0,0 +1,223 @@
# Building release binaries in containers
Release binaries must be built in a container based on an **old** Linux distribution.
This document is written to be executed, by a person or an agent, on a machine that
has nothing set up yet.
---
## 1. Why this exists
glibc compatibility runs one way only. A binary linked against glibc 2.35 demands
symbol versions that glibc 2.31 does not have, and refuses to start. A binary linked
against glibc 2.29 runs on 2.29, 2.31 and 2.35 alike.
Measured on the actual fleet, 2026-08-25:
| binary | max GLIBC required | runs on |
|---|---|---|
| what all four 20.04 seeds run today (`v1.0.3-d159e7208`) | `GLIBC_2.29` | 18.04, 20.04, 22.04 |
| anything built on seed 176 today (Ubuntu 22.04) | `GLIBC_2.34` | 22.04 only |
The second binary will not start on four of our own five seeds. The loader reports:
```
/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found
/lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.30' not found
```
Nothing new is being *called*. glibc 2.34 merged libpthread and libdl into libc and
re-versioned every `pthread_*`, `dlsym` and `dladdr` symbol; 2.33 replaced the old
`__xstat` inlines with real `stat`/`fstat`/`lstat64`. All of those functions exist in
2.31 under older tags. Building against older headers is the entire fix.
**Do not try to solve this with full static linking.** The daemon calls `getaddrinfo`,
`gethostbyname` and `getnameinfo`, and it must resolve `node1..node5.dragonx.is`, which
are hard-coded and injected into `-addnode` on every node. Under a fully static glibc
binary those go through NSS, which `dlopen`s `libnss_dns.so.2` at run time — it either
fails or silently requires the target to have the same glibc you linked against, which
defeats the purpose.
---
## 2. What already exists in this repo
Do not write a new build system. Two pieces are already here:
- **`Dockerfile.compat`** — an Ubuntu base image that installs the toolchain, copies the
tree, **deletes any host-built `depends/` and object files**, runs `./util/build.sh`,
and strips the three binaries.
- **`./build.sh --linux-compat`** — builds that image, creates a throwaway container,
copies `dragonxd`, `dragonx-cli` and `dragonx-tx` out into
`release/dragonx-<version>-linux-amd64-ubuntu2004/`, adds `bootstrap-dragonx.sh`,
`asmap.dat` and the two sapling params, fixes ownership, and prints the binary's
maximum required GLIBC version.
The base image is parameterised via `ARG BASE_IMAGE` (default `ubuntu:20.04`), so the
same Dockerfile can target several glibc floors.
---
## 3. Prerequisites
Docker (the scripted path uses `docker` specifically; podman works for the manual path
if you alias or substitute it).
```sh
sudo apt-get update
sudo apt-get install -y docker.io git
sudo usermod -aG docker "$USER" # then log out and back in, or every command needs sudo
```
Budget, measured on a 4-core box:
| resource | needs |
|---|---|
| disk | ~15 GB free (the `depends/` tree alone is ~1.6 GB per target, plus image layers) |
| RAM | 4 GB minimum, 8 GB comfortable — the link step is the peak |
| time | **12 hours per base image on first build.** `depends/` builds boost, BDB, wolfssl, libevent, libsodium, libcurl and rust from source. Later builds reuse Docker layer cache unless the tree changed. |
`depends/` downloads and builds its own rust toolchain, so the host's rust (or absence
of it) is irrelevant.
---
## 4. Build one target
```sh
git clone https://git.dragonx.is/DragonX/dragonx
cd dragonx
git checkout <the tag or branch you are releasing>
./build.sh --linux-compat
```
Output lands in `release/dragonx-<version>-linux-amd64-ubuntu2004/` and the script
prints the max GLIBC at the end. `<version>` is read from `configure.ac`, not
hardcoded, so it always matches what the binaries report.
---
## 5. Build several targets
```sh
for BASE in ubuntu:18.04 ubuntu:20.04 ubuntu:22.04; do
TAG="dragonx-compat-${BASE#ubuntu:}"
TAG="${TAG//./}"
docker build --build-arg "BASE_IMAGE=$BASE" -f Dockerfile.compat -t "$TAG" .
OUT="release/dragonx-$(grep -oP 'define\(_CLIENT_VERSION_MAJOR, \K[0-9]+' configure.ac).$(grep -oP 'define\(_CLIENT_VERSION_MINOR, \K[0-9]+' configure.ac).$(grep -oP 'define\(_CLIENT_VERSION_REVISION, \K[0-9]+' configure.ac)-linux-amd64-${BASE#ubuntu:}"
mkdir -p "$OUT"
CID=$(docker create "$TAG")
for b in dragonxd dragonx-cli dragonx-tx; do docker cp "$CID:/build/src/$b" "$OUT/$b"; done
docker rm "$CID" >/dev/null
cp util/bootstrap-dragonx.sh contrib/asmap/asmap.dat sapling-output.params sapling-spend.params "$OUT/" 2>/dev/null || true
done
```
### Which base to choose
| base | glibc it provides | default GCC | verdict |
|---|---|---|---|
| `ubuntu:18.04` | 2.27 | 7 | **Verify before relying on it.** The tree is built with `-std=c++17`; GCC 7's C++17 support is incomplete and its cmake (3.10) may be too old for RandomX. Attempt only if you need to reach 18.04 users, and treat a successful build as the proof. |
| `ubuntu:20.04` | 2.31 | 9 | **Recommended floor.** GCC 9 covers C++17 fully. Evidence it works: the binary the fleet runs today requires only `GLIBC_2.29`, i.e. the code touches nothing newer, so a 20.04 build reaches 18.04 machines anyway. |
| `ubuntu:22.04` | 2.35 | 11 | **Do not ship this.** It is what we already have and what excludes four of our own seeds. Useful only for development. |
Ubuntu 20.04 left standard support in April 2025, which is precisely why it belongs in
a container on a patched host rather than on a build box someone has to maintain.
---
## 6. Verify — this step is not optional
A build that silently targets the wrong glibc looks completely normal until a user
reports that nothing starts.
```sh
BIN=release/dragonx-<version>-linux-amd64-ubuntu2004/dragonxd
# The ceiling. Must be <= the glibc of the OLDEST system you intend to support.
objdump -p "$BIN" | grep -oE 'GLIBC_2\.[0-9]+' | sort -t. -k2 -n | tail -1
objdump -p "$BIN" | grep -oE 'GLIBCXX_3\.4\.[0-9]+' | sort -t. -k3 -n | tail -1
# If the ceiling is too high, this names the symbols responsible.
readelf --dyn-syms --wide "$BIN" | grep -E '@GLIBC_2\.(3[2-9])'
```
Expected for a 20.04 build: `GLIBC_2.29` or lower, `GLIBCXX_3.4.26` or lower.
Then actually run it somewhere old. A ceiling check proves the loader will resolve the
symbols; it does not prove the binary works. `./dragonxd --version` on a real 20.04 box
is a ten-second confirmation.
---
## 7. Traps
Each of these has cost real time.
**`ETXTBSY` when installing over a running daemon.** `cp` onto the binary fails with
"Text file busy" *even after the process has exited*`pgrep` returning nothing is not
sufficient, the kernel still holds the text mapping. Stage into the same directory and
`mv` (rename is not blocked), allow ~10 s to settle, and **sha256-verify the installed
file before starting it**. A failed copy that goes unnoticed leaves the old binary
running and looks like a successful deploy.
**Never touch `configure.ac` in a configured tree.** Even `cp`-ing back a byte-identical
copy updates its mtime, which makes `make` regenerate `aclocal.m4` and `configure` and
then re-run `configure`, which fails with `libdb_cxx headers missing` because the
depends prefix is not on the command line. If it happens: confirm
`git diff --quiet HEAD -- configure.ac`, then restore mtime order oldest-to-newest with
one-second gaps — `configure.ac`/`Makefile.am`, then `aclocal.m4`, then
`configure`/`Makefile.in`, then `config.status`, then `Makefile`. Inside a container
this cannot happen, which is one more reason to build there.
**Never blind-`touch` a path that might not exist.** `touch src/config/hush-config.h`
silently *creates* an empty stray file; the real header is `bitcoin-config.h`. Check
`git status` after any timestamp surgery.
**RandomX must be built with `ARCH=default`.** `util/build.sh` already passes it and the
comment there explains why: `ARCH=native` tunes to the build machine, and a build on an
AVX-512 host emitted 746 `zmm` instructions into `librandomx.a`, which `SIGILL`s on the
entire fleet. If you ever invoke cmake by hand, pass `-DARCH=default`.
**Strip before distributing.** Unstripped is ~220 MB, stripped ~16 MB. `Dockerfile.compat`
already strips inside the container.
**`util/build-win.sh` discards every argument.** There is no `"$@"` handling in it, so
`-j$(nproc)` and `--disable-tests` are dropped on the floor and the Windows build is
single-threaded. Expect it to be far slower than you planned.
**Windows also needs `-Wa,-mbig-obj` and `-DARCH=default`.** Both are in
`util/build-win.sh` today. The mingw flag was missing from `dev` for a month; without it
the cross-compile fails at link because boost-heavy translation units exceed the
PE/COFF section limit. Do not lose it on a re-branch.
**macOS cannot be containerised.** `util/build-mac.sh` is a native-Mac script, and there
is no darwin cross-compile path in `depends/` at all: `hosts/darwin.mk` wants
`native_cctools`, which has no package definition, and there is no SDK in the tree. It
also hardcodes an Intel Homebrew GCC path, so it produces x86_64 only — no arm64, no
universal binary. macOS needs a real Mac.
**The `contrib/gitian-descriptors/` files are not a build path.** They are unmodified
upstream Bitcoin files (`name: "bitcoin-win-0.11"`, suite `trusty`) with zero DragonX
content. Ignore them.
---
## 8. Handoff checklist
- [ ] Docker installed, user in the `docker` group, ~15 GB free
- [ ] Correct tag or branch checked out, tree clean (`git status`)
- [ ] Version in `configure.ac` is the one you intend to release
- [ ] `./build.sh --linux-compat` completes
- [ ] GLIBC ceiling is **2.31 or lower** (2.29 expected)
- [ ] GLIBCXX ceiling is **3.4.28 or lower** (3.4.26 expected)
- [ ] `dragonxd --version` runs on a real machine of the oldest supported distro
- [ ] Binaries stripped, `release/` contains the bootstrap script, `asmap.dat` and both sapling params
- [ ] sha256 recorded for each artifact
One more thing that is not a build step but belongs in the same conversation: the
in-app daemon updater refuses any release without a detached signature
(`kDaemonRequireSignature = true`). Publishing checksums alone means no existing user
can update in place.

View File

@@ -243,6 +243,7 @@ BITCOIN_CORE_H = \
wallet/asyncrpcoperation_mergetoaddress.h \
wallet/asyncrpcoperation_saplingconsolidation.h \
wallet/asyncrpcoperation_sweep.h \
wallet/asyncrpcoperation_autoshieldcoinbase.h \
wallet/asyncrpcoperation_sendmany.h \
wallet/asyncrpcoperation_shieldcoinbase.h \
wallet/crypter.h \
@@ -321,6 +322,7 @@ libbitcoin_wallet_a_SOURCES = \
wallet/asyncrpcoperation_mergetoaddress.cpp \
wallet/asyncrpcoperation_saplingconsolidation.cpp \
wallet/asyncrpcoperation_sweep.cpp \
wallet/asyncrpcoperation_autoshieldcoinbase.cpp \
wallet/asyncrpcoperation_sendmany.cpp \
wallet/asyncrpcoperation_shieldcoinbase.cpp \
wallet/crypter.cpp \

View File

@@ -96,18 +96,21 @@ void AsyncRPCQueue::run(size_t workerId) {
*
* Don't use std::make_shared<AsyncRPCOperation>().
*/
void AsyncRPCQueue::addOperation(const std::shared_ptr<AsyncRPCOperation> &ptrOperation) {
bool AsyncRPCQueue::addOperation(const std::shared_ptr<AsyncRPCOperation> &ptrOperation) {
std::lock_guard<std::mutex> guard(lock_);
// Don't add if queue is closed or finishing
// Don't add if queue is closed or finishing. Report it: silently dropping the
// operation made callers announce work that would never run.
// (isClosed/isFinishing read atomics, so calling them under the guard is safe.)
if (isClosed() || isFinishing()) {
return;
return false;
}
AsyncRPCOperationId id = ptrOperation->getId();
operation_map_.emplace(id, ptrOperation);
operation_id_queue_.push(id);
this->condition_.notify_one();
return true;
}
/**

View File

@@ -63,7 +63,12 @@ public:
size_t getOperationCount() const;
std::shared_ptr<AsyncRPCOperation> getOperationForId(AsyncRPCOperationId) const;
std::shared_ptr<AsyncRPCOperation> popOperationForId(AsyncRPCOperationId);
void addOperation(const std::shared_ptr<AsyncRPCOperation> &ptrOperation);
// Returns false if the queue is closed or finishing, in which case the
// operation was NOT queued and will never run. Callers must react: a caller
// that ignores this both reports success for work that will not happen and
// leaves any state it set for the operation (running flags, coin locks)
// stranded for the life of the process.
bool addOperation(const std::shared_ptr<AsyncRPCOperation> &ptrOperation);
std::vector<AsyncRPCOperationId> getAllOperationIds() const;
private:

View File

@@ -29,7 +29,7 @@
//! These need to be macros, as clientversion.cpp's and bitcoin*-res.rc's voodoo requires it
// Must be kept in sync with configure.ac , ugh!
#define CLIENT_VERSION_MAJOR 1
#define CLIENT_VERSION_MINOR 1
#define CLIENT_VERSION_MINOR 2
#define CLIENT_VERSION_REVISION 0
#define CLIENT_VERSION_BUILD 50

View File

@@ -139,3 +139,103 @@ TEST(mnemonic_compat, RawEntropyDiffersFromMnemonicSeed)
const std::string entropyT = DeriveTAddrFromSeedBytes(zeros); // wrong (32-byte)
EXPECT_NE(seedT, entropyT);
}
// New storage form: the HD seed IS the expanded 64-byte BIP39 seed, the chain is
// NOT flagged mnemonic, and the phrase comes from the separate entropy record.
TEST(mnemonic_compat, ExpandedSeedIsStoredDirectly)
{
SelectParams(CBaseChainParams::MAIN);
CWallet wallet;
ASSERT_TRUE(wallet.SetHDSeedFromMnemonic(ABANDON_ART));
// Stored bytes == the 64-byte BIP39 seed, fed to derivation unchanged.
HDSeed stored;
ASSERT_TRUE(wallet.GetHDSeed(stored));
auto raw = stored.RawSeed();
EXPECT_EQ(raw.size(), (size_t)64);
EXPECT_EQ(HexStr(raw.begin(), raw.end()), std::string(SEED64_HEX));
// No CHDChain version bump / no mnemonic flag: an older binary reads this
// wallet and derives the same tree.
EXPECT_FALSE(wallet.GetHDChain().fMnemonicSeed);
EXPECT_LT(wallet.GetHDChain().nVersion, CHDChain::VERSION_HD_MNEMONIC);
HDSeed forDerivation;
ASSERT_TRUE(wallet.GetHDSeedForDerivation(forDerivation));
EXPECT_EQ(forDerivation.RawSeed(), raw);
// The phrase is still exportable, and IsMnemonicSeed() (which gates
// z_exportmnemonic) still says yes.
EXPECT_TRUE(wallet.IsMnemonicSeed());
EXPECT_TRUE(wallet.HaveMnemonicEntropy());
std::string exported;
ASSERT_TRUE(wallet.GetMnemonicPhrase(exported));
EXPECT_EQ(exported, std::string(ABANDON_ART));
}
// Backwards compatibility: a wallet in the OLD form (stored HD seed == 32-byte
// entropy, fMnemonicSeed = true) must still derive and still export its phrase.
TEST(mnemonic_compat, LegacyEntropySeedStillWorks)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed zeros(32, 0), seed64;
ASSERT_TRUE(Bip39SeedFromEntropy(zeros, seed64));
CWallet wallet;
{
LOCK(wallet.cs_wallet);
RawHDSeed entropy(32, 0);
HDSeed legacy(entropy);
ASSERT_TRUE(wallet.InstallHDSeed(legacy, true, 1));
}
EXPECT_TRUE(wallet.GetHDChain().fMnemonicSeed);
EXPECT_FALSE(wallet.HaveMnemonicEntropy());
EXPECT_TRUE(wallet.IsMnemonicSeed());
// Still expanded on read -> same key tree as the new form.
HDSeed forDerivation;
ASSERT_TRUE(wallet.GetHDSeedForDerivation(forDerivation));
EXPECT_EQ(forDerivation.RawSeed(), seed64);
{
LOCK(wallet.cs_wallet);
EXPECT_EQ(EncodePaymentAddress(wallet.GenerateNewSaplingZKey()),
DeriveZAddrFromSeed64(seed64));
}
std::string exported;
ASSERT_TRUE(wallet.GetMnemonicPhrase(exported));
EXPECT_EQ(exported, std::string(ABANDON_ART));
}
// The entropy record refuses replacement, and a phrase that does not restore the
// installed seed is never printed.
TEST(mnemonic_compat, MnemonicEntropyGuards)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed zeros(32, 0), ones(32, 1);
// Refuse-to-replace.
CWallet wallet;
ASSERT_TRUE(wallet.SetHDSeedFromMnemonic(ABANDON_ART));
EXPECT_FALSE(wallet.SetMnemonicEntropy(ones));
EXPECT_FALSE(wallet.SetMnemonicEntropy(RawHDSeed())); // empty is not "installed"
// Mismatched entropy -> no phrase. Install a 64-byte seed that is NOT the
// expansion of `zeros`, then attach `zeros` as entropy.
RawHDSeed otherSeed64;
ASSERT_TRUE(Bip39SeedFromEntropy(ones, otherSeed64));
CWallet mismatched;
ASSERT_TRUE(mismatched.SetHDSeedFromHex(HexStr(otherSeed64.begin(), otherSeed64.end())));
ASSERT_TRUE(mismatched.SetMnemonicEntropy(zeros));
std::string phrase;
EXPECT_FALSE(mismatched.GetMnemonicPhrase(phrase));
// Matching entropy attached to a hex-restored wallet -> phrase available.
CWallet matched;
ASSERT_TRUE(matched.SetHDSeedFromHex(std::string(SEED64_HEX)));
ASSERT_TRUE(matched.SetMnemonicEntropy(zeros));
ASSERT_TRUE(matched.GetMnemonicPhrase(phrase));
EXPECT_EQ(phrase, std::string(ABANDON_ART));
}

View File

@@ -60,6 +60,7 @@
#include "wallet/wallet.h"
#include "wallet/walletdb.h"
#include "wallet/asyncrpcoperation_saplingconsolidation.h"
#include "wallet/asyncrpcoperation_autoshieldcoinbase.h"
#include "wallet/asyncrpcoperation_sweep.h"
#endif
#include <stdint.h>
@@ -471,7 +472,7 @@ std::string HelpMessage(HelpMessageMode mode)
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; cross-wallet restore parity is mainnet-only -- testnet/regtest derive a different HD coin_type). 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("-usemnemonic", strprintf(_("Create new wallets from a fresh BIP39 seed phrase so the 24 words can be exported (z_exportmnemonic) and used in SilentDragonXLite. Set to 0 for a raw random seed with no recovery phrase; existing wallets are never changed (default: %u)"), 1));
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)"));
@@ -489,6 +490,12 @@ std::string HelpMessage(HelpMessageMode mode)
strUsage += HelpMessageOpt("-zsweepexternal", _("Enable sweeping to an external wallet (default false)"));
strUsage += HelpMessageOpt("-zsweepexclude", _("Addresses to exclude from sweeping (default none)"));
strUsage += HelpMessageOpt("-autoshield", _("Automatically shield matured coinbase (mining rewards) into a seed-derived wallet z-address (default: true for wallets created or restored by this software, false when the HD seed provenance is unknown). No-op when not mining or wallet is locked."));
strUsage += HelpMessageOpt("-autoshieldinterval", strprintf(_("Block interval between automatic coinbase-shielding rounds (default: %i, min 5)"), 25));
strUsage += HelpMessageOpt("-autoshieldaddress=<zaddr>", _("Destination Sapling z-address for auto-shielded coinbase (default: reuse or create a wallet z-address). Must be spendable by this wallet."));
strUsage += HelpMessageOpt("-autoshieldfee", strprintf(_("Fee in puposhis for automatic coinbase-shielding transactions (default: %i)"), 10000));
strUsage += HelpMessageOpt("-autoshieldminutxos", strprintf(_("Only auto-shield once at least this many matured coinbase UTXOs exist (default: %i)"), 1));
strUsage += HelpMessageOpt("-deletetx", _("Enable Old Transaction Deletion"));
strUsage += HelpMessageOpt("-deleteinterval", strprintf(_("Delete transaction every <n> blocks during inital block download (default: %i)"), DEFAULT_TX_DELETE_INTERVAL));
strUsage += HelpMessageOpt("-keeptxnum", strprintf(_("Keep the last <n> transactions (default: %i)"), DEFAULT_TX_RETENTION_LASTTX));
@@ -2264,7 +2271,10 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
if (nLoadWalletRet != DB_LOAD_OK)
{
if (nLoadWalletRet == DB_CORRUPT)
strErrors << _("Error loading wallet.dat: Wallet corrupted") << "\n";
strErrors << _("Error loading wallet.dat: Wallet corrupted. If this wallet was last opened "
"by an older version, move wallet.dat aside and restore from your seed "
"phrase with -mnemonic=\"<your seed phrase>\" -rescan (see debug.log for "
"the specific record at fault).") << "\n";
else if (nLoadWalletRet == DB_NONCRITICAL_ERROR)
{
string msg(_("Warning: error reading wallet.dat! All keys read correctly, but transaction data"
@@ -2301,6 +2311,23 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
if (!pwalletMain->HaveHDSeed())
{
// Does this wallet predate the seed we are about to install? If so,
// that seed cannot appear in any backup the user already holds.
// Checked BEFORE installing, and before the restore path pre-derives
// its gap of keys.
bool fWalletHadContent = false;
{
LOCK(pwalletMain->cs_wallet);
std::set<CKeyID> setExistingKeys;
pwalletMain->GetKeys(setExistingKeys); // keystore.h:60 / crypter.h:212
std::set<libzcash::SaplingPaymentAddress> setExistingZAddrs;
pwalletMain->GetSaplingPaymentAddresses(setExistingZAddrs); // keystore.h:226-238
fWalletHadContent = !setExistingKeys.empty() ||
!setExistingZAddrs.empty() ||
!pwalletMain->mapWallet.empty() || // wallet.h:1041
pwalletMain->IsCrypted(); // crypter.h:174
}
std::string mnemonic = GetArg("-mnemonic", "");
std::string hdSeedHex = GetArg("-hdseed", "");
bool restoring = false;
@@ -2332,6 +2359,19 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
pwalletMain->GenerateNewSeed();
}
pwalletMain->SetHDSeedOrigin(restoring
? CWallet::HDSEED_ORIGIN_RESTORED
: (fWalletHadContent ? CWallet::HDSEED_ORIGIN_RETROFIT
: CWallet::HDSEED_ORIGIN_CREATED));
if (pwalletMain->hdSeedOrigin == CWallet::HDSEED_ORIGIN_RETROFIT)
{
LogPrintf("%s: WARNING: generated a new HD seed for a wallet that already held keys or "
"transactions. This seed is in NO backup you made before now.\n", __func__);
InitWarning(_("A new HD seed was generated for this pre-existing wallet. Any backup you "
"made before now does not contain it: back the wallet up again "
"(z_exportwallet) before receiving funds to newly derived addresses."));
}
if (restoring)
{
// Pre-derive keys (birthday = genesis) so the startup rescan finds
@@ -2350,6 +2390,15 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
LogPrintf("%s: pre-derived %d transparent and %d sapling keys for restore rescan\n", __func__, (int)tGap, (int)zGap);
}
}
else if (pwalletMain->hdSeedOrigin == CWallet::HDSEED_ORIGIN_UNRECORDED)
{
// The seed was installed by a build that predates this record, so
// we cannot tell whether it was minted onto a pre-existing wallet
// (and is therefore absent from the user's older backups). Assume
// the worst; the user can still opt in explicitly.
pwalletMain->SetHDSeedOrigin(CWallet::HDSEED_ORIGIN_UNKNOWN);
LogPrintf("%s: HD seed predates seed-provenance recording; recorded origin as unknown\n", __func__);
}
//Set Sapling Consolidation
pwalletMain->fSaplingConsolidationEnabled = GetBoolArg("-consolidation", false);
@@ -2451,6 +2500,88 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
}
}
//Set Automatic Coinbase Shielding (default ON, conditional: self-guards
//on nodes where it cannot act - no owned coinbase, external mineraddress,
//or locked wallet). Closes the transparent-coinbase leak for miners.
// Default ON only when this wallet's HD seed provenance says the
// destination is genuinely recoverable. Autoshield sends mined coinbase to
// a seed-derived z-address (resolveDestination), so for a seed retrofitted
// onto a pre-existing wallet - or one predating provenance recording - we
// cannot assume the user holds it. Those wallets opt in with -autoshield=1
// after backing the seed up.
const bool fAutoShieldSeedKnown =
(pwalletMain->hdSeedOrigin == CWallet::HDSEED_ORIGIN_CREATED ||
pwalletMain->hdSeedOrigin == CWallet::HDSEED_ORIGIN_RESTORED);
pwalletMain->fAutoShieldEnabled = GetBoolArg("-autoshield", fAutoShieldSeedKnown);
if (!fAutoShieldSeedKnown && !mapArgs.count("-autoshield")) {
LogPrintf("%s: autoshield left OFF by default: HD seed origin %d is not known-recoverable. "
"Back the seed up (z_exportwallet, or z_exportmnemonic on a mnemonic wallet) and "
"pass -autoshield=1 to enable.\n", __func__, pwalletMain->hdSeedOrigin);
}
if (pwalletMain->fAutoShieldEnabled) {
int autoShieldInterval = GetArg("-autoshieldinterval", 25);
if (autoShieldInterval < 5) {
fprintf(stderr,"%s: Invalid autoshield interval of %d < 5, setting to default of 25\n", __func__, autoShieldInterval);
autoShieldInterval = 25;
}
pwalletMain->autoShieldInterval = autoShieldInterval;
pwalletMain->nextAutoShield = pwalletMain->autoShieldInterval + chainActive.Height();
// Validate the fee: floor it above the relay minimum and cap it to
// guard against a fat-finger (e.g. -autoshieldfee=5000000000) that
// would otherwise build an over-fee or malformed shield tx that
// fails mempool admission every round.
CAmount autoShieldFee = GetArg("-autoshieldfee", 10000);
const CAmount AUTOSHIELD_MIN_FEE = 1000; // comfortably above minRelayTxFee for a small tx
const CAmount AUTOSHIELD_MAX_FEE = 10000000; // 0.1 DRGX; no sane autoshield fee exceeds this
if (autoShieldFee < AUTOSHIELD_MIN_FEE || autoShieldFee > AUTOSHIELD_MAX_FEE) {
fprintf(stderr,"%s: -autoshieldfee=%lld out of range [%lld,%lld], using default 10000\n",
__func__, (long long)autoShieldFee, (long long)AUTOSHIELD_MIN_FEE, (long long)AUTOSHIELD_MAX_FEE);
autoShieldFee = 10000;
}
pwalletMain->autoShieldFee = autoShieldFee;
pwalletMain->autoShieldMinUtxos = GetArg("-autoshieldminutxos", 1);
if (pwalletMain->autoShieldMinUtxos < 1) {
pwalletMain->autoShieldMinUtxos = 1;
}
LogPrintf("%s: autoshield enabled, nextAutoShield=%d interval=%d\n", __func__, pwalletMain->nextAutoShield, pwalletMain->autoShieldInterval);
//Optional explicit destination z-address. Must be a Sapling zaddr the
//wallet can spend, else the shielded coinbase would be unrecoverable.
std::string autoShieldAddress = GetArg("-autoshieldaddress", "");
if (!autoShieldAddress.empty()) {
auto zdest = DecodePaymentAddress(autoShieldAddress);
if (!IsValidPaymentAddress(zdest) ||
boost::get<libzcash::SaplingPaymentAddress>(&zdest) == nullptr) {
return InitError("Invalid -autoshieldaddress: must be a Sapling z-address");
}
auto hasSpendingKey = boost::apply_visitor(HaveSpendingKeyForPaymentAddress(pwalletMain), zdest);
if (!hasSpendingKey) {
return InitError("Wallet must hold the spending key of -autoshieldaddress (else shielded coinbase would be unrecoverable)");
}
pwalletMain->autoShieldAddress = autoShieldAddress;
} else {
// No explicit destination. Resolve the seed-derived one now, read-only,
// so z_autoshieldstatus can say where coinbase will go BEFORE the first
// round rather than reporting an empty string until one fires. This
// never generates a key: a fresh account must not be a side effect of
// populating a status field. A brand-new wallet holds nothing in-gap
// yet, so the field stays empty and the RPC explains why.
LOCK(pwalletMain->cs_wallet);
if (!pwalletMain->IsLocked()) {
libzcash::SaplingPaymentAddress destAddr;
std::string destStr;
uint32_t destAccount = AUTOSHIELD_ACCOUNT_NONE;
if (ResolveAutoShieldDestinationReadOnly(destAddr, destStr, destAccount)
== AutoShieldDestStatus::Resolved) {
pwalletMain->autoShieldAddress = destStr;
LogPrintf("%s: autoshield destination %s (seed-derived sapling account %u)\n",
__func__, destStr, (unsigned)destAccount);
}
}
}
}
//Set Transaction Deletion Options
fTxDeleteEnabled = GetBoolArg("-deletetx", false);
fTxConflictDeleteEnabled = GetBoolArg("-deleteconflicttx", true);

View File

@@ -68,6 +68,42 @@ bool CBasicKeyStore::GetHDSeed(HDSeed& seedOut) const
}
}
bool CBasicKeyStore::SetMnemonicEntropy(const RawHDSeed& entropy)
{
LOCK(cs_SpendingKeyStore);
if (entropy.empty()) {
// Never "install" nothing: HaveMnemonicEntropy() would stay false while
// the caller was told the call succeeded.
return false;
}
if (!mnemonicEntropy.empty()) {
// Same refuse-to-replace rule as SetHDSeed above, for a sharper reason:
// this is the printable form of the seed. If it could be swapped while
// hdSeed stayed put, the wallet would print a seed phrase that does not
// restore it -- strictly worse than printing none.
return false;
}
mnemonicEntropy = entropy;
return true;
}
bool CBasicKeyStore::HaveMnemonicEntropy() const
{
LOCK(cs_SpendingKeyStore);
return !mnemonicEntropy.empty();
}
bool CBasicKeyStore::GetMnemonicEntropy(RawHDSeed& entropyOut) const
{
LOCK(cs_SpendingKeyStore);
if (mnemonicEntropy.empty()) {
return false;
} else {
entropyOut = mnemonicEntropy;
return true;
}
}
bool CBasicKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey)
{
LOCK(cs_KeyStore);

View File

@@ -117,6 +117,12 @@ class CBasicKeyStore : public CKeyStore
{
protected:
HDSeed hdSeed;
// BIP39 entropy for a mnemonic-recoverable wallet, kept BESIDE hdSeed, never
// instead of it. hdSeed holds the bytes actually fed to derivation (the
// expanded 64-byte BIP39 seed on new wallets); this record exists only so the
// seed phrase can be reprinted. Empty on legacy and hex-restored wallets,
// which is a normal state, not an error.
RawHDSeed mnemonicEntropy;
KeyMap mapKeys;
ScriptMap mapScripts;
WatchOnlySet setWatchOnly;
@@ -129,6 +135,18 @@ public:
bool SetHDSeed(const HDSeed& seed);
bool HaveHDSeed() const;
bool GetHDSeed(HDSeed& seedOut) const;
//! Mnemonic entropy: optional, present only on phrase-recoverable wallets.
//! Unlike the three seed accessors above -- which override pure virtuals on
//! CKeyStore (keystore.h:48-51) and therefore dispatch dynamically -- these
//! are plain non-virtual members: CKeyStore declares nothing for them and
//! nothing reaches the entropy through a base pointer. Every caller holds a
//! CWallet*, whose static type resolves to the CCryptoKeyStore overloads.
//! Do NOT add them to CKeyStore: that would force all four subclasses
//! (CBasicKeyStore, CCryptoKeyStore, CWallet, gtest's TestCCryptoKeyStore)
//! to implement them for zero call sites.
bool SetMnemonicEntropy(const RawHDSeed& entropy);
bool HaveMnemonicEntropy() const;
bool GetMnemonicEntropy(RawHDSeed& entropyOut) const;
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);
bool HaveKey(const CKeyID &address) const

View File

@@ -369,6 +369,7 @@ extern UniValue z_gettotalbalance(const UniValue& params, bool fHelp, const CPub
extern UniValue z_mergetoaddress(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
extern UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
extern UniValue z_sweepstatus(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
extern UniValue z_autoshieldstatus(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
extern UniValue z_consolidationstatus(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
extern UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp
extern UniValue z_getoperationstatus(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcwallet.cpp

View File

@@ -0,0 +1,512 @@
// Copyright (c) 2016-2024 The Hush developers
// 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
#include "asyncrpcoperation_autoshieldcoinbase.h"
#include "asyncrpcoperation_shieldcoinbase.h" // for ShieldCoinbaseUTXO
#include "consensus/upgrades.h"
#include "hush_defs.h" // ASSETCHAINS_TIMELOCKGTE
#include "init.h"
#include "key_io.h"
#include "main.h"
#include "rpc/protocol.h"
#include "sync.h"
#include "tinyformat.h"
#include "transaction_builder.h"
#include "util.h"
#include "utilmoneystr.h"
#include "wallet.h"
// Sietch dummy zaddr generator (defined in wallet/rpcwallet.cpp)
extern std::string randomSietchZaddr();
// Serialized-size estimates for one spent input (kept in sync with rpcwallet.cpp)
static const size_t AUTOSHIELD_CTXIN_DUST_SIZE = 148;
// Every autoshield tx carries THREE Sapling OutputDescriptions -- the change
// note to destZaddr plus the two Sietch dummies -- at ~948 bytes each. Reserving
// 2000 for "header + sietch outputs" was ~900 bytes short before a single input
// was counted, so a large enough round could build a tx over MAX_TX_SIZE.
static const size_t AUTOSHIELD_SAPLING_OUTPUT_SIZE = 948;
static const size_t AUTOSHIELD_TX_OVERHEAD = (3 * AUTOSHIELD_SAPLING_OUTPUT_SIZE) + 256;
// Hard cap on inputs per round, mirroring z_shieldcoinbase's
// SHIELD_COINBASE_DEFAULT_LIMIT. The byte estimate alone is not a safe bound:
// with a P2PKH coinbase (-mineraddress) the 148-byte figure is exact rather than
// conservative, so an under-estimate translates directly into an oversize tx.
// The remainder is simply shielded on the next round.
static const size_t AUTOSHIELD_MAX_INPUTS = 400;
// Unrelated to the cap above despite sharing the value: this is a SIZE IN BYTES for
// one spent P2SH input, mirroring CTXIN_SPEND_P2SH_SIZE in rpcwallet.cpp.
static const size_t AUTOSHIELD_CTXIN_P2SH_SIZE = 400;
// Expire unmined autoshield txs after this many blocks, so a tx cannot straddle
// a network-upgrade activation.
static const int AUTOSHIELD_EXPIRY_DELTA = 15;
AsyncRPCOperation_autoshieldcoinbase::AsyncRPCOperation_autoshieldcoinbase(int targetHeight)
: targetHeight_(targetHeight) {}
AsyncRPCOperation_autoshieldcoinbase::~AsyncRPCOperation_autoshieldcoinbase() {}
void AsyncRPCOperation_autoshieldcoinbase::main() {
if (isCancelled()) {
// Only the CURRENT op owns the scheduler flag; a stale/cancelled op must
// not clear it out from under a freshly-enqueued successor.
if (pwalletMain) {
LOCK(pwalletMain->cs_wallet);
if (getId() == pwalletMain->saplingAutoShieldOperationId) {
pwalletMain->fAutoShieldRunning = false;
}
}
return;
}
set_state(OperationStatus::EXECUTING);
start_execution_clock();
bool success = false;
try {
success = main_impl();
} catch (const UniValue& objError) {
int code = find_value(objError, "code").get_int();
std::string message = find_value(objError, "message").get_str();
set_error_code(code);
set_error_message(message);
} catch (const runtime_error& e) {
set_error_code(-1);
set_error_message("runtime error: " + string(e.what()));
} catch (const logic_error& e) {
set_error_code(-1);
set_error_message("logic error: " + string(e.what()));
} catch (const exception& e) {
set_error_code(-1);
set_error_message("general exception: " + string(e.what()));
} catch (...) {
set_error_code(-2);
set_error_message("unknown error");
}
stop_execution_clock();
// ALWAYS advance the interval and clear the running flag, on success AND
// failure AND exception, so a failed/oversized/locked round still lets the
// next round fire. Only the CURRENT op does this bookkeeping: if a newer op
// has already superseded this one, leave its state untouched.
if (pwalletMain) {
LOCK2(cs_main, pwalletMain->cs_wallet);
if (getId() == pwalletMain->saplingAutoShieldOperationId) {
int tipHeight = (chainActive.Tip() != NULL) ? chainActive.Tip()->GetHeight() : targetHeight_;
pwalletMain->nextAutoShield = pwalletMain->autoShieldInterval + tipHeight;
pwalletMain->fAutoShieldRunning = false;
}
}
set_state(success ? OperationStatus::SUCCESS : OperationStatus::FAILED);
setResult();
LogPrintf("%s: autoshield operation finished (status=%s, txs=%d, shielded=%s)\n",
getId(), getStateAsString(), numTxCreated_, FormatMoney(amountShielded_));
}
// Read-only half of destination resolution, shared with init.cpp so the answer to
// "where will auto-shielding send?" is available before the first round runs rather
// than only after one has fired. Mutates nothing: no key generation, no caching.
AutoShieldDestStatus ResolveAutoShieldDestinationReadOnly(
libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut, uint32_t& accountOut) {
accountOut = AUTOSHIELD_ACCOUNT_NONE;
// 1. Explicit -autoshieldaddress override (validated as a spendable Sapling
// zaddr in init.cpp). This doubles as the per-process cache for whatever
// the derivation below resolved on an earlier round.
if (!pwalletMain->autoShieldAddress.empty()) {
auto decoded = DecodePaymentAddress(pwalletMain->autoShieldAddress);
if (boost::get<libzcash::SaplingPaymentAddress>(&decoded) != nullptr) {
destOut = boost::get<libzcash::SaplingPaymentAddress>(decoded);
destStrOut = pwalletMain->autoShieldAddress;
return AutoShieldDestStatus::Resolved;
}
return AutoShieldDestStatus::InvalidOverride;
}
// 2. Walk the restore window m/32'/coin'/[0, gap)' derived from the seed.
HDSeed seed;
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
return AutoShieldDestStatus::NoSeed;
}
// Mirror init.cpp's own clamp, and cap into the hardened index space so
// (i | ZIP32_HARDENED_KEY_LIMIT) below stays well formed.
int64_t gapArg = GetArg("-mnemonicsaplinggap", 100);
if (gapArg < 0) {
gapArg = 0;
}
if (gapArg > (int64_t)ZIP32_HARDENED_KEY_LIMIT) {
gapArg = (int64_t)ZIP32_HARDENED_KEY_LIMIT;
}
const uint32_t saplingGap = (uint32_t)gapArg;
// Same derivation path as CWallet::GenerateNewSaplingZKey (wallet.cpp:139-152).
const uint32_t bip44CoinType = Params().BIP44CoinType();
auto m = libzcash::SaplingExtendedSpendingKey::Master(seed);
auto m_32h = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT);
auto m_32h_cth = m_32h.Derive(bip44CoinType | ZIP32_HARDENED_KEY_LIMIT);
for (uint32_t i = 0; i < saplingGap; i++) {
auto xsk = m_32h_cth.Derive(i | ZIP32_HARDENED_KEY_LIMIT);
auto addr = xsk.DefaultAddress();
// Spendable AND registered: GetSaplingExtendedSpendingKey resolves
// addr -> ivk -> fvk -> spending key (keystore.cpp:215-223), so a hit
// means the wallet both recognises notes sent to `addr` and can spend
// them. Exactly the pair of properties the shield needs. Lowest index
// wins: stable for the life of the wallet and reproducible from the seed
// alone, unlike std::set order over the random diversifier
// (zcash/Address.hpp:95-98).
libzcash::SaplingExtendedSpendingKey held;
if (pwalletMain->GetSaplingExtendedSpendingKey(addr, held)) {
destOut = addr;
destStrOut = EncodePaymentAddress(addr);
accountOut = i;
return AutoShieldDestStatus::Resolved;
}
}
return AutoShieldDestStatus::NotFound;
}
// Resolve the Sapling destination for auto-shielded coinbase.
//
// Recoverability is the hard requirement: coinbase we shield must land in an
// address that a bare -mnemonic/-hdseed restore of THIS wallet's seed re-derives
// on its own. A restore pre-derives exactly -mnemonicsaplinggap sapling accounts
// starting at index 0, with saplingAccountCounter reset to 0 (init.cpp:2349-2355),
// so the only self-recoverable destinations are the default addresses of
// m/32'/<coin>'/i' for i < gap.
//
// We therefore DERIVE those accounts from the seed and pick the lowest index the
// wallet already holds. Deriving is the only authoritative test. In particular
// CKeyMetadata is NOT evidence of provenance: z_importkey / z_importwallet copy
// both hdKeypath and seedFp verbatim out of the import source
// (wallet.cpp:5522-5529 <- rpcdump.cpp:511-516), so a foreign key can claim any
// keypath and any seed fingerprint. Filtering on metadata would let an imported
// key win as "account 0" and silently receive every shielded reward.
//
// Caller must hold cs_wallet and must already have checked the wallet is unlocked.
bool AsyncRPCOperation_autoshieldcoinbase::resolveDestination(
libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut) {
uint32_t account = AUTOSHIELD_ACCOUNT_NONE;
switch (ResolveAutoShieldDestinationReadOnly(destOut, destStrOut, account)) {
case AutoShieldDestStatus::Resolved:
// Cache for the life of the process; the override branch of the resolver
// short-circuits later rounds. Safe: we only cache post-validation.
pwalletMain->autoShieldAddress = destStrOut;
if (account == AUTOSHIELD_ACCOUNT_NONE) {
LogPrintf("%s: autoshield destination %s (configured)\n", getId(), destStrOut);
} else {
LogPrintf("%s: autoshield destination %s (seed-derived sapling account %u)\n",
getId(), destStrOut, (unsigned)account);
}
return true;
case AutoShieldDestStatus::InvalidOverride:
LogPrintf("%s: configured -autoshieldaddress is not a valid Sapling address\n", getId());
return false;
case AutoShieldDestStatus::NoSeed:
LogPrintf("%s: no HD seed available; refusing to pick an autoshield destination\n", getId());
return false;
case AutoShieldDestStatus::NotFound:
break; // nothing in the window yet: fall through and derive one
}
// Re-establish the derivation context the resolver used, for step 3 below.
HDSeed seed;
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
LogPrintf("%s: no HD seed available; refusing to pick an autoshield destination\n", getId());
return false;
}
int64_t gapArg = GetArg("-mnemonicsaplinggap", 100);
if (gapArg < 0) {
gapArg = 0;
}
if (gapArg > (int64_t)ZIP32_HARDENED_KEY_LIMIT) {
gapArg = (int64_t)ZIP32_HARDENED_KEY_LIMIT;
}
const uint32_t saplingGap = (uint32_t)gapArg;
const uint32_t bip44CoinType = Params().BIP44CoinType();
auto m = libzcash::SaplingExtendedSpendingKey::Master(seed);
auto m_32h = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT);
auto m_32h_cth = m_32h.Derive(bip44CoinType | ZIP32_HARDENED_KEY_LIMIT);
// 3. Nothing usable in the window yet: derive the next account, but only if
// GenerateNewSaplingZKey will land INSIDE the window. It does NOT derive
// at saplingAccountCounter: its do/while skips every index whose spending
// key we already hold (wallet.cpp:150-157), so a bare "counter < gap"
// test is unsound - counter 98 with gap 100 can still land on 100.
// Predict min{ i >= counter : we do not hold i } instead.
const uint32_t counter = pwalletMain->GetHDChain().saplingAccountCounter;
uint32_t predicted = saplingGap; // sentinel: "would land outside the window"
for (uint32_t i = counter; i < saplingGap; i++) {
auto xsk = m_32h_cth.Derive(i | ZIP32_HARDENED_KEY_LIMIT);
if (!pwalletMain->HaveSaplingSpendingKey(xsk.expsk.full_viewing_key())) {
predicted = i;
break;
}
}
if (predicted == saplingGap) {
LogPrintf("%s: no free sapling account below -mnemonicsaplinggap=%u (account counter is %u). "
"A newly derived z-address would NOT be re-derived by a seed restore, so the "
"shielded coinbase could not be recovered from the seed alone. Refusing to "
"autoshield this round. Fix: point -autoshieldaddress at an existing in-gap "
"wallet z-address, or raise -mnemonicsaplinggap here AND use the same value on "
"any future restore.\n",
getId(), (unsigned)saplingGap, (unsigned)counter);
return false;
}
if (pwalletMain->IsLocked()) {
LogPrintf("%s: wallet is locked; cannot derive an autoshield destination z-address\n", getId());
return false;
}
try {
auto expectedAddr = m_32h_cth.Derive(predicted | ZIP32_HARDENED_KEY_LIMIT).DefaultAddress();
libzcash::SaplingPaymentAddress newAddr = pwalletMain->GenerateNewSaplingZKey();
// Post-verify rather than trust the prediction: cheap, and it closes the
// whole class of "the counter moved further than expected" bugs.
if (!(newAddr == expectedAddr)) {
LogPrintf("%s: newly derived z-address is not sapling account %u as predicted "
"(counter %u -> %u); not using it as the autoshield destination\n",
getId(), (unsigned)predicted, (unsigned)counter,
(unsigned)pwalletMain->GetHDChain().saplingAccountCounter);
return false;
}
destOut = newAddr;
destStrOut = EncodePaymentAddress(newAddr);
pwalletMain->autoShieldAddress = destStrOut;
LogPrintf("%s: generated new autoshield destination z-address %s (seed-derived sapling account %u)\n",
getId(), destStrOut, (unsigned)predicted);
return true;
} catch (const std::exception& e) {
LogPrintf("%s: could not generate a destination z-address: %s\n", getId(), e.what());
return false;
}
}
bool AsyncRPCOperation_autoshieldcoinbase::main_impl() {
auto opid = getId();
LogPrintf("%s: Beginning asyncrpcoperation_autoshieldcoinbase.\n", opid);
auto consensusParams = Params().GetConsensus();
int tipHeight;
{
LOCK(cs_main);
tipHeight = (chainActive.Tip() != NULL) ? chainActive.Tip()->GetHeight() : targetHeight_;
}
// Don't create a tx that could be mined before, but expire after, a NU
// activation. Key this off tipHeight (the height we actually set the expiry
// from below), not the stale enqueue-time targetHeight_, so a queue delay
// cannot slip a straddling expiry past this guard.
auto nextActivationHeight = NextActivationHeight(tipHeight, consensusParams);
if (nextActivationHeight && tipHeight + AUTOSHIELD_EXPIRY_DELTA >= nextActivationHeight.get()) {
LogPrintf("%s: autoshield tx could expire across a NU activation. Skipping this round.\n", opid);
return true;
}
libzcash::SaplingPaymentAddress destZaddr;
std::string destStr;
std::vector<ShieldCoinbaseUTXO> inputs;
// Proof building below runs WITHOUT cs_wallet (deliberately, so wallet RPCs
// are not stalled), which leaves a multi-second window in which a manual
// z_shieldcoinbase or z_sendmany over the same miner address would re-select
// these same coinbase outputs. AvailableCoins honours IsLockedCoin, so lock
// them for the duration exactly as z_shieldcoinbase does. RAII because there
// are several early returns between here and commit, and a leaked lock would
// silently exclude those coins from every future round.
struct ScopedCoinLocks {
std::vector<COutPoint> locked;
~ScopedCoinLocks() {
// A destructor is noexcept by default; letting the lock acquisition
// escape would turn a contended mutex into std::terminate.
try {
if (locked.empty()) return;
LOCK2(cs_main, pwalletMain->cs_wallet);
// UnlockCoin takes a non-const reference (upstream signature).
for (COutPoint& op : locked) pwalletMain->UnlockCoin(op);
} catch (...) {}
}
} coinLocks;
CAmount shieldedValue = 0;
unsigned int max_tx_size = MAX_TX_SIZE_AFTER_SAPLING;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
// Defensive: the scheduler already skips while locked, but the wallet
// could have been locked between enqueue and execution.
if (pwalletMain->IsLocked()) {
LogPrintf("%s: wallet is locked, skipping autoshield round\n", opid);
return true;
}
if (!resolveDestination(destZaddr, destStr)) {
LogPrintf("%s: no spendable destination z-address available, skipping\n", opid);
return true;
}
// Gather matured, spendable coinbase UTXOs, byte-capped to a single tx.
// AvailableCoins excludes immature coinbase unconditionally (wallet.cpp,
// `IsCoinBase() && GetBlocksToMaturity() > 0`) and only ever returns outputs
// we own, so external -mineraddress / pool coinbase yields zero inputs. The
// second argument here is fOnlyConfirmed, not fOnlySpendable.
size_t estimatedTxSize = AUTOSHIELD_TX_OVERHEAD;
std::vector<COutput> vecOutputs;
pwalletMain->AvailableCoins(vecOutputs, true, NULL, false, true);
for (const COutput& out : vecOutputs) {
if (!out.fSpendable || !out.tx->IsCoinBase()) {
continue;
}
CTxDestination address;
if (!ExtractDestination(out.tx->vout[out.i].scriptPubKey, address)) {
continue;
}
size_t increase = (boost::get<CScriptID>(&address) != nullptr)
? AUTOSHIELD_CTXIN_P2SH_SIZE : AUTOSHIELD_CTXIN_DUST_SIZE;
if (inputs.size() >= AUTOSHIELD_MAX_INPUTS) {
LogPrintf("%s: reached per-round input cap (%d); deferring remaining coinbase to next round\n",
opid, (int)AUTOSHIELD_MAX_INPUTS);
break;
}
if (estimatedTxSize + increase >= max_tx_size) {
// Size-safe batch; the remainder is shielded next round.
LogPrintf("%s: reached per-tx size cap; deferring remaining coinbase to next round\n", opid);
break;
}
estimatedTxSize += increase;
ShieldCoinbaseUTXO utxo = { out.tx->GetHash(), out.i,
out.tx->vout[out.i].scriptPubKey,
out.tx->vout[out.i].nValue };
inputs.push_back(utxo);
shieldedValue += out.tx->vout[out.i].nValue;
}
for (const ShieldCoinbaseUTXO& t : inputs) {
COutPoint outpt(t.txid, t.vout);
pwalletMain->LockCoin(outpt);
coinLocks.locked.push_back(outpt);
}
}
CAmount fee = pwalletMain->autoShieldFee;
if (inputs.size() < (size_t)pwalletMain->autoShieldMinUtxos) {
LogPrintf("%s: %d matured coinbase utxo(s) < min %d, skipping this round\n",
opid, (int)inputs.size(), pwalletMain->autoShieldMinUtxos);
return true;
}
if (shieldedValue <= fee) {
LogPrintf("%s: matured coinbase value %s <= fee %s, skipping\n",
opid, FormatMoney(shieldedValue), FormatMoney(fee));
return true;
}
// Common outgoing viewing key derived from the HD seed, exactly as
// z_shieldcoinbase does for t->z (keeps the note recoverable).
HDSeed seed;
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
LogPrintf("%s: HD seed not available, skipping\n", opid);
return true;
}
uint256 ovk = ovkForShieldingFromTaddr(seed);
// Build the t->z shield tx. Proof generation happens in Build() WITHOUT
// holding cs_wallet (mirrors the sweep op) so we don't stall wallet RPCs.
// tipHeight, not targetHeight_: the builder's height selects the consensus
// branch id (transaction_builder.cpp CurrentEpochBranchId), and the NU-straddle
// guard above plus SetExpiryHeight below are both keyed off tipHeight. Using the
// stale enqueue-time height here meant the guard was checking a height the
// transaction was not actually signed against.
auto builder = TransactionBuilder(consensusParams, tipHeight, pwalletMain);
builder.SetExpiryHeight(tipHeight + AUTOSHIELD_EXPIRY_DELTA);
builder.SetFee(fee);
for (const auto& t : inputs) {
if (t.amount >= ASSETCHAINS_TIMELOCKGTE) {
builder.SetLockTime((uint32_t)tipHeight);
builder.AddTransparentInput(COutPoint(t.txid, t.vout), t.scriptPubKey, t.amount, 0xfffffffe);
} else {
builder.AddTransparentInput(COutPoint(t.txid, t.vout), t.scriptPubKey, t.amount);
}
}
// All input value (less fee) goes back to our own z-address as change.
builder.SendChangeTo(destZaddr, ovk);
// Sietch padding: mirror z_shieldcoinbase's two dummy zouts so autoshield
// txs are structurally indistinguishable from manual coinbase shields.
for (int i = 0; i < 2; i++) {
auto zdust = DecodePaymentAddress(randomSietchZaddr());
if (IsValidPaymentAddress(zdust)) {
builder.AddSaplingOutput(ovk, boost::get<libzcash::SaplingPaymentAddress>(zdust), 0);
}
}
auto maybe_tx = builder.Build();
if (!maybe_tx) {
LogPrintf("%s: Failed to build autoshield transaction.\n", opid);
return false;
}
CTransaction tx = maybe_tx.get();
if (isCancelled()) {
LogPrintf("%s: Cancelled before commit.\n", opid);
return false;
}
if (pwalletMain->CommitAutomatedTx(tx)) {
LogPrintf("%s: shielded %s coinbase (%d utxos) into %s via txid=%s\n",
opid, FormatMoney(shieldedValue - fee), (int)inputs.size(),
destStr, tx.GetHash().ToString());
amountShielded_ += shieldedValue - fee;
shieldTxIds_.push_back(tx.GetHash().ToString());
numTxCreated_++;
return true;
}
LogPrintf("%s: autoshield tx FAILED in CommitTransaction, txid=%s\n", opid, tx.GetHash().ToString());
return false;
}
void AsyncRPCOperation_autoshieldcoinbase::setResult() {
UniValue res(UniValue::VOBJ);
res.push_back(Pair("num_tx_created", numTxCreated_));
res.push_back(Pair("amount_shielded", FormatMoney(amountShielded_)));
UniValue txIds(UniValue::VARR);
for (const std::string& txId : shieldTxIds_) {
txIds.push_back(txId);
}
res.push_back(Pair("shield_txids", txIds));
set_result(res);
}
void AsyncRPCOperation_autoshieldcoinbase::cancel() {
// Cancelling is how the scheduler stops an in-flight round, so unlike the base
// class this must be able to move an EXECUTING operation to CANCELLED. What it
// must not do is overwrite a state that is already terminal: the scheduler
// cancels the previous operation when it enqueues the next one, and that one may
// have already SUCCEEDED, whose result would otherwise be relabelled as cancelled.
if (isSuccess() || isFailed() || isCancelled())
return;
set_state(OperationStatus::CANCELLED);
}
UniValue AsyncRPCOperation_autoshieldcoinbase::getStatus() const {
UniValue v = AsyncRPCOperation::getStatus();
UniValue obj = v.get_obj();
obj.push_back(Pair("method", "autoshieldcoinbase"));
obj.push_back(Pair("target_height", targetHeight_));
return obj;
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) 2016-2024 The Hush developers
// 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
#ifndef ASYNCRPCOPERATION_AUTOSHIELDCOINBASE_H
#define ASYNCRPCOPERATION_AUTOSHIELDCOINBASE_H
#include "amount.h"
#include "asyncrpcoperation.h"
#include "univalue.h"
#include "zcash/Address.hpp"
#include "zcash/zip32.h"
// Default fee for automatic coinbase-shielding transactions
static const CAmount DEFAULT_AUTOSHIELD_FEE = 10000;
// Sentinel for "not a derived account" (i.e. the configured -autoshieldaddress).
static const uint32_t AUTOSHIELD_ACCOUNT_NONE = UINT32_MAX;
enum class AutoShieldDestStatus {
Resolved, // destOut/destStrOut are set
NotFound, // no in-gap account held yet; the operation will derive one
InvalidOverride, // -autoshieldaddress is set but is not a Sapling address
NoSeed, // no HD seed available (e.g. locked wallet)
};
// Resolve the auto-shield destination WITHOUT mutating the wallet: the configured
// -autoshieldaddress if set, else the lowest in-gap seed-derived account the wallet
// already holds. It deliberately does NOT generate a key, so init can call it purely
// to answer "where will this send?" -- deriving a fresh account as a side effect of
// populating a status field would be wrong. The operation's own resolveDestination
// falls through to generation when this returns NotFound.
// Caller must hold cs_wallet.
AutoShieldDestStatus ResolveAutoShieldDestinationReadOnly(
libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut, uint32_t& accountOut);
// A periodic, wallet-local operation that drains matured *transparent* coinbase
// UTXOs into a wallet-owned Sapling z-address in size-bounded batches. It is the
// automatic sibling of the manual z_shieldcoinbase RPC and mirrors the dispatch
// model of AsyncRPCOperation_sweep (self-gathers on the async worker thread,
// commits via CWallet::CommitAutomatedTx). It never mints a transparent output,
// so it respects the ac_private=1 transparent-output ban, and it deliberately
// does NOT toggle mining (unlike z_shieldcoinbase) so it can run every interval
// on a mining node without thrashing the miner.
class AsyncRPCOperation_autoshieldcoinbase : public AsyncRPCOperation
{
public:
AsyncRPCOperation_autoshieldcoinbase(int targetHeight);
virtual ~AsyncRPCOperation_autoshieldcoinbase();
// We don't want to be copied or moved around
AsyncRPCOperation_autoshieldcoinbase(AsyncRPCOperation_autoshieldcoinbase const&) = delete;
AsyncRPCOperation_autoshieldcoinbase(AsyncRPCOperation_autoshieldcoinbase&&) = delete;
AsyncRPCOperation_autoshieldcoinbase& operator=(AsyncRPCOperation_autoshieldcoinbase const&) = delete;
AsyncRPCOperation_autoshieldcoinbase& operator=(AsyncRPCOperation_autoshieldcoinbase&&) = delete;
virtual void main();
virtual void cancel();
virtual UniValue getStatus() const;
private:
int targetHeight_;
int numTxCreated_ = 0;
CAmount amountShielded_ = 0;
std::vector<std::string> shieldTxIds_;
bool main_impl();
// Resolve a spendable, wallet-owned Sapling destination: the configured
// -autoshieldaddress if set, else the first spendable z-addr the wallet
// holds, else a freshly generated one (requires an unlocked wallet).
// Returns false if none is available (e.g. locked wallet with no z-addr).
bool resolveDestination(libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut);
void setResult();
};
#endif /* ASYNCRPCOPERATION_AUTOSHIELDCOINBASE_H */

View File

@@ -28,8 +28,17 @@ AsyncRPCOperation_saplingconsolidation::AsyncRPCOperation_saplingconsolidation(i
AsyncRPCOperation_saplingconsolidation::~AsyncRPCOperation_saplingconsolidation() {}
void AsyncRPCOperation_saplingconsolidation::main() {
if (isCancelled())
if (isCancelled()) {
// Only the current op owns the scheduler flag; a stale/cancelled op must
// not clear it out from under a freshly-enqueued successor.
if (pwalletMain) {
LOCK(pwalletMain->cs_wallet);
if (getId() == pwalletMain->saplingConsolidationOperationId) {
pwalletMain->fConsolidationRunning = false;
}
}
return;
}
set_state(OperationStatus::EXECUTING);
start_execution_clock();
@@ -76,6 +85,21 @@ void AsyncRPCOperation_saplingconsolidation::main() {
LogPrintf("%s", s);
unlock_notes(); // clean up
LogPrint("zrpc", "%s: consolidation input notes unlocked\n", getId());
// Advance the interval and clear the running flag on EVERY terminal state
// (success, failure, exception) so consolidation runs once per interval
// instead of every block, and a failed round still lets the next one fire.
// Only the CURRENT op does this bookkeeping. This fixes the pre-existing
// wedge where nextConsolidation never advanced and fConsolidationRunning
// was never set/reset.
if (pwalletMain) {
LOCK2(cs_main, pwalletMain->cs_wallet);
if (getId() == pwalletMain->saplingConsolidationOperationId) {
int tipHeight = (chainActive.Tip() != NULL) ? chainActive.Tip()->GetHeight() : targetHeight_;
pwalletMain->nextConsolidation = pwalletMain->consolidationInterval + tipHeight;
pwalletMain->fConsolidationRunning = false;
}
}
}
bool AsyncRPCOperation_saplingconsolidation::main_impl() {
@@ -281,6 +305,13 @@ void AsyncRPCOperation_saplingconsolidation::setConsolidationResult(int numTxCre
}
void AsyncRPCOperation_saplingconsolidation::cancel() {
// Cancelling is how the scheduler stops an in-flight round, so unlike the base
// class this must be able to move an EXECUTING operation to CANCELLED. What it
// must not do is overwrite a state that is already terminal: the scheduler
// cancels the previous operation when it enqueues the next one, and that one may
// have already SUCCEEDED, whose result would otherwise be relabelled as cancelled.
if (isSuccess() || isFailed() || isCancelled())
return;
set_state(OperationStatus::CANCELLED);
}

View File

@@ -27,8 +27,17 @@ AsyncRPCOperation_sweep::AsyncRPCOperation_sweep(int targetHeight, bool fromRpc)
AsyncRPCOperation_sweep::~AsyncRPCOperation_sweep() {}
void AsyncRPCOperation_sweep::main() {
if (isCancelled())
if (isCancelled()) {
// Only the current op owns the scheduler flag; a stale/cancelled op must
// not clear it out from under a freshly-enqueued successor.
if (pwalletMain) {
LOCK(pwalletMain->cs_wallet);
if (getId() == pwalletMain->saplingSweepOperationId) {
pwalletMain->fSweepRunning = false;
}
}
return;
}
set_state(OperationStatus::EXECUTING);
start_execution_clock();
@@ -64,6 +73,23 @@ void AsyncRPCOperation_sweep::main() {
set_state(OperationStatus::FAILED);
}
// Scheduler bookkeeping, done here so it runs on success AND failure AND
// exception (main_impl's terminal code is skipped when it throws). Only the
// current op mutates scheduler state. Preserves the "keep draining every
// block until swept" model: on a successful-but-incomplete round we leave
// fSweepRunning set and nextSweep unadvanced so the next block continues.
// On completion OR on failure/exception we release fSweepRunning and back
// off one interval — critically, a persistently failing sweep no longer
// leaves fSweepRunning stuck true and wedges consolidation + autoshield.
if (pwalletMain) {
LOCK2(cs_main, pwalletMain->cs_wallet);
if (getId() == pwalletMain->saplingSweepOperationId && (!success || sweepComplete_)) {
int tipHeight = (chainActive.Tip() != NULL) ? chainActive.Tip()->GetHeight() : targetHeight_;
pwalletMain->nextSweep = pwalletMain->sweepInterval + tipHeight;
pwalletMain->fSweepRunning = false;
}
}
std::string s = strprintf("%s: Sweep operation finished. (status=%s", getId(), getStateAsString());
if (success) {
s += strprintf(", success)\n");
@@ -314,10 +340,11 @@ bool AsyncRPCOperation_sweep::main_impl() {
}
}
if (sweepComplete) {
pwalletMain->nextSweep = pwalletMain->sweepInterval + chainActive.Tip()->GetHeight();
pwalletMain->fSweepRunning = false;
}
// Record whether the wallet is fully swept; the scheduler bookkeeping
// (advancing nextSweep / clearing fSweepRunning) is done in main() so it
// also runs on the failure/exception/cancel paths and cannot wedge the
// shared fSweepRunning flag (which now also gates consolidation + autoshield).
sweepComplete_ = sweepComplete;
LogPrintf("%s: Created %d transactions with total output amount=%s, status=%d\n", getId(), numTxCreated, FormatMoney(amountSwept), (int)status);
setSweepResult(numTxCreated, amountSwept, sweepTxIds);
@@ -337,6 +364,13 @@ void AsyncRPCOperation_sweep::setSweepResult(int numTxCreated, const CAmount& am
}
void AsyncRPCOperation_sweep::cancel() {
// Cancelling is how the scheduler stops an in-flight round, so unlike the base
// class this must be able to move an EXECUTING operation to CANCELLED. What it
// must not do is overwrite a state that is already terminal: the scheduler
// cancels the previous operation when it enqueues the next one, and that one may
// have already SUCCEEDED, whose result would otherwise be relabelled as cancelled.
if (isSuccess() || isFailed() || isCancelled())
return;
set_state(OperationStatus::CANCELLED);
}

View File

@@ -34,6 +34,10 @@ public:
private:
int targetHeight_;
bool fromRPC_;
// Set by main_impl(): true iff there was nothing left to sweep this round.
// Read by main() to decide scheduler bookkeeping. Defaults false so an
// exception (which skips main_impl's assignment) is treated as "not done".
bool sweepComplete_ = false;
bool main_impl();

View File

@@ -159,6 +159,34 @@ static bool DecryptHDSeed(
return seed.Fingerprint() == seedFp;
}
uint256 MnemonicEntropyFingerprint(const RawHDSeed& entropy)
{
// The local copy is not gratuitous -- see the declaration in crypter.h.
// It is secure_allocator-backed, so it is memory_cleanse()d on destruction
// (support/allocators/secure.h:45-52).
RawHDSeed tmp(entropy);
return HDSeed(tmp).Fingerprint();
}
static bool DecryptMnemonicEntropy(
const CKeyingMaterial& vMasterKey,
const std::vector<unsigned char>& vchCryptedSecret,
const uint256& entropyFp,
RawHDSeed& entropyOut)
{
CKeyingMaterial vchSecret;
// Use the entropy's fingerprint as IV, mirroring DecryptHDSeed above.
if (!DecryptSecret(vMasterKey, vchCryptedSecret, entropyFp, vchSecret))
return false;
// RawHDSeed and CKeyingMaterial are the SAME type (both are
// std::vector<unsigned char, secure_allocator<unsigned char>>), so this is a
// plain copy of the same bytes, not a reinterpretation.
entropyOut = vchSecret;
return MnemonicEntropyFingerprint(entropyOut) == entropyFp;
}
static bool DecryptKey(const CKeyingMaterial& vMasterKey, const std::vector<unsigned char>& vchCryptedSecret, const CPubKey& vchPubKey, CKey& key)
{
CKeyingMaterial vchSecret;
@@ -233,6 +261,19 @@ bool CCryptoKeyStore::Unlock(const CKeyingMaterial& vMasterKeyIn)
keyPass = true;
}
}
// Deliberately NO arm here for cryptedMnemonicEntropy. This function is
// the "some keys decrypt but not all" corruption detector and a keyFail
// ends at the assert(false) below. The mnemonic entropy is an optional,
// non-spending, display-only record: legacy wallets, hex-restored
// wallets and every wallet predating this feature legitimately have a
// seed and no entropy, and a wallet whose every key decrypts while its
// entropy does not is not corrupt in any sense that should abort the
// process -- it simply cannot print its seed phrase. It is decrypted
// lazily in GetMnemonicEntropy() instead, so that case becomes a false
// return from one RPC while derivation and spending (which read the
// seed, not the entropy) carry on. Note the arm above caches nothing
// either -- `seed` is discarded; it only votes keyPass/keyFail -- so
// nothing is lost by omitting one here.
CryptedKeyMap::const_iterator mi = mapCryptedKeys.begin();
for (; mi != mapCryptedKeys.end(); ++mi)
{
@@ -344,6 +385,82 @@ bool CCryptoKeyStore::GetHDSeed(HDSeed& seedOut) const
return DecryptHDSeed(vMasterKey, cryptedHDSeed.second, cryptedHDSeed.first, seedOut);
}
bool CCryptoKeyStore::SetMnemonicEntropy(const RawHDSeed& entropy)
{
{
LOCK(cs_SpendingKeyStore);
if (!IsCrypted()) {
return CBasicKeyStore::SetMnemonicEntropy(entropy);
}
if (IsLocked())
return false;
if (entropy.empty())
return false;
std::vector<unsigned char> vchCryptedSecret;
// Use the entropy's fingerprint as IV
// TODO: Handle this properly when we make encryption a supported feature
auto entropyFp = MnemonicEntropyFingerprint(entropy);
// RawHDSeed IS CKeyingMaterial, so `entropy` binds directly here.
if (!EncryptSecret(vMasterKey, entropy, entropyFp, vchCryptedSecret))
return false;
// Virtual: this calls into CWallet to store the crypted entropy to disk.
if (!SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret))
return false;
}
return true;
}
bool CCryptoKeyStore::SetCryptedMnemonicEntropy(
const uint256& entropyFp,
const std::vector<unsigned char>& vchCryptedSecret)
{
{
LOCK(cs_SpendingKeyStore);
if (!IsCrypted()) {
return false;
}
if (!cryptedMnemonicEntropy.first.IsNull()) {
// Don't allow existing entropy to be changed, mirroring
// SetCryptedHDSeed: a phrase that no longer matches the installed
// seed is worse than no phrase at all.
return false;
}
cryptedMnemonicEntropy = std::make_pair(entropyFp, vchCryptedSecret);
}
return true;
}
bool CCryptoKeyStore::HaveMnemonicEntropy() const
{
LOCK(cs_SpendingKeyStore);
if (!IsCrypted())
return CBasicKeyStore::HaveMnemonicEntropy();
return !cryptedMnemonicEntropy.second.empty();
}
bool CCryptoKeyStore::GetMnemonicEntropy(RawHDSeed& entropyOut) const
{
LOCK(cs_SpendingKeyStore);
if (!IsCrypted())
return CBasicKeyStore::GetMnemonicEntropy(entropyOut);
if (cryptedMnemonicEntropy.second.empty())
return false;
// Decrypted lazily, on demand, and deliberately NOT in Unlock(): see the
// comment there for why the entropy must not vote in the keyPass/keyFail
// corruption detector.
return DecryptMnemonicEntropy(vMasterKey, cryptedMnemonicEntropy.second,
cryptedMnemonicEntropy.first, entropyOut);
}
bool CCryptoKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey)
{
{
@@ -505,6 +622,30 @@ bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn)
}
hdSeed = HDSeed();
}
if (!mnemonicEntropy.empty()) {
{
std::vector<unsigned char> vchCryptedSecret;
// Use the entropy's fingerprint as IV
// TODO: Handle this properly when we make encryption a supported feature
auto entropyFp = MnemonicEntropyFingerprint(mnemonicEntropy);
if (!EncryptSecret(vMasterKeyIn, mnemonicEntropy, entropyFp, vchCryptedSecret)) {
return false;
}
// Virtual: calls into CWallet to store the crypted entropy to disk.
if (!SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) {
return false;
}
}
// Drop the plaintext. swap() rather than `= RawHDSeed()`: assigning a
// shorter vector destroys the elements but KEEPS the capacity, so the
// old bytes would linger in the locked buffer. swap() hands the buffer
// to a temporary whose destructor deallocates it, and
// secure_allocator::deallocate memory_cleanse()s
// (support/allocators/secure.h:45-52). The `hdSeed = HDSeed();` above
// has the same weakness but cannot be fixed here: HDSeed's raw vector
// is private with no swap accessor (zip32.h:23-33).
RawHDSeed().swap(mnemonicEntropy);
}
BOOST_FOREACH(KeyMap::value_type& mKey, mapKeys)
{
const CKey &key = mKey.second;

View File

@@ -138,6 +138,21 @@ public:
}
};
/** Keystore which keeps the private keys encrypted.
* It derives from the basic key store, which is used if no encryption is active.
*/
//! Fingerprint of a BIP39 entropy blob, computed exactly as HDSeed::Fingerprint
//! does (BLAKE2b, ZCASH_HD_SEED_FP_PERSONAL). It is an IV / integrity tag and a
//! wallet.dat record key -- never a key-derivation input. Declared here rather
//! than duplicated because three call sites must produce identical bytes:
//! CCryptoKeyStore::SetMnemonicEntropy, CCryptoKeyStore::EncryptKeys, and
//! CWallet::SetMnemonicEntropy (which keys the plaintext record with it).
//!
//! It takes a copy internally on purpose: HDSeed's constructor takes a NON-const
//! RawHDSeed& (zip32.h:28), so HDSeed(entropy).Fingerprint() does not compile
//! against a const reference or a member read from a const method.
uint256 MnemonicEntropyFingerprint(const RawHDSeed& entropy);
/** Keystore which keeps the private keys encrypted.
* It derives from the basic key store, which is used if no encryption is active.
*/
@@ -145,6 +160,10 @@ class CCryptoKeyStore : public CBasicKeyStore
{
private:
std::pair<uint256, std::vector<unsigned char>> cryptedHDSeed;
// Encrypted mnemonic entropy, shaped exactly like cryptedHDSeed above:
// .first is the entropy's fingerprint (AES IV + integrity tag on decrypt),
// .second is the ciphertext.
std::pair<uint256, std::vector<unsigned char>> cryptedMnemonicEntropy;
CryptedKeyMap mapCryptedKeys;
//CryptedSproutSpendingKeyMap mapCryptedSproutSpendingKeys;
CryptedSaplingSpendingKeyMap mapCryptedSaplingSpendingKeys;
@@ -194,6 +213,14 @@ public:
bool SetHDSeed(const HDSeed& seed);
bool HaveHDSeed() const;
bool GetHDSeed(HDSeed& seedOut) const;
//! Mnemonic entropy, mirroring the four HD-seed members above.
//! SetCryptedMnemonicEntropy MUST stay virtual for the same reason
//! SetCryptedHDSeed is: CWallet overrides it to persist the record, and
//! SetMnemonicEntropy() below reaches that override through the vtable.
virtual bool SetCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char> &vchCryptedSecret);
bool SetMnemonicEntropy(const RawHDSeed& entropy);
bool HaveMnemonicEntropy() const;
bool GetMnemonicEntropy(RawHDSeed& entropyOut) const;
virtual bool AddCryptedKey(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret);
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);

View File

@@ -1039,8 +1039,10 @@ UniValue z_exportmnemonic(const UniValue& params, bool fHelp, const CPubKey& myp
"\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"
"New wallets get a seed phrase by default (-usemnemonic=0 opts out);\n"
"wallets restored with -mnemonic have one too. Wallets created before\n"
"this feature, or from a raw -hdseed, have no phrase -- use\n"
"z_exportwallet for those. Requires the wallet be unlocked.\n"
"\nResult:\n"
"{\n"
" \"mnemonic\" : \"word1 ... word24\", (string) the BIP39 seed phrase\n"

View File

@@ -3345,6 +3345,83 @@ UniValue z_sweepstatus(const UniValue& params, bool fHelp, const CPubKey& mypk)
return ret;
}
UniValue z_autoshieldstatus(const UniValue& params, bool fHelp, const CPubKey& mypk)
{
if (!EnsureWalletIsAvailable(fHelp))
return NullUniValue;
if (fHelp || params.size() > 0)
throw runtime_error(
"z_autoshieldstatus\n"
"\nReport the state of automatic coinbase shielding: whether it is on, where it sends,\n"
"and -- when it is off -- why.\n"
"\nResult:\n"
"{\n"
" \"autoshield\" : true|false, (boolean) whether auto-shielding is enabled\n"
" \"running\" : true|false, (boolean) whether a round is in flight\n"
" \"next_autoshield\" : n, (numeric) height of the next round\n"
" \"autoshieldinterval\" : n, (numeric) blocks between rounds\n"
" \"autoshieldaddress\" : \"zaddr\", (string) resolved destination; empty until first resolved\n"
" \"autoshieldfee\" : n, (numeric) fee in puposhis\n"
" \"autoshieldminutxos\" : n, (numeric) minimum matured coinbase utxos per round\n"
" \"hdseedorigin\" : n, (numeric) 0 unrecorded, 1 created, 2 restored, 3 retrofit, 4 unknown\n"
" \"hdseedorigin_desc\" : \"...\", (string) readable form of hdseedorigin\n"
" \"seed_recoverable\" : true|false, (boolean) whether a seed phrase can be exported\n"
" \"disabled_reason\" : \"...\" (string) why auto-shielding is not running, if it is not\n"
"}\n"
"\nExamples:\n"
+ HelpExampleCli("z_autoshieldstatus", "")
+ HelpExampleRpc("z_autoshieldstatus", "")
);
LOCK2(cs_main, pwalletMain->cs_wallet);
UniValue ret(UniValue::VOBJ);
ret.push_back(Pair("autoshield", pwalletMain->fAutoShieldEnabled));
ret.push_back(Pair("running", pwalletMain->fAutoShieldRunning));
ret.push_back(Pair("next_autoshield", pwalletMain->nextAutoShield));
ret.push_back(Pair("autoshieldinterval", pwalletMain->autoShieldInterval));
ret.push_back(Pair("autoshieldaddress", pwalletMain->autoShieldAddress));
ret.push_back(Pair("autoshieldfee", pwalletMain->autoShieldFee));
ret.push_back(Pair("autoshieldminutxos", pwalletMain->autoShieldMinUtxos));
int origin = pwalletMain->hdSeedOrigin;
std::string desc;
switch (origin) {
case CWallet::HDSEED_ORIGIN_CREATED: desc = "created on an empty wallet"; break;
case CWallet::HDSEED_ORIGIN_RESTORED: desc = "restored from -mnemonic/-hdseed"; break;
case CWallet::HDSEED_ORIGIN_RETROFIT: desc = "retrofitted onto a pre-existing wallet"; break;
case CWallet::HDSEED_ORIGIN_UNKNOWN: desc = "predates provenance recording"; break;
default: desc = "not yet recorded"; break;
}
ret.push_back(Pair("hdseedorigin", origin));
ret.push_back(Pair("hdseedorigin_desc", desc));
ret.push_back(Pair("seed_recoverable", pwalletMain->IsMnemonicSeed()));
// Say why it is off. A silent "false" is exactly what made the destination
// un-inspectable in the first place.
std::string why = "";
if (!pwalletMain->fAutoShieldEnabled) {
if (origin != CWallet::HDSEED_ORIGIN_CREATED && origin != CWallet::HDSEED_ORIGIN_RESTORED)
why = "HD seed origin is not known-recoverable; back the seed up and pass -autoshield=1";
else
why = "disabled by -autoshield=0";
} else if (pwalletMain->IsLocked()) {
why = "wallet is locked; rounds are skipped until it is unlocked";
} else if (pwalletMain->fSweepRunning || pwalletMain->fConsolidationRunning) {
// Autoshield is mutually exclusive with sweep and consolidation. Without
// this the RPC reports autoshield=true, running=false and an empty
// reason while no round can actually start.
why = strprintf("deferred while %s is running; rounds resume when it finishes",
pwalletMain->fSweepRunning ? "z_sweep" : "sapling consolidation");
} else if (pwalletMain->autoShieldAddress.empty()) {
why = "no destination resolved yet; one will be derived from the HD seed on the first round";
}
ret.push_back(Pair("disabled_reason", why));
return ret;
}
UniValue z_listreceivedaddress(const UniValue& params, bool fHelp,const CPubKey&)
{
if (!EnsureWalletIsAvailable(fHelp))
@@ -5466,7 +5543,10 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk)
// Create operation and add to global queue
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_sendmany(builder, contextualTx, fromaddress, taddrRecipients, zaddrRecipients, saplingNoteInputs, nMinDepth, nFee, contextInfo, opret) );
q->addOperation(operation);
if (!q->addOperation(operation)) {
throw JSONRPCError(RPC_INTERNAL_ERROR,
"Async RPC queue is shutting down; the operation was not queued");
}
if(fZdebug)
LogPrintf("%s: Submitted to async queue\n", __FUNCTION__);
@@ -5694,7 +5774,13 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp, const CPubKey& myp
// Create operation and add to global queue
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> operation( new AsyncRPCOperation_shieldcoinbase(builder, contextualTx, inputs, destaddress, nFee, donation, contextInfo) );
q->addOperation(operation);
// The constructor has already locked the selected coins. Coin locks are
// memory-only, so a refused queue at shutdown reclaims them with the process;
// what must not happen is returning an opid for work that will never run.
if (!q->addOperation(operation)) {
throw JSONRPCError(RPC_INTERNAL_ERROR,
"Async RPC queue is shutting down; the operation was not queued");
}
AsyncRPCOperationId operationId = operation->getId();
// Return continuation information
@@ -6048,7 +6134,10 @@ UniValue z_mergetoaddress(const UniValue& params, bool fHelp, const CPubKey& myp
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> operation(
new AsyncRPCOperation_mergetoaddress(builder, contextualTx, utxoInputs, saplingNoteInputs, recipient, nFee, contextInfo) );
q->addOperation(operation);
if (!q->addOperation(operation)) {
throw JSONRPCError(RPC_INTERNAL_ERROR,
"Async RPC queue is shutting down; the operation was not queued");
}
AsyncRPCOperationId operationId = operation->getId();
// Return continuation information
@@ -6349,6 +6438,7 @@ static const CRPCCommand commands[] =
{ "wallet", "z_gettotalbalance", &z_gettotalbalance, false },
{ "wallet", "z_mergetoaddress", &z_mergetoaddress, false },
{ "wallet", "z_sweepstatus", &z_sweepstatus, true },
{ "wallet", "z_autoshieldstatus", &z_autoshieldstatus, true },
{ "wallet", "z_consolidationstatus", &z_consolidationstatus, true },
{ "wallet", "z_sendmany", &z_sendmany, false },
{ "wallet", "z_shieldcoinbase", &z_shieldcoinbase, false },

View File

@@ -40,6 +40,7 @@
#include "coins.h"
#include "wallet/asyncrpcoperation_saplingconsolidation.h"
#include "wallet/asyncrpcoperation_sweep.h"
#include "wallet/asyncrpcoperation_autoshieldcoinbase.h"
#include <random>
#include <limits>
#include <thread>
@@ -555,6 +556,9 @@ void CWallet::ChainTip(const CBlockIndex *pindex,
if (fSweepEnabled) {
RunSaplingSweep(pindex->GetHeight());
}
if (fAutoShieldEnabled) {
RunAutoShieldCoinbase(pindex->GetHeight());
}
if (fTxDeleteEnabled) {
DeleteWalletTransactions(pindex);
}
@@ -581,7 +585,38 @@ void CWallet::RunSaplingSweep(int blockHeight) {
if (blockHeight == 0)
return;
AssertLockHeld(cs_wallet);
// Take cs_wallet ourselves: ChainTip (the notify-thread caller) does NOT
// hold it here, and we mutate fSweepRunning/nextSweep/saplingSweepOperationId
// and enqueue below. Matches RunSaplingConsolidation/RunAutoShieldCoinbase.
// (The old AssertLockHeld(cs_wallet) was a no-op in release builds and thus
// masked an unsynchronized mutation.) cs_wallet is recursive, so this is
// safe even on any path that already holds it.
LOCK(cs_wallet);
// Stale-baton guard. A successful-but-incomplete sweep round deliberately
// returns with fSweepRunning still set and nextSweep unadvanced (see
// AsyncRPCOperation_sweep::main), as a "continue draining next block" baton.
// But every early return below leaves that baton set WITHOUT re-dispatching,
// and RunSaplingConsolidation -- which is gated on fSweepRunning -- then
// returns without advancing nextConsolidation, so the "consolidation is
// within 5 blocks" blackout at the top of this function never lifts. That
// is a self-sustaining three-way deadlock: sweep waits on consolidation,
// consolidation waits on sweep, and autoshield shares the same gate, so a
// wedged sweep silently disables coinbase shielding forever.
// Only honour the baton while a sweep operation genuinely is in flight.
if (fSweepRunning) {
std::shared_ptr<AsyncRPCQueue> sweepQueue = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> inFlightSweep =
(sweepQueue != nullptr) ? sweepQueue->getOperationForId(saplingSweepOperationId) : nullptr;
bool inFlight = (inFlightSweep != nullptr) &&
(inFlightSweep->isReady() || inFlightSweep->isExecuting());
if (!inFlight) {
LogPrintf("%s: clearing stale fSweepRunning at blockHeight=%d (no sweep operation in flight)\n",
__func__, blockHeight);
fSweepRunning = false;
}
}
if (!fSweepEnabled) {
return;
}
@@ -604,17 +639,33 @@ void CWallet::RunSaplingSweep(int blockHeight) {
return;
}
//Don't Run While auto-shield is running.
if (fAutoShieldRunning) {
LogPrintf("%s: not sweeping since autoshield is currently running at height=%d\n", __func__, blockHeight);
return;
}
fSweepRunning = true;
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> lastOperation = q->getOperationForId(saplingSweepOperationId);
if (lastOperation != nullptr) {
lastOperation->cancel();
// Drop it from the queue's map as well. Nothing else ever removes these:
// popOperationForId is only reached from z_getoperationresult, so on a node
// running this every interval the map grew without bound.
q->popOperationForId(saplingSweepOperationId);
}
pendingSaplingSweepTxs.clear();
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_sweep(blockHeight + 5));
saplingSweepOperationId = operation->getId();
q->addOperation(operation);
if (!q->addOperation(operation)) {
// Queue is closing (shutdown). Release the flag we just set, or it stays
// set with no operation in flight and blocks every later round.
LogPrintf("%s: async queue is not accepting operations; skipping this round\n", __func__);
fSweepRunning = false;
return;
}
}
void CWallet::RunSaplingConsolidation(int blockHeight) {
@@ -634,6 +685,12 @@ void CWallet::RunSaplingConsolidation(int blockHeight) {
return;
}
// Self-guard: an op is already in flight (nextConsolidation only advances
// when it completes). Don't cancel + re-enqueue a fresh op every block.
if (fConsolidationRunning) {
return;
}
LogPrintf("%s: consolidation enabled at blockHeight=%d fSweepRunning=%d\n", __func__, blockHeight, fSweepRunning );
if (fSweepRunning) {
@@ -641,22 +698,104 @@ void CWallet::RunSaplingConsolidation(int blockHeight) {
return;
}
if (fAutoShieldRunning) {
LogPrintf("%s: not consolidating since autoshield is currently running at height=%d\n", __func__, blockHeight);
return;
}
LogPrintf("%s: creating consolidation operation at blockHeight=%d\n", __func__, blockHeight);
fConsolidationRunning = true;
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> lastOperation = q->getOperationForId(saplingConsolidationOperationId);
if (lastOperation != nullptr) {
lastOperation->cancel();
// Drop it from the queue's map as well. Nothing else ever removes these:
// popOperationForId is only reached from z_getoperationresult, so on a node
// running this every interval the map grew without bound.
q->popOperationForId(saplingConsolidationOperationId);
}
pendingSaplingConsolidationTxs.clear();
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_saplingconsolidation(blockHeight + 5));
saplingConsolidationOperationId = operation->getId();
q->addOperation(operation);
if (!q->addOperation(operation)) {
// Queue is closing (shutdown). Release the flag we just set, or it stays
// set with no operation in flight and blocks every later round.
LogPrintf("%s: async queue is not accepting operations; skipping this round\n", __func__);
fConsolidationRunning = false;
return;
}
}
// Periodically drain matured transparent coinbase into a wallet-owned Sapling
// z-address. Default-ON but conditional: this is enqueue-only (all gathering
// happens on the async worker thread inside the op, which is why we must not
// take cs_main here — ChainTip runs from the wallet-notify context). It is a
// silent no-op wherever it cannot act (locked wallet, no owned coinbase,
// external -mineraddress), so it is safe to run on every node.
void CWallet::RunAutoShieldCoinbase(int blockHeight) {
// Sapling is always active from height 1 on DragonX+HACs.
if (blockHeight == 0)
return;
LOCK(cs_wallet);
if (!fAutoShieldEnabled) {
return;
}
if (nextAutoShield > blockHeight) {
return;
}
// Self-guard: an op is already in flight (nextAutoShield only advances when
// it completes). Don't cancel + re-enqueue a fresh op every block.
if (fAutoShieldRunning) {
return;
}
// Mutual exclusion: sweep/consolidation share the single async worker and
// cs_wallet; don't queue an autoshield in the same connected block.
if (fSweepRunning || fConsolidationRunning) {
LogPrintf("%s: not autoshielding since sweep/consolidation is running at height=%d\n", __func__, blockHeight);
return;
}
// Silent no-op while locked: we can neither sign the shield nor derive a
// destination z-addr. Advance the interval so we don't retry every block.
if (IsLocked()) {
LogPrintf("%s: wallet locked; matured coinbase will accumulate until unlocked (height=%d)\n", __func__, blockHeight);
nextAutoShield = autoShieldInterval + blockHeight;
return;
}
fAutoShieldRunning = true;
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> lastOperation = q->getOperationForId(saplingAutoShieldOperationId);
if (lastOperation != nullptr) {
lastOperation->cancel();
// Drop it from the queue's map as well. Nothing else ever removes these:
// popOperationForId is only reached from z_getoperationresult, so on a node
// running this every interval the map grew without bound.
q->popOperationForId(saplingAutoShieldOperationId);
}
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_autoshieldcoinbase(blockHeight + 5));
saplingAutoShieldOperationId = operation->getId();
if (!q->addOperation(operation)) {
// Queue is closing (shutdown). Release the flag we just set, or it stays
// set with no operation in flight and blocks every later round.
LogPrintf("%s: async queue is not accepting operations; skipping this round\n", __func__);
fAutoShieldRunning = false;
return;
}
}
bool CWallet::CommitAutomatedTx(const CTransaction& tx) {
CWalletTx wtx(this, tx);
CReserveKey reservekey(pwalletMain);
fprintf(stderr,"%s: %s\n",__func__,tx.ToString().c_str());
// No tx dump here: CommitTransaction already LogPrintf's the same wtx.ToString(),
// and ToString() emits a line per vin, so with the 400-input autoshield cap this
// printed tens of KB to stderr on every automated round.
return CommitTransaction(wtx, reservekey);
}
@@ -2388,30 +2527,47 @@ void CWallet::GenerateNewSeed()
// 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)) {
//
// NO SILENT FALLBACK. Falling back to a random seed here produced a wallet
// that looks mnemonic-capable but whose words can never be exported
// (z_exportmnemonic refuses non-mnemonic wallets, rpcdump.cpp:1031+) and
// that no seed phrase can restore. A user who asked for -usemnemonic must
// get that or a hard failure.
if (GetBoolArg("-usemnemonic", true)) {
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__);
if (!GenerateMnemonicEntropy(256, entropy))
throw std::runtime_error(std::string(__func__) + ": -usemnemonic entropy generation failed");
// Store the EXPANDED 64-byte BIP39 seed as the HD seed, with
// fMnemonic = false. Every binary -- old or new -- then feeds the stored
// bytes straight into derivation, so the key tree is identical
// everywhere and no CHDChain version bump or minversion fence is needed.
// The 32-byte entropy is kept in a separate, display-only record purely
// so the phrase can be reprinted. Addresses are unchanged from the
// previous format, which expanded the stored entropy on every read.
RawHDSeed seed64;
if (!Bip39SeedFromEntropy(entropy, seed64))
throw std::runtime_error(std::string(__func__) + ": BIP39 seed expansion failed");
HDSeed seed(seed64);
// ORDER IS LOAD-BEARING: seed first, entropy second, never the reverse.
// A crash between the two leaves a wallet with a seed and no phrase --
// recoverable via z_exportwallet, merely inconvenient. The reverse order
// would leave entropy with no seed; the next start would mint a
// DIFFERENT seed while the wallet still held a phrase for the old one.
// (GetMnemonicPhrase cross-checks the two and would refuse to print it,
// but do not rely on that here.)
if (!InstallHDSeed(seed, false, nCreationTime))
throw std::runtime_error(std::string(__func__) + ": installing the mnemonic HD seed failed");
if (!SetMnemonicEntropy(entropy))
throw std::runtime_error(std::string(__func__) + ": storing the mnemonic entropy failed");
return;
}
auto seed = HDSeed::Random(HD_WALLET_SEED_LENGTH);
// If the wallet is encrypted and locked, this will fail.
if (!SetHDSeed(seed))
auto seed = HDSeed::Random(HD_WALLET_SEED_LENGTH);
if (!InstallHDSeed(seed, false, nCreationTime))
throw std::runtime_error(std::string(__func__) + ": SetHDSeed failed");
// store the key creation time together with
// the child index counter in the database
// as a hdchain object
CHDChain newHdChain;
newHdChain.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
newHdChain.seedFp = seed.Fingerprint();
newHdChain.nCreateTime = nCreationTime;
SetHDChain(newHdChain, false);
}
bool CWallet::SetHDSeed(const HDSeed& seed)
@@ -2445,14 +2601,38 @@ bool CWallet::SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned
{
LOCK(cs_wallet);
if (pwalletdbEncryption)
return pwalletdbEncryption->WriteCryptedHDSeed(seedFp, vchCryptedSecret);
else
return CWalletDB(strWalletFile).WriteCryptedHDSeed(seedFp, vchCryptedSecret);
// Write the encrypted record, then drop the plaintext one. Both go
// through the same CWalletDB (and therefore the same transaction when
// EncryptWallet supplied pwalletdbEncryption), because CDB::Rewrite at
// the end of EncryptWallet copies every surviving record into the fresh
// file -- a leftover plaintext "hdseed" would keep the unencrypted seed
// on disk for the life of the wallet.
//
// The erase is deliberately best-effort: a hard failure here propagates
// into CCryptoKeyStore::EncryptKeys, which CWallet::EncryptWallet turns
// into assert(false) with half the keys encrypted in memory. A logged
// warning is strictly better than that.
if (pwalletdbEncryption) {
if (!pwalletdbEncryption->WriteCryptedHDSeed(seedFp, vchCryptedSecret))
return false;
if (!pwalletdbEncryption->EraseHDSeed(seedFp))
LogPrintf("%s: WARNING: could not erase the plaintext hdseed record; "
"the unencrypted HD seed may remain in wallet.dat\n", __func__);
return true;
} else {
CWalletDB walletdb(strWalletFile);
if (!walletdb.WriteCryptedHDSeed(seedFp, vchCryptedSecret))
return false;
if (!walletdb.EraseHDSeed(seedFp))
LogPrintf("%s: WARNING: could not erase the plaintext hdseed record; "
"the unencrypted HD seed may remain in wallet.dat\n", __func__);
return true;
}
}
return false;
}
void CWallet::SetHDChain(const CHDChain& chain, bool memonly)
{
LOCK(cs_wallet);
@@ -2462,6 +2642,20 @@ void CWallet::SetHDChain(const CHDChain& chain, bool memonly)
hdChain = chain;
}
void CWallet::SetHDSeedOrigin(int origin)
{
LOCK(cs_wallet);
hdSeedOrigin = origin;
// Deliberately non-fatal, unlike SetHDChain: losing this record must never
// stop a node from starting. The cost of a failed write is that the next
// start re-classifies the wallet, and re-classification of an already-seeded
// wallet yields HDSEED_ORIGIN_UNKNOWN, i.e. the safe answer.
if (fFileBacked && !CWalletDB(strWalletFile).WriteHDSeedOrigin((int64_t)origin))
LogPrintf("%s: WARNING: could not record HD seed origin %d in wallet.dat\n", __func__, origin);
}
bool CWallet::LoadHDSeed(const HDSeed& seed)
{
return CBasicKeyStore::SetHDSeed(seed);
@@ -2471,21 +2665,94 @@ bool CWallet::LoadCryptedHDSeed(const uint256& seedFp, const std::vector<unsigne
{
return CCryptoKeyStore::SetCryptedHDSeed(seedFp, seed);
}
bool CWallet::SetMnemonicEntropy(const RawHDSeed& entropy)
{
if (!CCryptoKeyStore::SetMnemonicEntropy(entropy)) {
return false;
}
if (!fFileBacked) {
return true;
}
{
LOCK(cs_wallet);
if (!IsCrypted()) {
// Keyed by fingerprint exactly as "hdseed" is, so ReadKeyValue can
// integrity-check it and EraseMnemonicEntropy can find it later.
return CWalletDB(strWalletFile).WriteMnemonicEntropy(
MnemonicEntropyFingerprint(entropy), entropy);
}
}
return true;
}
bool CWallet::SetCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char>& vchCryptedSecret)
{
if (!CCryptoKeyStore::SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) {
return false;
}
if (!fFileBacked) {
return true;
}
{
LOCK(cs_wallet);
// Same write-then-erase discipline as SetCryptedHDSeed: CDB::Rewrite at
// the end of EncryptWallet copies every surviving record, so a leftover
// plaintext "mnementropy" would keep the seed phrase recoverable from an
// encrypted wallet.dat. The erase is best-effort for the same reason: a
// hard failure would propagate into EncryptKeys -> assert(false).
if (pwalletdbEncryption) {
if (!pwalletdbEncryption->WriteCryptedMnemonicEntropy(entropyFp, vchCryptedSecret))
return false;
if (!pwalletdbEncryption->EraseMnemonicEntropy(entropyFp))
LogPrintf("%s: WARNING: could not erase the plaintext mnementropy record\n", __func__);
return true;
} else {
CWalletDB walletdb(strWalletFile);
if (!walletdb.WriteCryptedMnemonicEntropy(entropyFp, vchCryptedSecret))
return false;
if (!walletdb.EraseMnemonicEntropy(entropyFp))
LogPrintf("%s: WARNING: could not erase the plaintext mnementropy record\n", __func__);
return true;
}
}
return false;
}
bool CWallet::LoadMnemonicEntropy(const RawHDSeed& entropy)
{
return CBasicKeyStore::SetMnemonicEntropy(entropy);
}
bool CWallet::LoadCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char>& vchCryptedSecret)
{
return CCryptoKeyStore::SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret);
}
bool CWallet::InstallHDSeed(const HDSeed& seed, bool fMnemonic, int64_t nCreateTime)
{
AssertLockHeld(cs_wallet);
if (!SetHDSeed(seed))
return false;
// Chain BEFORE seed. A crash between the two records must never leave a
// wallet that holds a seed with no hdchain: on the next load hdChain would
// silently revert to its defaults, clearing fMnemonicSeed (which switches
// the derivation input, wallet.cpp:2615-2633) and resetting
// saplingAccountCounter. The opposite torn state — chain without seed — is
// harmless and self-healing: HaveHDSeed() is false, so init installs a seed
// again and overwrites the chain.
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);
SetHDChain(newHdChain, false); // throws if the write fails
if (!SetHDSeed(seed))
return false;
return true;
}
@@ -2524,10 +2791,29 @@ bool CWallet::SetHDSeedFromMnemonic(const std::string& phrase)
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
// Store the EXPANDED 64-byte BIP39 seed as the HD seed (fMnemonic = false);
// the entropy goes in its own record and is used only to reprint the phrase.
// Derivation therefore reads the stored bytes directly on any binary, and
// the resulting addresses are byte-identical to the previous format, which
// expanded the stored entropy on every derivation. SilentDragonXLite
// interop is unaffected: the same words still yield the same seed64.
RawHDSeed seed64;
if (!Bip39SeedFromEntropy(entropy, seed64))
return false;
HDSeed seed(seed64);
// Seed first, entropy second -- see the ordering note in GenerateNewSeed.
if (!InstallHDSeed(seed, false, 1)) // birthday = genesis for a restore
return false;
// Non-fatal on a restore, unlike GenerateNewSeed: the user already holds the
// phrase (they just typed it), the seed is installed and the wallet is fully
// functional; only z_exportmnemonic is lost.
if (!SetMnemonicEntropy(entropy)) {
LogPrintf("%s: WARNING: HD seed installed but the mnemonic entropy record could not be "
"stored; z_exportmnemonic will be unavailable on this wallet\n", __func__);
}
return true;
}
bool CWallet::GetHDSeedForDerivation(HDSeed& seedOut) const
@@ -2552,6 +2838,36 @@ bool CWallet::GetHDSeedForDerivation(HDSeed& seedOut) const
bool CWallet::GetMnemonicPhrase(std::string& phraseOut) const
{
// Preferred form: the HD seed is the EXPANDED 64-byte BIP39 seed and the
// entropy sits in its own record.
RawHDSeed entropy;
if (GetMnemonicEntropy(entropy)) { // false on an encrypted+locked wallet
// NEVER hand out a phrase that does not restore THIS wallet. Prove the
// entropy expands to the exact bytes derivation consumes; if it does
// not (a torn install, a wallet.dat edited by hand, an entropy record
// paired with a different seed), refuse rather than print a phrase that
// silently restores someone else's key tree. Costs one PBKDF2 on a
// user-initiated RPC.
RawHDSeed seed64;
if (!Bip39SeedFromEntropy(entropy, seed64))
return false;
HDSeed derivationSeed;
if (!GetHDSeedForDerivation(derivationSeed))
return false;
if (derivationSeed.RawSeed() != seed64) {
LogPrintf("%s: refusing to export a seed phrase: the stored mnemonic entropy does not "
"expand to this wallet's HD seed\n", __func__);
return false;
}
return EntropyToMnemonic(entropy, phraseOut);
}
// Legacy form (earlier builds of this branch): the stored HD seed IS the
// 32-byte BIP39 entropy, expanded on every derivation. Consistent by
// construction, so no cross-check is possible or needed.
if (!hdChain.fMnemonicSeed)
return false;

View File

@@ -784,10 +784,8 @@ private:
TxNullifiers mapTxSaplingNullifiers;
std::vector<CTransaction> pendingSaplingConsolidationTxs;
AsyncRPCOperationId saplingConsolidationOperationId;
std::vector<CTransaction> pendingSaplingSweepTxs;
AsyncRPCOperationId saplingSweepOperationId;
void AddToTransparentSpends(const COutPoint& outpoint, const uint256& wtxid);
void AddToSaplingSpends(const uint256& nullifier, const uint256& wtxid);
@@ -802,6 +800,9 @@ public:
int64_t nWitnessCacheSize;
bool needsRescan = false;
int nextConsolidation = 0;
// Id of the in-flight consolidation op; read by the op to confirm it is
// still the current one before mutating scheduler state.
AsyncRPCOperationId saplingConsolidationOperationId;
bool fSaplingConsolidationEnabled = false;
bool fConsolidationRunning = false;
@@ -809,6 +810,12 @@ public:
bool fSweepExternalEnabled = false;
bool fSweepRunning = false;
// Automatic coinbase shielding (t->z). Default ON but conditional: it is a
// silent no-op on nodes where it cannot act (no wallet, external
// -mineraddress, non-mining, or locked wallet). See RunAutoShieldCoinbase.
bool fAutoShieldEnabled = true;
bool fAutoShieldRunning = false;
std::atomic<bool> fAbortRescan{false};
// abort current rescan
void AbortRescan() { fAbortRescan = true; }
@@ -823,6 +830,9 @@ public:
int rescanStartHeight = 0;
int nextSweep = 0;
// Id of the in-flight sweep op; read by the op to confirm it is still the
// current one before mutating scheduler state.
AsyncRPCOperationId saplingSweepOperationId;
int amountSwept = 0;
int amountConsolidated = 0;
int sweepInterval = 10;
@@ -833,6 +843,31 @@ public:
std::vector<std::string> sweepExcludeAddresses;
std::string consolidationAddress = "";
int nextAutoShield = 0;
int autoShieldInterval = 25;
CAmount autoShieldFee = 10000;
// Minimum matured coinbase UTXOs before a round fires, to avoid per-interval
// fee churn on a single freshly-matured reward.
int autoShieldMinUtxos = 1;
// Configured destination z-addr override; also used to cache the resolved
// wallet-owned destination so we keep reusing one address.
std::string autoShieldAddress = "";
// Id of the in-flight autoshield op; read by the op to confirm it is still
// the current one before mutating scheduler state.
AsyncRPCOperationId saplingAutoShieldOperationId;
// Provenance of this wallet's HD seed, recorded once in wallet.dat the
// first time a build that knows about it opens the wallet. Features that
// move funds into addresses only the seed can re-derive must not turn
// themselves ON by default unless the user can actually restore that seed.
enum HDSeedOrigin {
HDSEED_ORIGIN_UNRECORDED = 0, // no record in wallet.dat yet
HDSEED_ORIGIN_CREATED = 1, // minted onto a brand-new empty wallet
HDSEED_ORIGIN_RESTORED = 2, // user supplied -mnemonic / -hdseed
HDSEED_ORIGIN_RETROFIT = 3, // minted onto a pre-existing seedless wallet
HDSEED_ORIGIN_UNKNOWN = 4, // seed predates this record
};
int hdSeedOrigin = HDSEED_ORIGIN_UNRECORDED;
void ClearNoteWitnessCache();
int64_t NullifierCount();
@@ -1224,6 +1259,7 @@ public:
const CBlock *pblock,
boost::optional<std::pair<SproutMerkleTree, SaplingMerkleTree>> added);
void RunSaplingConsolidation(int blockHeight);
void RunAutoShieldCoinbase(int blockHeight);
bool CommitAutomatedTx(const CTransaction& tx);
/** Saves witness caches and best block locator to disk. */
void SetBestChain(const CBlockLocator& loc);
@@ -1309,6 +1345,14 @@ public:
bool SetHDSeed(const HDSeed& seed);
bool SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret);
/* Record this wallet's BIP39 entropy so its seed phrase can be reprinted.
Display-only: derivation never reads it (the HD seed holds the bytes that
are actually derived from). Refuses to replace an existing record.
SetCryptedMnemonicEntropy overrides the CCryptoKeyStore virtual so the
record reaches disk; SetMnemonicEntropy merely hides the base version,
which is safe because no call site holds a base pointer. */
bool SetMnemonicEntropy(const RawHDSeed& entropy);
bool SetCryptedMnemonicEntropy(const uint256& entropyFp, 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
@@ -1327,8 +1371,14 @@ public:
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; }
/* True if this wallet has a BIP39 seed phrase available. Two storage forms
qualify:
- current: the HD seed is the EXPANDED 64-byte BIP39 seed and the
entropy lives in its own record (HaveMnemonicEntropy());
- legacy: hdChain.fMnemonicSeed, where the stored HD seed IS the
32-byte entropy and is expanded on every derivation.
Gates z_exportmnemonic (rpcdump.cpp). */
bool IsMnemonicSeed() const { return hdChain.fMnemonicSeed || HaveMnemonicEntropy(); }
/* 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
@@ -1349,11 +1399,22 @@ public:
void SetHDChain(const CHDChain& chain, bool memonly);
const CHDChain& GetHDChain() const { return hdChain; }
/* Record (in memory and in wallet.dat) how this wallet's HD seed came to
exist. Best-effort: a failed write is logged, not fatal — the next start
simply re-classifies, and re-classification always errs toward
HDSEED_ORIGIN_UNKNOWN, which is the conservative answer. */
void SetHDSeedOrigin(int origin);
/* Set the current HD seed, without saving it to disk (used by LoadWallet) */
bool LoadHDSeed(const HDSeed& key);
/* Set the current encrypted HD seed, without saving it to disk (used by LoadWallet) */
bool LoadCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char>& seed);
/* Set the mnemonic entropy, without saving it to disk (used by LoadWallet) */
bool LoadMnemonicEntropy(const RawHDSeed& entropy);
/* Set the encrypted mnemonic entropy, without saving it to disk (used by LoadWallet) */
bool LoadCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char>& vchCryptedSecret);
/* Find notes filtered by payment address, min depth, ability to spend */
void GetFilteredNotes(std::vector<SaplingNoteEntry>& saplingEntries,

View File

@@ -34,6 +34,15 @@
#include <boost/scoped_ptr.hpp>
#include <boost/thread.hpp>
// Out-of-line definitions for CHDChain's in-class static constants. These are
// only initialised in the class body, so any ODR use -- binding one to a const
// reference, which is exactly what gtest's EXPECT_*/ASSERT_* macros do -- needs
// a definition or the link fails. hush-gtest hit this on VERSION_HD_MNEMONIC.
const int CHDChain::VERSION_HD_BASE;
const int CHDChain::VERSION_HD_TRANSPARENT;
const int CHDChain::VERSION_HD_MNEMONIC;
const int CHDChain::CURRENT_VERSION;
using namespace std;
static uint64_t nAccountingEntryNumber = 0;
@@ -216,6 +225,12 @@ bool CWalletDB::WriteWitnessCacheSize(int64_t nWitnessCacheSize)
return Write(std::string("witnesscachesize"), nWitnessCacheSize);
}
bool CWalletDB::WriteHDSeedOrigin(int64_t nOrigin)
{
nWalletDBUpdated++;
return Write(std::string("hdseedorigin"), nOrigin);
}
bool CWalletDB::ReadPool(int64_t nPool, CKeyPool& keypool)
{
return Read(std::make_pair(std::string("pool"), nPool), keypool);
@@ -403,12 +418,19 @@ public:
bool fAnyUnordered;
int nFileVersion;
vector<uint256> vWalletUpgrade;
// True once a well-formed "hdchain" record has been loaded.
bool fHDChainRead;
// True when that record had to be repaired on read (see the "hdchain" case
// in ReadKeyValue); LoadWallet rewrites it in full form afterwards.
bool fHDChainRepaired;
CWalletScanState() {
nKeys = nCKeys = nKeyMeta = nZKeys = nCZKeys = nZKeyMeta = nSapZAddrs = 0;
fIsEncrypted = false;
fAnyUnordered = false;
nFileVersion = 0;
fHDChainRead = false;
fHDChainRepaired = false;
}
};
@@ -833,9 +855,90 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
else if (strType == "hdchain")
{
CHDChain chain;
ssValue >> chain;
// Keep an untouched copy: a failed >> has already consumed part of ssValue.
CDataStream ssRetry(ssValue.begin(), ssValue.end(), ssValue.GetType(), ssValue.GetVersion());
try {
ssValue >> chain;
} catch (...) {
// Downgrade repair. A build predating VERSION_HD_TRANSPARENT writes
// this record back with only the four base fields while leaving
// nVersion at whatever it read, so the version-gated reads above run
// off the end. Without this, one address generated under such a build
// makes the wallet unopenable here ("Wallet corrupted") even though
// nothing is actually lost.
//
// Recovering is safe for a v1/v2 record: everything derivation needs
// is either in the four-field prefix or in the separate hdseed record,
// and the trailing counters are self-healing -- DeriveNewChildKey and
// GenerateNewSaplingZKey both skip indices whose key the wallet
// already holds, so restarting a counter at 0 re-walks past existing
// keys instead of reissuing them.
chain = CHDChain();
try {
ssRetry >> chain.nVersion;
ssRetry >> chain.seedFp;
ssRetry >> chain.nCreateTime;
ssRetry >> chain.saplingAccountCounter;
} catch (...) {
// Short even in the base fields: genuinely corrupt.
strErr = "Error reading wallet database: hdchain record is corrupt";
return false;
}
if (chain.nVersion >= CHDChain::VERSION_HD_MNEMONIC) {
// A record claiming to carry fMnemonicSeed must not have it
// guessed: that flag selects the derivation input, so defaulting
// it wrong yields a different key tree in silence. Fail loud, as
// this branch always did.
strErr = "Error reading wallet database: hdchain record is corrupt";
return false;
}
chain.transparentChildCounter = 0;
chain.fMnemonicSeed = false;
wss.fHDChainRepaired = true;
LogPrintf("Repairing a truncated hdchain record (nVersion=%d): it was last written by "
"a wallet build that predates the transparent HD counter\n", chain.nVersion);
}
wss.fHDChainRead = true;
pwallet->SetHDChain(chain, true);
}
else if (strType == "hdseedorigin")
{
int64_t nOrigin = 0;
ssValue >> nOrigin;
pwallet->hdSeedOrigin = (int)nOrigin;
}
else if (strType == "mnementropy")
{
uint256 entropyFp;
RawHDSeed entropy;
ssKey >> entropyFp;
ssValue >> entropy;
if (MnemonicEntropyFingerprint(entropy) != entropyFp)
{
strErr = "Error reading wallet database: mnemonic entropy corrupt";
return false;
}
if (!pwallet->LoadMnemonicEntropy(entropy))
{
strErr = "Error reading wallet database: LoadMnemonicEntropy failed";
return false;
}
}
else if (strType == "cmnementropy")
{
uint256 entropyFp;
vector<unsigned char> vchCryptedSecret;
ssKey >> entropyFp;
ssValue >> vchCryptedSecret;
if (!pwallet->LoadCryptedMnemonicEntropy(entropyFp, vchCryptedSecret))
{
strErr = "Error reading wallet database: LoadCryptedMnemonicEntropy failed";
return false;
}
wss.fIsEncrypted = true;
}
} catch (...)
{
return false;
@@ -847,6 +950,10 @@ static bool IsKeyType(string strType)
{
return (strType == "key" || strType == "wkey" ||
strType == "hdseed" || strType == "chdseed" ||
// The mnemonic entropy must survive a keys-only salvage: without it
// a recovered wallet keeps its seed (and stays fully spendable) but
// silently loses the ability to reprint its seed phrase.
strType == "mnementropy" || strType == "cmnementropy" ||
strType == "zkey" || strType == "czkey" ||
strType == "sapzkey" || strType == "csapzkey" ||
strType == "vkey" ||
@@ -947,6 +1054,35 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
if (fNoncriticalErrors && result == DB_LOAD_OK)
result = DB_NONCRITICAL_ERROR;
// Rewrite a repaired record in full form so the next load is clean and the
// transparent counter starts being persisted again.
if (wss.fHDChainRepaired && pwallet->HaveHDSeed())
{
try {
pwallet->SetHDChain(pwallet->GetHDChain(), false);
LogPrintf("Rewrote the repaired hdchain record in full form\n");
} catch (const std::exception& e) {
LogPrintf("Could not rewrite the repaired hdchain record: %s\n", e.what());
}
}
// A wallet that holds an HD seed but whose hdchain record is missing or
// unreadable is NOT safe to run. hdChain would fall back to its SetNull
// defaults (walletdb.h:105-113), which (a) clears fMnemonicSeed, switching
// HD derivation from the 64-byte BIP39 seed to the raw 32-byte entropy
// (CWallet::GetHDSeedForDerivation, wallet.cpp:2615-2633) -> an entirely
// different key tree, and (b) resets saplingAccountCounter to 0, so the
// next GenerateNewSaplingZKey walks back over accounts that already exist.
// Both are silent today (a bad hdchain read is only DB_NONCRITICAL_ERROR).
// Fail loud instead of quietly deriving into the wrong tree.
if (pwallet->HaveHDSeed() && !wss.fHDChainRead)
{
LogPrintf("Error loading wallet.dat: HD seed present but the hdchain record is missing or corrupt. "
"Recover by restoring from the seed phrase: move wallet.dat aside and start with "
"-mnemonic=\"<your seed phrase>\" -rescan\n");
return DB_CORRUPT;
}
// Any wallet corruption at all: skip any rewriting or
// upgrading, we don't want to make it worse.
if (result != DB_LOAD_OK)
@@ -1240,7 +1376,13 @@ bool CWalletDB::Recover(CDBEnv& dbenv, const std::string& filename, bool fOnlyKe
fReadOK = ReadKeyValue(&dummyWallet, ssKey, ssValue,
wss, strType, strErr);
}
if (!IsKeyType(strType))
// "hdchain" is not a key type, but it must survive a keys-only
// salvage: a recovered wallet that keeps its seed while losing its
// hdchain silently derives from a different key tree (fMnemonicSeed
// cleared -> raw entropy instead of the 64-byte BIP39 seed) and
// re-issues sapling accounts from 0. CWalletDB::LoadWallet now
// refuses such a wallet outright, so preserve the record here.
if (!IsKeyType(strType) && strType != "hdchain")
continue;
if (!fReadOK)
{
@@ -1290,6 +1432,34 @@ bool CWalletDB::WriteCryptedHDSeed(const uint256& seedFp, const std::vector<unsi
return Write(std::make_pair(std::string("chdseed"), seedFp), vchCryptedSecret);
}
bool CWalletDB::EraseHDSeed(const uint256& seedFp)
{
nWalletDBUpdated++;
// CDB::Erase honours activeTxn, so when this runs inside EncryptWallet's
// transaction the erase commits or aborts atomically with the chdseed write.
// It also returns true for DB_NOTFOUND, so erasing a record that was never
// written (e.g. a wallet encrypted at creation time) is not a failure.
return Erase(std::make_pair(std::string("hdseed"), seedFp));
}
bool CWalletDB::WriteMnemonicEntropy(const uint256& entropyFp, const RawHDSeed& entropy)
{
nWalletDBUpdated++;
return Write(std::make_pair(std::string("mnementropy"), entropyFp), entropy);
}
bool CWalletDB::WriteCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char>& vchCryptedSecret)
{
nWalletDBUpdated++;
return Write(std::make_pair(std::string("cmnementropy"), entropyFp), vchCryptedSecret);
}
bool CWalletDB::EraseMnemonicEntropy(const uint256& entropyFp)
{
nWalletDBUpdated++;
return Erase(std::make_pair(std::string("mnementropy"), entropyFp));
}
bool CWalletDB::WriteHDChain(const CHDChain& chain)
{
nWalletDBUpdated++;

View File

@@ -191,6 +191,9 @@ public:
bool WriteWitnessCacheSize(int64_t nWitnessCacheSize);
//! Record how this wallet's HD seed came to exist (CWallet::HDSeedOrigin).
bool WriteHDSeedOrigin(int64_t nOrigin);
bool ReadPool(int64_t nPool, CKeyPool& keypool);
bool WritePool(int64_t nPool, const CKeyPool& keypool);
bool ErasePool(int64_t nPool);
@@ -219,6 +222,17 @@ public:
bool WriteHDSeed(const HDSeed& seed);
bool WriteCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char>& vchCryptedSecret);
//! Remove the PLAINTEXT hdseed record. Must be called once the seed has been
//! written in encrypted form: CDB::Rewrite (invoked at the end of
//! CWallet::EncryptWallet) copies whatever records still exist into the new
//! file, so a leftover "hdseed" leaves the unencrypted seed on disk forever.
bool EraseHDSeed(const uint256& seedFp);
//! BIP39 entropy for a phrase-recoverable wallet. Display-only: derivation
//! never reads it. Record names are deliberately distinct prefixes from
//! "hdseed"/"chdseed" so they cannot collide.
bool WriteMnemonicEntropy(const uint256& entropyFp, const RawHDSeed& entropy);
bool WriteCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char>& vchCryptedSecret);
bool EraseMnemonicEntropy(const uint256& entropyFp);
//! write the hdchain model (external chain child index counter)
bool WriteHDChain(const CHDChain& chain);