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

multi: Introduce AssumeValid. #2839

Merged
merged 8 commits into from Dec 11, 2021
Merged

multi: Introduce AssumeValid. #2839

merged 8 commits into from Dec 11, 2021

Conversation

rstaudt2
Copy link
Member

@rstaudt2 rstaudt2 commented Dec 3, 2021

This introduces an AssumeValid parameter and configuration option. It is broken down into several commits to ease the review process.


Motivation: Checkpoints vs AssumeValid

Prior to this PR, Checkpoints were being used for two distinct purposes:

  • Reject blocks that fork the main chain before the most recently known checkpoint
  • Allow several validation checks to be skipped for blocks that are an ancestor of the most recently known checkpoint

This PR introduces a new parameter, AssumeValid, to control when validation checks can be skipped rather than continuing to use Checkpoints for this purpose.

The separation of Checkpoints and AssumeValid provides a few benefits:

  • It allows for node operators to configure each capability independently
  • It allows for testing the distinct sync options independently
  • It allows for specifying an AssumeValid hash that is closer to tip
    • This is safe since when bypassing some validation checks with assume valid for a given block, the block is still validated to be part of the chain with the most proof of work and additionally must be an ancestor of the assumed valid block, meaning that changes in any transaction data would be detected as it would necessarily change the merkle root of the assumed valid block
    • This would not be safe with Checkpoints since rejecting forks before a certain point affects consensus

The --assumevalid config option

Since the AssumeValid hash can be closer to tip, this PR adds the ability to set it without a code change via an --assumevalid option. It can be specified as follows:

  • (flag not specified): defaults to the hard-coded value in source
  • --assumevalid=0: disable AssumeValid
  • --assumevalid=[blockhash]: set AssumeValid to the specified block hash

Specifying an AssumeValid hash closer to tip allows for faster syncing. This is useful since the hard-coded AssumeValid block becomes deeper in the chain (and therefore more validation needs to be done post the AssumeValid block) the longer a particular release build is out.

As noted in the implementation, AssumeValid is clamped back as needed to ensure that it is at least 2 weeks worth of blocks behind the best header. This protects against malicious actors trying to play games by tricking users into using an assumed valid hash near the tip.

Copy link
Member

@davecgh davecgh left a comment

Choose a reason for hiding this comment

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

I haven't reviewed the tests yet, but I spotted an issue I wanted to call out in the mean time.

blockchain/chain.go Outdated Show resolved Hide resolved
blockchain/process.go Outdated Show resolved Hide resolved
@matthawkins90
Copy link
Contributor

As I'm going through the readme and the code, I noticed that you seem to be using "AssumeValid Hash" and "AssumeValid Node" interchangeably.

For instance, in the comment above:

Specifying an AssumeValid hash closer to tip allows for faster syncing. This is useful since the hard-coded AssumeValid node becomes deeper in the chain (and therefore more validation needs to be done post the AssumeValid node) the longer a particular release build is out.

Did you do this intentionally, and I'm just misunderstanding?

Otherwise, I think it makes more sense to say that you specify "AssumeValid Hash" or an "AssumeValid Block", and if a node is broadcasting either of those things, then I suppose you can call it an "AssumeValid Node."

@davecgh
Copy link
Member

davecgh commented Dec 4, 2021

It's because the term node is overloaded. It refers to both the peer (network node) as well as the internal nodes (blockNode) of the data structure that comprise the full block tree that a specific branch that ultimately becomes the blockchain is selected from.

So, the hash is what you specify as the config option. The node (aka block / blockNode) is what that hash resolves to (if any).

It would probably be clearer to those less familiar with the code to refer to them as blocks whenever they're being referencing in the same context as network nodes/peers.

EDIT: As for the specific text in question, I agree that it would probably read better as:

... This is useful since the hard-coded AssumeValid block becomes ...

//
// Block 00000000000000001251efb83ad1a5c71351b50fe9195f009cf0bf5a7cd99d52
// Height: 606000
AssumeValid: *newHashFromStr("00000000000000001251efb83ad1a5c71351b50fe9195f009cf0bf5a7cd99d52"),
Copy link
Member

Choose a reason for hiding this comment

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

Confirming this hash:

$ dcrctl getblockhash 606000
00000000000000001251efb83ad1a5c71351b50fe9195f009cf0bf5a7cd99d52

//
// Block 00000000d64ceb1a686315ed56235e9a6838e3a22e9ec9bd92c2e04c09e0778b
// Height: 807905
AssumeValid: *newHashFromStr("00000000d64ceb1a686315ed56235e9a6838e3a22e9ec9bd92c2e04c09e0778b"),
Copy link
Member

Choose a reason for hiding this comment

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

Confirming this hash as well:

$ dcrctl --testnet getblockhash 807905
00000000d64ceb1a686315ed56235e9a6838e3a22e9ec9bd92c2e04c09e0778b

Copy link
Member

@davecgh davecgh left a comment

Choose a reason for hiding this comment

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

The test coverage looks great overall. Do you think it's worth adding one for the clamping on a side chain case identified during the review to prevent any possible regressions in that regard?

blockchain/process_test.go Outdated Show resolved Hide resolved
blockchain/process_test.go Outdated Show resolved Hide resolved
blockchain/process_test.go Outdated Show resolved Hide resolved
@rstaudt2
Copy link
Member Author

rstaudt2 commented Dec 4, 2021

Do you think it's worth adding one for the clamping on a side chain case identified during the review to prevent any possible regressions in that regard?

Yep, I was thinking the same. I added a test for this and validated that the test will fail if the fix is reverted.

@rstaudt2
Copy link
Member Author

rstaudt2 commented Dec 4, 2021

As I'm going through the readme and the code, I noticed that you seem to be using "AssumeValid Hash" and "AssumeValid Node" interchangeably.

Did you do this intentionally, and I'm just misunderstanding?

@matthawkins90 Thanks for pointing out that the terminology was a bit confusing. As @davecgh noted, the term "node" is overloaded, and when used in place of "block", it is referring to the internal struct blockNode that represents a block.

I've updated to use "block" rather than "node" for the cases that are not directly referring to a variable of the blockNode type.

Copy link
Member

@davecgh davecgh left a comment

Choose a reason for hiding this comment

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

In addition to the code review, I also tested this as follows:

  • Added prints for chosen flags and script execution policy to manually inspect the results
  • Ran mainnet and testnet, and simnet with the default values
  • Ran mainnet and testnet, and simnet with various custom values, including 0 to disable
  • Forced clamping, both on the main chain and side chain via simnet
  • Triggered reorgs of varying complexity via simnet

@matthawkins90
Copy link
Contributor

Thanks for the explanation! Yup, sorry, I wasn't aware of the blockNode struct terminology. I see now that there's already a pretty heavily embedded syntax of variables and functions using node to refer to the blocks in the tree: blockNode, nodeStatus, node.hash, node.height. Makes sense now.

doc.go Outdated Show resolved Hide resolved
This introduces an AssumeValid parameter that is the hash of a block
that has been externally verified to be valid.  In a subsequent commit,
it will be used to allow several validation checks to be skipped for
blocks that are both an ancestor of the assumed valid block and an
ancestor of the best header.

Note that the AssumeValid block hash that is specified for mainnet and
testnet is for the same block that was used for MinKnownChainWork.
Using the same block for both of these slightly simplifies the process
for external verification of the values when they are updated
periodically with new releases.
This adds an assumeValid field to BlockChain and sets it based on the
AssumeValid chain parameter.
This adds tracking of the assumed valid node to BlockChain.  The assumed
valid node will be used in place of checkpoints to set the BFFastAdd
flag in a subsequent commit.
This updates BlockChain to set the BFFastAdd flag if a block is both an
ancestor of the assumed valid block and an ancestor of the best header.

A helper function isAssumeValidAncestor is introduced that determines
whether the provided node is both an ancestor of the assumed valid node
and an ancestor of the best header.  False is returned when the assume
valid node has not been discovered or when assume valid is disabled.

The assumed valid node is clamped back as needed to ensure that it is at
least 2 weeks worth of blocks behind the best header.  This protects
against malicious actors trying to play games by tricking users into
using an assumed valid hash near the tip.
This updates block validation to not run scripts if a block is both an
ancestor of the assumed valid block and an ancestor of the best header.
This updates block validation to not run scripts if bulk import mode is
enabled.  Bulk import mode is only enabled when importing blocks that
are already known to be valid.
This adds tests that validate that it is correctly determined whether or
not a node is an ancestor of an assumed valid node under a variety of
conditions.
This adds an assumevalid config option which allows for specifying a
hash of an assumed valid block or disabling assume valid.

The usage is as follows:

* (flag not specified): defaults to the hard-coded value in source
* --assumevalid=0: disable AssumeValid
* --assumevalid=[blockhash]: set AssumeValid to the specified block hash
@davecgh davecgh merged commit c6dc5e0 into decred:master Dec 11, 2021
@rstaudt2 rstaudt2 deleted the assume-valid branch December 20, 2021 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants