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

WIP: Taking finalization states into account when deciding to switch to the new chain #170

Closed
wants to merge 4 commits into from

Conversation

frolosofsky
Copy link
Member

@frolosofsky frolosofsky commented Oct 15, 2018

It's a clean version of previous PR #164 after refactoring and fixing style comments.

Rough implementation of applying finalization to chain reorganization. It's WIP and in fact just a result of my initial unite/bitcoin sources research.

Some bullets/todo:

  • Need to explain new rule in an issue/ADR.
  • Because we started to reject parallel chains from older justified epochs, some of bitcoin's functional tests fail, e.g. we have 10 blocks in the main chain and 5th has been justified: now we reject all the blocks from parallel chains which height is less or equal 5. Discussed with @Gnappuraz, working around.
  • Tests
  • It's might be a good optimization to store FinalizationState per chain or per checkpoint instead of per block.

@frolosofsky frolosofsky added the wip Work in progress which is not supposed to be merged yet label Oct 15, 2018
@frolosofsky frolosofsky self-assigned this Oct 15, 2018
src/validation.cpp Outdated Show resolved Hide resolved
src/validation.cpp Outdated Show resolved Hide resolved
@@ -216,6 +218,13 @@ class FinalizationState : public FinalizationStateData {

mutable CCriticalSection cs_esperanza;

struct Storage {
std::map<uint256, FinalizationState> states;
Copy link
Member

Choose a reason for hiding this comment

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

A problem I see with this is that you never delete stuff from the map. Ideally whenever we reach finalization we can drop everything that comes before. This should behave more like a rolling window.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point.

auto const hash = index->GetBlockHash();
auto it = states.find(hash);
if (it == states.end()) {
it = states.emplace(hash, FinalizationState(*FindOrCreate(index->pprev)))
Copy link
Member

Choose a reason for hiding this comment

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

I don't think recursion is a good idea here, if someone serves you a very long header chain and then sends you the last block you would create a bazillion of intermediate states (easy DoS).

Copy link
Member Author

Choose a reason for hiding this comment

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

And again due my wrong assumption, I was thinking that recursion call MUST return in one hop because we already have state for previous tip of the chain new block belongs to.

Copy link
Member

@Gnappuraz Gnappuraz Oct 17, 2018

Choose a reason for hiding this comment

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

This might become a correct solution if we manage to handle the block reconstruction so that we can assure consequentiality and ordering of stored blocks.

FinalizationState *FinalizationState::Storage::FindOrCreate(
const CBlockIndex *index) {
LOCK(cs_storage);
if (index == nullptr || index->pprev == nullptr ||
Copy link
Member

Choose a reason for hiding this comment

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

if for some reason the block passed does not have a pointer to the pprev you are going to return the current main chain head state. Why?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's not current main chain. esperanzaState.get() returns state for the genesis block. I thought that empty pprev means that it's block is genesis.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah this you have to check thoroughly cause I wouldn't assume that there is no chance of a pIndex having pprev nullptr because of the previous block not being arrived yet.


if (n->GetLastJustifiedEpoch() < c->GetLastJustifiedEpoch()) {
// UNIT-E TODO delete from the disk? If not, skip this condition.
LogPrintf("Reject outdated justified epoch: %d %d\n", n->GetLastJustifiedEpoch(), c->GetLastJustifiedEpoch());
Copy link
Member

Choose a reason for hiding this comment

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

As we discussed we don't reject but we keep it in case it becomes justified at a longer height.

}

// Check whether new block's chain has forked after current's chain last justified block
uint256 checkpoint = c->GetCheckpointHash(c->GetLastJustifiedEpoch());
Copy link
Member

@Gnappuraz Gnappuraz Oct 16, 2018

Choose a reason for hiding this comment

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

This should be GetLastFinalizedEpoch() and in general this check should be done when we receive the header since we don't need the full block to verify that.

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess you meant GetLastFinalizedEpoch?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah sure, sorry for that! Fixed.

// Check whether new block's chain has forked after current's chain last justified block
uint256 checkpoint = c->GetCheckpointHash(c->GetLastJustifiedEpoch());
if (!checkpoint.IsNull() && !IsForkedAfter(checkpoint, pindex)) {
// UNIT-E TODO delete from the disk?
Copy link
Member

Choose a reason for hiding this comment

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

If we reject before AcceptBlock() nothing we'll be saved in disk.

@Gnappuraz
Copy link
Member

Is a very good point that we should document the fork choice rule in a ADR, this would
also help implementation and review cause people have a reference where to get information
about this whole topic.

There are 4 main points for the fork-choice rule from casper (https://arxiv.org/pdf/1710.09437.pdf par.4):

  • never reorg before the last finalized checkpoint
  • build on top (follow) of the longest justified checkpoint
    In case of no justification and forking within the same dynasty we fallback in order to
  • haviest chain (more stakes)
  • longest chain

Furthermore blocks that have forking before our last justification should be rejected straight away.
When we receive the header of a block we can already eagerly discard it in this way.
I think this will save a great deal of disk space compared to the current bitcoin behaviour.

scravy
scravy previously requested changes Oct 16, 2018
Copy link
Member

@scravy scravy left a comment

Choose a reason for hiding this comment

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

I think a feature like this

  • needs to be thoroughly tested
  • should be documented/discussed in an ADR first

src/validation.cpp Outdated Show resolved Hide resolved
@scravy scravy dismissed their stale review November 2, 2018 00:00

New code was pushed, I am dismissing my old request changes review.

}
return error("invalid header received");
Copy link
Member

Choose a reason for hiding this comment

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

I do not think that this move is correct. Previously: Would return this error in case of potential Denial-of-Service. Now with this change: Would return error in that case and if ProcessNewBlockHeaders/AcceptBlockHeader fails (for other reviewers: DoS is only checked then, hence the move one layer of if-conditions up – look at the context).

AFAICS: The AcceptBlockHeader looks at a single header. ProcessNewBlockHeaders looks at several headers. If it returns false then it did not validate some header, but there are also some valid headers which should be processed further down. Moving the return here will preempt that.

Copy link
Member Author

Choose a reason for hiding this comment

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

I had too many attempts to implement fork-choice, it might be an accidental artifact from previous one. Will check, thanks for this catch!

@@ -2537,8 +2570,32 @@ CBlockIndex* CChainState::FindMostWorkChain() {
do {
CBlockIndex *pindexNew = nullptr;

if (chainActive.Tip() != nullptr) {
Copy link
Member

Choose a reason for hiding this comment

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

I remember that in some pull request I was asked whether the comparison withnullptr is necessary and a reference to the cpp core guidelines was given. I personally do not give a rats arse about this. But it popped into my mind – in that other pull request I obeyed the cpp core guidelines and removed the comparison with nullptr actually...

Copy link
Member Author

@frolosofsky frolosofsky Nov 2, 2018

Choose a reason for hiding this comment

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

I remember that conversation. I'm not trying to go against the tide actually but it's just a strong habit. Previously I mentioned implicit type casting, but moreover this way is much more clear to understand I think. And actually cpp guidelines are bit inconsistent in this, e.g. at first they told:

void f(int i)
{
    if (i)            // suspect
    // ...
    if (i == success) // possibly better
    // ...
}

And then...

// These all mean "if `p` is `nullptr`"
if (!p) { ... }           // good
if (p == 0) { ... }       // redundant `== 0`; bad: don't use `0` for pointers
if (p == nullptr) { ... } // redundant `== nullptr`, not recommended

So in first example it's not redundant, but in seconds it's it. I don't see any logic in this. For sure I can review and change all such checks, it's not something I gonna fight for. But better I'd like to have discussion and consensus in the team and if we follow this guideline, let's put it in documentation. My point is to not follow guideline just because it's a guideline, but to understand why do we do so.

Copy link
Member

Choose a reason for hiding this comment

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

Actually I do like the explicit versions == nullptr and != nullptr better. But above all I value consistency :D

Anyways. Its a very very small nit. @Ruteri If you sent a pull request referencing the cpp core guidelines in developer notes the discussion could be taken there ;-)

@frolosofsky
Copy link
Member Author

@scravy It's not finished yet, I still need to add bunch of tests at least, so I guess your previous change request remains valid.

@scravy scravy changed the title Taking finalization states into account when deciding to switch to the new chain WIP: Taking finalization states into account when deciding to switch to the new chain Nov 2, 2018
Signed-off-by: Stanislav Frolov <[email protected]>
Signed-off-by: Stanislav Frolov <[email protected]>
@frolosofsky
Copy link
Member Author

I'm pausing development on this PR because it's blocked by #219.

@mergeable
Copy link

mergeable bot commented Nov 29, 2018

There has not been any activity in the past 10 days. Is this still active?

4 similar comments
@mergeable
Copy link

mergeable bot commented Dec 9, 2018

There has not been any activity in the past 10 days. Is this still active?

@mergeable
Copy link

mergeable bot commented Dec 19, 2018

There has not been any activity in the past 10 days. Is this still active?

@mergeable
Copy link

mergeable bot commented Dec 29, 2018

There has not been any activity in the past 10 days. Is this still active?

@mergeable
Copy link

mergeable bot commented Jan 8, 2019

There has not been any activity in the past 10 days. Is this still active?

@frolosofsky
Copy link
Member Author

Dear Bot,

I'm working on the different branch by picking some pieces from here. This guy is not supposed to be merged, and I will close PR and delete this branch after I finish all the commits and fork-choice rule related stuff.

Thank you,

@mergeable
Copy link

mergeable bot commented Jan 18, 2019

There has not been any activity in the past 10 days. Is this still active?

@frolosofsky
Copy link
Member Author

Superseded by #525.

@frolosofsky frolosofsky closed this Feb 5, 2019
@kostyantyn kostyantyn deleted the reorg-3 branch February 6, 2019 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
finalization wip Work in progress which is not supposed to be merged yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants