Skip to content

Conversation

LarryRuane
Copy link
Contributor

@LarryRuane LarryRuane commented Nov 7, 2020

This PR was suggested in this comment in #19594, and builds on that PR (the first commit here).

This PR allows the blocks in files specified by the -loadblock=file command line (configuration) options (there can be multiple) to be unsorted. This is already allowed by -reindex, but currently when -loadblock encounters a block with an unknown parent, it is ignored. This means the blocks must be sorted by height to be loaded successfully. This PR fixes this restriction, allowing the blocks across all the specified blocks files to be in any order. This makes the -loadblock option more useful. Also, the affected code is cleaner and more maintainable.

(I'll simplify this description move details to a separate comment before this PR gets merged.)

@DrahtBot
Copy link
Contributor

DrahtBot commented Nov 7, 2020

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

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #22937 (refactor: Forbid calling unsafe fs::path(std::string) constructor and fs::path::string() method by ryanofsky)
  • #16981 (Improve runtime performance of --reindex by LarryRuane)

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.

@LarryRuane
Copy link
Contributor Author

A useful follow-on to this PR, since loading unordered blocks would become possible, might be the addition of a -loadblocksdir= argument that takes a directory path and loads all blocks found in the directory. That way it's not necessary to specify a very large number of -loadblock arguments. It would then be easy to import an entire blockchain.

Another way to accomplish approximately the same thing would be to replace the entire blocks directory in the data directory and then -reindex. But using -loadblocksdir would allow you to "merge" blocks into your existing blocks list (suppressing duplicates), which is kind of cool.

@adamjonas
Copy link
Member

Appears that the CI picked up a couple of argument issues with the LoadExternalBlockFile fuzz test:

test/fuzz/load_external_block_file.cpp:33:9: error: no matching function for call to 'LoadExternalBlockFile'
        LoadExternalBlockFile(Params(), fuzzed_block_file, &flat_file_pos, &blocks_with_unknown_parent);
        ^~~~~~~~~~~~~~~~~~~~~
./validation.h:162:6: note: candidate function not viable: requires 5 arguments, but 4 were provided
void LoadExternalBlockFile(
     ^
test/fuzz/load_external_block_file.cpp:35:9: error: no matching function for call to 'LoadExternalBlockFile'
        LoadExternalBlockFile(Params(), fuzzed_block_file);
        ^~~~~~~~~~~~~~~~~~~~~
./validation.h:162:6: note: candidate function not viable: requires 5 arguments, but 2 were provided
void LoadExternalBlockFile(
     ^
2 errors generated.

@LarryRuane
Copy link
Contributor Author

I don't know if anyone has been confused by -loadblock not loading all the blocks, but that did happen on the zcash network (a bitcoin core fork that I also work on): zcash/zcash#4679

@LarryRuane
Copy link
Contributor Author

LarryRuane commented Nov 10, 2020

Thanks, @adamjonas, force-pushed to fix the fuzz test.

Comment on lines 1151 to 1189
FILE* file = fsbridge::fopen(path, "rb");
if (!file) {
return error("ReadBlockFromDisk: open failed for %s", path.string());
}
if (offset > 0 && fseek(file, offset, SEEK_SET)) {
fclose(file);
return error("ReadBlockFromDisk: fseek failed for %s offset %u", path.string(), offset);
}
CAutoFile filein(file, SER_DISK, CLIENT_VERSION);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this change made? Could you explain how the behavior of this is changing, aside from the function signature taking const fs::path& path, unsigned int offset instead of const FlatFilePos& pos?

Copy link
Contributor

Choose a reason for hiding this comment

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

Note: I'm guessing this change was made bc of what you do at line 4810: ReadBlockFromDisk(*pblockrecursive, blk_paths[child_pos.nFile], child_pos.nPos, chainparams.GetConsensus())

But I'm failing to understand why you can't just do: ReadBlockFromDisk(*pblockrecursive, child_pos, chainparams.GetConsensus())

I'm probably missing something here, but I just want to check

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I reached several dead-ends before arriving at this code factoring. We can't use ReadBlockFromDisk(..., child_pos, ...) here because the pos argument is used in lower-level code (OpenFlockFile()) to construct and open a pathname like .../.bitcoin/blocks/blknnnnn.dat, which is just what we want for the -reindex case, but not for the -loadblock case (both share this code), because in the latter, we have a list of arbitrary pathnames to read from. Instead of calling the pos variant in the -reindex case and the path, offset variant in the -loadblock case, it's simpler to just call it in the same way for both cases.

src/init.cpp Outdated
} else {
LogPrintf("Warning: Could not open blocks file %s\n", path.string());
}
if (vImportFiles.size() > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you need the additional code branch here? Would it be preferable to just invoke LoadExternalBlockFiles("Importing", chainparams, vImportFiles, true); no matter what, and the for loop just won't iterate through anything if vImportFiles is empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, it's not really needed, but I think it's useful to show the reader here that loading blocks this isn't always being done; otherwise, to know nothing's being done, the reader has to go look at LoadExternalBlockFiles().

I also like that the conditional makes this part of the code similar to the if (fReindex) code just above it; they are doing very similar work.

But I could go either way (no strong preference).

Comment on lines 4693 to 4672
FILE* file = fsbridge::fopen(blk_paths[n_file], "rb");
if (file == nullptr) {
LogPrintf("%s: Warning: Could not open blocks file %s\n", __func__, blk_paths[n_file]);
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this kinda duplication of what ReadBlockFromDisk does? I'm wondering why FlatFilePos pos(n_file, 0); isn't created, passed to ReadBlockFromDisk (like before, when signature took FlatFilePos), and then you can pass it to LoadExternalBlockFile

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This duplication exists even without this PR. Before this PR, this fopen() happens here.

To summarize why this happens in two places: There are two ways blocks are "loaded" into memory when reindexing (and, now, with this PR, when using -loadblock). First, a blocks file is opened (here) and read from disk (using large reads) sequentially. At this point, we don't know the individual blocks' starting offsets, so we can't create a FlatFilePos to pass to ReadBlockFromDisk() as you suggest. Besides, doing these large reads is more efficient, typically many blocks per read -- plus a partial block at the end (since, again, the block boundaries are unknown). Once in memory, the block boundaries are discovered, and each block is deserialized. But the blocks in these blocks files are mostly out of order, so most of the blocks can't be accepted (have AcceptBlock() called on it) because its parent has not yet been seen.

When such a parent is later found, the child block is then re-read from disk using ReadBlockFromDisk(), deserialized a second time, and then passed to AcceptBlock() (allowed because its parent is now present). In this case, we're reading exactly a single block, because we know where it starts (in the file) and its length (which is what a FlatFilePos encodes).

src/validation.h Outdated
Comment on lines 164 to 165
const std::vector<fs::path>& blk_paths,
size_t n_file,
Copy link
Contributor

Choose a reason for hiding this comment

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

Given the name LoadExternalBlockFile I'm confused why blk_paths plural is passed as a param, instead of just a single file/path

Copy link
Contributor Author

@LarryRuane LarryRuane Nov 11, 2020

Choose a reason for hiding this comment

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

That function, LoadExternalBlockFile(), does (attempt to) load the blocks from exactly one block file, but, doing so may cause, as a side effect, the loading of blocks seen earlier (in earlier files) but that could not be loaded then because their parent was missing (and is now present, see my earlier comment). This requires reaching back and reading specific blocks from earlier files, and that requires the full list of block file paths (the blk_paths argument). This basic flow is the same as before this PR. I agree this is somewhat confusing, maybe a comment would help. But maybe not, because perhaps there's no shortcut to just studying the code, I'm not sure.

Copy link
Contributor Author

@LarryRuane LarryRuane left a comment

Choose a reason for hiding this comment

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

@mjdietzx, thank you for your review!

src/init.cpp Outdated
} else {
LogPrintf("Warning: Could not open blocks file %s\n", path.string());
}
if (vImportFiles.size() > 0) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, it's not really needed, but I think it's useful to show the reader here that loading blocks this isn't always being done; otherwise, to know nothing's being done, the reader has to go look at LoadExternalBlockFiles().

I also like that the conditional makes this part of the code similar to the if (fReindex) code just above it; they are doing very similar work.

But I could go either way (no strong preference).

Comment on lines 1151 to 1189
FILE* file = fsbridge::fopen(path, "rb");
if (!file) {
return error("ReadBlockFromDisk: open failed for %s", path.string());
}
if (offset > 0 && fseek(file, offset, SEEK_SET)) {
fclose(file);
return error("ReadBlockFromDisk: fseek failed for %s offset %u", path.string(), offset);
}
CAutoFile filein(file, SER_DISK, CLIENT_VERSION);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I reached several dead-ends before arriving at this code factoring. We can't use ReadBlockFromDisk(..., child_pos, ...) here because the pos argument is used in lower-level code (OpenFlockFile()) to construct and open a pathname like .../.bitcoin/blocks/blknnnnn.dat, which is just what we want for the -reindex case, but not for the -loadblock case (both share this code), because in the latter, we have a list of arbitrary pathnames to read from. Instead of calling the pos variant in the -reindex case and the path, offset variant in the -loadblock case, it's simpler to just call it in the same way for both cases.

Comment on lines 4693 to 4672
FILE* file = fsbridge::fopen(blk_paths[n_file], "rb");
if (file == nullptr) {
LogPrintf("%s: Warning: Could not open blocks file %s\n", __func__, blk_paths[n_file]);
return;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This duplication exists even without this PR. Before this PR, this fopen() happens here.

To summarize why this happens in two places: There are two ways blocks are "loaded" into memory when reindexing (and, now, with this PR, when using -loadblock). First, a blocks file is opened (here) and read from disk (using large reads) sequentially. At this point, we don't know the individual blocks' starting offsets, so we can't create a FlatFilePos to pass to ReadBlockFromDisk() as you suggest. Besides, doing these large reads is more efficient, typically many blocks per read -- plus a partial block at the end (since, again, the block boundaries are unknown). Once in memory, the block boundaries are discovered, and each block is deserialized. But the blocks in these blocks files are mostly out of order, so most of the blocks can't be accepted (have AcceptBlock() called on it) because its parent has not yet been seen.

When such a parent is later found, the child block is then re-read from disk using ReadBlockFromDisk(), deserialized a second time, and then passed to AcceptBlock() (allowed because its parent is now present). In this case, we're reading exactly a single block, because we know where it starts (in the file) and its length (which is what a FlatFilePos encodes).

src/validation.h Outdated
Comment on lines 164 to 165
const std::vector<fs::path>& blk_paths,
size_t n_file,
Copy link
Contributor Author

@LarryRuane LarryRuane Nov 11, 2020

Choose a reason for hiding this comment

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

That function, LoadExternalBlockFile(), does (attempt to) load the blocks from exactly one block file, but, doing so may cause, as a side effect, the loading of blocks seen earlier (in earlier files) but that could not be loaded then because their parent was missing (and is now present, see my earlier comment). This requires reaching back and reading specific blocks from earlier files, and that requires the full list of block file paths (the blk_paths argument). This basic flow is the same as before this PR. I agree this is somewhat confusing, maybe a comment would help. But maybe not, because perhaps there's no shortcut to just studying the code, I'm not sure.

@@ -49,6 +49,7 @@
#include <validationinterface.h>
#include <warnings.h>

#include <cassert>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This isn't needed, I'll remove in a later force-push.

@@ -11,6 +11,7 @@
#include <validation.h>

#include <cstdint>
#include <map>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This isn't needed, I'll remove in a later force-push.

@LarryRuane
Copy link
Contributor Author

Here's a manual test procedure (I'll describe testnet because it's faster, but mainnet works too)

  • testnet node is not running
  • wait for your testnet3 data directory to get out of date (don't run the client for at least several days)
  • change directory to your .bitcoin data directory
  • rename testnet3 to testnet3-save
  • start the node with -testnet, wait for it to sync (IBD), it will have more blocks (and more blocks files) than testnet3-save
  • stop the node
  • rename testnet3 to testnet3-new
  • rename testnet3-save to testnet3
  • start the node as follows, except replace 191 with the highest-numbered file in the testnet3-save directory:
bitcoind -testnet -networkactive=0 $(seq 0 191|xargs printf '-loadblock=testnet3-new/blocks/blk%05u.dat ')

This generates a command line like:

bitcoind -testnet -networkactive=0 -loadblock=testnet3-new/blocks/blk00000.dat -loadblock=testnet3-new/blocks/blk00001.dat ...
  • observe that, even though not syncing with peers, the node successfully reaches a recent block height (look at the most recent UpdateTip in debug.log).

@promag
Copy link
Contributor

promag commented Nov 14, 2020

@LarryRuane I guess it's possible to make a functional test then?

@LarryRuane
Copy link
Contributor Author

@promag I didn't write a new functional test, but I did modify the existing loadblock functional test to cover the new behavior (the test fails without the PR). Is that sufficient?

@dongcarl
Copy link
Contributor

dongcarl commented Dec 3, 2020

Verified that the test fails without the new behaviour, cool!

A couple of things that could be done:

  1. Instead of asserting based on the debug log (which is a bit more fragile), perhaps the assertion can be made based on the number of blocks connected (tip height). We may need to disable networking for this to work reliably.
  2. I think the changes in test/functional should be split into its own commit and put as the first commit in this patchset, this way it's easier for reviewers to verify that the new behavior does indeed do what it's supposed to do.

@LarryRuane
Copy link
Contributor Author

LarryRuane commented Dec 4, 2020

Force-pushed to implement both suggestions from @dongcarl (thank you!).

Reminder, this PR is layered on top of #19594 so please review only the last two commits (of the 3).

UPDATE Mar 5, 2021: I squashed the last two commits, so only review the last commit.

@LarryRuane
Copy link
Contributor Author

Rebased, no functional changes.

@LarryRuane
Copy link
Contributor Author

This required rebase also splits the second commit into two commits to separate refactoring from the functional changes. Ready for review.

@LarryRuane
Copy link
Contributor Author

Rebased. The second commit is now much simpler because of #21727.

@sipa
Copy link
Member

sipa commented Jun 10, 2021

utACK dbd7f72

@LarryRuane
Copy link
Contributor Author

Rebased to resolve merge conflicts.

hebasto and others added 3 commits July 27, 2021 17:07
This lower-level version takes a path and a seek offset as arguments.
Currently unused but is needed by the next commit.
Add LoadExternalBlockFiles() (plural) that accepts a list of paths and
loads all the blocks found in these paths. The blocks can be completely
out of order; block C (child) can be seen first and its parent P not
seen until a later file, then block C will be processed.  This requires
re-reading block C from the earlier file; block C isn't kept in memory
during this interval.

The list of paths is either the blocks/blk*.dat files (during reindexing)
or given with one or more -loadblk arguments.

LoadExternalBlockFiles() calls LoadExternalBlockFile() (singular) to
read a single file, but out-of-order blocks requires that this function
be able to read individual blocks from earlier files.
@DrahtBot
Copy link
Contributor

🐙 This pull request conflicts with the target branch and needs rebase.

Want to unsubscribe from rebase notifications on this pull request? Just convert this pull request to a "draft".

@DrahtBot
Copy link
Contributor

There hasn't been much activity lately and the patch still needs rebase. What is the status here?
  • Is it still relevant? ➡️ Please solve the conflicts to make it ready for review and to ensure the CI passes.
  • Is it no longer relevant? ➡️ Please close.
  • Did the author lose interest or time to work on this? ➡️ Please close it and mark it 'Up for grabs' with the label, so that it can be picked up in the future.

@LarryRuane
Copy link
Contributor Author

The -loadblock feature doesn't seem to be often used, so improvements to it aren't much in demand, so closing this. If this feature becomes more important, we can resurrect this PR.

@LarryRuane LarryRuane closed this Mar 8, 2022
@bitcoin bitcoin locked and limited conversation to collaborators Mar 8, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants