(tl;dr This is mainly a question about when an assumeutxo snapshot is loaded, if it makes more sense for the original chainstate to continue downloading and attaching blocks in the normal way, or if it should only download and attach blocks leading up to the snapshot block.)
It seems unclear how validation code should behave when an assumeutxo snapshot is loaded, and then new headers are received pointing to a forked chain with more proof of work than any known chain including the snapshot block. This question come up in #29519 (review).
This is not an urgent question, because if this situation arose on the network there would be bigger issues to confront than assumeutxo behavior, but it’s worth raising because there seem to be different ways of dealing with it that impact that design of validation code, and maybe also have implications in other cases like eclipse attacks.
Background
When an assumeutxo snapshot is loaded, a new chainstate object is created. A chainstate is just a UTXO database and a pointer to the last block added to the database.
So immediately after the assumeutxo snapshot is loaded there are two chainstates:
-
The original chainstate pointing at the most-work block which has been locally validated. (This block should normally be an ancestor of the snapshot block, but doesn’t have to be.)
-
A new snapshot chainstate pointing at the snapshot block, which has been not been locally validated and probably not downloaded, but is assumed-valid.
After this point, because of missing block and undo data before the snapshot, the snapshot chainstate is constrained to only sync to chains including the snapshot block. If headers for chains with more work not including the snapshot block are found, the snapshot chainstate needs to ignore them, because even if it downloaded blocks on those chains, it would lack the undo data needed to reorg and actually validate them.
It is less clear whether the original chainstate should also ignore chains not including the snapshot block.
Possible behavior: Original chainstate targets the most-work chain
This is not what currently happens, but simplest approach might be for the original chainstate to be unaffected by the snapshot chainstate, and to continue to download and attach the same blocks it otherwise would have if no snapshot were loaded. It would just do it more slowly due to a reduced cache size and lower priority for block requests compared to the snapshot chainstate.
Current behavior: Original chainstate targets the snapshot block
Currently, instead of the original chainstate being unaffected by the snapshot chainstate, it’s constrained to only download and blocks that are ancestors of the snapshot block, and ignore other chains.
Tradeoffs
Possible advantages of original chainstate targeting the most work chain:
- Probably simpler implemenation. If original chainstate behaves the same way whether or not a snapshot is loaded, fewer special cases need to exist when a snapshot is loaded.
- Would sync to most-work chain faster if most-work chain did not include the snapshot block and turned out to be valid (i.e. not a hard fork)
- Maybe more philosophically neutral, because the node continues normal behavior of syncing to the most-work chain, instead of ignoring any chain not including the snapshot block.
Possible advantages of original chainstate targeting the snapshot block:
- This what code currently does so would not require further changes.
- Maybe could provide resilience against hard forks that contain more work? If loading a snapshot makes a node temporarily ignore any chain not containing a snapshot block, maybe that is a useful feature if the chain with more work turns out to be invalid.
- Could help in an eclipse attack? If headers or blocks after the snapshot block were withheld, this could temporarily stop the node from syncing to a undesirable fork excluding the snapshot block that seemed to have more work.
- [Maybe other reasons? Personally I’m more inclined towards the first approach, and have a weak understanding of things like forks and eclipse attacks, so I’m struggling to think of advantages to this approach.]
Questions
As long as the most-work header chain includes the snapshot block, there should be no real differences in behavior between the two approaches described above. But if the most-work header chain doesn’t include the snapshot block, it raises questions about which approach might be preferable. It also raises questions about what other behaviors we should consider implementing if this state is detected, like warning the user, shutting down, changing sync behavior, maybe adjusting relative priorities of the two chainstates. The main question is if we should be doing anything different than we are doing now.