Add `ChainstateManager::ProcessTransaction` (refactoring, validation)

Host: jnewbery  -  PR author: jnewbery

The PR branch HEAD was f87e07c6f at the time of this review club meeting.


  • CChainState represents the node’s knowledge of a block chain, with an (optional) mempool that is synchronized with that chain. It was introduced in PR 10279 to eventually be an exposed “libconsensus”. Initially it was intended to clarify the internal interfaces within bitcoind.

  • ChainstateManager is the class in validation that manages one or more CChainState objects. It was introduced in PR 17737 in order to provide unified interface to the multiple chainstates required by the AssumeUTXO project. Now that a validation instance can contain multiple chainstates, it is eventually intended to be the main interface into validation from the rest of the code.

  • The ChainstateManager::ProcessNewBlock() and ChainstateManager::ProcessNewBlockHeaders() public functions were added in PR 18698 as high-level interfaces methods to validation. The callers (most notably net_processing) do not need to be aware of validation’s chainstates (or even whether there are multiple chainstates), and simply pass the block or block headers to be validated.

  • AcceptToMemoryPool() (often called ATMP) is a free function in the global namespace. Prior to this PR, it was the interface through which the rest of the code could call into validation to submit a transaction to the mempool. The calling code would need to provide the chainstate and mempool as arguments to the function, which breaks the abstraction that calling code doesn’t need to know about validation’s internals.

  • This PR adds a new ChainstateManager::ProcessTransaction() interface function, similar to ProcessNewBlock() and ProcessNewBlockHeaders(), which provides a high-level interface method for submitting transactions to be validated. The caller no longer needs to know about the chainstate or mempool inside validation.

  • The PR also removes the responsibility of calling the CTxMemPool::check() consistency check function from the caller (net_processing) and moves it to validation.


  1. Did you review the PR? Concept ACK, approach ACK, tested ACK, or NACK?

  2. What is cs_main?

  3. Which components currently call AcceptToMemoryPool()? List all the places that we’ll call ATMP.

  4. What does CTxMemPool::check() do? Whose responsibility is it to call that function?

  5. What does the bypass_limits argument in ATMP do? In which circumstances is ATMP call with bypass_limits set to true?

  6. One of the commit logs reads:

    “This logic is not necessary for memory pool transactions, as AcceptToMemoryPool already refuses previously-known transaction ids entirely.” refers to the logic at, which was later removed in commit 450cbb0944cd20a06ce806e6679a1f4c83c50db2.

    What was the change that removed that logic?

  7. The last commit is to Always call mempool.check() after processing a new transaction. Does that add any overhead to processing/relaying transactions? Why/why not?

Meeting Log

  117:00 <jnewbery> #startmeeting
  217:00 <jnewbery> Hi folks! Welcome to Bitcoin Core PR Review Club.
  317:00 <jnewbery> Feel free to say hi to let everyone know you're here.
  417:01 <stickies-v> hi everyone!
  517:01 <tr3xx> Hi guys!
  617:01 <michaelfolkson> hi
  717:01 <seaona> hi :)
  817:01 <ccdle12> hi everyone
  917:01 <jnewbery> phew. I was a bit worried that it was just me this week!
 1017:01 <nickbar86> Hi!
 1117:01 <jnewbery> Thanks everyone for coming to learn more about the Bitcoin protocol, Bitcoin Core, and the review process.
 1217:02 <jnewbery> Is anyone here for the first time?
 1317:02 <nickbar86> First time for me
 1417:02 <jnewbery> nickbar86: welcome! Feel free to jump in and ask questions at any point.
 1517:03 <jnewbery> We're all here to learn, so everyone should feel comfortable asking questions if anything is unclear
 1617:03 <Clint2> First timer here.
 1717:03 <jnewbery> Clint2: welcome :)
 1817:03 <Clint2> Thanks
 1917:03 <jnewbery> Notes and questions are in the normal place:
 2017:04 <jnewbery> ok, let's get into it. Who had a chance to review the PR / notes&questions? (y/n)
 2117:04 <stickies-v> y
 2217:04 <tr3xx> y
 2317:04 <jnewbery> no worries if it's an 'n' this week.
 2417:04 <seaona> y
 2517:04 <ccdle12> y
 2617:04 <michaelfolkson> 0.5y
 2717:05 <jnewbery> oh great. That's a lot of review. What did you all think? Concept ACK? approach ACK? code review ACK? NACK?
 2817:05 <Kaizen_Kintsugi> hello
 2917:05 <Kaizen_Kintsugi> y
 3017:05 <stickies-v> utACK
 3117:06 <jnewbery> anyone want to have a go at explaining the motivation?
 3217:06 <Kaizen_Kintsugi> ACK to the best of my ability. I'm quite interested in this as it seems to push assume UTXO forward.
 3317:06 <ccdle12> concept ACK for the code redesign
 3417:06 <jnewbery> Kaizen_Kintsugi: can you explain why you think it pushes assume UTXO forward?
 3517:07 <Kaizen_Kintsugi> afaik this is a refactor for cleaner abstraction, the chain state manager handles relevant responsibilities
 3617:07 <Kaizen_Kintsugi> I read it in the description
 3717:07 <lsilva_> The PR adapts the ATMP code to Assume UTXO.
 3817:07 <michaelfolkson> Concept ACK, Approach ACK. Just skimmed commits so far (commit messages are great)
 3917:07 <Kaizen_Kintsugi> but I dont know how or the specifics, i remember reading that AssumeUTXO was a large undertaking and a large optimization
 4017:09 <stickies-v> code outside of validation.cpp should not need to care about chainstate management, so through ChainstateManager::ProcessTransaction those callers can now add transactions without needing a reference to mempool or chaintip
 4117:09 <jnewbery> Kaizen_Kintsugi: AssumeUTXO is a huge project that touches lots of parts of validation and init. This PR wasn't motivated specifically by AssumeUTXO, but I'd argue that clarifying interfaces should help add new features if done right
 4217:09 <jnewbery> stickies-v: exactly right!
 4317:09 <jnewbery> let's get into more specifics
 4417:09 <Kaizen_Kintsugi> jnewbery ty, thx stickies
 4517:09 <jnewbery> What is cs_main?
 4617:10 <Kaizen_Kintsugi> that is a thread lock
 4717:10 <Kaizen_Kintsugi> and that part baffles me how the locks work in this
 4817:10 <lsilva_> A mutex to protect critical data ?
 4917:10 <stickies-v> a mutex - a bit fuzzy on the details but i think it tries to ensure validation consistency by only allowing a single thread at the same time to modify crucial components like chainstate etc?
 5017:10 <seaona> it is a mutex class, and it is locked in several places of the codebase, specially around wallet sync and mempool processes ..
 5117:10 <ccdle12> the main mutex in bitcoin, I think it guards mainly chainstate and validation
 5217:11 <jnewbery> Kaizen_Kintsugi: being pedantic for a second, cs_main is a *mutex*, and that mutex is *locked* in code paths, which excludes other threads from reading/writing data at the same time
 5317:11 <jnewbery> lots of great answers!
 5417:11 <Kaizen_Kintsugi> Aye, I just refreshed my understanding of what a mutex is
 5517:12 <jnewbery> anyone know why it's called cs_main ?
 5617:12 <ccdle12> it used to live in the main.cpp file when it existed?
 5717:12 <lsilva_> Most of the protected code was in main.cpp ?
 5817:12 <stickies-v> it was used in satoshi's main.cpp which used to be much bigger than it is now
 5917:13 <jnewbery> yes, that's where the "main" part comes from. How about "cs_" ?
 6017:13 <sipa> stickies-v: not just bigger; it existed :)
 6117:13 <jnewbery> a bunch of the mutexes in bitcoin core are called "cs_". Why?
 6217:13 <ccdle12> chain state? :)
 6317:13 <seaona> chain state?
 6417:13 <lsilva_> Critical Section ?
 6517:13 <jnewbery> haha, chain state is a good guess, but it's actually Critical Section. Well done lsilva_.
 6617:13 <sipa> (main.cpp was split into validation.cpp and net_processing.cpp at some point, after many other things were already moved elsewhere)
 6717:14 <stickies-v> sipa hah hadn't even noticed that it was gone entirely, ty!
 6817:14 <jnewbery> sipa: right, that was done in
 6917:15 <tr3xx> lol @ TheBlueMatt's initial comment on that PR
 7017:15 <Kaizen_Kintsugi> is there a visual map of this stuff to help me wrap my head around this?
 7117:15 <michaelfolkson> Were all PRs introduced with song lyrics in 2016 lol
 7217:15 <jnewbery> The mutexes from satoshi's time and for some time after were called "cs_", presumably because satoshi was developing on windows, and CriticalSection is a windows concurrency class:
 7317:16 <jnewbery> Kaizen_Kintsugi: not that I'm aware of, unfortunately. It's mostly in people's heads
 7417:16 <ccdle12> Kaizen_Kintsugi: I'm not sure, but this presetnation by jamesob helped me alot:
 7517:17 <jnewbery> so to summarize, cs_main is the big lock in Bitcoin Core. It _should_ be predominantly to protect chainstate and validation, but there's also a lot of state in net_processing and other places that are guarded by cs_main
 7617:18 <jnewbery> sorry cs_main is the big *mutex. I'll try to be more precise in my language
 7717:18 <Kaizen_Kintsugi> ccdle12: this is amazing ty
 7817:18 <jnewbery> Breaking that mutex up so that not everything is guarded under the same mutex is a long-term goal of many contributors
 7917:19 <jnewbery> I'll ask the next question, but if anything so far is unclear, don't hesitate to go back and ask questions about it
 8017:19 <jnewbery> Which components currently call AcceptToMemoryPool()? List all the places that we’ll call ATMP.
 8117:19 <stickies-v> src/bench/block_assemble.cpp; src/net_processing.cpp; src/node/transaction.cpp; src/rpc/rawtransaction.cpp; src/test/fuzz/tx_pool.cpp; src/test/util/setup_common.cpp; src/validation.cpp
 8217:19 <lsilva_>
 8317:20 <seaona> CChainState::MaybeUpdateMempoolForReorg()
 8417:20 <seaona> MempoolAcceptResult ChainstateManager::ProcessTransaction()
 8517:21 <jnewbery> Ah, I see you know how to grep!
 8617:21 <jnewbery> ok, in words, which are the important call sites
 8717:21 <Kaizen_Kintsugi> stickies-v: how did you do that grep?
 8817:21 <glozow> (1) loading mempool.dat from disk, (2) receiving a new tx on p2p, (3) adding transactions from disconnected blocks (4) from clients like wallet and rpc via BroadcastTransaction
 8917:21 <jnewbery> glozow: great answer!
 9017:21 <stickies-v> Kaizen_Kintsugi I cheated and used VSCode's find all references...
 9117:22 <jnewbery> I'll add (4b) directly from rpc for `testmempoolaccept`
 9217:22 <jnewbery> but otherwise I think that's exhaustive, excludings tests/fuzzers/benches
 9317:23 <jnewbery> everyone happy with that? Or anyone have questions?
 9417:23 <stickies-v> I think it's also called when doing package validation? But that's not merged yet I think
 9517:23 <Kaizen_Kintsugi> 1 breaks my intuitive understanding, why is there a mempool dat? I thought that was just in memory
 9617:24 <Kaizen_Kintsugi> mempool is written to the db for a reason?
 9717:24 <glozow> package validation does not call ATMP, no
 9817:24 <lsilva_> To persist mempool when the node restarts
 9917:24 <jnewbery> follow up question. Out of (1), (2), (3), (4) and (4b), which are calls from inside validation, and which are from external client code (where by client I mean other components within bitcoin core, not external programs)
10017:24 <Kaizen_Kintsugi> oh cool cool that makes sense
10117:25 <Kaizen_Kintsugi> 1 -validate, 2-validate- 3-external? 4=external
10217:25 <Kaizen_Kintsugi> maybe 2 is external
10317:25 <Kaizen_Kintsugi> shit
10417:25 <lsilva_> 1 - validation 2 - validation 3 - validation 4 - external
10517:26 <jnewbery> stickies-v: yes, the same logic is involved in package validation, although that doesn't actually use the ATMP function. Package validation would be from the same call sites as (2) and (4) when it gets merged
10617:27 <jnewbery> yeah, mempool.dat is only written on shutdown and read on startup usually. I think there may be an rpc to dump it manually but I'm not 100% sure about that
10717:27 <jnewbery> ok so 1 and 3 are definitely from within validation
10817:27 <lsilva_> Yes, savemempool RPC dumps it manually
10917:28 <jnewbery> here's 1:
11017:28 <jnewbery> (it's actually calling AcceptToMemoryPoolWithTime() but whatever)
11117:29 <jnewbery> and here's 3:
11217:29 <lsilva_> The (2) is originally in net_processing
11317:29 <jnewbery> (2) is being called from net_processing
11417:29 <lsilva_> Before this PR, I mean
11517:29 <jnewbery> lsilva_: yes, exactly right!
11617:30 <jnewbery> I'd call net_processing a client of validation. It calls into various functions to either provide new data for validation to validate, or to to request what the current state is
11717:30 <jnewbery> make sense?
11817:30 <jnewbery> and (4) is here:
11917:30 <jnewbery> that's the interface through which the rpc and wallet clients submit transactions to the mempool
12017:31 <lsilva_> Yes. net_processing calls validation layer to validate incoming data.
12117:31 <jnewbery> lsilva_: yep!
12217:31 <jnewbery> ok, onwards. What does CTxMemPool::check() do? Whose responsibility is it to call that function?
12317:32 <stickies-v> CTxMemPool::check() verifies that all the transactions in the mempool are consistent, e.g. they don't spend the same inputs twice. Since it's by default not used on mainnet, I think this is mostly a dev/debug feature?
12417:33 <seaona> it asserts that total tx sizes, fees.. in the mempool are correct
12517:33 <lsilva_> It validates the mempool. It was introduced in the PR #2876.
12617:33 <jnewbery> very good!
12717:34 <lsilva_> Net Processing layer (ProcessOrphanTx and ProcessMessage (NetMsgType::TX) calls this function.
12817:34 <stickies-v> Previously, it was the responsibility of anyone interacting with the mempool. Since this PR, ChainstateManager::ProcessTransaction takes care of this automatically.
12917:34 <jnewbery> exactly right. Lots of good answers today. Everyone's done their homework :)
13017:35 <lsilva_> This PR changes the responsibility from net processing to Validation layer (ProcessTransaction and ActivateBestChainStep).
13117:35 <jnewbery> stickies-v's point about it being off by default is important. If that wasn't the case, then this PR might be a performance pessimization. But since check() is generally only used for testing, it's not a problem.
13217:36 <jnewbery> Next question. What does the bypass_limits argument in ATMP do? In which circumstances is ATMP call with bypass_limits set to true?
13317:36 <pg156> If `bypass_limits` is true:
13417:36 <pg156> - the mempool size is no longer limited to the default 300MB.
13517:36 <pg156> - the fee rate of a package doesn't need to be above a threshold
13617:36 <seaona> bypass_limits means that we won't force the mempool fee and capacity limits. I don't know in which circumstances would be used
13717:37 <pg156> I can see `bypass_limits` is set to true here:
13817:37 <pg156>
13917:37 <pg156> but don't understand exactly why
14017:37 <jnewbery> pg156: Almost! It's the feerate of the individual transaction that doesn't need to be above a the mempool's min feerate
14117:37 <lsilva_> When true, bypass_limits doesn't enforce mempool fee and capacity limits.
14217:37 <jnewbery> rather than a package (the mempool doesn't currently accept packages atomically)
14317:38 <jnewbery> (glozow is working on fixing that!)
14417:38 <jnewbery> There's actually only one place where bypass_limits is set to true, and it's in MaybeUpdateMempoolForReorg()
14517:39 <jnewbery> What is that function doing, and why do we want to bypass those limits there? Any guesses?
14617:39 <seaona> for testing?
14717:39 <Kaizen_Kintsugi> is this when a new chain state is chosen
14817:40 <jnewbery> nope and nope. Keep guessing :)
14917:40 <Kaizen_Kintsugi> and transactions from orphaned blocks need to be put back into the mempool?
15017:40 <pg156> When a node is disconnected?
15117:40 <jnewbery> Kaizen_Kintsugi pg156: yes!
15217:40 <lsilva_> To remove or re-add transactions from disconnected blocks ?
15317:40 <jnewbery> right!
15417:41 <pg156> But what exactly happens when a node is disconnected and how it rebuilds the mempool? I am confused.
15517:42 <jnewbery> during a reorg, we'll disconnect one or more blocks and then connect one or more competing blocks. The blocks that are disconnected will probably have a bunch of transactions in them.
15617:42 <Kaizen_Kintsugi> I imagine a lot of the transactions will be similar
15717:42 <jnewbery> Now usually, we'd expect _most_ of those transactions to appear in the new blocks that are connected
15817:42 <jnewbery> but there may be some that aren't, and it'd be a shame to lose those, so we try to put them back in our mempool
15917:43 <stickies-v> so hypothetically, a very large reorg could crash low resource nodes because they run out of memory?
16017:43 <jnewbery> stickies-v: that has been a potential problem in the past. We try to limit the worst case memory usage
16117:44 <Kaizen_Kintsugi> yea it seems like potentially a giant blob of transactions could hit the mempool really quick
16217:44 <jnewbery> here's the comment for the class that handles this:
16317:44 <Kaizen_Kintsugi> i would assume older transactions would push out new transactions from the mempool if a node cant hold them?
16417:44 <glozow> not just a shame, pretty sure that makes reorgs more dangerous if the disconnected blocks’ transactions are lost
16517:45 <glozow> LimitMempoolSize expires transactions more than 2 weeks old first, then evicts by descendant feerate
16617:46 <jnewbery> and here's where we limit the size of that disconnectpool to ensure that a large reorg doesn't blow up our memory usage:
16717:47 <Kaizen_Kintsugi> oh neat, so there is a disconnect pool inside the mempool if I understand correctly, there is a 'partition' for this stuff to happen?
16817:47 <glozow> pg156: not when a node is disconnected, when a block is disconnected.
16917:48 <pg156> glozow: right, i can see that now
17017:48 <jnewbery> Kaizen_Kintsugi: It's not inside the mempool. It's a separate object that is created during the disconnect/reconnect sequence
17117:48 <jnewbery> short-lived
17217:48 <Kaizen_Kintsugi> ah okay okay that makes sense
17317:48 <lsilva_> What is exact the "disconnected block" concept ? Block that will be discarded in reorg ?
17417:48 <Kaizen_Kintsugi> its only created when we need it
17517:49 <jnewbery> lsilva_: "connecting" a block is applying its transactions to the UTXO set (removing outputs that are spent by txs in the block and adding new outputs that are created) and updating the chainstate
17617:49 <stickies-v> hmm so during reorg we ignore the default mempool size of 300MB but instead use a (default) MAX_DISCONNECTED_TX_POOL_SIZE of 20MB of disconnected transactions? Then doesn't it make more sense to just keep the 300MB mempool limit?
17717:50 <jnewbery> "disconnecting" a block is the reverse: removing the UTXOs that were created in the block and re-adding those that were spent in the block (which were stored in the 'undo data' for that block on disk)
17817:50 <jnewbery> so during a re-org we disconnect one or more block from the tip of the block chain, and then connect the competing blocks
17917:51 <Kaizen_Kintsugi> so disconnect -> undo utxo set -> add new block
18017:51 <pg156> so for those transactions from disconnected blocks (but not in new connected blocks), are they added to mempool while bypassing the limits? If so, what if the fee rate of them is too low?
18117:51 <lsilva_> jnewbery Thanks !!
18217:51 <stickies-v> although I suppose pushing out 20MB of transactions from the mempool (assuming it's full) is much more costly than just temporarily allocating an extra 20MB
18317:51 <jnewbery> stickies-v: that's a good question! I'm not entirely clear about why we should ignore the 300MB limit when we're putting transactions into the mempool during a re-org
18417:51 <Kaizen_Kintsugi> is it incase the reorg is really big?
18517:52 <Kaizen_Kintsugi> like 400-500 blocks
18617:52 <Kaizen_Kintsugi> which kinda sounds insane
18717:52 <Kaizen_Kintsugi> and near infeasable
18817:52 <glozow> that’s 3 days worth of blocks
18917:52 <Kaizen_Kintsugi> yea
19017:53 <jnewbery> I think it may be due to ancestor/descendant chains. Since the transactions can only be added sequentially, we can't look at the ancestor/descendant feerates when trying to add the transactions back, so it's perhaps better to add everything, and then limit the mempool size if necessary
19117:53 <Kaizen_Kintsugi> that sounds like a complication with child pays for parent or something?
19217:54 <jnewbery> No, I don't think it's anything to do with a super-large re-org. In that case we'd lose most of the transactions, since the MAX_DISCONNECTED_TX_POOL_SIZE is limited to 20MB
19317:54 <jnewbery> to be honest, I haven't figured out exactly what the rationale is, but this PR doesn't change the behaviour for bypass_limits at all so that's ok :)
19417:55 <jnewbery> Question 6: One of the commit logs reads: [...] What was the change that removed that logic?
19517:56 <jnewbery> I'm not going to paste the entire code comment into the chat. You can read it here:
19617:57 <Kaizen_Kintsugi> looks like it removes a redundancy
19717:57 <lsilva_> Pruned blocks ?
19817:57 <lsilva_> ?
19917:57 <glozow> well if you were at 290mb and disconnectpool is 20mb, your only choices are (1) cut down to 280mb and then accept or (2) allow going to 310mb and then cut right? the former means you might end up with lower fee transactions
20017:58 <glozow> i suppose you could statically analyze the disconnect pool, split them into packages, and then submit
20117:58 <jnewbery> glozow: Yes, I agree!
20217:58 <glozow> that’d be a use case for no-topology-restrictions package submission
20317:59 <jnewbery> lsilva_: not pruned blocks, but pruning spent TXOs from our storage
20417:59 <jnewbery> ultraprune was a really important change. If you're interested, you can hear Pieter talking about it here:
20517:59 <jnewbery> ok, that's time. I gotta run. Thanks everyone!
20617:59 <jnewbery> #endmeeting
20717:59 <Kaizen_Kintsugi> Thanks! Learning this is so awesome
20817:59 <svav> Thanks
20917:59 <lsilva_> Thanks
21017:59 <Kaizen_Kintsugi> I need to understand UTXO set better
21118:00 <glozow> thanks jnewbery
21218:00 <tr3xx> Thanks for a great session jnewberry!
21318:00 <pg156> Thank you John and everyone!
21418:00 <ccdle12> thanks!
21518:00 <Kaizen_Kintsugi> from what I understand it is a set of all the unspent transactions in the entire blockchain
21618:00 <stickies-v> thanks everyone!
21718:00 <BlueMoon> Thanks!!
21818:00 <Kaizen_Kintsugi> and is it a part of validation? chain state? where does that thing live?
21918:01 <Kaizen_Kintsugi> *unpsent transaction outputs*
22018:01 <seaona> thank you all!
22118:01 <michaelfolkson> Kaizen_Kintsugi: You can think in terms of blocks or in terms of UTXOs and spent TXOs. Different data structures
22218:02 <michaelfolkson> UTXO set is updated every time there is a new block
22318:02 <lsilva_> Yes, it is part of validation. You can check class CChainState in validation.h
22418:20 <michaelfolkson> Some of these Doxygen graphs are fun
22518:27 <Kaizen_Kintsugi> Gah
22618:27 <Kaizen_Kintsugi> that's some spagetti but really helpful for me
22718:28 <Kaizen_Kintsugi> from my reading so far, CCoinsView is the UTXO set?
22818:28 <Kaizen_Kintsugi> or where it is kept?
22918:38 <sipa> ccoinview is the abstract interface for things that represent *a* utxo set
23018:39 <sipa> there are multiple of those; the one on disk, the cache in memory of the one one disk, the one implied by the mempool, ...
23118:54 <Kaizen_Kintsugi> thanks,
23219:18 <michaelfolkson> sipa: What is the UTXO set implied by the mempool? There's no predictive element of what will be in the next mined block is there? So the mempool is treated by the codebase as irrelevant to any future changes to the UTXO set?
23319:22 <sipa> michaelfolkson: for the mempool utxo set, the mempool is counted
23419:22 <sipa> for the chain one, it isn not
23519:23 <sipa> the mempool utxo set is effectively: take the tip of the active chain, and pretend every mempool transaction were mined
23619:23 <michaelfolkson> A UTXO set assuming all the transactions in the mempool are applied to the current UTXO set?
23719:23 <sipa> right
23819:23 <sipa> tyat's what new mempool tx are validated against
23919:24 <sipa> because those can spend outputs that only exist in the mempool
24019:25 <michaelfolkson> Wow, did not know that. Presumably the UTXO set is updated every time a tx is booted from the mempool or a RBF transaction comes in?
24119:26 <sipa> there is nothing "updated"
24219:26 <sipa> the mempool just implies a utxo set
24319:26 <sipa> it's an abstract concept; there is no materialized database
24419:26 <michaelfolkson> Oh gotcha. I thought you meant it was stored and updated with every change to the mempool
24519:26 * michaelfolkson sweats
24619:27 <michaelfolkson> Ok yeah you said "the one implied by the mempool". Sorry, my misunderstanding
24722:40 <Kaizen_Kintsugi> okay that makes sense now, the mempool is a utxo set implicitly.