From fd67a1a15af87c68b78dd98ed0644c9ce40d6582 Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 9 Oct 2026 13:31:01 -0500 Subject: [PATCH] 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 --- src/wallet/wallet.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index e596767d7..109ad2237 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -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); } } }