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

pallet-mmr: handle forks without collisions in offchain storage #11594

Merged
merged 35 commits into from
Jul 7, 2022
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4ded369
pallet-mmr: fix some typos
acatangiu Jun 1, 2022
eb9eea8
pallet-mmr: make the MMR resilient to chain forks
acatangiu Jun 1, 2022
7c8aac0
pallet-mmr: get hash for block that added node
acatangiu Jun 2, 2022
c82ac27
beefy-mmr: add debug logging
acatangiu Jun 21, 2022
98abe4a
add explanatory comment
acatangiu Jun 22, 2022
db41f81
account for block offset of pallet activation
acatangiu Jun 22, 2022
bfd4142
add support for finding all nodes added by leaf
acatangiu Jun 22, 2022
417de11
minor improvements
acatangiu Jun 23, 2022
25468f9
add helper to return all nodes added to mmr with a leaf append
Lederstrumpf Jun 23, 2022
19b35ee
simplify leaf_node_index_to_leaf_index
Lederstrumpf Jun 23, 2022
f9f202a
dead fish: this also doesn't work
acatangiu Jun 23, 2022
05c5f26
simplify rightmost_leaf_node_index_from_pos
Lederstrumpf Jun 23, 2022
e15810d
minor fix
acatangiu Jun 24, 2022
f803ce9
move leaf canonicalization to offchain worker
acatangiu Jun 24, 2022
94e3b61
move storage related code to storage.rs
acatangiu Jun 24, 2022
ecb77a7
on offchain reads use canonic key for old leaves
acatangiu Jun 24, 2022
a17cb65
fix offchain worker write using canon key
acatangiu Jun 24, 2022
e9ab363
fix pallet-mmr tests
acatangiu Jun 24, 2022
b75b0ae
Merge branch 'master' of github.com:paritytech/substrate into mmr-han…
acatangiu Jun 27, 2022
8856db1
add documentation and fix logging
acatangiu Jun 27, 2022
dfc5b45
add offchain mmr canonicalization test
acatangiu Jun 28, 2022
e9edd28
test canon + generate + verify
acatangiu Jun 28, 2022
f472e9b
fix pallet-beefy-mmr tests
acatangiu Jun 28, 2022
5ed95c7
implement review suggestions
acatangiu Jun 28, 2022
2c8b101
improve test
acatangiu Jun 29, 2022
56f2eaa
pallet-mmr: add offchain pruning of forks
acatangiu Jun 30, 2022
e22a936
Merge branch 'master' of github.com:paritytech/substrate into mmr-han…
acatangiu Jun 30, 2022
2629734
pallet-mmr: improve offchain pruning
acatangiu Jul 1, 2022
58d116f
Merge branch 'master' of github.com:paritytech/substrate into mmr-han…
acatangiu Jul 1, 2022
4e6de82
pallet-mmr: improve MMRStore<OffchainStorage>::get()
acatangiu Jul 1, 2022
e72e344
Merge branch 'master' of github.com:paritytech/substrate into mmr-han…
acatangiu Jul 5, 2022
6335512
pallet-mmr: storage: improve logs
acatangiu Jul 5, 2022
eab718c
fix tests: correctly persist overlay
acatangiu Jul 5, 2022
8a8f3d2
pallet-mmr: fix numeric typo in test
Lederstrumpf Jul 6, 2022
ba9fd71
add comment around LeafData requirements
acatangiu Jul 7, 2022
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
22 changes: 12 additions & 10 deletions frame/beefy-mmr/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,12 @@ pub fn beefy_log(log: ConsensusLog<BeefyId>) -> DigestItem {
DigestItem::Consensus(BEEFY_ENGINE_ID, log.encode())
}

fn offchain_key(pos: usize) -> Vec<u8> {
(<Test as pallet_mmr::Config>::INDEXING_PREFIX, pos as u64).encode()
}

fn read_mmr_leaf(ext: &mut TestExternalities, index: usize) -> MmrLeaf {
fn read_mmr_leaf(ext: &mut TestExternalities, key: Vec<u8>) -> MmrLeaf {
type Node = pallet_mmr::primitives::DataOrHash<Keccak256, MmrLeaf>;
ext.persist_offchain_overlay();
let offchain_db = ext.offchain_db();
offchain_db
.get(&offchain_key(index))
.get(&key)
.map(|d| Node::decode(&mut &*d).unwrap())
.map(|n| match n {
Node::Data(d) => d,
Expand Down Expand Up @@ -105,12 +101,17 @@ fn should_contain_mmr_digest() {

#[test]
fn should_contain_valid_leaf_data() {
fn node_offchain_key(parent_hash: H256, pos: usize) -> Vec<u8> {
(<Test as pallet_mmr::Config>::INDEXING_PREFIX, parent_hash, pos as u64).encode()
}

let mut ext = new_test_ext(vec![1, 2, 3, 4]);
ext.execute_with(|| {
let parent_hash = ext.execute_with(|| {
init_block(1);
<frame_system::Pallet<Test>>::parent_hash()
});

let mmr_leaf = read_mmr_leaf(&mut ext, 0);
let mmr_leaf = read_mmr_leaf(&mut ext, node_offchain_key(parent_hash, 0));
assert_eq!(
mmr_leaf,
MmrLeaf {
Expand All @@ -128,11 +129,12 @@ fn should_contain_valid_leaf_data() {
);

// build second block on top
ext.execute_with(|| {
let parent_hash = ext.execute_with(|| {
init_block(2);
<frame_system::Pallet<Test>>::parent_hash()
});

let mmr_leaf = read_mmr_leaf(&mut ext, 1);
let mmr_leaf = read_mmr_leaf(&mut ext, node_offchain_key(parent_hash, 1));
assert_eq!(
mmr_leaf,
MmrLeaf {
Expand Down
71 changes: 66 additions & 5 deletions frame/merkle-mountain-range/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@

use codec::Encode;
use frame_support::weights::Weight;
use sp_runtime::traits::{self, One, Saturating};
use sp_runtime::{
traits::{self, One, Saturating},
SaturatedConversion,
};

#[cfg(any(feature = "runtime-benchmarks", test))]
mod benchmarking;
Expand Down Expand Up @@ -116,12 +119,12 @@ pub mod pallet {
/// Prefix for elements stored in the Off-chain DB via Indexing API.
///
/// Each node of the MMR is inserted both on-chain and off-chain via Indexing API.
/// The former does not store full leaf content, just it's compact version (hash),
/// The former does not store full leaf content, just its compact version (hash),
/// and some of the inner mmr nodes might be pruned from on-chain storage.
/// The latter will contain all the entries in their full form.
///
/// Each node is stored in the Off-chain DB under key derived from the
/// [`Self::INDEXING_PREFIX`] and it's in-tree index (MMR position).
/// [`Self::INDEXING_PREFIX`] and its in-tree index (MMR position).
const INDEXING_PREFIX: &'static [u8];

/// A hasher type for MMR.
Expand Down Expand Up @@ -215,8 +218,29 @@ pub mod pallet {
<RootHash<T, I>>::put(root);

let peaks_after = mmr::utils::NodesUtils::new(leaves).number_of_peaks();

T::WeightInfo::on_initialize(peaks_before.max(peaks_after))
}

fn offchain_worker(n: T::BlockNumber) {
use mmr::storage::{OffchainStorage, Storage};
// MMR pallet uses offchain storage to hold full MMR and leaves.
// The leaves are saved under fork-unique keys `(parent_hash, pos)`.
// MMR Runtime depends on `frame_system::block_hash(block_num)` mappings to find
// parent hashes for particular nodes or leaves.
// This MMR offchain worker function moves a rolling window of the same size
// as `frame_system::block_hash` map, where nodes/leaves added by blocks that are just
// about to exit the window are "canonicalized" so that their offchain key no longer
// depends on `parent_hash` therefore on access to `frame_system::block_hash`.
//
// This approach works to eliminate fork-induced leaf collisions in offchain db,
// under the assumption that no fork will be deeper than `frame_system::BlockHashCount`
// blocks (2400 blocks on Polkadot, Kusama, Rococo, etc):
// entries pertaining to block `N` where `N < current-2400` are moved to a key based
// solely on block number. The only way to have collisions is if two competing forks
// are deeper than 2400 blocks and they both "canonicalize" their view of block `N`.
Storage::<OffchainStorage, T, I, LeafOf<T, I>>::canonicalize_offchain(n);
}
}
}

Expand Down Expand Up @@ -254,17 +278,54 @@ where
}

impl<T: Config<I>, I: 'static> Pallet<T, I> {
fn offchain_key(pos: NodeIndex) -> sp_std::prelude::Vec<u8> {
/// Build offchain key from `parent_hash` of block that originally added node `pos` to MMR.
///
/// This combination makes the offchain (key,value) entry resilient to chain forks.
fn node_offchain_key(
parent_hash: <T as frame_system::Config>::Hash,
pos: NodeIndex,
) -> sp_std::prelude::Vec<u8> {
(T::INDEXING_PREFIX, parent_hash, pos).encode()
}

/// Build canonical offchain key for node `pos` in MMR.
///
/// Used for nodes added by now finalized blocks.
fn node_canon_offchain_key(pos: NodeIndex) -> sp_std::prelude::Vec<u8> {
(T::INDEXING_PREFIX, pos).encode()
}

/// Build offchain key for the pruning map.
///
/// Nodes and leaves are initially saved under fork-specific keys in offchain db,
/// eventually they are "canonicalized" and this map is used to prune non-canon entries.
fn pruning_map_offchain_key() -> sp_std::prelude::Vec<u8> {
(T::INDEXING_PREFIX, mmr::storage::OFFCHAIN_PRUNING_MAP_KEY_SUFFIX).encode()
}

/// Provide the parent number for the block that added `leaf_index` to the MMR.
fn leaf_index_to_parent_block_num(
leaf_index: LeafIndex,
leaves_count: LeafIndex,
) -> <T as frame_system::Config>::BlockNumber {
// leaves are zero-indexed and were added one per block since pallet activation,
// while block numbers are one-indexed, so block number that added `leaf_idx` is:
// `block_num = block_num_when_pallet_activated + leaf_idx + 1`
// `block_num = (current_block_num - leaves_count) + leaf_idx + 1`
// `parent_block_num = current_block_num - leaves_count + leaf_idx`.
<frame_system::Pallet<T>>::block_number()
.saturating_sub(leaves_count.saturated_into())
.saturating_add(leaf_index.saturated_into())
}

/// Generate a MMR proof for the given `leaf_indices`.
///
/// Note this method can only be used from an off-chain context
/// (Offchain Worker or Runtime API call), since it requires
/// all the leaves to be present.
/// It may return an error or panic if used incorrectly.
acatangiu marked this conversation as resolved.
Show resolved Hide resolved
pub fn generate_batch_proof(
leaf_indices: Vec<NodeIndex>,
leaf_indices: Vec<LeafIndex>,
) -> Result<
(Vec<LeafOf<T, I>>, primitives::BatchProof<<T as Config<I>>::Hash>),
primitives::Error,
Expand Down
Loading