Skip to content

Commit

Permalink
Periodically check the malicious operator status and register a new o…
Browse files Browse the repository at this point in the history
…perator when it is slashed

This commit also enforce epoch transition immediately to accelerate the onboard
of the new malicious operator, also it increase the endowed balance of the sudo
account to ensure it always have enough balance to register operator

Signed-off-by: linning <linningde25@gmail.com>
  • Loading branch information
NingLin-P committed Dec 8, 2023
1 parent 5d58bd3 commit 949f672
Show file tree
Hide file tree
Showing 3 changed files with 332 additions and 6 deletions.
60 changes: 60 additions & 0 deletions Cargo.lock

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

272 changes: 269 additions & 3 deletions crates/subspace-malicious-operator/src/malicious_bundle_producer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,51 @@ use subspace_runtime::{
use subspace_runtime_primitives::opaque::Block as CBlock;
use subspace_runtime_primitives::{AccountId, Balance, Nonce};

const MALICIOUS_OPR_STAKE_MULTIPLIER: Balance = 3;

enum MaliciousOperatorStatus {
Registering(OperatorPublicKey),
Registered {
operator_id: OperatorId,
signing_key: OperatorPublicKey,
},
NoStatus,
}

impl MaliciousOperatorStatus {
fn registering(&mut self, signing_key: OperatorPublicKey) {
*self = MaliciousOperatorStatus::Registering(signing_key)
}

fn registered(&mut self, operator_id: OperatorId, signing_key: OperatorPublicKey) {
*self = MaliciousOperatorStatus::Registered {
operator_id,
signing_key,
}
}

fn no_status(&mut self) {
*self = MaliciousOperatorStatus::NoStatus
}

fn registered_operator(&self) -> Option<(&OperatorId, &OperatorPublicKey)> {
match self {
MaliciousOperatorStatus::Registered {
operator_id,
signing_key,
} => Some((operator_id, signing_key)),
_ => None,
}
}

fn registering_signing_key(&self) -> Option<OperatorPublicKey> {
match self {
MaliciousOperatorStatus::Registering(key) => Some(key.clone()),
_ => None,
}
}
}

pub struct MaliciousBundleProducer<Client, CClient, TransactionPool> {
domain_id: DomainId,
sudo_acccount: AccountId,
Expand Down Expand Up @@ -175,16 +220,165 @@ where
}
}
}

// Periodically check the malicious operator status
if u64::from(slot) % 10 == 0 {
if let Err(err) = self.update_malicious_operator_status() {
tracing::error!(?err, "Failed to update malicious operator status");
}
}
}
}

fn update_malicious_operator_status(&mut self) -> Result<(), Box<dyn Error>> {
let consensus_best_hash = self.consensus_client.info().best_hash;
let (mut current_operators, next_operators) = self
.consensus_client
.runtime_api()
.domain_operators(consensus_best_hash, self.domain_id)?
.ok_or_else(|| {
sp_blockchain::Error::Application(
format!("Operator set for domain {} not found", self.domain_id).into(),
)
})?;

if let Some((malicious_operator_id, _)) =
self.malicious_operator_status.registered_operator()
{
if next_operators.contains(malicious_operator_id) {
return Ok(());
} else {
tracing::info!(
?malicious_operator_id,
"Current malicious operator is missing from next operator set, probably got slashed"
);
// Remove the current malicious operator to not account its stake toward
// `current_total_stake` otherwise the next malicious operator will stake
// more and more fund
current_operators.remove(malicious_operator_id);
self.malicious_operator_status.no_status();
}
}

let signing_key = match &self.malicious_operator_status.registering_signing_key() {
Some(k) => k.clone(),
None => {
let public_key: OperatorPublicKey = self
.operator_keystore
.sr25519_generate_new(OperatorPublicKey::ID, None)?
.into();

self.malicious_operator_status
.registering(public_key.clone());

tracing::info!(?public_key, "Start register new malicious operator");

public_key
}
};

let maybe_operator_id = self
.consensus_client
.runtime_api()
.operator_id_by_signing_key(consensus_best_hash, signing_key.clone())?;

// The `signing_key` is linked to a operator means the previous registeration request is succeeded
// otherwise we need to retry
match maybe_operator_id {
None => {
let nonce = self.sudo_acccount_nonce()?;
let current_total_stake: Balance = current_operators.into_values().sum();
self.submit_register_operator(
nonce,
signing_key,
// Ideally we should use the `next_total_stake` but it is tricky to get
MALICIOUS_OPR_STAKE_MULTIPLIER * current_total_stake,
)?;
self.submit_force_staking_epoch_transition(nonce + 1)?;
}
Some(operator_id) => {
if !next_operators.contains(&operator_id) {
// The operator id not present in `next_operators` means the operator is deregistered
// or slashed, which should not happen since we haven't use this operator to submit bad
// ER yet. But just set `malicious_operator_status` to `NoStatus` to register a new operator.
self.malicious_operator_status.no_status();
} else if !current_operators.contains_key(&operator_id) {
self.submit_force_staking_epoch_transition(self.sudo_acccount_nonce()?)?;
} else {
tracing::info!(
?operator_id,
?signing_key,
"Registered a new malicious operator"
);
self.malicious_operator_status
.registered(operator_id, signing_key);
}
}
}

Ok(())
}

fn sudo_acccount_nonce(&self) -> Result<Nonce, Box<dyn Error>> {
Ok(self.consensus_client.runtime_api().account_nonce(
self.consensus_client.info().best_hash,
self.sudo_acccount.clone(),
)?)
}

fn submit_bundle(
&self,
opaque_bundle: OpaqueBundleFor<DomainBlock, CBlock>,
) -> Result<(), Box<dyn Error>> {
let call = UncheckedExtrinsic::new_unsigned(
pallet_domains::Call::submit_bundle { opaque_bundle }.into(),
);
let call = pallet_domains::Call::submit_bundle { opaque_bundle };
self.submit_consensus_extrinsic(None, call.into())
}

fn submit_register_operator(
&self,
nonce: Nonce,
signing_key: OperatorPublicKey,
staking_amount: Balance,
) -> Result<(), Box<dyn Error>> {
let call = pallet_domains::Call::register_operator {
domain_id: self.domain_id,
amount: staking_amount,
config: OperatorConfig {
signing_key,
minimum_nominator_stake: Balance::MAX,
nomination_tax: Default::default(),
},
};
self.submit_consensus_extrinsic(Some(nonce), call.into())
}

fn submit_force_staking_epoch_transition(&self, nonce: Nonce) -> Result<(), Box<dyn Error>> {
let call = pallet_sudo::Call::sudo {
call: Box::new(RuntimeCall::Domains(
pallet_domains::Call::force_staking_epoch_transition {
domain_id: self.domain_id,
},
)),
};
self.submit_consensus_extrinsic(Some(nonce), call.into())
}

fn submit_consensus_extrinsic(
&self,
maybe_nonce: Option<Nonce>,
call: RuntimeCall,
) -> Result<(), Box<dyn Error>> {
let etx = match maybe_nonce {
Some(nonce) => construct_signed_extrinsic(
&self.consensus_keystore,
self.consensus_client.info(),
call.clone(),
self.sudo_acccount.clone(),
nonce,
)?,
None => UncheckedExtrinsic::new_unsigned(call.clone()),
};

self.consensus_offchain_tx_pool_factory
.offchain_transaction_pool(self.consensus_client.info().best_hash)
.submit_transaction(etx.encode())
Expand All @@ -198,3 +392,75 @@ where
Ok(())
}
}

pub fn construct_signed_extrinsic(
consensus_keystore: &KeystorePtr,
consensus_chain_info: Info<CBlock>,
call: RuntimeCall,
caller: AccountId,
nonce: Nonce,
) -> Result<UncheckedExtrinsic, Box<dyn Error>> {
let period = u64::from(<<Runtime as frame_system::Config>::BlockHashCount as Get<
u32,
>>::get())
.checked_next_power_of_two()
.map(|c| c / 2)
.unwrap_or(2);
let extra: SignedExtra = (
frame_system::CheckNonZeroSender::<Runtime>::new(),
frame_system::CheckSpecVersion::<Runtime>::new(),
frame_system::CheckTxVersion::<Runtime>::new(),
frame_system::CheckGenesis::<Runtime>::new(),
frame_system::CheckMortality::<Runtime>::from(generic::Era::mortal(
period,
consensus_chain_info.best_number.into(),
)),
frame_system::CheckNonce::<Runtime>::from(nonce),
frame_system::CheckWeight::<Runtime>::new(),
pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(0u128),
CheckStorageAccess,
DisablePallets,
);
let raw_payload = generic::SignedPayload::<RuntimeCall, SignedExtra>::from_raw(
call.clone(),
extra.clone(),
(
(),
subspace_runtime::VERSION.spec_version,
subspace_runtime::VERSION.transaction_version,
consensus_chain_info.genesis_hash,
consensus_chain_info.best_hash,
(),
(),
(),
(),
(),
),
);

let signature = match Sr25519Keyring::from_account_id(&caller) {
Some(keyring) => raw_payload.using_encoded(|e| keyring.sign(e)),
None => {
let public_key =
sp_core::sr25519::Public::unchecked_from(<AccountId as Into<[u8; 32]>>::into(
caller.clone(),
));
raw_payload
.using_encoded(|e| {
consensus_keystore
.sr25519_sign(FarmerPublicKey::ID, &public_key, e)
})?
.ok_or(format!(
"Failed to sign extrinsic, sudo key pair missing from keystore?, public_key {:?}",
public_key
))?
}
};

Ok(UncheckedExtrinsic::new_signed(
call,
sp_runtime::MultiAddress::Id(caller),
signature.into(),
extra,
))
}
6 changes: 3 additions & 3 deletions crates/subspace-node/src/chain_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ pub fn devnet_config_compiled() -> Result<ConsensusChainSpec<RuntimeGenesisConfi
AccountId::from_ss58check("5CXTmJEusve5ixyJufqHThmy4qUrrm6FyLCR7QfE4bbyMTNC")
.expect("Wrong root account address");

let mut balances = vec![(sudo_account.clone(), 1_000 * SSC)];
let mut balances = vec![(sudo_account.clone(), Balance::MAX / 2)];
let vesting_schedules = TOKEN_GRANTS
.iter()
.flat_map(|&(account_address, amount)| {
Expand Down Expand Up @@ -327,7 +327,7 @@ pub fn dev_config() -> Result<ConsensusChainSpec<RuntimeGenesisConfig>, String>
get_account_id_from_seed("Alice"),
// Pre-funded accounts
vec![
(get_account_id_from_seed("Alice"), 1_000 * SSC),
(get_account_id_from_seed("Alice"), Balance::MAX / 2),
(get_account_id_from_seed("Bob"), 1_000 * SSC),
(get_account_id_from_seed("Alice//stash"), 1_000 * SSC),
(get_account_id_from_seed("Bob//stash"), 1_000 * SSC),
Expand Down Expand Up @@ -387,7 +387,7 @@ pub fn local_config() -> Result<ConsensusChainSpec<RuntimeGenesisConfig>, String
get_account_id_from_seed("Alice"),
// Pre-funded accounts
vec![
(get_account_id_from_seed("Alice"), 1_000 * SSC),
(get_account_id_from_seed("Alice"), Balance::MAX / 2),
(get_account_id_from_seed("Bob"), 1_000 * SSC),
(get_account_id_from_seed("Charlie"), 1_000 * SSC),
(get_account_id_from_seed("Dave"), 1_000 * SSC),
Expand Down

0 comments on commit 949f672

Please sign in to comment.