Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Lean BEEFY to Full BEEFY - don't skip (older) mandatory blocks and import justifications #11821

Merged
merged 25 commits into from
Jul 29, 2022

Conversation

acatangiu
Copy link
Contributor

@acatangiu acatangiu commented Jul 12, 2022

The first step from Lean->Full BEEFY is to have the worker enforce uninterrupted line of BEEFY finalized mandatory blocks.

There is one mandatory block per session (the first block in the session). As such, votes processing and votes generation now enforces that all mandatory blocks are finalized in strict monotonically increasing sequence and no block N will be worked on if there is any GRANDPA finalized but BEEFY non-final mandatory block M, where M < N.

Implementation details:

  • Plugged into the BlockImport queue and now forwarding all valid justifications to the voter.
  • Introduced 'VoterOracle' to separate the voting decisions logic, and track new/pending sessions.
  • New sessions get queued up with the worker operating either:
    1. up-to-date - all mandatory blocks leading up to current GRANDPA finalized: queue has ONE element, the 'current session' where mandatory_done == true,
    2. lagging behind GRANDPA: queue has [1, N] elements, where all mandatory_done == false. In this state, everytime a session gets its mandatory block BEEFY finalized, the session is popped off the queue, eventually getting to operating mode 1. up-to-date.
  • Justifications and votes get triaged and those that fall withing the VoterOracle allowed window get processed, the others get dropped if stale, or buffered for later processing (when they reach the window).
  • Worker general code was also updated to fall in one of two roles:
    1. react to external events and change internal 'state',
    2. generate events/votes based on internal 'state'.

Fixes #11788
Fixes #11789


Future PR will implement SYNC mechanism for on-demand justifications.


polkadot companion: paritytech/polkadot#5796

The first step from Lean->Full BEEFY is to have the worker
enforce uninterrupted line of BEEFY finalized mandatory blocks.

There is one mandatory block per session (the first block in the
session). As such, votes processing and votes generation now
enforces that all mandatory blocks are finalized in strict
monotonically increasing sequence and no block 'N' will be worked
on if there is any GRANDPA finalized but BEEFY non-final mandatory
block 'M', where 'M < N'.

Implementation details:

- Introduced 'VoterOracle' to separate the voting decisions logic,
  and track new/pending sessions.

- New sessions get queued up with the worker operating either:
  1. up-to-date - all mandatory blocks leading up to current GRANDPA
     finalized: queue has ONE element, the 'current session' where
     `mandatory_done == true`,
  2. lagging behind GRANDPA: queue has [1, N] elements, where all
     `mandatory_done == false`.
     In this state, everytime a session gets its mandatory block
     BEEFY finalized, the session is popped off the queue,
     eventually getting to operating mode `1. up-to-date`.

- Votes get triaged and those that fall withing the `VoterOracle`
  allowed window get processed, the others get dropped if stale,
  or buffered for later processing (when they reach the window).

- Worker general code was also updated to fall in one of two roles:
  1. react to external events and change internal 'state',
  2. generate events/votes based on internal 'state'.

Signed-off-by: acatangiu <[email protected]>
@acatangiu acatangiu added A0-please_review Pull request needs code review. B0-silent Changes should not be mentioned in any release notes C1-low PR touches the given topic and has a low impact on builders. D2-notlive 💤 PR contains changes in a runtime directory that is not deployed to a chain that requires an audit. labels Jul 12, 2022
@acatangiu acatangiu self-assigned this Jul 12, 2022
@acatangiu acatangiu added D2-notlive 💤 PR contains changes in a runtime directory that is not deployed to a chain that requires an audit. and removed D2-notlive 💤 PR contains changes in a runtime directory that is not deployed to a chain that requires an audit. labels Jul 12, 2022
@acatangiu acatangiu changed the title Lean BEEFY to Full BEEFY - part 1 - don't skip (older) mandatory blocks [Draft] Lean BEEFY to Full BEEFY - part 1 - don't skip (older) mandatory blocks Jul 12, 2022
@acatangiu
Copy link
Contributor Author

acatangiu commented Jul 12, 2022

Actually hold off on reviewing this for a couple of days while I confirm this approach still works when plugging in block import (and later sync protocol).

L.E. done

@acatangiu acatangiu changed the title [Draft] Lean BEEFY to Full BEEFY - part 1 - don't skip (older) mandatory blocks Lean BEEFY to Full BEEFY - don't skip (older) mandatory blocks and import justifications Jul 13, 2022
@acatangiu
Copy link
Contributor Author

Actually hold off on reviewing this for a couple of days while I confirm this approach still works when plugging in block import (and later sync protocol).

Plugged in block import now, please review.

Copy link
Contributor

@svyatonik svyatonik 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 finished yet - just couple of dumb questions :)

.and_then(|just| just.get(BEEFY_ENGINE_ID).cloned());

// Run inner block import.
let import_result = self.inner.import_block(block, new_cache).await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

The inner here would be (or include) grandpa block import, right? If so, then iiuc in its current implementation (here, here and here) it won't pass BEEFY justification to the client => it won't be stored in the database. Am I missing something, or it isn't a part of this PR?

Copy link
Contributor

Choose a reason for hiding this comment

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

Another q: if justification is supposed to be saved in this call, then what if it is invalid? Since it is checked after this line, it may be invalid, but it still be saved to the db during this call, right?

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, inner here will be (or in the future include) grandpa block import.

it won't pass BEEFY justification to the client => it won't be stored in the database

I think you're right, I didn't realize the client will need it as well for storing in the db.
I cloned the beefy just before passing it down the line exactly because I saw that grandpa can take/consume all of them, so the BeefyWorker does get the justification, but the client backend+db might not.

Let's see what @andresilva thinks here before I start hacking the grandpa code.

if justification is supposed to be saved in this call, then what if it is invalid?

Also good catch! I guess as long as we filter before BeefyWorker it still works, but filtering out invalid justif early avoids us caching and forwarding invalid justifications. I will fix this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

filtering out invalid justif early avoids us caching and forwarding invalid justifications. I will fix this.

done

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because we don't want to tie BEEFY with GRANDPA logic:

  1. the BEEFY justification is removed from list going downstream,
  2. once all inner finish successfully, justification is manually appended to backend if it is valid.

.runtime_api()
.validator_set(&block_id)
.map_err(|e| ConsensusError::ClientImport(e.to_string()))?
// Deploying `BeefyBlockImport` on chains with dummy BeefyApi will
Copy link
Contributor

Choose a reason for hiding this comment

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

Dumb q: is it really rejecting such blocks, or just ignore BEEFY justifications? I'm asking because inner.import_block() has been (successfully) called already and here we just returning error here. Sorry - many things have changed in block import since I've last checked it, so maybe there's some transactional mechanism here, but I haven't found it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also (I believe you've said about that yesterday) - you're going to implement mandatory justifications requests in follow-up PRs, right? There's still needs_justification field in the import result - it won't be used in BEEFY pipeline, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought it actually rejected the block, even if inner components have accepted/imported it. I'll look into it.

you're going to implement mandatory justifications requests in follow-up PRs, right? There's still needs_justification field in the import result - it won't be used in BEEFY pipeline, right?

Right, we'll have our own on-demand justifications protocol, so we won't be using needs_justification (or any of the client-sync code I believe). I believe @andresilva has more details here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the block is still imported, only an error log and event are emitted:

Err(err) => {
warn!(
target: logging_target,
"Error with block built on {:?}: {}", parent_hash, err,
);
telemetry!(
telemetry;
CONSENSUS_WARN;
"slots.err_with_block_built_on";
"hash" => ?parent_hash,
"err" => ?err,
);
},

I'll remove the incorrect comment in beefy block import.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reworked the block import logic now to:

  • check validity early
  • remove BEEFY justification before passing into inner, and manually add it to backend if all layers successful
  • pass justif to worker only if all inner block imports are successful
  • removed the incorrect comment around block not being imported in case of bad justification

I tried to split work in self-sufficient commits for easier review.

client/beefy/src/worker.rs Outdated Show resolved Hide resolved
Copy link
Contributor

@svyatonik svyatonik left a comment

Choose a reason for hiding this comment

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

Everything looks sane to me, great job! I'll give it another (last) look tomorrow :)

let status = self.client.info();
if number <= status.finalized_number &&
Some(hash) ==
self.client
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this .hash() call really required? Can we reuse hash from above here, or is it different? I wonder if following scenario may happen: (1) you have best finalized header = 100 and hash of finalized block 50 = Hash50 (2) then we import alternate header 50 here with hash AlternateHash50 and then when you call client.hash(50) here, it returns Hash50, so we'll try to append justification for AlternateHash50 to Hash50.

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 check is to protect against exactly that:
number and hash are the block number and hash of the currently importing block that came with current justification.

Right before actually importing justification we double-check that this block was in fact finalized (by GRANDPA or whatever finality gadget is underneath - we don't want to import any BEEFY justification for non-final blocks):

  1. number <= status.finalized_number verifies that this block could be in the canonical fork/chain,
  2. while Some(hash) == self.client.hash(number) builds on top of that and verifies this hash is part of the best fork (which is also finalized as checked in 1.)

(1) you have best finalized header = 100 and hash of finalized block 50 = Hash50 (2) then we import alternate header 50 here with hash AlternateHash50 and then when you call client.hash(50) here, it returns Hash50, so we'll try to append justification for AlternateHash50 to Hash50.

say we import both AlternateHash50 and Hash50:
only Hash50 == self.client.hash(50) when 50 <= status.finalized_number, so we drop any justification for AlternateHash50 (and log error - it can only happen if authorities are double voting) and only import one from Hash50.

In theory this should not be possible (BEEFY justifications should only be possible for finalized blocks, never for alternates), but this check is here as an extra layer of defence.

Copy link
Contributor

@svyatonik svyatonik left a comment

Choose a reason for hiding this comment

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

LGTM!

Copy link
Contributor

@andresilva andresilva left a comment

Choose a reason for hiding this comment

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

looks good to me. great work @acatangiu


match (beefy_proof, &inner_import_result) {
(Some(proof), ImportResult::Imported(_)) => {
let status = self.client.info();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can avoid using the client here if we do instead self.backend.blockchain().info() (I didn't check though).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice! This way I could completely remove the need for Client: HeaderBackend generic from the block import object 👍

client/beefy/src/import.rs Show resolved Hide resolved
@acatangiu
Copy link
Contributor Author

bot merge

@paritytech-processbot paritytech-processbot bot merged commit 494a288 into paritytech:master Jul 29, 2022
DaviRain-Su pushed a commit to octopus-network/substrate that referenced this pull request Aug 23, 2022
…port justifications (paritytech#11821)

* client/beefy: don't accept vote for older rounds

* client/beefy: clean up and reorg the worker struct

* client/beefy: first step towards Full BEEFY

The first step from Lean->Full BEEFY is to have the worker
enforce uninterrupted line of BEEFY finalized mandatory blocks.

There is one mandatory block per session (the first block in the
session). As such, votes processing and votes generation now
enforces that all mandatory blocks are finalized in strict
monotonically increasing sequence and no block 'N' will be worked
on if there is any GRANDPA finalized but BEEFY non-final mandatory
block 'M', where 'M < N'.

Implementation details:

- Introduced 'VoterOracle' to separate the voting decisions logic,
  and track new/pending sessions.

- New sessions get queued up with the worker operating either:
  1. up-to-date - all mandatory blocks leading up to current GRANDPA
     finalized: queue has ONE element, the 'current session' where
     `mandatory_done == true`,
  2. lagging behind GRANDPA: queue has [1, N] elements, where all
     `mandatory_done == false`.
     In this state, everytime a session gets its mandatory block
     BEEFY finalized, the session is popped off the queue,
     eventually getting to operating mode `1. up-to-date`.

- Votes get triaged and those that fall withing the `VoterOracle`
  allowed window get processed, the others get dropped if stale,
  or buffered for later processing (when they reach the window).

- Worker general code was also updated to fall in one of two roles:
  1. react to external events and change internal 'state',
  2. generate events/votes based on internal 'state'.

Signed-off-by: acatangiu <[email protected]>

* client/beefy: sketch idea for block import and sync

Signed-off-by: acatangiu <[email protected]>

* client/beefy: add BEEFY block import

* client/beefy: process justifications from block import

* client/beefy: add TODOs for sync protocol

* client/beefy: add more docs and comments

* client/beefy-rpc: fix RPC error

* client/beefy: verify justification validity on block import

* client/beefy: more tests

* client/beefy: small fixes

- first handle and note the self vote before gossiping it,
- don't shortcircuit on err when processing pending votes.

* client/beefy: remove invalid justifications at block import

* todo: beefy block import tests

* RFC: ideas for multiple justifications per block

* Revert "RFC: ideas for multiple justifications per block"

This reverts commit 8256fb0.

* client/beefy: append justif to backend on block import

* client/beefy: groundwork for block import test

* client/beefy: groundwork2 for block import test

* client/beefy: groundwork3 for block import test

* client/beefy: add block import test

* client/beefy: add required trait bounds to block import builder

* remove client from beefy block import, backend gets the job done

Signed-off-by: acatangiu <[email protected]>
@acatangiu acatangiu deleted the beefy-import-justif branch December 13, 2022 08:39
ark0f pushed a commit to gear-tech/substrate that referenced this pull request Feb 27, 2023
…port justifications (paritytech#11821)

* client/beefy: don't accept vote for older rounds

* client/beefy: clean up and reorg the worker struct

* client/beefy: first step towards Full BEEFY

The first step from Lean->Full BEEFY is to have the worker
enforce uninterrupted line of BEEFY finalized mandatory blocks.

There is one mandatory block per session (the first block in the
session). As such, votes processing and votes generation now
enforces that all mandatory blocks are finalized in strict
monotonically increasing sequence and no block 'N' will be worked
on if there is any GRANDPA finalized but BEEFY non-final mandatory
block 'M', where 'M < N'.

Implementation details:

- Introduced 'VoterOracle' to separate the voting decisions logic,
  and track new/pending sessions.

- New sessions get queued up with the worker operating either:
  1. up-to-date - all mandatory blocks leading up to current GRANDPA
     finalized: queue has ONE element, the 'current session' where
     `mandatory_done == true`,
  2. lagging behind GRANDPA: queue has [1, N] elements, where all
     `mandatory_done == false`.
     In this state, everytime a session gets its mandatory block
     BEEFY finalized, the session is popped off the queue,
     eventually getting to operating mode `1. up-to-date`.

- Votes get triaged and those that fall withing the `VoterOracle`
  allowed window get processed, the others get dropped if stale,
  or buffered for later processing (when they reach the window).

- Worker general code was also updated to fall in one of two roles:
  1. react to external events and change internal 'state',
  2. generate events/votes based on internal 'state'.

Signed-off-by: acatangiu <[email protected]>

* client/beefy: sketch idea for block import and sync

Signed-off-by: acatangiu <[email protected]>

* client/beefy: add BEEFY block import

* client/beefy: process justifications from block import

* client/beefy: add TODOs for sync protocol

* client/beefy: add more docs and comments

* client/beefy-rpc: fix RPC error

* client/beefy: verify justification validity on block import

* client/beefy: more tests

* client/beefy: small fixes

- first handle and note the self vote before gossiping it,
- don't shortcircuit on err when processing pending votes.

* client/beefy: remove invalid justifications at block import

* todo: beefy block import tests

* RFC: ideas for multiple justifications per block

* Revert "RFC: ideas for multiple justifications per block"

This reverts commit 8256fb0.

* client/beefy: append justif to backend on block import

* client/beefy: groundwork for block import test

* client/beefy: groundwork2 for block import test

* client/beefy: groundwork3 for block import test

* client/beefy: add block import test

* client/beefy: add required trait bounds to block import builder

* remove client from beefy block import, backend gets the job done

Signed-off-by: acatangiu <[email protected]>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A0-please_review Pull request needs code review. B0-silent Changes should not be mentioned in any release notes C1-low PR touches the given topic and has a low impact on builders. D2-notlive 💤 PR contains changes in a runtime directory that is not deployed to a chain that requires an audit.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

BEEFY voters should block on mandatory blocks BEEFY clients should import BEEFY justifications
3 participants