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

Updates to Provisioner Guide for Async Backing #7106

Merged
29 changes: 13 additions & 16 deletions node/core/provisioner/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use ::test_helpers::{dummy_candidate_descriptor, dummy_hash};
use bitvec::bitvec;
use polkadot_primitives::{OccupiedCore, ScheduledCore};

const MOCK_GROUP_SIZE: usize = 5;

pub fn occupied_core(para_id: u32) -> CoreState {
CoreState::Occupied(OccupiedCore {
group_responsible: para_id.into(),
Expand Down Expand Up @@ -46,8 +48,8 @@ where
CoreState::Occupied(core)
}

pub fn default_bitvec(n_cores: usize) -> CoreAvailability {
bitvec![u8, bitvec::order::Lsb0; 0; n_cores]
pub fn default_bitvec(size: usize) -> CoreAvailability {
bitvec![u8, bitvec::order::Lsb0; 0; size]
}

pub fn scheduled_core(id: u32) -> ScheduledCore {
Expand Down Expand Up @@ -236,7 +238,7 @@ pub(crate) mod common {
mod select_candidates {
use super::{
super::*, build_occupied_core, common::test_harness, default_bitvec, occupied_core,
scheduled_core,
scheduled_core, MOCK_GROUP_SIZE,
};
use ::test_helpers::{dummy_candidate_descriptor, dummy_hash};
use futures::channel::mpsc;
Expand Down Expand Up @@ -400,7 +402,6 @@ mod select_candidates {
#[test]
fn selects_correct_candidates() {
let mock_cores = mock_availability_cores();
let n_cores = mock_cores.len();

let empty_hash = PersistedValidationData::<Hash, BlockNumber>::default().hash();

Expand Down Expand Up @@ -450,12 +451,12 @@ mod select_candidates {
commitments: Default::default(),
},
validity_votes: Vec::new(),
validator_indices: default_bitvec(n_cores),
validator_indices: default_bitvec(MOCK_GROUP_SIZE),
})
.collect();

test_harness(
|r| mock_overseer(r, expected_backed, ProspectiveParachainsMode::Disabled),
|r| mock_overseer(r, expected_backed, prospective_parachains_mode),
|mut tx: TestSubsystemSender| async move {
let result = select_candidates(
&mock_cores,
Expand All @@ -482,7 +483,6 @@ mod select_candidates {
#[test]
fn selects_max_one_code_upgrade() {
let mock_cores = mock_availability_cores();
let n_cores = mock_cores.len();

let empty_hash = PersistedValidationData::<Hash, BlockNumber>::default().hash();

Expand Down Expand Up @@ -512,13 +512,15 @@ mod select_candidates {
})
.collect();

// Input to select_candidates
let candidates: Vec<_> = committed_receipts.iter().map(|r| r.to_plain()).collect();
// Build possible outputs from select_candidates
let backed_candidates: Vec<_> = committed_receipts
.iter()
.map(|committed_receipt| BackedCandidate {
candidate: committed_receipt.clone(),
validity_votes: Vec::new(),
validator_indices: default_bitvec(n_cores),
validator_indices: default_bitvec(MOCK_GROUP_SIZE),
})
.collect();

Expand All @@ -532,7 +534,7 @@ mod select_candidates {
let prospective_parachains_mode = ProspectiveParachainsMode::Disabled;

test_harness(
|r| mock_overseer(r, expected_backed, ProspectiveParachainsMode::Disabled),
|r| mock_overseer(r, expected_backed, prospective_parachains_mode),
|mut tx: TestSubsystemSender| async move {
let result = select_candidates(
&mock_cores,
Expand Down Expand Up @@ -561,8 +563,6 @@ mod select_candidates {
#[test]
fn request_from_prospective_parachains() {
let mock_cores = mock_availability_cores();
let n_cores = mock_cores.len();

let empty_hash = PersistedValidationData::<Hash, BlockNumber>::default().hash();

let mut descriptor_template = dummy_candidate_descriptor(dummy_hash());
Expand Down Expand Up @@ -596,7 +596,7 @@ mod select_candidates {
commitments: Default::default(),
},
validity_votes: Vec::new(),
validator_indices: default_bitvec(n_cores),
validator_indices: default_bitvec(MOCK_GROUP_SIZE),
})
.collect();

Expand All @@ -605,10 +605,7 @@ mod select_candidates {
mock_overseer(
r,
expected_backed,
ProspectiveParachainsMode::Enabled {
max_candidate_depth: 0,
allowed_ancestry_len: 0,
},
prospective_parachains_mode,
)
},
|mut tx: TestSubsystemSender| async move {
Expand Down
126 changes: 99 additions & 27 deletions roadmap/implementers-guide/src/node/utility/provisioner.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

Relay chain block authorship authority is governed by BABE and is beyond the scope of the Overseer and the rest of the subsystems. That said, ultimately the block author needs to select a set of backable parachain candidates and other consensus data, and assemble a block from them. This subsystem is responsible for providing the necessary data to all potential block authors.

A major feature of the provisioner: this subsystem is responsible for ensuring that parachain block candidates are sufficiently available before sending them to potential block authors.

## Provisionable Data

There are several distinct types of provisionable data, but they share this property in common: all should eventually be included in a relay chain block.

### Backed Candidates

The block author can choose 0 or 1 backed parachain candidates per parachain; the only constraint is that each backed candidate has the appropriate relay parent. However, the choice of a backed candidate must be the block author's; the provisioner must ensure that block authors are aware of all available [`BackedCandidate`s](../../types/backing.md#backed-candidate).
The block author can choose 0 or 1 backed parachain candidates per parachain; the only constraint is that each backable candidate has the appropriate relay parent. However, the choice of a backed candidate must be the block author's.
BradleyOlson64 marked this conversation as resolved.
Show resolved Hide resolved

### Signed Bitfields

Expand Down Expand Up @@ -45,37 +43,17 @@ Block authors request the inherent data they should use for constructing the inh

## Block Production

When a validator is selected by BABE to author a block, it becomes a block producer. The provisioner is the subsystem best suited to choosing which specific backed candidates and availability bitfields should be assembled into the block. To engage this functionality, a `ProvisionerMessage::RequestInherentData` is sent; the response is a [`ParaInherentData`](../../types/runtime.md#parainherentdata). There are never two distinct parachain candidates included for the same parachain and that new parachain candidates cannot be backed until the previous one either gets declared available or expired. Appropriate bitfields, as outlined in the section on [bitfield selection](#bitfield-selection), and any dispute statements should be attached as well.
When a validator is selected by BABE to author a block, it becomes a block producer. The provisioner is the subsystem best suited to choosing which specific backed candidates and availability bitfields should be assembled into the block. To engage this functionality, a `ProvisionerMessage::RequestInherentData` is sent; the response is a [`ParaInherentData`](../../types/runtime.md#parainherentdata). Each relay chain block backs at most one backable parachain block candidate per parachain. Additionally no farther block candidate can be backed until the previous one either gets declared available or expired. Appropriate bitfields, as outlined in the section on [bitfield selection](#bitfield-selection), and any dispute statements should be attached as well.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:

Suggested change
When a validator is selected by BABE to author a block, it becomes a block producer. The provisioner is the subsystem best suited to choosing which specific backed candidates and availability bitfields should be assembled into the block. To engage this functionality, a `ProvisionerMessage::RequestInherentData` is sent; the response is a [`ParaInherentData`](../../types/runtime.md#parainherentdata). Each relay chain block backs at most one backable parachain block candidate per parachain. Additionally no farther block candidate can be backed until the previous one either gets declared available or expired. Appropriate bitfields, as outlined in the section on [bitfield selection](#bitfield-selection), and any dispute statements should be attached as well.
When a validator is selected by BABE to author a block, it becomes a block producer. The provisioner is the subsystem best suited to choosing which specific backed candidates and availability bitfields should be assembled into the block. To engage this functionality, a `ProvisionerMessage::RequestInherentData` is sent; the response is a [`ParaInherentData`](../../types/runtime.md#parainherentdata). Each relay chain block backs at most one backable parachain block candidate per parachain. Additionally no further block candidate can be backed until the previous one either gets declared available or expired. Appropriate bitfields, as outlined in the section on [bitfield selection](#bitfield-selection), and any dispute statements should be attached as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

Now that we're revisiting this section, it seems worth noting that "declared available" can happen within the same block as the subsequent candidate being backed.


### Bitfield Selection

Our goal with respect to bitfields is simple: maximize availability. However, it's not quite as simple as always including all bitfields; there are constraints which still need to be met:

- We cannot choose more than one bitfield per validator.
- Each bitfield must correspond to an occupied core.
- not more than one bitfield per validator
- each 1 bit must correspond to an occupied core

Beyond that, a semi-arbitrary selection policy is fine. In order to meet the goal of maximizing availability, a heuristic of picking the bitfield with the greatest number of 1 bits set in the event of conflict is useful.

### Candidate Selection

The goal of candidate selection is to determine which cores are free, and then to the degree possible, pick a candidate appropriate to each free core.

To determine availability:

- Get the list of core states from the runtime API
- For each core state:
- On `CoreState::Scheduled`, then we can make an `OccupiedCoreAssumption::Free`.
- On `CoreState::Occupied`, then we may be able to make an assumption:
- If the bitfields indicate availability and there is a scheduled `next_up_on_available`, then we can make an `OccupiedCoreAssumption::Included`.
- If the bitfields do not indicate availability, and there is a scheduled `next_up_on_time_out`, and `occupied_core.time_out_at == block_number_under_production`, then we can make an `OccupiedCoreAssumption::TimedOut`.
- If we did not make an `OccupiedCoreAssumption`, then continue on to the next core.
- Now compute the core's `validation_data_hash`: get the `PersistedValidationData` from the runtime, given the known `ParaId` and `OccupiedCoreAssumption`;
- Find an appropriate candidate for the core.
- There are two constraints: `backed_candidate.candidate.descriptor.para_id == scheduled_core.para_id && candidate.candidate.descriptor.validation_data_hash == computed_validation_data_hash`.
- In the event that more than one candidate meets the constraints, selection between the candidates is arbitrary. However, not more than one candidate can be selected per core.

The end result of this process is a vector of `BackedCandidate`s, sorted in order of their core index. Furthermore, this process should select at maximum one candidate which upgrades the runtime validation code.

### Dispute Statement Selection

This is the point at which the block author provides further votes to active disputes or initiates new disputes in the runtime state.
Expand All @@ -100,6 +78,80 @@ To compute bitfield availability, then:
- Update the availability. Conceptually, assuming bit vectors: `availability[validator_index] |= bitfield[core_idx]`
- Availability has a 2/3 threshold. Therefore: `3 * availability.count_ones() >= 2 * availability.len()`

### Candidate Selection: Prospective Parachains Mode

The state of the provisioner `PerRelayParent` tracks an important setting, `ProspectiveParachainsMode`. This setting determines which backable candidate selection method the provisioner uses.

`ProspectiveParachainsMode::Disabled` - The provisioner uses its own internal legacy candidate selection.
`ProspectiveParachainsMode::Enabled` - The provisioner requests that [prospective parachains](../backing/prospective-parachains.md) provide selected candidates.

Candidates selected with `ProspectiveParachainsMode::Enabled` are able to benefit from the increased block production time asynchronous backing allows. For this reason, once asynchronous backing has been sufficiently tested the code related to legacy candidate selection will be removed.
Copy link
Contributor

@rphmeier rphmeier Apr 20, 2023

Choose a reason for hiding this comment

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

Not just tested, but rather once the legacy code is obsolete.


### Prospective Parachains Candidate Selection

The goal of candidate selection is to determine which cores are free, and then to the degree possible, pick a candidate appropriate to each free core. In prospective parachains candidate selection the provisioner handles the former process while [prospective parachains](../backing/prospective-parachains.md) handles the latter.

To select backable candidates:

- Get the list of core states from the runtime API
- For each core state:
- On `CoreState::Free`
- The core is unscheduled and doesn’t need to be provisioned with a candidate
- On `CoreState::Scheduled`
- The availability core is scheduled to secure availability for the next block for a particular `para_id`. Also the core is not currently occupied by a candidate pending availability.
BradleyOlson64 marked this conversation as resolved.
Show resolved Hide resolved
- The provisioner requests a backable candidate from [prospective parachains](../backing/prospective-parachains.md) with the desired relay parent, the core’s scheduled `para_id`, and an empty required path.
- On `CoreState::Occupied`
- The availability core is occupied by a parachain block candidate pending availability. A further candidate need not be provided by the provisioner unless the core will be vacated this block. This is the case when either bitfields indicate the current core occupant has been made available or a timeout is reached.
- If `bitfields_indicate_availability`
- If `Some(scheduled_core) = occupied_core.next_up_on_available`, the core will be vacated and in need of a provisioned candidate. The provisioner requests a backable candidate from [prospective parachains](../backing/prospective-parachains.md) with the core’s scheduled `para_id` and a required path with one entry. This entry corresponds to the parablock candidate previously occupying this core, which was made available and can be built upon even though it hasn’t been seen as included in a relay chain block yet. See the Required Path section below for more detail.
- If `occupied_core.next_up_on_available` is `None`, then the core being vacated is unscheduled and doesn’t need to be provisioned with a candidate.
- Else-if `occupied_core.time_out_at == block_number`
- If `Some(scheduled_core) = occupied_core.next_up_on_timeout`, the core will be vacated and in need of a provisioned candidate. A candidate is requested in exactly the same way as with `CoreState::Scheduled`.
- Else the core being vacated is unscheduled and doesn’t need to be provisioned with a candidate


BradleyOlson64 marked this conversation as resolved.
Show resolved Hide resolved
The end result of this process is a vector of `CandidateHash`s, sorted in order of their core index.

Required Path:
BradleyOlson64 marked this conversation as resolved.
Show resolved Hide resolved

Required path is a parameter for `ProspectiveParachainsMessage::GetBackableCandidate`, which the provisioner sends in candidate selection.

An empty required path indicates that the requested candidate should be a direct child of the most recent parablock for the given `para_id` as of the given relay parent.
Copy link
Contributor

Choose a reason for hiding this comment

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

let's say "most recently included"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That does sound better!


In contrast, a required path with one or more entries prompts [prospective parachains](../backing/prospective-parachains.md) to step forward through its fragment tree for the given `para_id` and relay parent until the desired parablock is reached. We then select a direct child of that parablock to pass to the provisioner.

The parablocks making up a required path do not need to have been previously seen as included in relay chain blocks. Thus the ability to provision backable candidates based on a required path effectively decouples backing from inclusion, resulting in the tremendous performance increase of Asynchronous Backing.
BradleyOlson64 marked this conversation as resolved.
Show resolved Hide resolved

> TODO: The provisioner’s use of `required_path` doesn't work for parathreads, since no candidate will be provided if `scheduled_core.para_id != occupied_core.candidate_descriptor.para_id`. We lean hard on the assumption that cores are fixed to specific parachains within a session. https://github.com/paritytech/polkadot/issues/5492
BradleyOlson64 marked this conversation as resolved.
Show resolved Hide resolved

### Legacy Candidate Selection

Legacy candidate selection takes place in the provisioner. Thus the provisioner needs to keep an up to date record of all [backed_candidates](../../types/backing.md#backed-candidate) `PerRelayParent` to pick from.

The goal of candidate selection is to determine which cores are free, and then to the degree possible, pick a candidate appropriate to each free core.

To determine availability:

- Get the list of core states from the runtime API
- For each core state:
- On `CoreState::Scheduled`, then we can make an `OccupiedCoreAssumption::Free`.
- On `CoreState::Occupied`, then we may be able to make an assumption:
- If the bitfields indicate availability and there is a scheduled `next_up_on_available`, then we can make an `OccupiedCoreAssumption::Included`.
- If the bitfields do not indicate availability, and there is a scheduled `next_up_on_time_out`, and `occupied_core.time_out_at == block_number_under_production`, then we can make an `OccupiedCoreAssumption::TimedOut`.
- If we did not make an `OccupiedCoreAssumption`, then continue on to the next core.
- Now compute the core's `validation_data_hash`: get the `PersistedValidationData` from the runtime, given the known `ParaId` and `OccupiedCoreAssumption`;
- Find an appropriate candidate for the core.
- There are two constraints: `backed_candidate.candidate.descriptor.para_id == scheduled_core.para_id && candidate.candidate.descriptor.validation_data_hash == computed_validation_data_hash`.
- In the event that more than one candidate meets the constraints, selection between the candidates is arbitrary. However, not more than one candidate can be selected per core.

The end result of this process is a vector of `CandidateHash`s, sorted in order of their core index.

### Retrieving Full `BackedCandidate`s for Selected Hashes

Legacy candidate selection and prospective parachains candidate selection both leave us with a vector of `CandidateHash`s. These are passed to the backing subsystem with `CandidateBackingMessage::GetBackedCandidates`.

The response is a vector of `BackedCandidate`s, sorted in order of their core index and ready to be provisioned to block authoring. The candidate selection and retrieval process should select at maximum one candidate which upgrades the runtime validation code.

### Notes

See also: [Scheduler Module: Availability Cores](../../runtime/scheduler.md#availability-cores).
Expand All @@ -121,6 +173,26 @@ The subsystem should maintain a set of handles to Block Authorship Provisioning

Forward the message to the appropriate Block Authorship Provisioning Job, or discard if no appropriate job is currently active.

## Block Authorship Provisioning Job
### Block Authorship Provisioning Job
BradleyOlson64 marked this conversation as resolved.
Show resolved Hide resolved

Maintain the set of channels to block authors. On receiving provisionable data, send a copy over each channel.

## Glossary

- **Relay-parent:**
- A particular relay-chain block to which a process, perspective, and/or subset of state is linked.
BradleyOlson64 marked this conversation as resolved.
Show resolved Hide resolved
- **Active Leaf:**
- A relay chain block which is the head of an active fork of the relay chain.
- Block authorship provisioning jobs are spawned per active leaf and concluded for any leaves which become inactive.
- **Candidate Selection:**
- The process by which the provisioner selects backable parachain block candidates to pass to block authoring.
- Two versions, prospective parachains candidate selection and legacy candidate selection. See their respective protocol sections for details.
- **Availability Core:**
- Often referred to simply as "cores", availability cores are an abstraction used for resource management. For the provisioner, availability cores are most relevant in that core states determine which `para_id`s to provision backable candidates for.
- For more on availability cores see [scheduler](../../runtime/scheduler.md)
- **Availability Bitfield:**
- Often referred to simply as a "bitfield", an availability bitfield represents the view of parablock candidate availability from a particular validator's perspective. Each bit in the bitfield corresponds to a single [availability core](../runtime-api/availability-cores.md).
- For more on availability bitfields see [availability](../../types/availability.md)
- **Backable vs. Backed:**
- Note that we sometimes use "backed" to refer to candidates that are "backable", but not yet backed on chain.
- Backable means that a quorum of the candidate's assigned backing group have provided signed affirming statements.