Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fraud Proof: Invalid domain extrinsic root #1999

Merged
merged 8 commits into from
Sep 27, 2023
6 changes: 6 additions & 0 deletions Cargo.lock

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

21 changes: 12 additions & 9 deletions crates/pallet-domains/src/block_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::pallet::StateRoots;
use crate::{
BalanceOf, BlockTree, Config, ConsensusBlockHash, DomainBlockDescendants, DomainBlocks,
BalanceOf, BlockTree, Config, ConsensusBlockInfo, DomainBlockDescendants, DomainBlocks,
ExecutionInbox, ExecutionReceiptOf, HeadReceiptNumber, InboxedBundle,
};
use codec::{Decode, Encode};
Expand Down Expand Up @@ -169,8 +169,8 @@ pub(crate) fn verify_execution_receipt<T: Config>(
);

let excepted_consensus_block_hash =
match ConsensusBlockHash::<T>::get(domain_id, consensus_block_number) {
Some(hash) => hash,
match ConsensusBlockInfo::<T>::get(domain_id, consensus_block_number) {
Some((hash, _)) => hash,
// The `initialize_block` of non-system pallets is skipped in the `validate_transaction`,
// thus the hash of best block, which is recorded in the this pallet's `on_initialize` hook,
// is unavailable at this point.
Expand Down Expand Up @@ -312,7 +312,7 @@ pub(crate) fn process_execution_receipt<T: Config>(
// its receipt's `extrinsics_root` anymore.
let _ = ExecutionInbox::<T>::clear_prefix((domain_id, to_prune), u32::MAX, None);

ConsensusBlockHash::<T>::remove(
ConsensusBlockInfo::<T>::remove(
domain_id,
execution_receipt.consensus_block_number,
);
Expand Down Expand Up @@ -471,8 +471,11 @@ mod tests {
if block_number != 1 {
// `ConsensusBlockHash` should be set to `Some` since last consensus block contains bundle
assert_eq!(
ConsensusBlockHash::<Test>::get(domain_id, block_number - 1),
Some(frame_system::Pallet::<Test>::block_hash(block_number - 1))
ConsensusBlockInfo::<Test>::get(domain_id, block_number - 1),
Some((
frame_system::Pallet::<Test>::block_hash(block_number - 1),
H256::default()
))
);
// ER point to last consensus block should have `NewHead` type
assert_eq!(
Expand Down Expand Up @@ -551,7 +554,7 @@ mod tests {
verify_execution_receipt::<Test>(domain_id, &pruned_receipt),
Error::InvalidExtrinsicsRoots
);
assert!(ConsensusBlockHash::<Test>::get(
assert!(ConsensusBlockInfo::<Test>::get(
domain_id,
pruned_receipt.consensus_block_number,
)
Expand Down Expand Up @@ -757,10 +760,10 @@ mod tests {
verify_execution_receipt::<Test>(domain_id, &future_receipt),
Error::UnavailableConsensusBlockHash
);
ConsensusBlockHash::<Test>::insert(
ConsensusBlockInfo::<Test>::insert(
domain_id,
future_receipt.consensus_block_number,
future_receipt.consensus_block_hash,
(future_receipt.consensus_block_hash, H256::default()),
);

// Return `UnknownParentBlockReceipt` error as its parent receipt is missing from the block tree
Expand Down
159 changes: 126 additions & 33 deletions crates/pallet-domains/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ use frame_system::offchain::SubmitTransaction;
use frame_system::pallet_prelude::*;
pub use pallet::*;
use scale_info::TypeInfo;
use sp_core::storage::StorageKey;
use sp_core::H256;
use sp_domains::bundle_producer_election::{is_below_threshold, BundleProducerElectionParams};
use sp_domains::fraud_proof::{FraudProof, InvalidTotalRewardsProof};
use sp_domains::verification::StorageProofVerifier;
use sp_domains::verification::{
verify_invalid_domain_extrinsics_root_fraud_proof, verify_invalid_total_rewards_fraud_proof,
};
use sp_domains::{
DomainBlockLimit, DomainId, DomainInstanceData, ExecutionReceipt, OpaqueBundle, OperatorId,
OperatorPublicKey, ProofOfElection, RuntimeId, EMPTY_EXTRINSIC_ROOT,
OperatorPublicKey, ProofOfElection, RuntimeId, DOMAIN_EXTRINSICS_SHUFFLING_SEED_SUBJECT,
EMPTY_EXTRINSIC_ROOT,
};
use sp_runtime::traits::{BlakeTwo256, CheckedSub, Hash, One, Zero};
use sp_runtime::{RuntimeAppPublic, SaturatedConversion, Saturating};
Expand Down Expand Up @@ -136,7 +138,8 @@ mod pallet {
use frame_support::{Identity, PalletError};
use frame_system::pallet_prelude::*;
use sp_core::H256;
use sp_domains::fraud_proof::FraudProof;
use sp_domains::fraud_proof::{FraudProof, StorageKeys};
use sp_domains::inherents::{InherentError, InherentType, INHERENT_IDENTIFIER};
use sp_domains::transaction::InvalidTransactionCode;
use sp_domains::{
BundleDigest, DomainId, EpochIndex, GenesisDomain, OperatorId, ReceiptHash, RuntimeId,
Expand Down Expand Up @@ -281,6 +284,9 @@ mod pallet {

/// Randomness source.
type Randomness: RandomnessT<Self::Hash, BlockNumberFor<Self>>;

/// Trait impl to fetch storage keys.
type StorageKeys: StorageKeys;
}

#[pallet::pallet]
Expand Down Expand Up @@ -482,16 +488,24 @@ mod pallet {
OptionQuery,
>;

/// The consensus block hash used to verify ER, only store the consensus block hash for a domain
/// The consensus block hash and state root used to verify ER and storage proofs,
/// only store the consensus block hash for a domain
/// if that consensus block contains bundle of the domain, the hash will be pruned when the ER
/// that point to the consensus block is pruned.
///
/// TODO: this storage is unbounded in some cases, see https://github.com/subspace/subspace/issues/1673
/// for more details, this will be fixed once https://github.com/subspace/subspace/issues/1731 is implemented.
#[pallet::storage]
#[pallet::getter(fn consensus_hash)]
pub type ConsensusBlockHash<T: Config> =
StorageDoubleMap<_, Identity, DomainId, Identity, BlockNumberFor<T>, T::Hash, OptionQuery>;
#[pallet::getter(fn consensus_block_info)]
pub type ConsensusBlockInfo<T: Config> = StorageDoubleMap<
_,
Identity,
DomainId,
Identity,
BlockNumberFor<T>,
(T::Hash, T::Hash),
OptionQuery,
>;

/// A set of `BundleDigest` from all bundles that successfully submitted to the consensus block,
/// these bundles will be used to construct the domain block and `ExecutionInbox` is used to:
Expand Down Expand Up @@ -580,12 +594,12 @@ mod pallet {
ChallengingGenesisReceipt,
/// The descendants of the fraudulent ER is not pruned
DescendantsOfFraudulentERNotPruned,
/// Proof of total rewards is invalid.
InvalidTotalRewardsProof,
/// Invalid fraud proof since total rewards are not mismatched.
InvalidTotalRewardsFraudProof,
/// Invalid state root.
FailedToDecodeDomainBlockHash,
InvalidTotalRewardsFraudProof(sp_domains::verification::VerificationError),
/// Missing state root for a given consensus block
MissingConsensusStateRoot,
/// Invalid domain extrinsic fraud proof
InvalidExtrinsicRootFraudProof(sp_domains::verification::VerificationError),
}

impl<T> From<FraudProofError> for Error<T> {
Expand Down Expand Up @@ -1117,6 +1131,27 @@ mod pallet {

Ok(())
}

/// Submit parent state root to the blockchain.
#[pallet::call_index(11)]
#[pallet::weight((Weight::from_all(10_000), DispatchClass::Mandatory, Pays::No))]
Copy link
Member

Choose a reason for hiding this comment

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

Weight::from_all(10_000) seems not accurate, plz leave a comment for this.

pub fn store_parent_state_root(
origin: OriginFor<T>,
parent_state_root: T::Hash,
) -> DispatchResult {
ensure_none(origin)?;
let block_number = frame_system::Pallet::<T>::block_number();
let parent_number = block_number - One::one();

let domains_ids = DomainRegistry::<T>::iter_keys().collect::<Vec<DomainId>>();
for domain_id in domains_ids {
if let Some(info) = ConsensusBlockInfo::<T>::get(domain_id, parent_number) {
let info = (info.0, parent_state_root);
ConsensusBlockInfo::<T>::insert(domain_id, parent_number, info);
}
}
Ok(())
}
}

#[pallet::genesis_config]
Expand Down Expand Up @@ -1207,7 +1242,11 @@ mod pallet {
let parent_number = block_number - One::one();
let parent_hash = frame_system::Pallet::<T>::block_hash(parent_number);
for (domain_id, _) in SuccessfulBundles::<T>::drain() {
ConsensusBlockHash::<T>::insert(domain_id, parent_number, parent_hash);
ConsensusBlockInfo::<T>::insert(
domain_id,
parent_number,
(parent_hash, T::Hash::default()),
Copy link
Member

Choose a reason for hiding this comment

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

Would prefer to have a comment explaining why it is okay to use the default value here

);
}

Weight::zero()
Expand All @@ -1229,6 +1268,42 @@ mod pallet {
.build()
}

#[pallet::inherent]
impl<T: Config> ProvideInherent for Pallet<T> {
type Call = Call<T>;
type Error = InherentError;
const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER;

fn create_inherent(data: &InherentData) -> Option<Self::Call> {
let inherent_data = data
.get_data::<InherentType<T::Hash>>(&INHERENT_IDENTIFIER)
.expect("Domains inherent data not correctly encoded")
.expect("Domains inherent data must be provided");

let parent_state_root = inherent_data.parent_state_root;
Some(Call::store_parent_state_root { parent_state_root })
}

fn check_inherent(call: &Self::Call, data: &InherentData) -> Result<(), Self::Error> {
if let Call::store_parent_state_root { parent_state_root } = call {
let inherent_data = data
.get_data::<InherentType<T::Hash>>(&INHERENT_IDENTIFIER)
.expect("Domains inherent data not correctly encoded")
.expect("Domains inherent data must be provided");

if parent_state_root != &inherent_data.parent_state_root {
return Err(InherentError::IncorrectParentStateRoot);
}
}

Ok(())
}

fn is_inherent(call: &Self::Call) -> bool {
matches!(call, Call::store_parent_state_root { .. })
}
}

#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
Expand All @@ -1238,6 +1313,7 @@ mod pallet {
.map_err(|_| InvalidTransaction::Call.into()),
Call::submit_fraud_proof { fraud_proof } => Self::validate_fraud_proof(fraud_proof)
.map_err(|_| InvalidTransaction::Call.into()),
Call::store_parent_state_root { .. } => Ok(()),
_ => Err(InvalidTransaction::Call.into()),
}
}
Expand Down Expand Up @@ -1278,6 +1354,13 @@ mod pallet {
// TODO: proper tag value.
unsigned_validity("SubspaceSubmitFraudProof", fraud_proof)
}
Call::store_parent_state_root { .. } => {
ValidTransaction::with_tag_prefix("Domain Parent State root Inherent")
.priority(TransactionPriority::MAX)
.longevity(0)
.propagate(false)
.build()
}

_ => InvalidTransaction::Call.into(),
}
Expand Down Expand Up @@ -1494,25 +1577,35 @@ impl<T: Config> Pallet<T> {
FraudProofError::ChallengingGenesisReceipt
);

if let FraudProof::InvalidTotalRewards(InvalidTotalRewardsProof { storage_proof, .. }) =
fraud_proof
{
let state_root = bad_receipt.final_state_root.encode();
let state_root = T::Hash::decode(&mut state_root.as_slice())
.map_err(|_| FraudProofError::FailedToDecodeDomainBlockHash)?;
let storage_key =
StorageKey(sp_domains::fraud_proof::operator_block_rewards_final_key());
let storage_proof = storage_proof.clone();

let total_rewards = StorageProofVerifier::<T::Hashing>::verify_and_get_value::<
BalanceOf<T>,
>(&state_root, storage_proof, storage_key)
.map_err(|_| FraudProofError::InvalidTotalRewardsProof)?;

// if the rewards matches, then this is an invalid fraud proof since rewards must be different.
if bad_receipt.total_rewards == total_rewards {
return Err(FraudProofError::InvalidTotalRewardsFraudProof);
match fraud_proof {
FraudProof::InvalidTotalRewards(InvalidTotalRewardsProof { storage_proof, .. }) => {
verify_invalid_total_rewards_fraud_proof::<
T::Block,
T::DomainNumber,
T::DomainHash,
BalanceOf<T>,
T::Hashing,
>(bad_receipt, storage_proof)
.map_err(FraudProofError::InvalidTotalRewardsFraudProof)?;
}
FraudProof::InvalidExtrinsicsRoot(proof) => {
let consensus_state_root = ConsensusBlockInfo::<T>::get(
proof.domain_id,
bad_receipt.consensus_block_number,
)
.ok_or(FraudProofError::MissingConsensusStateRoot)?
.1;
verify_invalid_domain_extrinsics_root_fraud_proof::<
T::Block,
T::DomainNumber,
T::DomainHash,
BalanceOf<T>,
T::Hashing,
T::StorageKeys,
>(consensus_state_root, bad_receipt, proof)
.map_err(FraudProofError::InvalidExtrinsicRootFraudProof)?;
}
_ => {}
}

Ok(())
Expand Down Expand Up @@ -1691,7 +1784,7 @@ impl<T: Config> Pallet<T> {
}

pub fn extrinsics_shuffling_seed() -> T::Hash {
let seed: &[u8] = b"extrinsics-shuffling-seed";
let seed = DOMAIN_EXTRINSICS_SHUFFLING_SEED_SUBJECT;
let (randomness, _) = T::Randomness::random(seed);
randomness
}
Expand Down
Loading
Loading