Skip to content

Wallet.dat grows abnormally over time due to uninitialized Yerbas asset wallet metadata #5

Description

@Nikovash

Summary

Yerbas wallets appear to suffer from long-term abnormal wallet.dat growth even when the wallet has little or no direct user transaction activity. This has been a long-standing Yerbas-specific issue and does not appear to reproduce in Crystal Bitoreum / BTRM under comparable wallet usage.

Investigation points to a wallet ownership false-positive path related to Yerbas asset support. Specifically, CAssetOutputEntry contains uninitialized primitive fields, and CWallet::HasMyAssets() reads assetData.nAmount after calling GetDebit(...) even when no asset data was populated.

This can cause unrelated transactions to be incorrectly classified as wallet-related, causing AddToWalletIfInvolvingMe() to persist them into wallet.dat.

Over time, this can balloon normal wallets to extremely large sizes and cause very slow GUI startup, wallet verification, wallet loading, and automatic backup behavior.

Observed behavior

A regular Yerbas wallet with little expected user transaction activity showed:

wallet.dat size:     ~1.6 GB
mapWallet.size():   ~1,498,000 wallet transactions
wallet load time:   ~28.5 seconds
wallet verify time: ~20–30 seconds

Example startup log pattern:

init message: Verifying wallet(s)...
Using wallet wallet.dat
init message: Loading wallet...
nKeysLeftSinceAutoBackup: 1000
wallet                28512ms
setExternalKeyPool.size() = 1000
setInternalKeyPool.size() = 0
mapWallet.size() = 1497362

This causes a long delay between splash screen and main GUI availability. The issue initially looked like reindex or block index delay, but timing logs showed the main startup cost was wallet verification and wallet loading.

The relevant startup delay was traced to:

CWallet::InitAutoBackup()
CWallet::CreateWalletFromFile()
CWallet::LoadWallet()
CWalletDB::LoadWallet()
CWallet::AddToWalletIfInvolvingMe()

Automatic startup backups make the pain worse because the client copies the entire large wallet.dat before continuing startup, but backup is not the root cause of the growth.

Why this is likely a Yerbas wallet classification bug

The persistent wallet growth path is:

BlockConnected / TransactionAddedToMempool
    -> CWallet::SyncTransaction()
    -> CWallet::AddToWalletIfInvolvingMe()
    -> CWallet::AddToWallet()
    -> CWalletDB::WriteTx()
    -> wallet.dat grows

AddToWalletIfInvolvingMe() writes a transaction to the wallet if the wallet believes the transaction is mine or from me:

bool fExisted = mapWallet.count(tx.GetHash()) != 0;
if (fExisted && !fUpdate) return false;
if (fExisted || IsMine(tx) || IsFromMe(tx))
{
    ...
    return AddToWallet(wtx, false);
}

Yerbas extends IsFromMe() with asset-aware logic:

bool CWallet::IsFromMe(const CTransaction& tx) const
{
    return (GetDebit(tx, ISMINE_ALL) > 0 || HasMyAssets(tx));
}

The suspicious function is:

CAmount CWallet::HasMyAssets(const CTransaction& tx) const
{
    for (const CTxIn& txin : tx.vin)
    {
        CAssetOutputEntry assetData;
        GetDebit(txin, ISMINE_ALL, assetData);
        if (assetData.nAmount > 0)
            return true;
    }

     return (GetDebit(tx, ISMINE_ALL) > 0);
}

The problem is that CAssetOutputEntry assetData; is default-created without initialization, while the struct contains primitive fields with no constructor or default values:

struct CAssetOutputEntry
{
    txnouttype type;
    std::string assetName;
    CTxDestination destination;
    CAmount nAmount;
    std::string message;
    int64_t expireTime;
    int vout;
};

If GetDebit(txin, ISMINE_ALL, assetData) returns without setting asset data, assetData.nAmount may still contain uninitialized stack garbage. A random positive value would cause:

if (assetData.nAmount > 0)
    return true;

Then:

IsFromMe(tx) == true

Then:

AddToWalletIfInvolvingMe()

writes the unrelated transaction into wallet.dat.

Expected behavior

A wallet should only persist transactions that are actually relevant to that wallet:

  • Outputs spendable by the wallet
  • Inputs spending wallet-owned outputs
  • Watch-only outputs, if applicable
  • Legitimate asset transactions involving wallet-owned asset outputs

A normal idle wallet should not accumulate large numbers of unrelated chain transactions.

Actual behavior

Regular Yerbas wallets can grow dramatically over time, even with little or no user transaction activity. This suggests that transactions unrelated to the wallet are being misclassified as wallet-relevant and persisted.

In the observed case, mapWallet.size() reached approximately 1.5 million entries and wallet.dat reached approximately 1.6 GB.

Proposed fix

The attached diff makes two defensive changes.

1. Initialize CAssetOutputEntry primitive fields

 struct CAssetOutputEntry
 {
-    txnouttype type;
+    txnouttype type = TX_NONSTANDARD;
     std::string assetName;
     CTxDestination destination;
-    CAmount nAmount;
+    CAmount nAmount = 0;
     std::string message;
-    int64_t expireTime;
-    int vout;
+    int64_t expireTime = 0;
+    int vout = -1;
 };

This prevents uninitialized primitive values from being read anywhere this struct is used.

2. Value-initialize assetData and remove redundant debit fallback in HasMyAssets()

 CAmount CWallet::HasMyAssets(const CTransaction& tx) const
 {
     for (const CTxIn& txin : tx.vin)
     {
-        CAssetOutputEntry assetData;
+        CAssetOutputEntry assetData{};
         GetDebit(txin, ISMINE_ALL, assetData);
         if (assetData.nAmount > 0)
-            return true;
+            return assetData.nAmount;
     }
 
-     return (GetDebit(tx, ISMINE_ALL) > 0);
+    return 0;
 }

The removed fallback is redundant because IsFromMe() already checks normal wallet debit before calling HasMyAssets():

return (GetDebit(tx, ISMINE_ALL) > 0 || HasMyAssets(tx));

After this change, HasMyAssets() only reports asset-specific debit and does not duplicate the normal debit test.

Risk assessment

This is a low-risk defensive fix.

It does not change consensus behavior, transaction validation, block validation, network behavior, or asset serialization.

It only changes wallet-side classification safety by:

  • Preventing uninitialized primitive reads
  • Ensuring asset metadata defaults to safe values
  • Avoiding redundant debit logic in HasMyAssets()

Existing bloated wallets will not shrink automatically. This fix is expected to stop or reduce false-positive wallet growth going forward.

Suggested testing

Fresh wallet idle/sync test

  1. Backup or move existing wallet.dat.
  2. Start Yerbas with a fresh wallet.
  3. Allow the node to sync or idle normally.
  4. Monitor:
grep "mapWallet.size" ~/.yerbascore/debug.log | tail -20
ls -lh ~/.yerbascore/wallet.dat

Expected result: mapWallet.size() should not grow rapidly during normal idle syncing unless the wallet actually receives or spends funds/assets.

Existing wallet startup test

With an existing large wallet, compare startup behavior before and after the patch:

grep -E "Verifying wallet|Loading wallet| wallet |mapWallet.size" ~/.yerbascore/debug.log | tail -80

Expected result: existing large wallets may still load slowly because the old unrelated transaction records remain, but new false-positive growth should stop.

Optional debug instrumentation

Temporarily instrument AddToWalletIfInvolvingMe() to confirm whether transactions are being added due to IsMine, normal debit, or asset debit:

bool fMine = IsMine(tx);
bool fDebit = GetDebit(tx, ISMINE_ALL) > 0;
CAmount nAssetDebit = HasMyAssets(tx);

if (!fExisted) {
    static std::atomic<int> nWalletGrowthLogs{0};
    if (nWalletGrowthLogs.fetch_add(1) < 200) {
        LogPrintf("YERBAS-WALLET-GROWTH: tx=%s mine=%d debit=%d assetDebit=%d vin=%u vout=%u height=%d pos=%d\n",
                  tx.GetHash().ToString(),
                  fMine,
                  fDebit,
                  nAssetDebit,
                  tx.vin.size(),
                  tx.vout.size(),
                  pIndex ? pIndex->nHeight : -1,
                  posInBlock);
    }
}

Expected result after this patch: unrelated chain transactions should not be added with assetDebit caused by uninitialized CAssetOutputEntry::nAmount.

Notes

This issue appears to be separate from automatic wallet backups. Auto-backups make startup slower when wallet.dat is already large, but the root wallet growth appears to come from incorrect transaction classification and persistence.

A follow-up improvement may be to reconsider the default createwalletbackups=10, because copying very large wallet files on every startup is expensive. However, that is secondary to this wallet bloat fix.

Possible Solution

--- /mnt/data/wallet.h.bak	2026-06-28 08:37:30.235643191 +0000
+++ /mnt/data/wallet.h	2026-06-28 08:37:32.966988972 +0000
@@ -204,13 +204,13 @@
 /** YERBAS ASSETS START */
 struct CAssetOutputEntry
 {
-    txnouttype type;
+    txnouttype type = TX_NONSTANDARD;
     std::string assetName;
     CTxDestination destination;
-    CAmount nAmount;
+    CAmount nAmount = 0;
     std::string message;
-    int64_t expireTime;
-    int vout;
+    int64_t expireTime = 0;
+    int vout = -1;
 };
 /** YERBAS ASSETS END */
 
--- /mnt/data/wallet.cpp.bak	2026-06-28 08:37:30.213351631 +0000
+++ /mnt/data/wallet.cpp	2026-06-28 08:37:32.971675669 +0000
@@ -1832,13 +1832,13 @@
 {
     for (const CTxIn& txin : tx.vin)
     {
-        CAssetOutputEntry assetData;
+        CAssetOutputEntry assetData{};
         GetDebit(txin, ISMINE_ALL, assetData);
         if (assetData.nAmount > 0)
-            return true;
+            return assetData.nAmount;
     }
 
-     return (GetDebit(tx, ISMINE_ALL) > 0);
+    return 0;
 }
 
 CAmount CWallet::GetDebit(const CTransaction& tx, const isminefilter& filter) const

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions