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>
This commit is contained in:
@@ -855,6 +855,18 @@ public:
|
||||
// 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();
|
||||
|
||||
@@ -1373,6 +1385,12 @@ 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user