diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index f37345014f3a1..977aa71043eb4 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -1788,6 +1788,14 @@ impl_runtime_apis! { .map(|(leaf, proof)| (mmr::EncodableOpaqueLeaf::from_leaf(&leaf), proof)) } + fn generate_historical_proof( + leaf_index: pallet_mmr::primitives::LeafIndex, + leaves_count: pallet_mmr::primitives::LeafIndex, + ) -> Result<(mmr::EncodableOpaqueLeaf, mmr::Proof), mmr::Error> { + Mmr::generate_historical_proof(leaf_index, leaves_count) + .map(|(leaf, proof)| (mmr::EncodableOpaqueLeaf::from_leaf(&leaf), proof)) + } + fn verify_proof(leaf: mmr::EncodableOpaqueLeaf, proof: mmr::Proof) -> Result<(), mmr::Error> { diff --git a/client/beefy/src/tests.rs b/client/beefy/src/tests.rs index e568daba8e112..43c1313db26b0 100644 --- a/client/beefy/src/tests.rs +++ b/client/beefy/src/tests.rs @@ -243,6 +243,11 @@ macro_rules! create_test_api { unimplemented!() } + fn generate_historical_proof(_leaf_index: LeafIndex, _leaves_count: LeafIndex) + -> Result<(EncodableOpaqueLeaf, Proof), MmrError> { + unimplemented!() + } + fn verify_proof(_leaf: EncodableOpaqueLeaf, _proof: Proof) -> Result<(), MmrError> { unimplemented!() diff --git a/frame/merkle-mountain-range/rpc/src/lib.rs b/frame/merkle-mountain-range/rpc/src/lib.rs index 99359bfea8eb6..a2d36e9b57eac 100644 --- a/frame/merkle-mountain-range/rpc/src/lib.rs +++ b/frame/merkle-mountain-range/rpc/src/lib.rs @@ -74,6 +74,29 @@ pub trait MmrApi { leaf_index: LeafIndex, at: Option, ) -> Result>; + + /// Generate MMR proof for given leaf index. + /// + /// This method calls into a runtime with MMR pallet included and attempts to generate + /// MMR proof for leaf at given `leaf_index` with MMR fixed to the state with exactly + /// `leaves_count` leaves. `leaves_count` must be larger than the `leaf_index` for + /// function to succeed. + /// + /// Optionally, a block hash at which the runtime should be queried can be specified. + /// Note that specifying the block hash isn't super-useful here, unless you're generating + /// proof using non-finalized blocks where there are several competing forks. That's because + /// MMR state will be fixed to the state with `leaves_count`, which already points to some + /// historical block. + /// + /// Returns the (full) leaf itself and a proof for this leaf (compact encoding, i.e. hash of + /// the leaf). Both parameters are SCALE-encoded. + #[rpc(name = "mmr_generateHistoricalProof")] + fn generate_historical_proof( + &self, + leaf_index: LeafIndex, + leaves_index: LeafIndex, + at: Option, + ) -> Result>; } /// An implementation of MMR specific RPC methods. @@ -117,6 +140,30 @@ where Ok(LeafProof::new(block_hash, leaf, proof)) } + + fn generate_historical_proof( + &self, + leaf_index: LeafIndex, + leaves_count: LeafIndex, + at: Option<::Hash>, + ) -> Result::Hash>> { + let api = self.client.runtime_api(); + let block_hash = at.unwrap_or_else(|| + // If the block hash is not supplied assume the best block. + self.client.info().best_hash); + + let (leaf, proof) = api + .generate_historical_proof_with_context( + &BlockId::hash(block_hash), + sp_core::ExecutionContext::OffchainCall(None), + leaf_index, + leaves_count, + ) + .map_err(runtime_error_into_rpc_error)? + .map_err(mmr_error_into_rpc_error)?; + + Ok(LeafProof::new(block_hash, leaf, proof)) + } } const RUNTIME_ERROR: i64 = 8000; diff --git a/frame/merkle-mountain-range/src/lib.rs b/frame/merkle-mountain-range/src/lib.rs index 855eb0a7436dc..0f24b3e4843de 100644 --- a/frame/merkle-mountain-range/src/lib.rs +++ b/frame/merkle-mountain-range/src/lib.rs @@ -265,7 +265,23 @@ impl, I: 'static> Pallet { pub fn generate_proof( leaf_index: LeafIndex, ) -> Result<(LeafOf, primitives::Proof<>::Hash>), primitives::Error> { - let mmr: ModuleMmr = mmr::Mmr::new(Self::mmr_leaves()); + Self::generate_historical_proof(leaf_index, Self::mmr_leaves()) + } + + /// Generate a MMR proof for the given `leaf_index`. + /// + /// This method is not touching any runtime storage keys, as all it needs is coming + /// either from arguments, or from the offchain storage. + /// + /// 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. + pub fn generate_historical_proof( + leaf_index: LeafIndex, + leaves_count: LeafIndex, + ) -> Result<(LeafOf, primitives::Proof<>::Hash>), primitives::Error> { + let mmr: ModuleMmr = mmr::Mmr::new(leaves_count); mmr.generate_proof(leaf_index) } diff --git a/frame/merkle-mountain-range/src/tests.rs b/frame/merkle-mountain-range/src/tests.rs index 70d1395aa94d5..67bb0ed0ae49a 100644 --- a/frame/merkle-mountain-range/src/tests.rs +++ b/frame/merkle-mountain-range/src/tests.rs @@ -347,3 +347,101 @@ fn should_verify_on_the_next_block_since_there_is_no_pruning_yet() { assert_eq!(crate::Pallet::::verify_leaf(leaf, proof5), Ok(())); }); } + +#[test] +fn able_to_generate_historical_proofs() { + let _ = env_logger::try_init(); + let mut ext = new_test_ext(); + + // Given full client (where the code is executed) that has 7 blocks (aka 7 MMR leafs). + // Let's imagine that the state for blocks 1..=5 has been pruned already. + // + // And BEEFY light client, which has synced to the 3rd block and needs to verify some + // MMR proof there. It can't just use `generate_proof(block_3_hash)`, because the state + // has already been discarded. So it must use `generate_historical_proof` instead. + let mmr_root_known_to_light_client = ext.execute_with(|| { + new_block(); // 1 + new_block(); // 2 + new_block(); // 3 + let mmr_root_at_3 = crate::RootHash::::get(); + new_block(); // 4 + new_block(); // 5 + new_block(); // 6 + new_block(); // 7 + mmr_root_at_3 + }); + ext.persist_offchain_overlay(); + + // Try to generate historical proof using latest state. This requires the offchain + // extensions to be present to retrieve full leaf data. + register_offchain_ext(&mut ext); + ext.execute_with(|| { + // ensure that there are exactly 7 leaves + assert_eq!(crate::NumberOfLeaves::::get(), 7); + + // try to generate both regular and historical proofs + let leaf_index = 2; + let leaf_count_at_block_3 = 3; + let (_, regular_proof) = crate::Pallet::::generate_proof(leaf_index).unwrap(); + let (leaf, historical_proof) = + crate::Pallet::::generate_historical_proof(leaf_index, leaf_count_at_block_3) + .unwrap(); + + // verify that the regular proof has 3 items: + // + // D + // / \ + // / \ + // A B C + // / \ / \ / \ + // 1 2 3 4 5 6 7 + // + // we're proving 3 => we need { 4, A, C++7 } + assert_eq!(regular_proof.items.len(), 3); + + // verify that the historical proof has 1 item: + // + // A + // / \ + // 1 2 3 + // + // we're proving 3 => we need { A } + assert_eq!(historical_proof.items.len(), 1); + + // verify that the light client is able to verify historical proof using MMR root from block + // 3 + let verification_result = verify_leaf_proof::, LeafOf>( + mmr_root_known_to_light_client, + leaf.into(), + historical_proof, + ); + assert_eq!(verification_result, Ok(())); + }); +} + +#[test] +fn does_not_panic_when_generating_historical_proofs() { + let _ = env_logger::try_init(); + let mut ext = new_test_ext(); + + // given 7 blocks (7 MMR leaves) + ext.execute_with(|| init_chain(7)); + ext.persist_offchain_overlay(); + + // Try to generate historical proof with invalid arguments. This requires the offchain + // extensions to be present to retrieve full leaf data. + register_offchain_ext(&mut ext); + ext.execute_with(|| { + // when leaf index is invalid + assert_eq!( + crate::Pallet::::generate_historical_proof(10, 7), + Err(Error::LeafNotFound), + ); + + // when leaves count is invalid + assert_eq!( + crate::Pallet::::generate_historical_proof(3, 100), + Err(Error::GenerateProof), + ); + }); +} diff --git a/primitives/merkle-mountain-range/src/lib.rs b/primitives/merkle-mountain-range/src/lib.rs index a27536b8d35b7..dfce2b045af71 100644 --- a/primitives/merkle-mountain-range/src/lib.rs +++ b/primitives/merkle-mountain-range/src/lib.rs @@ -399,6 +399,17 @@ sp_api::decl_runtime_apis! { /// Generate MMR proof for a leaf under given index. fn generate_proof(leaf_index: LeafIndex) -> Result<(EncodableOpaqueLeaf, Proof), Error>; + /// Generate a historical MMR proof for a leaf with given index. + /// + /// This function may be used to generate proofs using MMR state at some earlier + /// point. To do that, you shall provide historical value of the `leaves_count`. + /// The proof generated by this function may be verified by clients that only have + /// some old MMR root (corresponding to the `leaves_count`). + fn generate_historical_proof( + leaf_index: LeafIndex, + leaves_count: LeafIndex, + ) -> Result<(EncodableOpaqueLeaf, Proof), Error>; + /// Verify MMR proof against on-chain MMR. /// /// Note this function will use on-chain MMR root hash and check if the proof