Skip to content

Commit 2e886c0

Browse files
drahnrBernhard Schustertomusdrw
authored
historical slashing w ocw w adhoc tree creation (#6220)
* draft * steps * chore: fmt * step by step * more details * make test public * refactor: split into on and offchain * test stab * tabs my friend * offchain overlay: split key into prefix and true key Simplifies inspection and makes key actually unique. * test: share state * fix & test * docs improv * address review comments * cleanup test chore * refactor, abbrev link text * chore: linewidth * fix prefix key split fallout * minor fallout * minor changes * addresses review comments * rename historical.rs -> historical/mod.rs * avoid shared::* wildcard import * fix: add missing call to store_session_validator_set_to_offchain * fix/compile: missing shared:: prefix * fix/test: flow * fix/review: Apply suggestions from code review Co-authored-by: Tomasz Drwięga <[email protected]> * fix/review: more review comment fixes * fix/review: make ValidatorSet private * fix/include: core -> sp_core * fix/review: fallout * fix/visbility: make them public API Ref #6358 * fix/review: review changes fallout - again Co-authored-by: Bernhard Schuster <[email protected]> Co-authored-by: Tomasz Drwięga <[email protected]>
1 parent 7017652 commit 2e886c0

File tree

5 files changed

+380
-7
lines changed

5 files changed

+380
-7
lines changed

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ targets = ["x86_64-unknown-linux-gnu"]
1414
[dependencies]
1515
serde = { version = "1.0.101", optional = true }
1616
codec = { package = "parity-scale-codec", version = "1.3.0", default-features = false, features = ["derive"] }
17+
sp-core = { version = "2.0.0-rc3", default-features = false, path = "../../primitives/core" }
1718
sp-std = { version = "2.0.0-rc3", default-features = false, path = "../../primitives/std" }
19+
sp-io = { version = "2.0.0-rc3", default-features = false, path = "../../primitives/io" }
1820
sp-runtime = { version = "2.0.0-rc3", default-features = false, path = "../../primitives/runtime" }
1921
sp-session = { version = "2.0.0-rc3", default-features = false, path = "../../primitives/session" }
2022
sp-staking = { version = "2.0.0-rc3", default-features = false, path = "../../primitives/staking" }
@@ -25,8 +27,6 @@ sp-trie = { version = "2.0.0-rc3", optional = true, default-features = false, pa
2527
impl-trait-for-tuples = "0.1.3"
2628

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

@@ -37,7 +37,9 @@ std = [
3737
"serde",
3838
"codec/std",
3939
"sp-std/std",
40+
"sp-io/std",
4041
"frame-support/std",
42+
"sp-core/std",
4143
"sp-runtime/std",
4244
"sp-session/std",
4345
"sp-staking/std",

src/historical.rs renamed to src/historical/mod.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ use sp_trie::{MemoryDB, Trie, TrieMut, Recorder, EMPTY_PREFIX};
3737
use sp_trie::trie_types::{TrieDBMut, TrieDB};
3838
use super::{SessionIndex, Module as SessionModule};
3939

40+
mod shared;
41+
pub mod offchain;
42+
pub mod onchain;
43+
4044
/// Trait necessary for the historical module.
4145
pub trait Trait: super::Trait {
4246
/// Full identification of the validator.
@@ -116,6 +120,7 @@ impl<T: Trait, I> crate::SessionManager<T::ValidatorId> for NoteHistoricalRoot<T
116120
where I: SessionManager<T::ValidatorId, T::FullIdentification>
117121
{
118122
fn new_session(new_index: SessionIndex) -> Option<Vec<T::ValidatorId>> {
123+
119124
StoredRange::mutate(|range| {
120125
range.get_or_insert_with(|| (new_index, new_index)).1 = new_index + 1;
121126
});
@@ -143,18 +148,21 @@ impl<T: Trait, I> crate::SessionManager<T::ValidatorId> for NoteHistoricalRoot<T
143148

144149
new_validators
145150
}
151+
146152
fn start_session(start_index: SessionIndex) {
147153
<I as SessionManager<_, _>>::start_session(start_index)
148154
}
155+
149156
fn end_session(end_index: SessionIndex) {
157+
onchain::store_session_validator_set_to_offchain::<T>(end_index);
150158
<I as SessionManager<_, _>>::end_session(end_index)
151159
}
152160
}
153161

154162
/// A tuple of the validator's ID and their full identification.
155163
pub type IdentificationTuple<T> = (<T as crate::Trait>::ValidatorId, <T as Trait>::FullIdentification);
156164

157-
/// a trie instance for checking and generating proofs.
165+
/// A trie instance for checking and generating proofs.
158166
pub struct ProvingTrie<T: Trait> {
159167
db: MemoryDB<T::Hashing>,
160168
root: T::Hash,
@@ -250,7 +258,6 @@ impl<T: Trait> ProvingTrie<T> {
250258
.ok()?
251259
.and_then(|raw| <IdentificationTuple<T>>::decode(&mut &*raw).ok())
252260
}
253-
254261
}
255262

256263
impl<T: Trait, D: AsRef<[u8]>> frame_support::traits::KeyOwnerProofSystem<(KeyTypeId, D)>
@@ -311,9 +318,9 @@ impl<T: Trait, D: AsRef<[u8]>> frame_support::traits::KeyOwnerProofSystem<(KeyTy
311318
}
312319

313320
#[cfg(test)]
314-
mod tests {
321+
pub(crate) mod tests {
315322
use super::*;
316-
use sp_core::crypto::key_types::DUMMY;
323+
use sp_runtime::key_types::DUMMY;
317324
use sp_runtime::testing::UintAuthorityId;
318325
use crate::mock::{
319326
NEXT_VALIDATORS, force_new_session,
@@ -323,7 +330,7 @@ mod tests {
323330

324331
type Historical = Module<Test>;
325332

326-
fn new_test_ext() -> sp_io::TestExternalities {
333+
pub(crate) fn new_test_ext() -> sp_io::TestExternalities {
327334
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
328335
crate::GenesisConfig::<Test> {
329336
keys: NEXT_VALIDATORS.with(|l|

src/historical/offchain.rs

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
// This file is part of Substrate.
2+
3+
// Copyright (C) 2019-2020 Parity Technologies (UK) Ltd.
4+
// SPDX-License-Identifier: Apache-2.0
5+
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
18+
//! Off-chain logic for creating a proof based data provided by on-chain logic.
19+
//!
20+
//! Validator-set extracting an iterator from an off-chain worker stored list containing
21+
//! historical validator-sets.
22+
//! Based on the logic of historical slashing, but the validation is done off-chain.
23+
//! Use [`fn store_current_session_validator_set_to_offchain()`](super::onchain) to store the
24+
//! required data to the offchain validator set.
25+
//! This is used in conjunction with [`ProvingTrie`](super::ProvingTrie) and
26+
//! the off-chain indexing API.
27+
28+
use sp_runtime::{offchain::storage::StorageValueRef, KeyTypeId};
29+
use sp_session::MembershipProof;
30+
31+
use super::super::{Module as SessionModule, SessionIndex};
32+
use super::{IdentificationTuple, ProvingTrie, Trait};
33+
34+
use super::shared;
35+
use sp_std::prelude::*;
36+
37+
38+
/// A set of validators, which was used for a fixed session index.
39+
struct ValidatorSet<T: Trait> {
40+
validator_set: Vec<IdentificationTuple<T>>,
41+
}
42+
43+
impl<T: Trait> ValidatorSet<T> {
44+
/// Load the set of validators for a particular session index from the off-chain storage.
45+
///
46+
/// If none is found or decodable given `prefix` and `session`, it will return `None`.
47+
/// Empty validator sets should only ever exist for genesis blocks.
48+
pub fn load_from_offchain_db(session_index: SessionIndex) -> Option<Self> {
49+
let derived_key = shared::derive_key(shared::PREFIX, session_index);
50+
StorageValueRef::persistent(derived_key.as_ref())
51+
.get::<Vec<(T::ValidatorId, T::FullIdentification)>>()
52+
.flatten()
53+
.map(|validator_set| Self { validator_set })
54+
}
55+
56+
#[inline]
57+
fn len(&self) -> usize {
58+
self.validator_set.len()
59+
}
60+
}
61+
62+
/// Implement conversion into iterator for usage
63+
/// with [ProvingTrie](super::ProvingTrie::generate_for).
64+
impl<T: Trait> sp_std::iter::IntoIterator for ValidatorSet<T> {
65+
type Item = (T::ValidatorId, T::FullIdentification);
66+
type IntoIter = sp_std::vec::IntoIter<Self::Item>;
67+
fn into_iter(self) -> Self::IntoIter {
68+
self.validator_set.into_iter()
69+
}
70+
}
71+
72+
/// Create a proof based on the data available in the off-chain database.
73+
///
74+
/// Based on the yielded `MembershipProof` the implementer may decide what
75+
/// to do, i.e. in case of a failed proof, enqueue a transaction back on
76+
/// chain reflecting that, with all its consequences such as i.e. slashing.
77+
pub fn prove_session_membership<T: Trait, D: AsRef<[u8]>>(
78+
session_index: SessionIndex,
79+
session_key: (KeyTypeId, D),
80+
) -> Option<MembershipProof> {
81+
let validators = ValidatorSet::<T>::load_from_offchain_db(session_index)?;
82+
let count = validators.len() as u32;
83+
let trie = ProvingTrie::<T>::generate_for(validators.into_iter()).ok()?;
84+
85+
let (id, data) = session_key;
86+
trie.prove(id, data.as_ref())
87+
.map(|trie_nodes| MembershipProof {
88+
session: session_index,
89+
trie_nodes,
90+
validator_count: count,
91+
})
92+
}
93+
94+
95+
/// Attempt to prune anything that is older than `first_to_keep` session index.
96+
///
97+
/// Due to re-organisation it could be that the `first_to_keep` might be less
98+
/// than the stored one, in which case the conservative choice is made to keep records
99+
/// up to the one that is the lesser.
100+
pub fn prune_older_than<T: Trait>(first_to_keep: SessionIndex) {
101+
let derived_key = shared::LAST_PRUNE.to_vec();
102+
let entry = StorageValueRef::persistent(derived_key.as_ref());
103+
match entry.mutate(|current: Option<Option<SessionIndex>>| -> Result<_, ()> {
104+
match current {
105+
Some(Some(current)) if current < first_to_keep => Ok(first_to_keep),
106+
// do not move the cursor, if the new one would be behind ours
107+
Some(Some(current)) => Ok(current),
108+
None => Ok(first_to_keep),
109+
// if the storage contains undecodable data, overwrite with current anyways
110+
// which might leak some entries being never purged, but that is acceptable
111+
// in this context
112+
Some(None) => Ok(first_to_keep),
113+
}
114+
}) {
115+
Ok(Ok(new_value)) => {
116+
// on a re-org this is not necessarily true, with the above they might be equal
117+
if new_value < first_to_keep {
118+
for session_index in new_value..first_to_keep {
119+
let derived_key = shared::derive_key(shared::PREFIX, session_index);
120+
let _ = StorageValueRef::persistent(derived_key.as_ref()).clear();
121+
}
122+
}
123+
}
124+
Ok(Err(_)) => {} // failed to store the value calculated with the given closure
125+
Err(_) => {} // failed to calculate the value to store with the given closure
126+
}
127+
}
128+
129+
/// Keep the newest `n` items, and prune all items older than that.
130+
pub fn keep_newest<T: Trait>(n_to_keep: usize) {
131+
let session_index = <SessionModule<T>>::current_index();
132+
let n_to_keep = n_to_keep as SessionIndex;
133+
if n_to_keep < session_index {
134+
prune_older_than::<T>(session_index - n_to_keep)
135+
}
136+
}
137+
138+
#[cfg(test)]
139+
mod tests {
140+
use super::super::{onchain, Module};
141+
use super::*;
142+
use crate::mock::{
143+
force_new_session, set_next_validators, Session, System, Test, NEXT_VALIDATORS,
144+
};
145+
use codec::Encode;
146+
use frame_support::traits::{KeyOwnerProofSystem, OnInitialize};
147+
use sp_core::crypto::key_types::DUMMY;
148+
use sp_core::offchain::{
149+
testing::TestOffchainExt,
150+
OffchainExt,
151+
StorageKind,
152+
};
153+
154+
use sp_runtime::testing::UintAuthorityId;
155+
156+
type Historical = Module<Test>;
157+
158+
pub fn new_test_ext() -> sp_io::TestExternalities {
159+
let mut ext = frame_system::GenesisConfig::default()
160+
.build_storage::<Test>()
161+
.expect("Failed to create test externalities.");
162+
163+
crate::GenesisConfig::<Test> {
164+
keys: NEXT_VALIDATORS.with(|l| {
165+
l.borrow()
166+
.iter()
167+
.cloned()
168+
.map(|i| (i, i, UintAuthorityId(i).into()))
169+
.collect()
170+
}),
171+
}
172+
.assimilate_storage(&mut ext)
173+
.unwrap();
174+
175+
176+
let mut ext = sp_io::TestExternalities::new(ext);
177+
178+
let (offchain, offchain_state) = TestOffchainExt::with_offchain_db(ext.offchain_db());
179+
180+
const ITERATIONS: u32 = 5u32;
181+
let mut seed = [0u8; 32];
182+
seed[0..4].copy_from_slice(&ITERATIONS.to_le_bytes());
183+
offchain_state.write().seed = seed;
184+
185+
ext.register_extension(OffchainExt::new(offchain));
186+
ext
187+
}
188+
189+
#[test]
190+
fn encode_decode_roundtrip() {
191+
use codec::{Decode, Encode};
192+
use super::super::super::Trait as SessionTrait;
193+
use super::super::Trait as HistoricalTrait;
194+
195+
let sample = (
196+
22u32 as <Test as SessionTrait>::ValidatorId,
197+
7_777_777 as <Test as HistoricalTrait>::FullIdentification);
198+
199+
let encoded = sample.encode();
200+
let decoded = Decode::decode(&mut encoded.as_slice()).expect("Must decode");
201+
assert_eq!(sample, decoded);
202+
}
203+
204+
#[test]
205+
fn onchain_to_offchain() {
206+
let mut ext = new_test_ext();
207+
208+
const DATA: &[u8] = &[7,8,9,10,11];
209+
ext.execute_with(|| {
210+
b"alphaomega"[..].using_encoded(|key| sp_io::offchain_index::set(key, DATA));
211+
});
212+
213+
ext.persist_offchain_overlay();
214+
215+
ext.execute_with(|| {
216+
let data =
217+
b"alphaomega"[..].using_encoded(|key| {
218+
sp_io::offchain::local_storage_get(StorageKind::PERSISTENT, key)
219+
});
220+
assert_eq!(data, Some(DATA.to_vec()));
221+
});
222+
}
223+
224+
225+
#[test]
226+
fn historical_proof_offchain() {
227+
let mut ext = new_test_ext();
228+
let encoded_key_1 = UintAuthorityId(1).encode();
229+
230+
ext.execute_with(|| {
231+
set_next_validators(vec![1, 2]);
232+
force_new_session();
233+
234+
System::set_block_number(1);
235+
Session::on_initialize(1);
236+
237+
// "on-chain"
238+
onchain::store_current_session_validator_set_to_offchain::<Test>();
239+
assert_eq!(<SessionModule<Test>>::current_index(), 1);
240+
241+
set_next_validators(vec![7, 8]);
242+
243+
force_new_session();
244+
});
245+
246+
ext.persist_offchain_overlay();
247+
248+
ext.execute_with(|| {
249+
250+
251+
System::set_block_number(2);
252+
Session::on_initialize(2);
253+
assert_eq!(<SessionModule<Test>>::current_index(), 2);
254+
255+
// "off-chain"
256+
let proof = prove_session_membership::<Test, _>(1, (DUMMY, &encoded_key_1));
257+
assert!(proof.is_some());
258+
let proof = proof.expect("Must be Some(Proof)");
259+
260+
assert!(Historical::check_proof((DUMMY, &encoded_key_1[..]), proof.clone()).is_some());
261+
});
262+
}
263+
}

0 commit comments

Comments
 (0)