The MiniMinerconstructor
accepts a mempool reference and a list of outpoints the wallet might be interested in spending.
Given an outpoint, it may be one of four possible types:
a confirmed UTXO
a UTXO created by a mempool transaction and has not been spent yet
a UTXO created by a mempool transaction and has already been spent by another mempool
transaction
a UTXO that does not exist in mempool or chainstate (perhaps not yet submitted to mempool)
How does the MiniMiner constructor detect and handle each case?
MiniMiner builds a descendant_set_by_txid cache, but not an ancestor_set_by_txid cache.
Instead, it
calculates
ancestor sets on the fly. Do you think this approach makes sense? Why or why not?
One potential approach for constructing the block is to define a custom ancestor score comparator
for MockMempoolEntry (or even just reuse
CompareTxMemPoolEntryByAncestorFee from txmempool like the BlockAssemblerdoes),
and then iterate through a list of entries sorted by ancestor score. Why would this approach work for
BlockAssembler but not for MiniMiner?
This functionality is only used by the wallet. Instead of adding CalculateBumpFees to the chain
interface,
should we just add it as a utility function in the wallet?
Describe the approach taken in the āBump unconfirmed parent txs to target feerateā
commit.
What test cases are included in wallet_spend_unconfirmed.py added in the same
commit?
Can you think of any other test cases to add?
Two coin selection results may require different fees for bumping ancestors. How does
the wallet choose which one to use? (Hint: can you identify how bump fees come into play in
GetSelectionWaste())?
How does the PR handle spending unconfirmed UTXOs with overlapping ancestry? (Hint: what does the code
here do)?
<glozow> We looked at concept and approach last week. This week, we'll go a bit deeper into the implementation. If you weren't here last week, that's totally fine, notes and logs are here: https://bitcoincore.reviews/26152
<LarryRuane> when the wallet generates a list of spendable outputs to hand to coin selection, it reduces the effective values of mempool transaction outputs, if needed to bring these transactions effective fee rates up to the desired fee rate
<glozow> The MiniMiner constructor accepts a mempool reference and a list of outpoints the wallet might be interested in spending. Given an outpoint, what are the 4 possible states?
<LarryRuane> it could be a confirmed UTXO, or unconfirmed (in the mempool), or an outpoint that is already being spent by an existing transaction in the mempool, or an outpoint that we've never heard of
<LarryRuane> a confirmed UTXO is easy, we don't need to "bump its fee" (really bumping our own fee to provide miner incentive to include us and all of our unconfirmed ancestors)
<LarryRuane> an unconfirmed UTXO, if the transaction providing it has a lower feerate than us, then we want to reduce its effective value (aka "bump" its effective feerate) -- this is the main effect of this PR i would say
<glozow> next question. MiniMiner builds a descendant_set_by_txid cache, but not an ancestor_set_by_txid cache. Instead, it calculates ancestor sets on the fly. Do you think this approach makes sense? Why or why not?
<LarryRuane> Makes sense because ancestor sets can reduce as we add transactions to the block template (those no longer need to be fee-bumped, we sort of pretend they're already mined, no longer our ancestors) -- unsure about this
<murchandamus> Descendants can never be included before the transaction itself is included, so they're stable throughout th eblock building until the transaction itself is included
<glozow> no, we always need the descendants when building the block template. when a transaction is "added" to the block, we need to update its descendants' ancestor sets
<glozow> the next question has a similar theme. One potential approach for constructing the block is to define a custom ancestor score comparator for MockMempoolEntry (or even just reuse CompareTxMemPoolEntryByAncestorFee from txmempool like the BlockAssembler does), and then iterate through a list of entries sorted by ancestor score. Why would this approach work for BlockAssembler but not for MiniMiner?
<glozow> No need to apologize! We are updating the cached ancestor information for each descendant of a transaction "included in the block." In `BlockAssembler`, we do so by updating `mapModifiedTx` and not by writing to `mapTx` itself (so we can iterate in ancestor score order without worrying about it changing). In MiniMiner, we're not working with mapTx but a map of `MockMempoolEntry`s, which we modify directly.
<glozow> So actually, the answer is the same as the answer to the last question. We can't just iterate in ancestor feerate order, because the ancestors change as we're going.
<LarryRuane> I noticed that we access the real mempool only in the constructor -- we build our own "private" data structures (the MiniMiner class) from the real mempool. Is this for performance reasons mostly? We don't want to hold the real mempool lock for that long?
<glozow> maybe poor connection again? In `BlockAssembler`, we do so by updating `mapModifiedTx` and not by writing to `mapTx` itself (so we can iterate in ancestor score order without worrying about it changing). In MiniMiner, we're not working with mapTx but a map of `MockMempoolEntry`s, which we modify directly.
<glozow> One could say it's not good to let go of the lock since there's a possibility the mempool contents can change while we're constructing the transaction, but haven't yet heard whether that's really problematic
<LarryRuane> let's say (unlikely) .... yes i was just going there ... suppose a new block arrives while we're constructing this tx ... is there some way to start over again?
<glozow> The argument *for* not holding the lock is yeah, theoretically the node should be able to continue doing it's thing while the wallet spends time calculating its bumpfees and whatnot
<LarryRuane> well also (i think this maybe what you were getting at), new transactions could arrive (they would only always be new descendants obviously)
<LarryRuane> "The argument *for* not holding the lock..." I like that design decision .. there will always be the possibility that things change *just after* constructing the tx anyway
<glozow> LarryRuane: I'm not exactly sure what happens in that case. My imagination is that the tx creation fails and the user needs to manually try again. It's also possible that cs_main is held the whole time so you can't accept a block while a tx is being created haha
<murchandamus> So, what's happening here is that we take the information we've learned about our UTXOs and change their effective values to reflect the amount of fees that go towards bumping their ancestry to the target feerate
<glozow> murchandamus: thank you for the great explanation! I'll follow up with the last question: How does the PR handle spending unconfirmed UTXOs with overlapping ancestry?
<LarryRuane> oh right .. and that's why it's tricky because you don't know which of these UTXOs (that have the common ancestry) coin selection will choose!
<murchandamus> LarryRuane: We run multiple different selection attempts in parallel now: via the different UTXO types, and via the different algorithms