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

historical slashing w ocw w adhoc tree creation #6220

Merged
merged 35 commits into from
Jun 16, 2020
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b7d4a2a
draft
Jun 2, 2020
d2bb3b8
steps
Jun 2, 2020
7a4426c
chore: fmt
Jun 2, 2020
8eb7a4c
step by step
Jun 3, 2020
fe0682b
more details
Jun 3, 2020
c24b0a3
make test public
Jun 4, 2020
5aec81e
refactor: split into on and offchain
Jun 4, 2020
f1e9367
test stab
Jun 5, 2020
f29babe
tabs my friend
Jun 5, 2020
388970b
offchain overlay: split key into prefix and true key
Jun 9, 2020
1c41dfb
test: share state
Jun 9, 2020
3249272
fix & test
Jun 9, 2020
2600769
docs improv
Jun 9, 2020
5383907
address review comments
Jun 9, 2020
d6a78ca
cleanup test chore
Jun 9, 2020
3e5d00c
refactor, abbrev link text
Jun 9, 2020
dbbb282
chore: linewidth
Jun 10, 2020
2032b6a
fix prefix key split fallout
Jun 10, 2020
1bb092f
minor fallout
Jun 10, 2020
4abd969
minor changes
Jun 10, 2020
1fdc7c1
addresses review comments
Jun 10, 2020
1d271db
rename historical.rs -> historical/mod.rs
Jun 10, 2020
acb67fd
avoid shared::* wildcard import
Jun 10, 2020
359dec9
fix/compile: missing shared:: prefix
Jun 11, 2020
126f6ea
fix: add missing call to store_session_validator_set_to_offchain
Jun 11, 2020
7a7c0b2
fix/test: flow
Jun 12, 2020
87c1696
Merge remote-tracking branch 'origin/master' into bernhard/historical…
Jun 15, 2020
23cc9cf
fix/review: Apply suggestions from code review
drahnr Jun 15, 2020
68ad304
fix/review: more review comment fixes
Jun 15, 2020
bf34c2c
fix/review: make ValidatorSet private
Jun 15, 2020
55f4271
fix/include: core -> sp_core
Jun 15, 2020
6c6c484
fix/review: fallout
Jun 15, 2020
7052484
fix/visbility: make them public API
Jun 15, 2020
6069f35
Merge remote-tracking branch 'origin/master' into bernhard/historical…
Jun 15, 2020
5231d06
fix/review: review changes fallout - again
Jun 16, 2020
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
7 changes: 6 additions & 1 deletion client/db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,12 @@ pub struct BlockImportOperation<Block: BlockT> {

impl<Block: BlockT> BlockImportOperation<Block> {
fn apply_offchain(&mut self, transaction: &mut Transaction<DbHash>) {
for (key, value_operation) in self.offchain_storage_updates.drain() {
for ((prefix, key), value_operation) in self.offchain_storage_updates.drain() {
let key: Vec<u8> = prefix
.into_iter()
.chain(b"/".into_iter().copied())
drahnr marked this conversation as resolved.
Show resolved Hide resolved
.chain(key.into_iter())
.collect();
match value_operation {
OffchainOverlayedChange::SetValue(val) => transaction.set_from_vec(columns::OFFCHAIN, &key, val),
OffchainOverlayedChange::Remove => transaction.remove(columns::OFFCHAIN, &key),
Expand Down
6 changes: 4 additions & 2 deletions frame/session/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
serde = { version = "1.0.101", optional = true }
codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] }
sp-core = { version = "2.0.0-rc3", default-features = false, path = "../../primitives/core" }
sp-std = { version = "2.0.0-rc3", default-features = false, path = "../../primitives/std" }
sp-io = { version = "2.0.0-rc3", default-features = false, path = "../../primitives/io" }
sp-runtime = { version = "2.0.0-rc3", default-features = false, path = "../../primitives/runtime" }
sp-session = { version = "2.0.0-rc3", default-features = false, path = "../../primitives/session" }
sp-staking = { version = "2.0.0-rc3", default-features = false, path = "../../primitives/staking" }
Expand All @@ -25,8 +27,6 @@ sp-trie = { version = "2.0.0-rc3", optional = true, default-features = false, pa
impl-trait-for-tuples = "0.1.3"

[dev-dependencies]
sp-core = { version = "2.0.0-rc3", path = "../../primitives/core" }
sp-io ={ version = "2.0.0-rc3", path = "../../primitives/io" }
sp-application-crypto = { version = "2.0.0-rc3", path = "../../primitives/application-crypto" }
lazy_static = "1.4.0"

Expand All @@ -38,6 +38,8 @@ std = [
"codec/std",
"sp-std/std",
"frame-support/std",
"sp-core/std",
"sp-std/std",
drahnr marked this conversation as resolved.
Show resolved Hide resolved
"sp-runtime/std",
"sp-session/std",
"sp-staking/std",
Expand Down
13 changes: 8 additions & 5 deletions frame/session/src/historical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ use sp_trie::{MemoryDB, Trie, TrieMut, Recorder, EMPTY_PREFIX};
use sp_trie::trie_types::{TrieDBMut, TrieDB};
use super::{SessionIndex, Module as SessionModule};

mod shared;
pub mod offchain;
pub mod onchain;
drahnr marked this conversation as resolved.
Show resolved Hide resolved

/// Trait necessary for the historical module.
pub trait Trait: super::Trait {
/// Full identification of the validator.
Expand Down Expand Up @@ -154,7 +158,7 @@ impl<T: Trait, I> crate::SessionManager<T::ValidatorId> for NoteHistoricalRoot<T
/// A tuple of the validator's ID and their full identification.
pub type IdentificationTuple<T> = (<T as crate::Trait>::ValidatorId, <T as Trait>::FullIdentification);

/// a trie instance for checking and generating proofs.
/// A trie instance for checking and generating proofs.
pub struct ProvingTrie<T: Trait> {
db: MemoryDB<T::Hashing>,
root: T::Hash,
Expand Down Expand Up @@ -250,7 +254,6 @@ impl<T: Trait> ProvingTrie<T> {
.ok()?
.and_then(|raw| <IdentificationTuple<T>>::decode(&mut &*raw).ok())
}

}

impl<T: Trait, D: AsRef<[u8]>> frame_support::traits::KeyOwnerProofSystem<(KeyTypeId, D)>
Expand Down Expand Up @@ -311,9 +314,9 @@ impl<T: Trait, D: AsRef<[u8]>> frame_support::traits::KeyOwnerProofSystem<(KeyTy
}

#[cfg(test)]
mod tests {
pub(crate) mod tests {
use super::*;
use sp_core::crypto::key_types::DUMMY;
use sp_runtime::key_types::DUMMY;
use sp_runtime::testing::UintAuthorityId;
use crate::mock::{
NEXT_VALIDATORS, force_new_session,
Expand All @@ -323,7 +326,7 @@ mod tests {

type Historical = Module<Test>;

fn new_test_ext() -> sp_io::TestExternalities {
pub(crate) fn new_test_ext() -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
crate::GenesisConfig::<Test> {
keys: NEXT_VALIDATORS.with(|l|
Expand Down
270 changes: 270 additions & 0 deletions frame/session/src/historical/offchain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
// This file is part of Substrate.

// Copyright (C) 2019-2020 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Off-chain logic for creating a proof based data provided by on-chain logic.
//!
//! Validator-set extracting an iterator from an off-chain worker stored list containing
//! historical validator-sets.
//! Based on the logic of historical slashing, but the validation is done off-chain.
//! Use [`fn store_current_session_validator_set_to_offchain()`](super::onchain) to store the
//! required data to the offchain validator set.
//! This is used in conjunction with [`ProvingTrie`](super::ProvingTrie) and
//! the off-chain indexing API.

use sp_runtime::{offchain::storage::StorageValueRef, KeyTypeId};
use sp_session::MembershipProof;

use super::super::{Module as SessionModule, SessionIndex};
use super::{IdentificationTuple, ProvingTrie, Trait};

use super::shared::*;
drahnr marked this conversation as resolved.
Show resolved Hide resolved
use sp_std::prelude::*;

pub struct ValidatorSet<T: Trait> {
drahnr marked this conversation as resolved.
Show resolved Hide resolved
validator_set: Vec<IdentificationTuple<T>>,
}

impl<T: Trait> ValidatorSet<T> {
/// Load the set of validators for a particular session index from the off-chain storage.
///
/// If none is found or decodable given `prefix` and `session`, it will return `None`.
/// Empty validator sets should only ever exist for genesis blocks.
pub fn load_from_offchain_db(session_index: SessionIndex) -> Option<Self> {
let derived_key = derive_key(PREFIX, session_index);
StorageValueRef::persistent(derived_key.as_ref())
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be the unimplemented StorageValueRef::local (in general got a hard time seing how this will work with reorg)?

Copy link
Contributor Author

@drahnr drahnr Jun 16, 2020

Choose a reason for hiding this comment

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

There was a discussion about re-orgs with @tomusdrw and @rphmeier and it turns out that the validator set per session is consistent across re-orgs regarding the session index. So for the case of a re-org, the same vec of validators would be written to the same key.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, this is because sessions are buffered by 1

.get::<Vec<(T::ValidatorId, T::FullIdentification)>>()
.flatten()
.map(|validator_set| Self { validator_set })
}

/// Access the underlying `ValidatorId` and `FullIdentification` tuples as slice.
pub fn as_slice(&self) -> &[(T::ValidatorId, T::FullIdentification)] {
self.validator_set.as_slice()
}

/// Convert `self` into a vector and consume `self`.
pub fn into_vec(self) -> Vec<(T::ValidatorId, T::FullIdentification)> {
self.validator_set
}
drahnr marked this conversation as resolved.
Show resolved Hide resolved

/// Attempt to prune anything that is older than `first_to_keep` session index.
///
/// Due to re-organisation it could be that the `first_to_keep` might be less
/// than the stored one, in which case the conservative choice is made to keep records
/// up to the one that is the lesser.
pub fn prune_older_than(first_to_keep: SessionIndex) {
drahnr marked this conversation as resolved.
Show resolved Hide resolved
let derived_key = LAST_PRUNE.to_vec();
let entry = StorageValueRef::persistent(derived_key.as_ref());
match entry.mutate(|current: Option<Option<SessionIndex>>| -> Result<_, ()> {
match current {
Some(Some(current)) if current < first_to_keep => Ok(first_to_keep),
// do not move the cursor, if the new one would be behind ours
Some(Some(current)) => Ok(current),
None => Ok(first_to_keep),
// if the storage contains undecodable data, overwrite with current anyways
// which might leak some entries being never purged, but that is acceptable
// in this context
Some(None) => Ok(first_to_keep),
}
}) {
Ok(Ok(new_value)) => {
// on a re-org this is not necessarily true, with the above they might be equal
if new_value < first_to_keep {
for session_index in new_value..first_to_keep {
let derived_key = derive_key(PREFIX, session_index);
let _ = StorageValueRef::persistent(derived_key.as_ref()).clear();
}
}
}
Ok(Err(_)) => {} // failed to store the value calculated with the given closure
Err(_) => {} // failed to calculate the value to store with the given closure
}
}

/// Keep the newest `n` items, and prune all items older than that.
pub fn keep_newest(n_to_keep: usize) {
drahnr marked this conversation as resolved.
Show resolved Hide resolved
let session_index = <SessionModule<T>>::current_index();
let n_to_keep = n_to_keep as SessionIndex;
if n_to_keep < session_index {
Self::prune_older_than(session_index - n_to_keep)
}
}

#[inline]
fn len(&self) -> usize {
self.validator_set.len()
}
}

/// Implement conversion into iterator for usage
/// with [ProvingTrie](super::ProvingTrie::generate_for).
impl<T: Trait> core::iter::IntoIterator for ValidatorSet<T> {
drahnr marked this conversation as resolved.
Show resolved Hide resolved
type Item = (T::ValidatorId, T::FullIdentification);
type IntoIter = sp_std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.validator_set.into_iter()
}
}

/// Create a proof based on the data available in the off-chain database.
///
/// Based on the yielded `MembershipProof` the implementer may decide what
/// to do, i.e. in case of a failed proof, enqueue a transaction back on
/// chain reflecting that, with all its consequences such as i.e. slashing.
pub fn prove_session_membership<T: Trait, D: AsRef<[u8]>>(
drahnr marked this conversation as resolved.
Show resolved Hide resolved
session_index: SessionIndex,
session_key: (KeyTypeId, D),
) -> Option<MembershipProof> {
let validators = ValidatorSet::<T>::load_from_offchain_db(session_index)?;
let count = validators.len() as u32;
let trie = ProvingTrie::<T>::generate_for(validators.into_iter()).ok()?;

let (id, data) = session_key;
trie.prove(id, data.as_ref())
.map(|trie_nodes| MembershipProof {
session: session_index,
trie_nodes,
validator_count: count,
})
}

#[cfg(test)]
mod tests {
use super::super::{onchain, Module};
use super::*;
use crate::mock::{
force_new_session, set_next_validators, Session, System, Test, NEXT_VALIDATORS,
};
use codec::Encode;
use frame_support::traits::{KeyOwnerProofSystem, OnInitialize};
use sp_core::crypto::key_types::DUMMY;
use sp_core::offchain::{
testing::TestOffchainExt,
OffchainExt,
StorageKind,
};

use sp_runtime::testing::UintAuthorityId;

type Historical = Module<Test>;

pub fn new_test_ext() -> sp_io::TestExternalities {
let mut ext = frame_system::GenesisConfig::default()
.build_storage::<Test>()
.expect("Failed to create test externalities.");

crate::GenesisConfig::<Test> {
keys: NEXT_VALIDATORS.with(|l| {
l.borrow()
.iter()
.cloned()
.map(|i| (i, i, UintAuthorityId(i).into()))
.collect()
}),
}
.assimilate_storage(&mut ext)
.unwrap();


let mut ext = sp_io::TestExternalities::new(ext);

let (offchain, offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db());

const ITERATIONS: u32 = 5u32;
let mut seed = [0u8; 32];
seed[0..4].copy_from_slice(&ITERATIONS.to_le_bytes());
offchain_state.write().seed = seed;

ext.register_extension(OffchainExt::new(offchain));
ext
}

#[test]
fn encode_decode_roundtrip() {
use codec::{Decode, Encode};
use super::super::super::Trait as SessionTrait;
use super::super::Trait as HistoricalTrait;

let sample = (
22u32 as <Test as SessionTrait>::ValidatorId,
7_777_777 as <Test as HistoricalTrait>::FullIdentification);

let encoded = sample.encode();
let decoded = Decode::decode(&mut encoded.as_slice()).expect("Must decode");
assert_eq!(sample, decoded);
}

#[test]
fn onchain_to_offchain() {
let mut ext = new_test_ext();

const DATA: &[u8] = &[7,8,9,10,11];
ext.execute_with(|| {
b"alphaomega"[..].using_encoded(|key| sp_io::offchain_index::set(key, DATA));
});

ext.sync_offchain_index_changes();

ext.execute_with(|| {
let data =
b"alphaomega"[..].using_encoded(|key| {
sp_io::offchain::local_storage_get(StorageKind::PERSISTENT, key)
});
assert_eq!(data, Some(DATA.to_vec()));
});
}


#[test]
fn historical_proof_offchain() {
let mut ext = new_test_ext();
let encoded_key_1 = UintAuthorityId(1).encode();

ext.execute_with(|| {
set_next_validators(vec![1, 2]);
force_new_session();

System::set_block_number(1);
Session::on_initialize(1);

// "on-chain"
onchain::store_current_session_validator_set_to_offchain::<Test>();
assert_eq!(<SessionModule<Test>>::current_index(), 1);

set_next_validators(vec![7, 8]);

force_new_session();
});

ext.sync_offchain_index_changes();

ext.execute_with(|| {


System::set_block_number(2);
Session::on_initialize(2);
assert_eq!(<SessionModule<Test>>::current_index(), 2);

// "off-chain"
let proof = prove_session_membership::<Test, _>(1, (DUMMY, &encoded_key_1));
assert!(proof.is_some());
let proof = proof.expect("Must be Some(Proof)");

assert!(Historical::check_proof((DUMMY, &encoded_key_1[..]), proof.clone()).is_some());
});
}
}
Loading