Dispute distribution implementation (#3282)

* Dispute protocol.

* Dispute distribution protocol.

* Get network requests routed.

* WIP: Basic dispute sender logic.

* Basic validator determination logic.

* WIP: Getting things to typecheck.

* Slightly larger timeout.

* More typechecking stuff.

* Cleanup.

* Finished most of the sending logic.

* Handle active leaves updates

- Cleanup dead disputes
- Update sends for new sessions
- Retry on errors

* Pass sessions in already.

* Startup dispute sending.

* Provide incoming decoding facilities

and use them in statement-distribution.

* Relaxed runtime util requirements.

We only need a `SubsystemSender` not a full `SubsystemContext`.

* Better usability of incoming requests.

Make it possible to consume stuff without clones.

* Add basic receiver functionality.

* Cleanup + fixes for sender.

* One more sender fix.

* Start receiver.

* Make sure to send responses back.

* WIP: Exposed authority discovery

* Make tests pass.

* Fully featured receiver.

* Decrease cost of `NotAValidator`.

* Make `RuntimeInfo` LRU cache size configurable.

* Cache more sessions.

* Fix collator protocol.

* Disable metrics for now.

* Make dispute-distribution a proper subsystem.

* Fix naming.

* Code style fixes.

* Factored out 4x copied mock function.

* WIP: Tests.

* Whitespace cleanup.

* Accessor functions.

* More testing.

* More Debug instances.

* Fix busy loop.

* Working tests.

* More tests.

* Cleanup.

* Fix build.

* Basic receiving test.

* Non validator message gets dropped.

* More receiving tests.

* Test nested and subsequent imports.

* Fix spaces.

* Better formatted imports.

* Import cleanup.

* Metrics.

* Message -> MuxedMessage

* Message -> MuxedMessage

* More review remarks.

* Add missing metrics.rs.

* Fix flaky test.

* Dispute coordinator - deliver confirmations.

* Send out `DisputeMessage` on issue local statement.

* Unwire dispute distribution.

* Review remarks.

* Review remarks.

* Better docs.
This commit is contained in:
Robert Klotzner
2021-07-09 04:29:53 +02:00
committed by GitHub
parent 20993b32b1
commit b5257b2407
52 changed files with 4040 additions and 407 deletions
+140 -42
View File
@@ -28,22 +28,21 @@
use std::collections::HashSet;
use std::sync::Arc;
use polkadot_node_primitives::{CandidateVotes, SignedDisputeStatement};
use polkadot_node_primitives::{CandidateVotes, DISPUTE_WINDOW, DisputeMessage, SignedDisputeStatement, DisputeMessageCheckError};
use polkadot_node_subsystem::{
overseer,
messages::{
DisputeCoordinatorMessage, ChainApiMessage, DisputeParticipationMessage,
},
SubsystemContext, FromOverseer, OverseerSignal, SpawnedSubsystem,
SubsystemError,
overseer, SubsystemContext, FromOverseer, OverseerSignal, SpawnedSubsystem, SubsystemError,
errors::{ChainApiError, RuntimeApiError},
messages::{
ChainApiMessage, DisputeCoordinatorMessage, DisputeDistributionMessage,
DisputeParticipationMessage, ImportStatementsResult
}
};
use polkadot_node_subsystem_util::rolling_session_window::{
RollingSessionWindow, SessionWindowUpdate,
};
use polkadot_primitives::v1::{
SessionIndex, CandidateHash, Hash, CandidateReceipt, DisputeStatement, ValidatorIndex,
ValidatorSignature, BlockNumber, ValidatorPair,
BlockNumber, CandidateHash, CandidateReceipt, DisputeStatement, Hash,
SessionIndex, SessionInfo, ValidatorIndex, ValidatorPair, ValidatorSignature
};
use futures::prelude::*;
@@ -61,10 +60,6 @@ mod tests;
const LOG_TARGET: &str = "parachain::dispute-coordinator";
// It would be nice to draw this from the chain state, but we have no tools for it right now.
// On Polkadot this is 1 day, and on Kusama it's 6 hours.
const DISPUTE_WINDOW: SessionIndex = 6;
struct State {
keystore: Arc<LocalKeystore>,
highest_session: Option<SessionIndex>,
@@ -134,6 +129,9 @@ pub enum Error {
#[error(transparent)]
Oneshot(#[from] oneshot::Canceled),
#[error("Oneshot send failed")]
OneshotSend,
#[error(transparent)]
Subsystem(#[from] SubsystemError),
@@ -308,6 +306,7 @@ async fn handle_incoming(
candidate_receipt,
session,
statements,
pending_confirmation,
} => {
handle_import_statements(
ctx,
@@ -318,6 +317,7 @@ async fn handle_incoming(
candidate_receipt,
session,
statements,
pending_confirmation,
).await?;
}
DisputeCoordinatorMessage::ActiveDisputes(rx) => {
@@ -400,8 +400,13 @@ async fn handle_import_statements(
candidate_receipt: CandidateReceipt,
session: SessionIndex,
statements: Vec<(SignedDisputeStatement, ValidatorIndex)>,
pending_confirmation: oneshot::Sender<ImportStatementsResult>,
) -> Result<(), Error> {
if state.highest_session.map_or(true, |h| session + DISPUTE_WINDOW < h) {
// It is not valid to participate in an ancient dispute (spam?).
pending_confirmation.send(ImportStatementsResult::InvalidImport).map_err(|_| Error::OneshotSend)?;
return Ok(());
}
@@ -479,37 +484,54 @@ async fn handle_import_statements(
let already_disputed = is_disputed && !was_undisputed;
let concluded_valid = votes.valid.len() >= supermajority_threshold;
let mut tx = db::v1::Transaction::default();
{ // Scope so we will only confirm valid import after the import got actually persisted.
let mut tx = db::v1::Transaction::default();
if freshly_disputed && !concluded_valid {
// add to active disputes and begin local participation.
update_active_disputes(
store,
config,
&mut tx,
|active| active.insert(session, candidate_hash),
)?;
if freshly_disputed && !concluded_valid {
ctx.send_message(DisputeParticipationMessage::Participate {
candidate_hash,
candidate_receipt,
session,
n_validators: n_validators as u32,
}).await;
let (report_availability, receive_availability) = oneshot::channel();
ctx.send_message(DisputeParticipationMessage::Participate {
candidate_hash,
candidate_receipt,
session,
n_validators: n_validators as u32,
report_availability,
}).await;
if !receive_availability.await.map_err(Error::Oneshot)? {
pending_confirmation.send(ImportStatementsResult::InvalidImport).map_err(|_| Error::OneshotSend)?;
tracing::debug!(
target: LOG_TARGET,
"Recovering availability failed - invalid import."
);
return Ok(())
}
// add to active disputes and begin local participation.
update_active_disputes(
store,
config,
&mut tx,
|active| active.insert(session, candidate_hash),
)?;
}
if concluded_valid && already_disputed {
// remove from active disputes.
update_active_disputes(
store,
config,
&mut tx,
|active| active.delete(session, candidate_hash),
)?;
}
tx.put_candidate_votes(session, candidate_hash, votes.into());
tx.write(store, &config.column_config())?;
}
if concluded_valid && already_disputed {
// remove from active disputes.
update_active_disputes(
store,
config,
&mut tx,
|active| active.delete(session, candidate_hash),
)?;
}
tx.put_candidate_votes(session, candidate_hash, votes.into());
tx.write(store, &config.column_config())?;
pending_confirmation.send(ImportStatementsResult::ValidImport).map_err(|_| Error::OneshotSend)?;
Ok(())
}
@@ -541,7 +563,7 @@ async fn issue_local_statement(
valid: bool,
) -> Result<(), Error> {
// Load session info.
let validators = match state.rolling_session_window.session_info(session) {
let info = match state.rolling_session_window.session_info(session) {
None => {
tracing::warn!(
target: LOG_TARGET,
@@ -551,9 +573,11 @@ async fn issue_local_statement(
return Ok(())
}
Some(info) => info.validators.clone(),
Some(info) => info,
};
let validators = info.validators.clone();
let votes = db::v1::load_candidate_votes(
store,
&config.column_config(),
@@ -604,8 +628,27 @@ async fn issue_local_statement(
}
}
// Get our message out:
for (statement, index) in &statements {
let dispute_message = match make_dispute_message(info, &votes, statement.clone(), *index) {
Err(err) => {
tracing::debug!(
target: LOG_TARGET,
?err,
"Creating dispute message failed."
);
continue
}
Ok(dispute_message) => dispute_message,
};
ctx.send_message(DisputeDistributionMessage::SendDispute(dispute_message)).await;
}
// Do import
if !statements.is_empty() {
let (pending_confirmation, _rx) = oneshot::channel();
handle_import_statements(
ctx,
store,
@@ -615,12 +658,67 @@ async fn issue_local_statement(
candidate_receipt,
session,
statements,
pending_confirmation,
).await?;
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
enum MakeDisputeMessageError {
#[error("There was no opposite vote available")]
NoOppositeVote,
#[error("Found vote had an invalid validator index that could not be found")]
InvalidValidatorIndex,
#[error("Statement found in votes had invalid signature.")]
InvalidStoredStatement,
#[error(transparent)]
InvalidStatementCombination(DisputeMessageCheckError),
}
fn make_dispute_message(
info: &SessionInfo,
votes: &CandidateVotes,
our_vote: SignedDisputeStatement,
our_index: ValidatorIndex
) -> Result<DisputeMessage, MakeDisputeMessageError> {
let validators = &info.validators;
let (valid_statement, valid_index, invalid_statement, invalid_index) =
if let DisputeStatement::Valid(_) = our_vote.statement() {
let (statement_kind, validator_index, validator_signature)
= votes.invalid.get(0).ok_or(MakeDisputeMessageError::NoOppositeVote)?.clone();
let other_vote = SignedDisputeStatement::new_checked(
DisputeStatement::Invalid(statement_kind),
our_vote.candidate_hash().clone(),
our_vote.session_index(),
validators.get(validator_index.0 as usize).ok_or(MakeDisputeMessageError::InvalidValidatorIndex)?.clone(),
validator_signature,
).map_err(|()| MakeDisputeMessageError::InvalidStoredStatement)?;
(our_vote, our_index, other_vote, validator_index)
} else {
let (statement_kind, validator_index, validator_signature)
= votes.valid.get(0).ok_or(MakeDisputeMessageError::NoOppositeVote)?.clone();
let other_vote = SignedDisputeStatement::new_checked(
DisputeStatement::Valid(statement_kind),
our_vote.candidate_hash().clone(),
our_vote.session_index(),
validators.get(validator_index.0 as usize).ok_or(MakeDisputeMessageError::InvalidValidatorIndex)?.clone(),
validator_signature,
).map_err(|()| MakeDisputeMessageError::InvalidStoredStatement)?;
(other_vote, validator_index, our_vote, our_index)
};
DisputeMessage::from_signed_statements(
valid_statement, valid_index,
invalid_statement, invalid_index,
votes.candidate_receipt.clone(),
info,
).map_err(MakeDisputeMessageError::InvalidStatementCombination)
}
fn determine_undisputed_chain(
store: &dyn KeyValueDB,
config: &Config,
@@ -25,7 +25,10 @@ use polkadot_node_subsystem_test_helpers::{make_subsystem_context, TestSubsystem
use sp_core::testing::TaskExecutor;
use sp_keyring::Sr25519Keyring;
use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr};
use futures::future::{self, BoxFuture};
use futures::{
channel::oneshot,
future::{self, BoxFuture},
};
use parity_scale_codec::Encode;
use assert_matches::assert_matches;
@@ -261,6 +264,7 @@ fn conflicting_votes_lead_to_dispute_participation() {
false,
).await;
let (pending_confirmation, _confirmation_rx) = oneshot::channel();
virtual_overseer.send(FromOverseer::Communication {
msg: DisputeCoordinatorMessage::ImportStatements {
candidate_hash,
@@ -270,9 +274,9 @@ fn conflicting_votes_lead_to_dispute_participation() {
(valid_vote, ValidatorIndex(0)),
(invalid_vote, ValidatorIndex(1)),
],
pending_confirmation,
},
}).await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeParticipation(DisputeParticipationMessage::Participate {
@@ -280,11 +284,13 @@ fn conflicting_votes_lead_to_dispute_participation() {
candidate_receipt: c_receipt,
session: s,
n_validators,
report_availability,
}) => {
assert_eq!(c_hash, candidate_hash);
assert_eq!(c_receipt, candidate_receipt);
assert_eq!(s, session);
assert_eq!(n_validators, test_state.validators.len() as u32);
report_availability.send(true).unwrap();
}
);
@@ -310,6 +316,7 @@ fn conflicting_votes_lead_to_dispute_participation() {
assert_eq!(votes.invalid.len(), 1);
}
let (pending_confirmation, _confirmation_rx) = oneshot::channel();
virtual_overseer.send(FromOverseer::Communication {
msg: DisputeCoordinatorMessage::ImportStatements {
candidate_hash,
@@ -318,6 +325,7 @@ fn conflicting_votes_lead_to_dispute_participation() {
statements: vec![
(invalid_vote_2, ValidatorIndex(2)),
],
pending_confirmation,
},
}).await;
@@ -371,6 +379,7 @@ fn positive_votes_dont_trigger_participation() {
true,
).await;
let (pending_confirmation, _confirmation_rx) = oneshot::channel();
virtual_overseer.send(FromOverseer::Communication {
msg: DisputeCoordinatorMessage::ImportStatements {
candidate_hash,
@@ -379,6 +388,7 @@ fn positive_votes_dont_trigger_participation() {
statements: vec![
(valid_vote, ValidatorIndex(0)),
],
pending_confirmation,
},
}).await;
@@ -404,6 +414,7 @@ fn positive_votes_dont_trigger_participation() {
assert!(votes.invalid.is_empty());
}
let (pending_confirmation, _confirmation_rx) = oneshot::channel();
virtual_overseer.send(FromOverseer::Communication {
msg: DisputeCoordinatorMessage::ImportStatements {
candidate_hash,
@@ -412,6 +423,7 @@ fn positive_votes_dont_trigger_participation() {
statements: vec![
(valid_vote_2, ValidatorIndex(1)),
],
pending_confirmation,
},
}).await;
@@ -472,6 +484,7 @@ fn wrong_validator_index_is_ignored() {
false,
).await;
let (pending_confirmation, _confirmation_rx) = oneshot::channel();
virtual_overseer.send(FromOverseer::Communication {
msg: DisputeCoordinatorMessage::ImportStatements {
candidate_hash,
@@ -481,6 +494,7 @@ fn wrong_validator_index_is_ignored() {
(valid_vote, ValidatorIndex(1)),
(invalid_vote, ValidatorIndex(0)),
],
pending_confirmation,
},
}).await;
@@ -541,6 +555,7 @@ fn finality_votes_ignore_disputed_candidates() {
false,
).await;
let (pending_confirmation, _confirmation_rx) = oneshot::channel();
virtual_overseer.send(FromOverseer::Communication {
msg: DisputeCoordinatorMessage::ImportStatements {
candidate_hash,
@@ -550,9 +565,21 @@ fn finality_votes_ignore_disputed_candidates() {
(valid_vote, ValidatorIndex(0)),
(invalid_vote, ValidatorIndex(1)),
],
pending_confirmation,
},
}).await;
let _ = virtual_overseer.recv().await;
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeParticipation(
DisputeParticipationMessage::Participate {
report_availability,
..
}
) => {
report_availability.send(true).unwrap();
}
);
{
let (tx, rx) = oneshot::channel();
@@ -624,6 +651,7 @@ fn supermajority_valid_dispute_may_be_finalized() {
false,
).await;
let (pending_confirmation, _confirmation_rx) = oneshot::channel();
virtual_overseer.send(FromOverseer::Communication {
msg: DisputeCoordinatorMessage::ImportStatements {
candidate_hash,
@@ -633,6 +661,7 @@ fn supermajority_valid_dispute_may_be_finalized() {
(valid_vote, ValidatorIndex(0)),
(invalid_vote, ValidatorIndex(1)),
],
pending_confirmation,
},
}).await;
@@ -650,12 +679,14 @@ fn supermajority_valid_dispute_may_be_finalized() {
statements.push((vote, ValidatorIndex(i as _)));
};
let (pending_confirmation, _confirmation_rx) = oneshot::channel();
virtual_overseer.send(FromOverseer::Communication {
msg: DisputeCoordinatorMessage::ImportStatements {
candidate_hash,
candidate_receipt: candidate_receipt.clone(),
session,
statements,
pending_confirmation,
},
}).await;
@@ -83,6 +83,9 @@ pub enum Error {
#[error(transparent)]
Oneshot(#[from] oneshot::Canceled),
#[error("Oneshot receiver died")]
OneshotSendFailed,
#[error(transparent)]
Participation(#[from] ParticipationError),
}
@@ -159,6 +162,7 @@ async fn handle_incoming(
candidate_receipt,
session,
n_validators,
report_availability,
} => {
if let Some((_, block_hash)) = state.recent_block {
participate(
@@ -168,6 +172,7 @@ async fn handle_incoming(
candidate_receipt,
session,
n_validators,
report_availability,
)
.await
} else {
@@ -184,6 +189,7 @@ async fn participate(
candidate_receipt: CandidateReceipt,
session: SessionIndex,
n_validators: u32,
report_availability: oneshot::Sender<bool>,
) -> Result<(), Error> {
let (recover_available_data_tx, recover_available_data_rx) = oneshot::channel();
let (code_tx, code_rx) = oneshot::channel();
@@ -203,14 +209,21 @@ async fn participate(
.await;
let available_data = match recover_available_data_rx.await? {
Ok(data) => data,
Ok(data) => {
report_availability.send(true).map_err(|_| Error::OneshotSendFailed)?;
data
}
Err(RecoveryError::Invalid) => {
report_availability.send(true).map_err(|_| Error::OneshotSendFailed)?;
// the available data was recovered but it is invalid, therefore we'll
// vote negatively for the candidate dispute
cast_invalid_vote(ctx, candidate_hash, candidate_receipt, session).await;
return Ok(());
}
Err(RecoveryError::Unavailable) => {
report_availability.send(false).map_err(|_| Error::OneshotSendFailed)?;
return Err(ParticipationError::MissingAvailableData(candidate_hash).into());
}
};
@@ -80,7 +80,7 @@ async fn activate_leaf(virtual_overseer: &mut VirtualOverseer, block_number: Blo
.await;
}
async fn participate(virtual_overseer: &mut VirtualOverseer) {
async fn participate(virtual_overseer: &mut VirtualOverseer) -> oneshot::Receiver<bool> {
let commitments = CandidateCommitments::default();
let candidate_receipt = {
let mut receipt = CandidateReceipt::default();
@@ -91,6 +91,8 @@ async fn participate(virtual_overseer: &mut VirtualOverseer) {
let session = 1;
let n_validators = 10;
let (report_availability, receive_availability) = oneshot::channel();
virtual_overseer
.send(FromOverseer::Communication {
msg: DisputeParticipationMessage::Participate {
@@ -98,12 +100,14 @@ async fn participate(virtual_overseer: &mut VirtualOverseer) {
candidate_receipt: candidate_receipt.clone(),
session,
n_validators,
report_availability,
},
})
.await;
})
.await;
receive_availability
}
async fn recover_available_data(virtual_overseer: &mut VirtualOverseer) {
async fn recover_available_data(virtual_overseer: &mut VirtualOverseer, receive_availability: oneshot::Receiver<bool>) {
let pov_block = PoV {
block_data: BlockData(Vec::new()),
};
@@ -122,6 +126,8 @@ async fn recover_available_data(virtual_overseer: &mut VirtualOverseer) {
},
"overseer did not receive recover available data message",
);
assert_eq!(receive_availability.await.expect("Availability should get reported"), true);
}
async fn fetch_validation_code(virtual_overseer: &mut VirtualOverseer) {
@@ -166,7 +172,7 @@ async fn store_available_data(virtual_overseer: &mut VirtualOverseer, success: b
fn cannot_participate_when_recent_block_state_is_missing() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
participate(&mut virtual_overseer).await;
let _ = participate(&mut virtual_overseer).await;
virtual_overseer
})
@@ -175,7 +181,7 @@ fn cannot_participate_when_recent_block_state_is_missing() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
participate(&mut virtual_overseer).await;
let _ = participate(&mut virtual_overseer).await;
// after activating at least one leaf the recent block
// state should be available which should lead to trying
@@ -199,7 +205,7 @@ fn cannot_participate_if_cannot_recover_available_data() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
participate(&mut virtual_overseer).await;
let receive_availability = participate(&mut virtual_overseer).await;
assert_matches!(
virtual_overseer.recv().await,
@@ -211,6 +217,8 @@ fn cannot_participate_if_cannot_recover_available_data() {
"overseer did not receive recover available data message",
);
assert_eq!(receive_availability.await.expect("Availability should get reported"), false);
virtual_overseer
})
});
@@ -221,8 +229,8 @@ fn cannot_participate_if_cannot_recover_validation_code() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer).await;
let receive_availability = participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer, receive_availability).await;
assert_matches!(
virtual_overseer.recv().await,
@@ -248,7 +256,7 @@ fn cast_invalid_vote_if_available_data_is_invalid() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
participate(&mut virtual_overseer).await;
let receive_availability = participate(&mut virtual_overseer).await;
assert_matches!(
virtual_overseer.recv().await,
@@ -260,6 +268,8 @@ fn cast_invalid_vote_if_available_data_is_invalid() {
"overseer did not receive recover available data message",
);
assert_eq!(receive_availability.await.expect("Availability should get reported"), true);
assert_matches!(
virtual_overseer.recv().await,
AllMessages::DisputeCoordinator(DisputeCoordinatorMessage::IssueLocalStatement(
@@ -281,8 +291,8 @@ fn cast_invalid_vote_if_validation_fails_or_is_invalid() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer).await;
let receive_availability = participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer, receive_availability).await;
fetch_validation_code(&mut virtual_overseer).await;
store_available_data(&mut virtual_overseer, true).await;
@@ -317,8 +327,8 @@ fn cast_invalid_vote_if_validation_passes_but_commitments_dont_match() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer).await;
let receive_availability = participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer, receive_availability).await;
fetch_validation_code(&mut virtual_overseer).await;
store_available_data(&mut virtual_overseer, true).await;
@@ -357,8 +367,8 @@ fn cast_valid_vote_if_validation_passes() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer).await;
let receive_availability = participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer, receive_availability).await;
fetch_validation_code(&mut virtual_overseer).await;
store_available_data(&mut virtual_overseer, true).await;
@@ -393,8 +403,8 @@ fn failure_to_store_available_data_does_not_preclude_participation() {
test_harness(|mut virtual_overseer| {
Box::pin(async move {
activate_leaf(&mut virtual_overseer, 10).await;
participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer).await;
let receive_availability = participate(&mut virtual_overseer).await;
recover_available_data(&mut virtual_overseer, receive_availability).await;
fetch_validation_code(&mut virtual_overseer).await;
// the store available data request should fail
store_available_data(&mut virtual_overseer, false).await;