Skip to content

Conversation

luke-jr
Copy link
Member

@luke-jr luke-jr commented Jul 8, 2020

This adds the ability to have any number of internal or external locks to prevent pruning specific block ranges.

Some examples where this would be useful:

  • Ensuring any known Core wallet won't become unable to sync due to pruning when it is unloaded. (Included in this PR)
  • Ensuring pruning doesn't go too far for a desired-functional wallet backup. (Questionable, since you probably lose the node if you need the backup anyway?)
  • Allowing users to temporarily disable block indexes and filters, without pruning cutting them off from catching up later.
  • Avoiding pruning out from under external wallets (Armory, EPS, etc) that might need block data.
  • External block indexes/filters, etc.

By design, users retain complete control over prune locks, and can delete them out from under applications using them. This is to avoid circumstances where a prune lock has been set, by an application that may no longer be used.

@DrahtBot
Copy link
Contributor

DrahtBot commented Jul 8, 2020

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

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/19463.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK fjahr

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:

  • #32427 ((RFC) kernel: Replace leveldb-based BlockTreeDB with flat-file based store by TheCharlatan)
  • #31860 (init: Take lock on blocks directory in BlockManager ctor by TheCharlatan)
  • #31144 ([IBD] multi-byte block obfuscation by l0rinc)
  • #29418 (rpc: provide per message stats for global traffic via new RPC 'getnetmsgstats' 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.

@andronoob
Copy link

I think redownloading specified block could be much more useful, just like the case that scantxoutset gives the corresponding block height, but the actual block had already been pruned - the ability to redownload (and then rescan) specified block fix this problem.

@andronoob
Copy link

Recently I heard (could be inaccurate/misunderstanding of myself) that some projects like BTCPay didn't keep full transaction data of received funds. Only the UTXO entry (txid, vout, amount and scriptPubkey) was kept. Therefore they are facing problems to fix the BIP143 vulnerability, because the full data of the previous transaction had been already discarded. I think the ability to redownload & rescan specified block(s) can relieve this problem.

@luke-jr
Copy link
Member Author

luke-jr commented Jul 9, 2020

I think redownloading specified block could be much more useful, just like the case that scantxoutset gives the corresponding block height, but the actual block had already been pruned - the ability to redownload (and then rescan) specified block fix this problem.

That's useful too, but for different use cases. You wouldn't want to be constantly redownloading the same block over and over. (Prune locks might be useful for it, though, to prevent the redownloaded block from being immediately repruned before use.)

Recently I heard (could be inaccurate/misunderstanding of myself) that some projects like BTCPay didn't keep full transaction data of received funds. Only the UTXO entry (txid, vout, amount and scriptPubkey) was kept.

Aside, that's a pretty serious bug. It's the receiver's responsibility to rebroadcast the transaction until it confirms...

@andronoob
Copy link

andronoob commented Jul 28, 2020

Allowing users to temporarily disable block indexes and filters, without pruning cutting them off from catching up later.

Honestly I don't get what's the point of this - to save a little more disk space under extreme conditions, like almostly exhausted disk space, so that the node could then keep running for a littel more time?

External block indexes/filters, etc.

This seems to justify the above point, however I think one of the most-wanted features is "external (and pluggable) block storage", rather than "external indices/filters etc", although the later also seems to be nice (despite both would complicate the things further).

Avoiding pruning out from under external wallets (Armory, EPS, etc) that might need block data.

Ensuring any known Core wallet won't become unable to sync due to pruning when it is unloaded. (Included in this PR)

Then, (without some filter/matching mechanism) it will in fact disable (pause) pruning until the wallet reloads, won't it? Because the node won't know whether a newly received block is unrelated (prunable), therefore the only choice is to stop pruning.

It has been a significant usability problem since quite a long time ago, that the user cannot load/import wallets/keys/addrs freely - nobody likes triggering the troubles of blockchain rescanning.

I think #15946 (together with something like #15845) should be able to fix such usability problem in a simpler,better way, that:

With local blockfilterindex, the blocks could be simply discarded (pruned). Then, the once pruned blocks could be redownloaded according to local blockfilterindex while reloading wallet/importing keys/addrs - no need to worry about whether a block could be needed in the future anymore.

However I still agree that a button allowing the user to simply "pause/resume" pruning would be nice, too - I hope that this could be displayed on some dashboard.

@andronoob
Copy link

That's useful too, but for different use cases. You wouldn't want to be constantly redownloading the same block over and over. (Prune locks might be useful for it, though, to prevent the redownloaded block from being immediately repruned before use.)

Yes, prune locks indeedly have its utility, especially to lock needed/related historical blocks. I just think that I see greater problems.

Copy link
Contributor

@mzumsande mzumsande left a comment

Choose a reason for hiding this comment

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

I can see how this could be useful for external applications in theory, but it would be good to know if there is an actual demand for this - I wonder if there are any applications out there that would actually use this / generate prune locks if this option was available for them?

const int max_height_first{pindexDelete->nHeight - 1};
for (auto& prune_lock : m_blockman.m_prune_locks) {
if (prune_lock.second.height_first <= max_height_first) continue;
if (prune_lock.second.height_first < max_height_first) continue;
Copy link
Contributor

@mzumsande mzumsande Apr 7, 2025

Choose a reason for hiding this comment

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

Commit 8ee1214:
would be good to have an explanation why this is being changed in the commit message.

I guess this means that if we have a chain at height N, have an index synced to height N-1 and disconnect the tip, we would change the prune lock of the index from N-1 to N-2. Why?

@@ -97,6 +97,7 @@ struct CBlockIndexHeightOnlyComparator {

struct PruneLockInfo {
int height_first{std::numeric_limits<int>::max()}; //! Height of earliest block that should be kept and not pruned
int height_last{std::numeric_limits<int>::max()}; //! Height of latest block that should be kept and not pruned
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a use case for allowing users to specify height_last, which would allow blocks higher than that to be pruned, so that anything requiring to sync the entire chain at a later point wouldn't work anymore?

if (lockid == "*") throw JSONRPCError(RPC_INVALID_PARAMETER, "id \"*\" only makes sense when deleting");
if (!height_param[0].isNum()) throw JSONRPCError(RPC_TYPE_ERROR, "Invalid start height");
lock_info.height_first = height_param[0].getInt<uint64_t>();
if (!height_param[1].isNull()) {
Copy link
Contributor

@mzumsande mzumsande Apr 7, 2025

Choose a reason for hiding this comment

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

Should probably return an error if height_first > height_last.

static RPCHelpMan listprunelocks()
{
return RPCHelpMan{"listprunelocks",
"\nReturns a list of pruning locks.\n",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"\nReturns a list of pruning locks.\n",
"Returns a list of pruning locks.\n",

the ci failure is a bit vague, but this should fix it


// Since there is no reasonable expectation for any follow-up to this prune lock, actually ensure it gets committed to disk immediately
if (!m_block_tree_db->DeletePruneLock(name)) {
LogError("%s: failed to %s prune lock '%s'\n", __func__, "erase", name);
Copy link
Member

Choose a reason for hiding this comment

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

nit: the error log message looks already unique enough and the __func__ can be removed

@DrahtBot
Copy link
Contributor

Turned into draft for now, due to failing CI and inactivity for two months.

@DrahtBot
Copy link
Contributor

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

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.

9 participants