wallet: don't abort building witnesses for viewing-key-only notes

A note found with only an incoming viewing key (z_importviewingkey) never
gets a nullifier: UpdateSaplingNullifierNoteMapWithTx needs the full
viewing key. VerifyAndSetInitialWitness built a witness for such a note
anyway and then dereferenced *item.second.nullifier, aborting the daemon
on the boost::optional assertion.

Importing a viewing key that had received funds therefore crashed the
node during the import's rescan, and again at every start and on every
block, since witnesses are rebuilt then too. Reproduced on regtest
(z_importviewingkey -> ScanForWalletTransactions -> BuildWitnessCache ->
VerifyAndSetInitialWitness).

Skip notes that have no nullifier and no full viewing key: they cannot
be spent and need no witness. Only call SaplingWitnessMinimumHeight when
the nullifier is set, and log the corrupt-data case where a note with a
full viewing key gets no witness, which used to abort as well. A note
whose spending key is imported later is picked up on the next rebuild.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 13:31:01 -05:00
co-authored by Claude Opus 5.5
parent 1faa349c27
commit fd67a1a15a
+12 -2
View File
@@ -1266,8 +1266,15 @@ int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessO
uint256 blockRoot;
uint256 witnessRoot;
if (!nd->nullifier)
if (!nd->nullifier) {
::ClearSingleNoteWitnessCache(nd);
// A note found with only an incoming viewing key (z_importviewingkey) never gets a
// nullifier (UpdateSaplingNullifierNoteMapWithTx needs the full viewing key) and can't be
// spent, so it needs no witness. Building one re-read its block on every call and then
// aborted on *item.second.nullifier below.
if (!HaveSaplingFullViewingKey(nd->ivk))
continue;
}
if (!nd->witnesses.empty() && nd->witnessHeight > 0) {
@@ -1358,7 +1365,10 @@ int CWallet::VerifyAndSetInitialWitness(const CBlockIndex* pindex, bool witnessO
}
nd->witnessHeight = pblockindex->GetHeight();
UpdateSaplingNullifierNoteMapWithTx(wtxItem.second);
nMinimumHeight = SaplingWitnessMinimumHeight(*item.second.nullifier, nd->witnessHeight, nMinimumHeight);
if (item.second.nullifier)
nMinimumHeight = SaplingWitnessMinimumHeight(*item.second.nullifier, nd->witnessHeight, nMinimumHeight);
else
LogPrintf("%s: no witness found for note %s:%d in its block; it stays unspendable until a rescan\n", __func__, wtxHash.ToString(), op.n);
}
}
}