Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rpc: Optimize serialization disk space of dumptxoutset #26045

Closed

Conversation

aureleoules
Copy link
Member

@aureleoules aureleoules commented Sep 8, 2022

This is an attempt to implement #25675.

I was able to reduce the serialized utxo set from 5GB to 4.1GB on mainnet.

Closes #25675.

@aureleoules aureleoules marked this pull request as draft September 8, 2022 08:37
@aureleoules aureleoules force-pushed the 2022-09-dumputxoset-compact branch 4 times, most recently from a00b91c to 80f0b0f Compare September 8, 2022 10:17
@aureleoules aureleoules marked this pull request as ready for review September 8, 2022 10:47
@aureleoules aureleoules force-pushed the 2022-09-dumputxoset-compact branch 3 times, most recently from 3822c2f to 8b0e9e9 Compare September 8, 2022 13:08
@DrahtBot
Copy link
Contributor

DrahtBot commented Sep 8, 2022

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage

For detailed information about the code coverage, see the test coverage report.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK jamesob, pablomartin4btc, jaonoctus, Sjors
Stale ACK TheCharlatan

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #29307 (util: check for errors after close and read in AutoFile by vasild)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

Copy link

@amovfx amovfx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have the skill set to approve this code but I can test and verify. I ran this on testnet and can confirm a smaller output file.

without:
{ "coins_written": 27908372, "base_hash": "0000000000000008f07ed39d6d03c19ee7346bc15b6a516cdda8402b6244b828", "base_height": 2345886, "path": "/Users/{$User}/Library/Application Support/Bitcoin/testnet3/./txoutset.txt", "txoutset_hash": "026617308d218e57fb43f02baa644134f5000594a1eea06b02cc9d02959d4d9b", "nchaintx": 63601314 }
Size of 3 473 536

With this optimization:
{ "coins_written": 27908372, "base_hash": "0000000000000008f07ed39d6d03c19ee7346bc15b6a516cdda8402b6244b828", "base_height": 2345886, "path": "/Users/${User}/Library/Application Support/Bitcoin/testnet3/txoutset.optimized.txt", "txoutset_hash": "026617308d218e57fb43f02baa644134f5000594a1eea06b02cc9d02959d4d9b", "nchaintx": 63601314 }
Size of 2 457 728

Looks like a win to me.

@luke-jr
Copy link
Member

luke-jr commented Sep 10, 2022

Not sure it's worth it to save 20%. Presumably generic compressors could do better anyway?

@aureleoules
Copy link
Member Author

Not sure it's worth it to save 20%. Presumably generic compressors could do better anyway?

Yes I agree but at least with this implementation the utxo set is still readable as is and doesn't need decompression.

@TheCharlatan
Copy link
Contributor

The main downside of this implementation is that the entire UTXO set must be loaded in RAM (in mapCoins) before being written to disk. So running dumptxoutset will consome a lot of RAM on mainnet since the utxo set is large.
Not sure how to improve this.

The keys in leveldb are guaranteed to be lexicographically sorted. You just need to cache the last txid and flush the txid and outpoint tuples once the next one is reached in the database iterator. I pushed a dirty proof of concept here and it produced the same output file as your current implementation.

Otherwise Concept ACK. For a file that might be lugged over a network or other data carrier this space improvement is nice. Users can still apply their own compression on top of it. I also like that this provides additional structure to the data.

@RCasatta
Copy link

Yeah as I specified in the issue and as @TheCharlatan wrote more ram shouldn't be needed because txid are iterated sorted from leveldb.

I was also wondering why the bitcoin-cli savetxoutset API (same goes for savemempool) requires to specify a filename instead of writing to stdout that would offer better composition while maintaining the possibility to write to file with >utxo.bin

@TheCharlatan
Copy link
Contributor

Re #26045 (comment)

I was also wondering why the bitcoin-cli savetxoutset API (same goes for savemempool) requires to specify a filename instead of writing to stdout that would offer better composition while maintaining the possibility to write to file with >utxo.bin

Writing to stdout would mean that the data would have to be carried over the rpc connection, right?

Copy link
Contributor

@TheCharlatan TheCharlatan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for picking this up again.

These are just patches for fixing up my initial code, I also put the entire diff here. The rest looks good to me, I tested dumptxoutset extensively on mainnet. The runtime performance of this branch is very similar to the base.

The following can be ignored in the context of this PR, but I still want to take note:

During review I noticed that the execution time of the command greatly relies on getting the utxo stats (getting the stats and writing to the file each take about 450s on my m.2 machine). I don't follow why these stats need to be collected by iterating through the entire set again, is there no better way to prepend the meta data? A count of the written entries as well as a hash of them can be provided by the same iteration loop that is used to write the file. Further, the m_coins_count in the SnapshotMetadata is populated with tip->nChainTx, which I am not sure always corresponds to the actual number of coins written, but is used to read that exact number of coins from the file when loading the snapshot.

src/rpc/blockchain.cpp Outdated Show resolved Hide resolved
src/rpc/blockchain.cpp Outdated Show resolved Hide resolved
src/rpc/blockchain.cpp Outdated Show resolved Hide resolved
@RCasatta
Copy link

Writing to stdout would mean that the data would have to be carried over the rpc connection, right?

Yes, would that be a problem?

@sipa
Copy link
Member

sipa commented Apr 30, 2023

There is no way we can currently send gigabytes of data as an RPC response; both the server and client likely buffer the result and would OOM.

@aureleoules
Copy link
Member Author

Thanks for the patch @TheCharlatan, I applied it.

@TheCharlatan
Copy link
Contributor

I ran some more numbers, dumping a mainnet snapshot on height 787535:

Filename Size (bytes)
this_branch.dat 4'697'836'541
this_branch.tar.xz 3'899'452'268
master.dat 5'718'308'597
master.tar.xz 3'925'608'176

So even in the compressed form the encoding saves some megabytes.

@TheCharlatan
Copy link
Contributor

ACK 7acfc2a

Since we are changing the format of dumptxoutset anyway here in a non backwards compatible fashion, I'd like to suggest moving the metadata to the end of the file. This would take care of the double iteration described in #26045 (review). In my eyes, this does not significantly hurt the integrity of the file. If an exception occurs during writing, only the temporary file remains. Other common file formats also embed metadata at the end.

@aureleoules
Copy link
Member Author

@aureleoules wen rebase? :-)

🫡

@aureleoules aureleoules marked this pull request as draft December 18, 2023 10:45
@aureleoules
Copy link
Member Author

Rebased

I had to slightly change the tests in feature_assumeutxo.py because I changed the encoding format of the dump. I added 2 bytes to the offset because of the new size (2 bytes) field.

@aureleoules aureleoules marked this pull request as ready for review December 18, 2023 11:01
@Sjors
Copy link
Member

Sjors commented Dec 22, 2023

Did some testing with f5d2014.

Using contrib/devtools/utxo_snapshot.sh on signet I get the same txoutset_hash. The resulting is also identical to what I generated in earlier, see #26045 (comment). I was then able to load the loadshot and sync the chain (to the tip in about a minute, wonderful) and complete the background sync.

I also generated a mainnet snapshot which was also identical to what I found in the last test, so I have not tried loading again.

Will review the code later.

Co-authored-by: TheCharlatan <seb.kung@gmail.com>
@aureleoules
Copy link
Member Author

Rebased. I had to change the offset again in feature_assumeutxo.py.

@@ -76,6 +76,7 @@ def test_invalid_snapshot_scenarios(self, valid_snapshot_path):
bad_snapshot_path = valid_snapshot_path + '.mod'

def expected_error(log_msg="", rpc_details=""):
print(log_msg)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to drop this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be addressed in #29612

Copy link
Member

@Sjors Sjors left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK. Found a few issues during code review, see inline.

@TheCharlatan wrote:

The keys in leveldb are guaranteed to be lexicographically sorted. You just need to cache the last txid and flush the txid and outpoint tuples once the next one is reached in the database iterator.

I would be good to document in the code that we rely on this behavior (maybe in the header file, so someone who wants to replace leveldb is reminded).

@luke-jr & @aureleoules wrote:

Not sure it's worth it to save 20%. Presumably generic compressors could do better anyway?

Yes I agree but at least with this implementation the utxo set is still readable as is and doesn't need decompression.

We might also at some point want to xor the file - for the same reason as #28207. That would make compression with external tools impossible.


auto write_coins_to_file = [&](AutoFile& afile, const uint256& last_hash, const std::vector<std::pair<uint32_t, Coin>>& coins) {
afile << last_hash;
afile << static_cast<uint16_t>(coins.size());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4e19464: In primitives/transaction.h we serialize std::vector<CTxOut> vout with a simple s << tx.vout; without any reference to the size of the vector. Not sure if any magic is happening there under the hood that I missed.

Also, uint16_t implies a limit of 65,536 outputs per transaction. I don't think that's a consensus rule?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think the magic happens here: https://github.com/bitcoin/bitcoin/blob/master/src/serialize.h#L674

However, we can't use that because we are not looking at a full transaction but rather the outpoints that are still left in the UTXO set. But we basically mimic that behavior here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the 65,536: I guess the blocksize solves this for us for now I think it makes sense to use VARINT/CompactSize here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A transaction with 65,537 OP_RETURN outputs should fit in a block.

If I start with P2TR outputs with this calculator, that's 2,818,159 vbyte. https://bitcoinops.org/en/tools/calc-size/

And then subtract 32 bytes per output: 2,818,159 - 65537 * 32 = 720,975 vbyte

cc @murchandamus can you add OP_RETURN to the dropdown? :-)

In any case it seems unsafe to rely on the block size here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, but OP_RETURNs are not included in the UTXO set and we are serializing the UTXO set here, so I think this could still not happen like this. But I think you are right there are non-standard cases imaginable that make this possible, like just sending to OP_TRUE for example. So we should still make this robust.

Anyway, I am using CompactSize now in #29612 :)

coins_file >> size;

if(size > coins_left) {
LogPrintf("[snapshot] mismatch in coins count in snapshot metadata and actual snapshot data\n");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the context of my remark above about maybe not serializing size: in that case this check would have to happen after the batch of coins is loaded, which seems fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Sjors I couldn't follow this comment here. If this is still relevant, could you please clarify in #29612? Thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing to do here, since we do have serialize size, as you explained: #26045 (comment)

Coin coin;
coins_file >> outpoint.n;
coins_file >> coin;
outpoint.hash = txid;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4e19464 nit: maybe move this above where you set outpoint.n

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be addressed in #29612

}
} catch (const std::ios_base::failure&) {
LogPrintf("[snapshot] bad snapshot format or truncated snapshot after deserializing %d coins\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, but why doesn't this use coins_processed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be addressed in #29612

@@ -5468,6 +5480,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot(

bool out_of_coins{false};
try {
COutPoint outpoint;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be:

Txid txid;
coins_file >> txid;

The current code might accidentally work because a COutPoint is a Txid followed by a single number (albeit uint32_t instead of uint16_t).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be addressed in #29612

Coin coin;
unsigned int iter{0};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This move doesn't seem necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be addressed in #29612


auto write_coins_to_file = [&](AutoFile& afile, const uint256& last_hash, const std::vector<std::pair<uint32_t, Coin>>& coins) {
afile << last_hash;
afile << static_cast<uint16_t>(coins.size());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think the magic happens here: https://github.com/bitcoin/bitcoin/blob/master/src/serialize.h#L674

However, we can't use that because we are not looking at a full transaction but rather the outpoints that are still left in the UTXO set. But we basically mimic that behavior here.

auto write_coins_to_file = [&](AutoFile& afile, const uint256& last_hash, const std::vector<std::pair<uint32_t, Coin>>& coins) {
afile << last_hash;
afile << static_cast<uint16_t>(coins.size());
for (auto [vout, coin] : coins) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should call vout here either n or vout_index, otherwise it might be confusing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be addressed in #29612


auto write_coins_to_file = [&](AutoFile& afile, const uint256& last_hash, const std::vector<std::pair<uint32_t, Coin>>& coins) {
afile << last_hash;
afile << static_cast<uint16_t>(coins.size());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the 65,536: I guess the blocksize solves this for us for now I think it makes sense to use VARINT/CompactSize here

@aureleoules
Copy link
Member Author

Thanks for the reviews but I don't have time to address the comments so please mark this pull as Up for grabs.

@Sjors
Copy link
Member

Sjors commented Mar 7, 2024

Thanks for the great start @aureleoules!

@fjahr can you take this one? Otherwise I can, but not sure how soon.

@fjahr
Copy link
Contributor

fjahr commented Mar 7, 2024

Thanks for the great start @aureleoules!

@fjahr can you take this one? Otherwise I can, but not sure how soon.

Yeah, I will reopen this shortly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Possible savings in dumptxoutset serialization format (~20%)