From 23f3e26d2f8718b0cef5e4bf67e437cbe25a1c18 Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 9 Oct 2026 13:31:01 -0500 Subject: [PATCH] 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 --- src/wallet/rpchushwallet.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/wallet/rpchushwallet.cpp b/src/wallet/rpchushwallet.cpp index 85574b7c7..74f0c6702 100644 --- a/src/wallet/rpchushwallet.cpp +++ b/src/wallet/rpchushwallet.cpp @@ -148,12 +148,16 @@ void zsTxSpendsToJSON(const CWalletTx& wtx, UniValue& spends, CAmount& totalSpen const SpendDescription& spendDesc = wtx.vShieldedSpend[i]; UniValue obj(UniValue::VOBJ); - SaplingOutPoint op = pwalletMain->mapSaplingNullifiersToNotes[spendDesc.nullifier]; + std::map::const_iterator nfIt = pwalletMain->mapSaplingNullifiersToNotes.find(spendDesc.nullifier); - if (pwalletMain->IsSaplingNullifierFromMe(spendDesc.nullifier)) { - const CWalletTx* parent = pwalletMain->GetWalletTx(pwalletMain->mapSaplingNullifiersToNotes[spendDesc.nullifier].hash); + // Same test as IsSaplingNullifierFromMe(), but find() so a read-only RPC never inserts into wallet maps. + 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 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); if (pt) {