Calculating a balance

For balance calculation we iterate mapWallet and add values to a Balance struct.

struct Balance {
    CAmount m_mine_trusted{0};           //!< Trusted, at depth=GetBalance.min_depth or more
    CAmount m_mine_untrusted_pending{0}; //!< Untrusted, but in mempool (pending)
    CAmount m_mine_immature{0};          //!< Immature coinbases in the main chain
    CAmount m_watchonly_trusted{0};
    CAmount m_watchonly_untrusted_pending{0};
    CAmount m_watchonly_immature{0};
};

We do some caching during iteration so that we avoid re-calculating the same values for multiple transactions.

Wallet balance terminology
debit

amount out

credit

amount in

availableCredit

amount available to send out (not dirty or immature)

Calculating the above requires using TxSpends and IsMine.

When a new transaction involving the wallet takes place, really what happens is that it’s marked as DIRTY, which deletes the cached entry for the parent transaction. This means that the next time GetBalance() is called, debit is recalculated correctly. This Bitcoin Core PR review club goes into more detail on coins being marked as DIRTY and FRESH in the cache.

TxSpends is calculated by looking at the outpoints in the transaction itself.

COutput vs COutPoint
COutPoint

a pair of txid : index, useful when you want to know which UTXO an input spends.

COutput

created for coin selection and contains the entire previous UTXO (script, amount), along with helpers for calculating fees and effective value.

COutputs are ephemeral — we create them, perform another operation with them and discard them. They are stored in availableCoins which is recreated when calling functions such as GetAvailableBalance(), ListCoins() and CreateTransactionInternal().

In a spending transaction all inputs have their corresponding OutPoints, and we map these to spending transactions in TxSpends.

We assume anything (i.e. transactions) that reach the wallet have already been validated by the node and we therefore blindly assume that it is valid in wallet code.

If a transaction is our own we check for validity with testMempoolAccept before submitting to the P2P network.

IsMine

For DSPKM running IsMine() is really simple: descriptors generate a list of ScriptPubKeys, and, if the SPK we are interested in is in the list, then it’s ours.

IsMine returns an enum. This is used as a return value, a filter and set of flags simultaneously. There is more background on the general IsMine semantics in the v0.21.0 release notes.

LSPKM can have watch-only and spendable flags set at the same time, but DSPKM is either or, because descriptor wallets do not allow mixtures of spendable and watch-only keys in the same SPKM. Because Legacy wallets are all key-based, we will need to see if a script could have been generated by one of our keys; what type of script it is; and if we have a (private) key for it.

For Legacy watch-only wallets we simply check "do we have this script stored as a script?" (where CScripts in the database are our watch-only scripts)". If we don’t have a CKey for a script but it exists in mapScripts then it’s implicitly watch-only.

A problem with this current method of IsMine for legacy wallets is that it’s tough to figure out what your wallet considers "Mine" — it’s probably a finite set, but maybe not…​

Another consideration is that the LSPKM IsMine includes P2PK outputs — which don’t have addresses! This un-enumerability can be an issue in migration of Legacy to Descriptor wallets.

There is also the possibility that someone can mutate address to different address type and you will still see it as IsMine. E.g. mutate P2PK into P2PKH address and wallet will still detect.

With descriptors we only look for scripts explicitly. With descriptor wallets IsMine might not recognise script hashes from scripts, because it was not told to watch for them and consider them as belonging to it.

We use the IsMine filters in many places, primarily to distinguish between spendable and watch-only:

IsMine::All

spendable and watch-only (use for legacy wallet)

IsMine::Used

not used by IsMine, but instead used as a filter for tracking when addresses have been reused.

PR 19602 enables migration of legacy wallets → descriptor wallets from Bitcoin Core version 24.0. Although legacy wallets are now effectively end of life it’s still relevant to have documentation for legacy wallets.

See the section on how wallets determine whether transactions belong to them using the enum for more in-depth information.

Conflict tracking

Conflict tracking is related to changing the state as the mempool tells us about conflicting transactions.

mapTxSpends is a multimap which permits having the same COutPoint mapping to two transactions. (i.e. two transactions spending the same input) This is how we can tell if things are conflicted: look up an outpoint and check to see how many transactions are there, if > 1 then we know that there was a conflict.

If there is a conflict we can look up the wallet transaction and see what state it’s in, and we can be sure about whether it is currently or previously conflicted.

Conflict tracking is particularly relevant for coin selection…​