Skip to content

Commit

Permalink
Merge pull request #2197 from subspace/bundle_equivocation
Browse files Browse the repository at this point in the history
Fraud Proof: Bundle equivocation
  • Loading branch information
vedhavyas authored Nov 8, 2023
2 parents 547e883 + 0e24833 commit 2e10e36
Show file tree
Hide file tree
Showing 21 changed files with 1,527 additions and 265 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

388 changes: 208 additions & 180 deletions crates/pallet-domains/src/lib.rs

Large diffs are not rendered by default.

24 changes: 23 additions & 1 deletion crates/pallet-domains/src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::block_tree::BlockTreeNode;
use crate::domain_registry::{DomainConfig, DomainObject};
use crate::staking::Operator;
use crate::{
self as pallet_domains, BalanceOf, BlockTree, BlockTreeNodes, BundleError, Config,
ConsensusBlockHash, DomainBlockNumberFor, DomainHashingFor, DomainRegistry, ExecutionInbox,
ExecutionReceiptOf, FraudProofError, FungibleHoldId, HeadReceiptNumber, NextDomainId, Operator,
ExecutionReceiptOf, FraudProofError, FungibleHoldId, HeadReceiptNumber, NextDomainId,
OperatorStatus, Operators, ReceiptHashFor,
};
use codec::{Decode, Encode, MaxEncodedLen};
Expand Down Expand Up @@ -261,6 +262,9 @@ pub(crate) struct MockDomainFraudProofExtension {
runtime_code: Vec<u8>,
tx_range: bool,
is_inherent: bool,
domain_total_stake: Balance,
bundle_slot_probability: (u64, u64),
operator_stake: Balance,
}

impl FraudProofHostFunctions for MockDomainFraudProofExtension {
Expand Down Expand Up @@ -309,6 +313,15 @@ impl FraudProofHostFunctions for MockDomainFraudProofExtension {
FraudProofVerificationInfoRequest::InherentExtrinsicCheck { .. } => {
FraudProofVerificationInfoResponse::InherentExtrinsicCheck(self.is_inherent)
}
FraudProofVerificationInfoRequest::DomainElectionParams { .. } => {
FraudProofVerificationInfoResponse::DomainElectionParams {
domain_total_stake: self.domain_total_stake,
bundle_slot_probability: self.bundle_slot_probability,
}
}
FraudProofVerificationInfoRequest::OperatorStake { .. } => {
FraudProofVerificationInfoResponse::OperatorStake(self.operator_stake)
}
};

Some(response)
Expand Down Expand Up @@ -995,6 +1008,9 @@ fn test_invalid_domain_extrinsic_root_proof() {
runtime_code: vec![1, 2, 3, 4],
tx_range: true,
is_inherent: true,
domain_total_stake: 100 * SSC,
operator_stake: 10 * SSC,
bundle_slot_probability: (0, 0),
}));
ext.register_extension(fraud_proof_ext);

Expand Down Expand Up @@ -1071,6 +1087,9 @@ fn test_true_invalid_bundles_inherent_extrinsic_proof() {
tx_range: true,
// return `true` indicating this is an inherent extrinsic
is_inherent: true,
domain_total_stake: 100 * SSC,
operator_stake: 10 * SSC,
bundle_slot_probability: (0, 0),
}));
ext.register_extension(fraud_proof_ext);

Expand Down Expand Up @@ -1133,6 +1152,9 @@ fn test_false_invalid_bundles_inherent_extrinsic_proof() {
tx_range: true,
// return `false` indicating this is not an inherent extrinsic
is_inherent: false,
domain_total_stake: 100 * SSC,
operator_stake: 10 * SSC,
bundle_slot_probability: (0, 0),
}));
ext.register_extension(fraud_proof_ext);

Expand Down
1 change: 1 addition & 0 deletions crates/sp-domains-fraud-proof/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ sc-service = { version = "0.10.0-dev", git = "https://github.com/subspace/polkad
subspace-test-client = { version = "0.1.0", path = "../../test/subspace-test-client" }
subspace-test-service = { version = "0.1.0", path = "../../test/subspace-test-service" }
subspace-runtime-primitives = { version = "0.1.0", path = "../../crates/subspace-runtime-primitives" }
substrate-test-runtime-client = { version = "2.0.0", git = "https://github.com/subspace/polkadot-sdk", rev = "892bf8e938c6bd2b893d3827d1093cd81baa59a1" }
tempfile = "3.8.0"
tokio = "1.32.0"

Expand Down
254 changes: 254 additions & 0 deletions crates/sp-domains-fraud-proof/src/bundle_equivocation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
//! Module to check bundle equivocation and produce the Equivocation fraud proof.
//! This is mostly derived from the `sc_consensus_slots::aux_schema` with changes adapted
//! for Bundle headers instead of block headers
use crate::fraud_proof::{BundleEquivocationProof, FraudProof};
use codec::{Decode, Encode};
use sc_client_api::backend::AuxStore;
use sp_api::{BlockT, HeaderT};
use sp_blockchain::{Error as ClientError, Result as ClientResult};
use sp_consensus_slots::Slot;
use sp_domains::SealedBundleHeader;
use sp_runtime::traits::NumberFor;
use std::sync::Arc;
use subspace_runtime_primitives::Balance;

const SLOT_BUNDLE_HEADER_MAP_KEY: &[u8] = b"slot_bundle_header_map";
const SLOT_BUNDLE_HEADER_START: &[u8] = b"slot_bundle_header_start";

// TODO: revisit these values when there more than 1000 domains.
/// We keep at least this number of slots in database.
const MAX_SLOT_CAPACITY: u64 = 1000;
/// We prune slots when they reach this number.
const PRUNING_BOUND: u64 = 2 * MAX_SLOT_CAPACITY;

fn load_decode<CClient, T>(client: &Arc<CClient>, key: &[u8]) -> ClientResult<Option<T>>
where
CClient: AuxStore,
T: Decode,
{
match client.get_aux(key)? {
None => Ok(None),
Some(t) => T::decode(&mut &t[..])
.map_err(|e| {
ClientError::Backend(format!("Slots DB is corrupted. Decode error: {}", e))
})
.map(Some),
}
}

pub type CheckEquivocationResult<CNumber, CHash, DomainHeader> =
ClientResult<Option<FraudProof<CNumber, CHash, DomainHeader>>>;

/// Checks if the header is an equivocation and returns the proof in that case.
///
/// Note: it detects equivocations only when slot_now - slot <= MAX_SLOT_CAPACITY.
pub fn check_equivocation<CClient, CBlock, DomainHeader>(
backend: &Arc<CClient>,
slot_now: Slot,
bundle_header: SealedBundleHeader<NumberFor<CBlock>, CBlock::Hash, DomainHeader, Balance>,
) -> CheckEquivocationResult<NumberFor<CBlock>, CBlock::Hash, DomainHeader>
where
CClient: AuxStore,
CBlock: BlockT,
DomainHeader: HeaderT,
{
let slot: Slot = bundle_header.header.proof_of_election.slot_number.into();

// We don't check equivocations for old headers out of our capacity.
if slot_now.saturating_sub(*slot) > MAX_SLOT_CAPACITY {
return Ok(None);
}

// Key for this slot.
let mut curr_slot_key = SLOT_BUNDLE_HEADER_MAP_KEY.to_vec();
slot.using_encoded(|s| curr_slot_key.extend(s));

// Get headers of this slot.
let mut headers_with_sig = load_decode::<
CClient,
Vec<SealedBundleHeader<NumberFor<CBlock>, CBlock::Hash, DomainHeader, Balance>>,
>(backend, &curr_slot_key[..])?
.unwrap_or_else(Vec::new);

// Get first slot saved.
let slot_header_start = SLOT_BUNDLE_HEADER_START.to_vec();
let first_saved_slot = load_decode::<_, Slot>(backend, &slot_header_start[..])?.unwrap_or(slot);

if slot_now < first_saved_slot {
// The code below assumes that slots will be visited sequentially.
return Ok(None);
}

for previous_bundle_header in headers_with_sig.iter() {
let operator_set_1 = (
previous_bundle_header.header.proof_of_election.operator_id,
previous_bundle_header.header.proof_of_election.domain_id,
);
let operator_set_2 = (
bundle_header.header.proof_of_election.operator_id,
bundle_header.header.proof_of_election.domain_id,
);

// A proof of equivocation consists of two headers:
// 1) signed by the same operator for same domain
if operator_set_1 == operator_set_2 {
// 2) with different hash
return if bundle_header.hash() != previous_bundle_header.hash() {
Ok(Some(FraudProof::BundleEquivocation(
BundleEquivocationProof {
domain_id: bundle_header.header.proof_of_election.domain_id,
slot,
first_header: previous_bundle_header.clone(),
second_header: bundle_header,
},
)))
} else {
// We don't need to continue in case of duplicated header,
// since it's already saved and a possible equivocation
// would have been detected before.
Ok(None)
};
}
}

let mut keys_to_delete = vec![];
let mut new_first_saved_slot = first_saved_slot;

if *slot_now - *first_saved_slot >= PRUNING_BOUND {
let prefix = SLOT_BUNDLE_HEADER_MAP_KEY.to_vec();
new_first_saved_slot = slot_now.saturating_sub(MAX_SLOT_CAPACITY);

for s in u64::from(first_saved_slot)..new_first_saved_slot.into() {
let mut p = prefix.clone();
s.using_encoded(|s| p.extend(s));
keys_to_delete.push(p);
}
}

headers_with_sig.push(bundle_header);

backend.insert_aux(
&[
(&curr_slot_key[..], headers_with_sig.encode().as_slice()),
(
&slot_header_start[..],
new_first_saved_slot.encode().as_slice(),
),
],
&keys_to_delete
.iter()
.map(|k| &k[..])
.collect::<Vec<&[u8]>>()[..],
)?;

Ok(None)
}

#[cfg(test)]
mod test {
use super::{check_equivocation, MAX_SLOT_CAPACITY, PRUNING_BOUND};
use domain_runtime_primitives::opaque::Header as DomainHeader;
use sp_core::crypto::UncheckedFrom;
use sp_domains::{
BundleHeader, DomainId, ExecutionReceipt, OperatorId, OperatorSignature, ProofOfElection,
SealedBundleHeader,
};
use std::sync::Arc;
use subspace_runtime_primitives::opaque::Block;
use subspace_runtime_primitives::{Balance, BlockNumber, Hash};

fn create_header(
number: BlockNumber,
slot_number: u64,
domain_id: DomainId,
operator_id: OperatorId,
) -> SealedBundleHeader<BlockNumber, Hash, DomainHeader, Balance> {
let mut poe = ProofOfElection::dummy(domain_id, operator_id);
poe.slot_number = slot_number;
SealedBundleHeader {
header: BundleHeader {
proof_of_election: poe,
receipt: ExecutionReceipt {
domain_block_number: number,
domain_block_hash: Default::default(),
domain_block_extrinsic_root: Default::default(),
parent_domain_block_receipt_hash: Default::default(),
consensus_block_number: number,
consensus_block_hash: Default::default(),
inboxed_bundles: vec![],
final_state_root: Default::default(),
execution_trace: vec![],
execution_trace_root: Default::default(),
total_rewards: 0,
},
bundle_size: 0,
estimated_bundle_weight: Default::default(),
bundle_extrinsics_root: Default::default(),
},
signature: OperatorSignature::unchecked_from([0u8; 64]),
}
}

#[test]
fn test_check_equivocation() {
let client = Arc::new(substrate_test_runtime_client::new());
let domain_id = DomainId::new(0);
let operator_id = 1;

let header1 = create_header(1, 2, domain_id, operator_id); // @ slot 2
let header2 = create_header(2, 2, domain_id, operator_id); // @ slot 2
let header3 = create_header(2, 4, domain_id, operator_id); // @ slot 4
let header4 = create_header(3, MAX_SLOT_CAPACITY + 4, domain_id, operator_id); // @ slot MAX_SLOT_CAPACITY + 4
let header5 = create_header(4, MAX_SLOT_CAPACITY + 4, domain_id, operator_id); // @ slot MAX_SLOT_CAPACITY + 4
let header6 = create_header(3, 4, domain_id, operator_id); // @ slot 4

// It's ok to sign same headers.
assert!(
check_equivocation::<_, Block, _>(&client, 2.into(), header1.clone())
.unwrap()
.is_none(),
);

assert!(
check_equivocation::<_, Block, _>(&client, 3.into(), header1.clone())
.unwrap()
.is_none(),
);

// But not two different headers at the same slot.
assert!(
check_equivocation::<_, Block, _>(&client, 4.into(), header2)
.unwrap()
.is_some(),
);

// Different slot is ok.
assert!(
check_equivocation::<_, Block, _>(&client, 5.into(), header3)
.unwrap()
.is_none(),
);

// Here we trigger pruning and save header 4.
assert!(
check_equivocation::<_, Block, _>(&client, (PRUNING_BOUND + 2).into(), header4,)
.unwrap()
.is_none(),
);

// This fails because header 5 is an equivocation of header 4.
assert!(
check_equivocation::<_, Block, _>(&client, (PRUNING_BOUND + 3).into(), header5,)
.unwrap()
.is_some(),
);

// This is ok because we pruned the corresponding header. Shows that we are pruning.
assert!(
check_equivocation::<_, Block, _>(&client, (PRUNING_BOUND + 4).into(), header6,)
.unwrap()
.is_none(),
);
}
}
Loading

0 comments on commit 2e10e36

Please sign in to comment.