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

implement custom proposer #1320

Merged
merged 65 commits into from
Jul 5, 2020
Merged
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
54873ea
network bridge skeleton
rphmeier Jun 17, 2020
5eea3bf
move some primitives around and add debug impls
rphmeier Jun 17, 2020
27456b5
protocol registration glue & abstract network interface
rphmeier Jun 17, 2020
3bf17fa
add send_msgs to subsystemctx
rphmeier Jun 18, 2020
e05abd2
select logic
rphmeier Jun 18, 2020
ba005c9
transform different events into actions and handle
rphmeier Jun 18, 2020
7572bba
implement remaining network bridge state machine
rphmeier Jun 18, 2020
24a91c4
start test skeleton
rphmeier Jun 22, 2020
064a00c
make network methods asynchronous
rphmeier Jun 22, 2020
c116061
extract subsystem out to subsystem crate
rphmeier Jun 24, 2020
e6162de
port over overseer to subsystem context trait
rphmeier Jun 24, 2020
844a9d1
fix minimal example
rphmeier Jun 24, 2020
cae1561
fix overseer doc test
rphmeier Jun 24, 2020
2c9be73
update network-bridge crate
rphmeier Jun 24, 2020
534535f
write a subsystem test-helpers crate
rphmeier Jun 25, 2020
8ac6269
write a network test helper for network-bridge
rphmeier Jun 25, 2020
51ff607
set up (broken) view test
rphmeier Jun 25, 2020
960029b
Revamp network to be more async-friendly and not require Sync
rphmeier Jun 25, 2020
eb52f9a
fix spacing
rphmeier Jun 25, 2020
f6526c4
fix test compilation
rphmeier Jun 25, 2020
0f5a1b1
insert side-channel for actions
rphmeier Jun 25, 2020
8f75746
Add some more message types to AllMessages
rphmeier Jun 25, 2020
940216b
introduce a test harness
rphmeier Jun 25, 2020
a20cfd5
impl ProvideInherent for InclusionInherent
coriolinus Jun 26, 2020
5c44804
reduce import churn; correct expect message
coriolinus Jun 26, 2020
6efd05a
move inclusion inherent identifier into primitives
coriolinus Jun 26, 2020
fedd931
bump kusama spec_version to placate CI
coriolinus Jun 26, 2020
d696e1b
copy sc_basic_authorship::{ProposerFactory, Proposer}
coriolinus Jun 26, 2020
9f05198
use polkadot custom proposer instead of basic-authorship one
coriolinus Jun 26, 2020
859bd8b
add some tests
rphmeier Jun 26, 2020
c504d12
Merge branch 'master' into rh-network-bridge
rphmeier Jun 26, 2020
4343f5c
ensure service compiles and passes tests
rphmeier Jun 26, 2020
98c4c54
fix typo
rphmeier Jun 26, 2020
d194838
fix service-new compilation
rphmeier Jun 26, 2020
d468a2b
Subsystem test helpers send messages synchronously
rphmeier Jun 27, 2020
b676adc
remove smelly action inspector
rphmeier Jun 27, 2020
7d44a62
remove superfluous let binding
rphmeier Jun 27, 2020
989e8b5
fix warnings
rphmeier Jun 27, 2020
dffb0ab
Merge remote-tracking branch 'origin/master' into prgn-custom-proposer
coriolinus Jun 29, 2020
da07b9e
add license header
coriolinus Jun 29, 2020
3110bf8
empty commit; maybe github will notice the one with changes
coriolinus Jun 29, 2020
64792f7
Update node/network/bridge/src/lib.rs
rphmeier Jun 29, 2020
6b88341
add sanity check to only include valid inherents
coriolinus Jun 30, 2020
2b6a7d0
stub: encapsulate block production mechanics instead of copying them
coriolinus Jun 30, 2020
01f2cd9
Merge remote-tracking branch 'origin/prgn-inclusioninherent-providein…
coriolinus Jun 30, 2020
d2af626
partial implementation of propose fn
coriolinus Jun 30, 2020
50b51dd
fix compilation
rphmeier Jun 30, 2020
fd1dea5
Merge remote-tracking branch 'origin/rh-network-bridge' into prgn-cus…
coriolinus Jun 30, 2020
2ab09a7
Merge remote-tracking branch 'origin/rh-network-bridge' into prgn-cus…
coriolinus Jun 30, 2020
ad5cbc8
clear a few more compile errors
coriolinus Jun 30, 2020
4577215
Merge remote-tracking branch 'origin/master' into prgn-custom-proposer
coriolinus Jul 1, 2020
5bcf147
finish fn propose
coriolinus Jul 1, 2020
2377744
broken: add timeout to proposal
coriolinus Jul 1, 2020
af699dd
add timeout to proposal
coriolinus Jul 1, 2020
3642dce
guide: provisioner is responsible for selecting parachain candidates
coriolinus Jul 1, 2020
75190d8
implement ProvisionerMessage::RequestInherentData & update fn propose
coriolinus Jul 1, 2020
3063303
Merge remote-tracking branch 'origin/master' into prgn-custom-proposer
coriolinus Jul 2, 2020
6e41882
impl CreateProposer::init; clean up
coriolinus Jul 2, 2020
cfec8c2
impl std::error::Error for Error
coriolinus Jul 2, 2020
6092ba0
document error-handling rationale
coriolinus Jul 2, 2020
c78dd28
cause polkadot-service-new to compile correctly
coriolinus Jul 2, 2020
d37e89e
Move potentially-blocking call from fn init -> fn propose
coriolinus Jul 3, 2020
b91be19
document struct Proposer
coriolinus Jul 3, 2020
9bae56e
extract provisioner data fetch
coriolinus Jul 3, 2020
0b0b86c
Merge branch 'master' into prgn-custom-proposer
gavofyork Jul 5, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ members = [
"service",
"validation",

"node/core/proposer",
"node/network/bridge",
"node/overseer",
"node/primitives",
Expand Down
27 changes: 27 additions & 0 deletions node/core/proposer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "polkadot-node-core-proposer"
version = "0.1.0"
authors = ["Parity Technologies <[email protected]>"]
edition = "2018"

[dependencies]
futures = "0.3.4"
futures-timer = "3.0.1"
log = "0.4.8"
parity-scale-codec = "1.3.0"
polkadot-node-subsystem = { path = "../../subsystem" }
polkadot-overseer = { path = "../../overseer" }
polkadot-primitives = { path = "../../../primitives" }
sc-basic-authorship = { git = "https://github.com/paritytech/substrate", branch = "master" }
sc-block-builder = { git = "https://github.com/paritytech/substrate", branch = "master" }
sc-client-api = { git = "https://github.com/paritytech/substrate", branch = "master" }
sc-telemetry = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-api = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-blockchain = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-consensus = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-inherents = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-transaction-pool = { git = "https://github.com/paritytech/substrate", branch = "master" }
tokio-executor = { version = "0.2.0-alpha.6", features = ["blocking"] }
wasm-timer = "0.2.4"
215 changes: 215 additions & 0 deletions node/core/proposer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
use futures::prelude::*;
use futures::select;
use polkadot_node_subsystem::{messages::{AllMessages, ProvisionerInherentData, ProvisionerMessage}, SubsystemError};
use polkadot_overseer::OverseerHandler;
use polkadot_primitives::{
inclusion_inherent,
parachain::ParachainHost,
Block, Header,
};
use sc_block_builder::{BlockBuilderApi, BlockBuilderProvider};
use sp_api::{ApiExt, ProvideRuntimeApi};
use sp_blockchain::HeaderBackend;
use sp_consensus::{Proposal, RecordProof};
use sp_inherents::InherentData;
use sp_runtime::traits::{DigestFor, HashFor};
use sp_transaction_pool::TransactionPool;
use std::{fmt, pin::Pin, sync::Arc, time};

/// How long proposal can take before we give up and err out
const PROPOSE_TIMEOUT: core::time::Duration = core::time::Duration::from_secs(2);

/// Custom Proposer factory for Polkadot
pub struct ProposerFactory<TxPool, Backend, Client> {
inner: sc_basic_authorship::ProposerFactory<TxPool, Backend, Client>,
overseer: OverseerHandler,
}

impl<TxPool, Backend, Client> ProposerFactory<TxPool, Backend, Client> {
pub fn new(
client: Arc<Client>,
transaction_pool: Arc<TxPool>,
overseer: OverseerHandler,
) -> Self {
ProposerFactory {
inner: sc_basic_authorship::ProposerFactory::new(
client,
transaction_pool,
None,
),
overseer,
}
}
}

impl<TxPool, Backend, Client> sp_consensus::Environment<Block>
for ProposerFactory<TxPool, Backend, Client>
where
TxPool: 'static + TransactionPool<Block = Block>,
Client: 'static
+ BlockBuilderProvider<Backend, Block, Client>
+ ProvideRuntimeApi<Block>
+ HeaderBackend<Block>
+ Send
+ Sync,
Client::Api:
ParachainHost<Block> + BlockBuilderApi<Block> + ApiExt<Block, Error = sp_blockchain::Error>,
Backend:
'static + sc_client_api::Backend<Block, State = sp_api::StateBackendFor<Client, Block>>,
// Rust bug: https://github.com/rust-lang/rust/issues/24159
sp_api::StateBackendFor<Client, Block>: sp_api::StateBackend<HashFor<Block>> + Send,
{
type CreateProposer = Pin<Box<
dyn Future<Output = Result<Self::Proposer, Self::Error>> + Send + 'static,
>>;
type Proposer = Proposer<TxPool, Backend, Client>;
type Error = Error;

fn init(&mut self, parent_header: &Header) -> Self::CreateProposer {
// create the inner proposer
let proposer = self.inner.init(parent_header).into_inner();

// clone the overseer handle (lightweight) so it can be moved into the future
let mut overseer = self.overseer.clone();

// we know that this function will be called at least once per proposed block,
// because this is where the parent header is supplied. Therefore, we can send
// the request for authorship data here.
let (sender, receiver) = futures::channel::oneshot::channel();
let parent_header_hash = parent_header.hash();

async move {
overseer.send_msg(AllMessages::Provisioner(
ProvisionerMessage::RequestInherentData(parent_header_hash, sender),
)).await?;
coriolinus marked this conversation as resolved.
Show resolved Hide resolved
Ok(Proposer {
inner: proposer?,
provisioner_inherent_data: receiver,
})
}.boxed()
}
}

pub struct Proposer<TxPool: TransactionPool<Block = Block>, Backend, Client> {
coriolinus marked this conversation as resolved.
Show resolved Hide resolved
inner: sc_basic_authorship::Proposer<Backend, Block, Client, TxPool>,
provisioner_inherent_data: futures::channel::oneshot::Receiver<ProvisionerInherentData>,
}

impl<TxPool, Backend, Client> sp_consensus::Proposer<Block> for Proposer<TxPool, Backend, Client>
where
TxPool: 'static + TransactionPool<Block = Block>,
Client: 'static
+ BlockBuilderProvider<Backend, Block, Client>
+ ProvideRuntimeApi<Block>
+ HeaderBackend<Block>
+ Send
+ Sync,
Client::Api:
ParachainHost<Block> + BlockBuilderApi<Block> + ApiExt<Block, Error = sp_blockchain::Error>,
Backend:
'static + sc_client_api::Backend<Block, State = sp_api::StateBackendFor<Client, Block>>,
// Rust bug: https://github.com/rust-lang/rust/issues/24159
sp_api::StateBackendFor<Client, Block>: sp_api::StateBackend<HashFor<Block>> + Send,
{
type Transaction = sc_client_api::TransactionFor<Backend, Block>;
type Proposal = Pin<Box<
dyn Future<Output = Result<Proposal<Block, sp_api::TransactionFor<Client, Block>>, Error>> + Send,
>>;
type Error = Error;

fn propose(
self,
mut inherent_data: InherentData,
inherent_digests: DigestFor<Block>,
max_duration: time::Duration,
record_proof: RecordProof,
) -> Self::Proposal {
let mut proposal = async move {
let provisioner_inherent_data = self.provisioner_inherent_data.await.map_err(Error::ClosedChannelFromProvisioner)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

The implication of returning an error here is that the validator node will not author any block. I think that it's better to author an "empty" block (at least in terms of inherent data) than no block, if the overseer or subsystems are unresponsive. Coupled with a critical warning of some kind. This would make the chain more robust.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does it actually make the chain more robust, though? If this inherent data doesn't get injected, the inclusion_inherent module panics:

data.get_data(&Self::INHERENT_IDENTIFIER)
.expect("inclusion inherent data failed to decode")

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 think 9bae56e takes care of this as well; do you agree?


inherent_data.put_data(
inclusion_inherent::INHERENT_IDENTIFIER,
&provisioner_inherent_data,
)?;

self.inner
.propose(inherent_data, inherent_digests, max_duration, record_proof)
.await
.map_err(Into::into)
}
.boxed()
.fuse();

let mut timeout = wasm_timer::Delay::new(PROPOSE_TIMEOUT).fuse();
coriolinus marked this conversation as resolved.
Show resolved Hide resolved

async move {
select! {
proposal_result = proposal => proposal_result,
_ = timeout => Err(Error::Timeout),
}
}
.boxed()
}
}

// It would have been more ergonomic to use thiserror to derive the
// From implementations, Display, and std::error::Error, but unfortunately
// two of the wrapped errors (sp_inherents::Error, SubsystemError) also
// don't impl std::error::Error, which breaks the thiserror derive.
#[derive(Debug)]
pub enum Error {
coriolinus marked this conversation as resolved.
Show resolved Hide resolved
Consensus(sp_consensus::Error),
Blockchain(sp_blockchain::Error),
Inherent(sp_inherents::Error),
Timeout,
ClosedChannelFromProvisioner(futures::channel::oneshot::Canceled),
Subsystem(SubsystemError)
}

impl From<sp_consensus::Error> for Error {
fn from(e: sp_consensus::Error) -> Error {
Error::Consensus(e)
}
}

impl From<sp_blockchain::Error> for Error {
fn from(e: sp_blockchain::Error) -> Error {
Error::Blockchain(e)
}
}

impl From<sp_inherents::Error> for Error {
fn from(e: sp_inherents::Error) -> Error {
Error::Inherent(e)
}
}

impl From<SubsystemError> for Error {
fn from(e: SubsystemError) -> Error {
Error::Subsystem(e)
}
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Consensus(err) => write!(f, "consensus error: {}", err),
Self::Blockchain(err) => write!(f, "blockchain error: {}", err),
Self::Inherent(err) => write!(f, "inherent error: {:?}", err),
Self::Timeout => write!(f, "timeout: provisioner did not return inherent data after {:?}", PROPOSE_TIMEOUT),
Self::ClosedChannelFromProvisioner(err) => write!(f, "provisioner closed inherent data channel before sending: {}", err),
Self::Subsystem(err) => write!(f, "subsystem error: {:?}", err),
}
}
}

impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Consensus(err) => Some(err),
Self::Blockchain(err) => Some(err),
Self::ClosedChannelFromProvisioner(err) => Some(err),
_ => None
}
}
}
2 changes: 1 addition & 1 deletion node/service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ kusama-runtime = { path = "../../runtime/kusama" }
westend-runtime = { path = "../../runtime/westend" }
polkadot-network = { path = "../../network", optional = true }
polkadot-rpc = { path = "../../rpc" }
polkadot-node-core-proposer = { path = "../core/proposer" }
sp-io = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-api = { git = "https://github.com/paritytech/substrate", branch = "master" }
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" }
Expand Down Expand Up @@ -56,7 +57,6 @@ sp-session = { git = "https://github.com/paritytech/substrate", branch = "master
sp-offchain = { package = "sp-offchain", git = "https://github.com/paritytech/substrate", branch = "master" }
prometheus-endpoint = { package = "substrate-prometheus-endpoint", git = "https://github.com/paritytech/substrate", branch = "master" }
frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "master" }
sc-basic-authorship = { git = "https://github.com/paritytech/substrate", branch = "master" }

[dev-dependencies]
polkadot-test-runtime-client = { path = "../../runtime/test-runtime/client" }
Expand Down
7 changes: 4 additions & 3 deletions node/service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use polkadot_subsystem::{
Subsystem, SubsystemContext, SpawnedSubsystem,
messages::{CandidateValidationMessage, CandidateBackingMessage},
};
use polkadot_node_core_proposer::ProposerFactory;
pub use service::{
Role, PruningMode, TransactionPoolOptions, Error, RuntimeGenesis,
TFullClient, TLightClient, TFullBackend, TLightBackend, TFullCallExecutor, TLightCallExecutor,
Expand Down Expand Up @@ -360,6 +361,7 @@ macro_rules! new_full {
.collect();

let (overseer, handler) = real_overseer(leaves, spawner)?;
let handler_clone = handler.clone();

task_manager.spawn_essential_handle().spawn_blocking("overseer", Box::pin(async move {
use futures::{pin_mut, select, FutureExt};
Expand All @@ -386,11 +388,10 @@ macro_rules! new_full {
let can_author_with =
consensus_common::CanAuthorWithNativeVersion::new(client.executor().clone());

// TODO: custom proposer (https://github.com/paritytech/polkadot/issues/1248)
let proposer = sc_basic_authorship::ProposerFactory::new(
let proposer = ProposerFactory::new(
client.clone(),
transaction_pool,
None,
handler_clone,
);

let babe_config = babe::BabeParams {
Expand Down
13 changes: 12 additions & 1 deletion node/subsystem/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use sc_network::{ObservedRole, ReputationChange, PeerId};
use polkadot_primitives::{BlockNumber, Hash, Signature};
use polkadot_primitives::parachain::{
AbridgedCandidateReceipt, PoVBlock, ErasureChunk, BackedCandidate, Id as ParaId,
SignedAvailabilityBitfield, SigningContext, ValidatorId, ValidationCode, ValidatorIndex,
SignedAvailabilityBitfield, SignedAvailabilityBitfields, SigningContext, ValidatorId, ValidationCode, ValidatorIndex,
};
use polkadot_node_primitives::{
MisbehaviorReport, SignedFullStatement, View, ProtocolId,
Expand Down Expand Up @@ -190,6 +190,11 @@ pub enum ProvisionableData {
Dispute(Hash, Signature),
}

/// This data needs to make its way from the provisioner into the InherentData.
///
/// There, it is used to construct the InclusionInherent.
pub type ProvisionerInherentData = (SignedAvailabilityBitfields, Vec<BackedCandidate>);

/// Message to the Provisioner.
///
/// In all cases, the Hash is that of the relay parent.
Expand All @@ -198,6 +203,12 @@ pub enum ProvisionerMessage {
/// This message allows potential block authors to be kept updated with all new authorship data
/// as it becomes available.
RequestBlockAuthorshipData(Hash, mpsc::Sender<ProvisionableData>),
/// This message allows external subsystems to request the set of bitfields and backed candidates
/// associated with a particular potential block hash.
///
/// This is expected to be used by a proposer, to inject that information into the InherentData
/// where it can be assembled into the InclusionInherent.
RequestInherentData(Hash, oneshot::Sender<ProvisionerInherentData>),
/// This data should become part of a relay chain block
ProvisionableData(ProvisionableData),
}
Expand Down
6 changes: 6 additions & 0 deletions roadmap/implementors-guide/src/node/utility/provisioner.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ At initialization, this subsystem has no outputs. Block authors can send a `Prov

Note that block authors must re-send a `ProvisionerMessage::RequestBlockAuthorshipData` for each relay parent they are interested in receiving provisionable data for.

## 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 set of non-conflicting candidates and the appropriate bitfields. Non-conflicting generally means that there are never two distinct parachain candidates included for the same parachain.

One might ask: given `ProvisionerMessage::RequestInherentData`, what's the point of `ProvisionerMessage::RequestBlockAuthorshipData`? The answer is that the block authorship data includes more information than is present in the inherent data; disputes, for example.
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't any consumer of RequestInherentData be interested in disputes 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.

In practice our main consumer is the block authorship logic, which is clearly interested in disputes so it can report them to the chain. I am not aware of any other component which would be interested in disputes or dispute-related reports.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The proposer consumes RequestInherentData, and if it cares about disputes, I haven't yet seen why it does.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, on further inspection we probably don't need the proposer to be aware of disputes. I was thinking about a more antiquated approach where the Proposer constructs the dispute transactions and submits them to the block directly.

It seems that the "right way" to submit reports (based on @andresilva's work in GRANDPA/BABE) is to call a runtime API that submits a transaction to the queue. I guess that will end up being the main role of the MA subsystem - acting as an intermediary between POD reports and the Runtime API subsystem. Maybe we should rename it to Misbehavior Reporting, as most reports are "complete", and the actual dispute resolution / arbitration process will happen on-chain in the Validity module.

Copy link
Contributor

Choose a reason for hiding this comment

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

Then we shouldn't need the dispute records to be present in the Provisioner then either, because the assumption is that anything that the Provisioner is responsible for everything that the Proposer will need. If the Proposer doesn't need them, then the Provisioner shouldn't either. And then I suggest removing RequestBlockAuthorshipData, as I don't see an immediate need for it if there are no disputes and no other intended users of the Provisioner.

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 think I agree with those proposals, but I also think they're out of scope for 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.

Yup, that's fair. We have a discussion on-record to reference now at least

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created an issue from this so we don't lose it: #1362.


## Functionality

The subsystem should maintain a set of handles to Block Authorship Provisioning Jobs that are currently live.
Expand Down
Loading