wallet: stop zsTxSpendsToJSON inserting into the nullifier and note maps

zsTxSpendsToJSON looked spends up with operator[]: every foreign nullifier
in a listed tx inserted a null outpoint into mapSaplingNullifiersToNotes,
and the note-data lookup could insert an empty SaplingNoteData. A read-only
RPC was mutating wallet state.

z_viewtransaction then found those null outpoints and failed with RPC -1
"map::at" for every tx with a foreign Sapling spend (most incoming z->z
payments) once z_listsentbyaddress or getalldata had run, until restart.
ObsidianDragon treats that error as transient and retried those txs on
every refresh.

Use find() with the same test as IsSaplingNullifierFromMe(). This call
produces the same rows in every case; z_viewtransaction now answers as it
does on a freshly started daemon.

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 08afe5bb3b
commit 23f3e26d2f
+8 -4
View File
@@ -148,12 +148,16 @@ void zsTxSpendsToJSON(const CWalletTx& wtx, UniValue& spends, CAmount& totalSpen
const SpendDescription& spendDesc = wtx.vShieldedSpend[i]; const SpendDescription& spendDesc = wtx.vShieldedSpend[i];
UniValue obj(UniValue::VOBJ); UniValue obj(UniValue::VOBJ);
SaplingOutPoint op = pwalletMain->mapSaplingNullifiersToNotes[spendDesc.nullifier]; std::map<uint256, SaplingOutPoint>::const_iterator nfIt = pwalletMain->mapSaplingNullifiersToNotes.find(spendDesc.nullifier);
if (pwalletMain->IsSaplingNullifierFromMe(spendDesc.nullifier)) { // Same test as IsSaplingNullifierFromMe(), but find() so a read-only RPC never inserts into wallet maps.
const CWalletTx* parent = pwalletMain->GetWalletTx(pwalletMain->mapSaplingNullifiersToNotes[spendDesc.nullifier].hash); if (nfIt != pwalletMain->mapSaplingNullifiersToNotes.end() && pwalletMain->mapWallet.count(nfIt->second.hash)) {
const SaplingOutPoint op = nfIt->second;
const CWalletTx* parent = pwalletMain->GetWalletTx(op.hash);
mapSaplingNoteData_t::const_iterator ndIt = parent->mapSaplingNoteData.find(op);
if (ndIt == parent->mapSaplingNoteData.end()) continue; // stale entry: old code inserted an empty note that never decrypts
const OutputDescription& output = parent->vShieldedOutput[op.n]; const OutputDescription& output = parent->vShieldedOutput[op.n];
const SaplingNoteData& nd = pwalletMain->mapWallet[pwalletMain->mapSaplingNullifiersToNotes[spendDesc.nullifier].hash].mapSaplingNoteData[op]; const SaplingNoteData& nd = ndIt->second;
auto pt = libzcash::SaplingNotePlaintext::decrypt(output.encCiphertext,nd.ivk,output.ephemeralKey,output.cm); auto pt = libzcash::SaplingNotePlaintext::decrypt(output.encCiphertext,nd.ivk,output.ephemeralKey,output.cm);
if (pt) { if (pt) {