diff --git a/substrate/native-runtime/support/src/lib.rs b/substrate/native-runtime/support/src/lib.rs index 7adf9a904b85fe05692a9df192196a5494597561..47755c3f757510b58a317493e45322d692c6cc0c 100644 --- a/substrate/native-runtime/support/src/lib.rs +++ b/substrate/native-runtime/support/src/lib.rs @@ -151,4 +151,20 @@ mod tests { false })); } + + #[test] + fn read_storage_works() { + let mut t = TestExternalities { storage: map![ + b":test".to_vec() => b"\x0b\0\0\0Hello world".to_vec() + ], }; + + with_externalities(&mut t, || { + let mut v = [0u8; 4]; + assert!(read_storage(b":test", &mut v[..], 0) >= 4); + assert_eq!(v, [11u8, 0, 0, 0]); + let mut w = [0u8; 11]; + assert!(read_storage(b":test", &mut w[..], 4) >= 11); + assert_eq!(&w, b"Hello world"); + }); + } } diff --git a/substrate/wasm-runtime/polkadot/src/codec/slicable.rs b/substrate/wasm-runtime/polkadot/src/codec/slicable.rs index 1c783e39dfd173f701ced473f7a8ef60ec454600..1ccf2ce32dacb172083de081ee5c6404b5cf8d76 100644 --- a/substrate/wasm-runtime/polkadot/src/codec/slicable.rs +++ b/substrate/wasm-runtime/polkadot/src/codec/slicable.rs @@ -96,3 +96,16 @@ impl Slicable for Vec<u8> { u32::from_slice(&data[0..4]).map(|i| (i + 4) as usize) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn vec_is_slicable() { + let v = b"Hello world".to_vec(); + v.as_slice_then(|ref slice| + assert_eq!(slice, &b"\x0b\0\0\0Hello world") + ); + } +} diff --git a/substrate/wasm-runtime/polkadot/src/runtime/governance.rs b/substrate/wasm-runtime/polkadot/src/runtime/governance.rs index d7d45fb0d07d67e81eb25b3b4f98fb80cc315d49..e4665d38383abd3e797cef2aadbb55ea9ecedb93 100644 --- a/substrate/wasm-runtime/polkadot/src/runtime/governance.rs +++ b/substrate/wasm-runtime/polkadot/src/runtime/governance.rs @@ -34,14 +34,19 @@ use runtime::{staking, system, session}; // TRANSACTION API -pub fn propose(transactor: &AccountID, proposal: &Proposal) { +/// Propose a sensitive action to be taken. Any action that is enactable by `Proposal` is valid. +/// Proposal is by the `transactor` and will automatically count as an approval. Transactor must +/// be a current validator. It is illegal to propose when there is already a proposal in effect. +pub fn propose(validator: &AccountID, proposal: &Proposal) { if Proposal::lookup(b"gov:pro").is_some() { panic!("there may only be one proposal per era."); } proposal.store(b"gov:pro"); - approve(transactor, staking::current_era()); + approve(validator, staking::current_era()); } +/// Approve the current era's proposal. Transactor must be a validator. This may not be done more +/// than once for any validator in an era. pub fn approve(validator: &AccountID, era_index: BlockNumber) { if era_index != staking::current_era() { panic!("approval vote applied on non-current era.") @@ -59,18 +64,25 @@ pub fn approve(validator: &AccountID, era_index: BlockNumber) { true.store(&key); } +/// Set the proportion of validators that must approve for a proposal to be enacted at the end of +/// its era. The value, `ppm`, is measured as a fraction of 1000 rounded down to the nearest whole +/// validator. `1000` would require the approval of all validators; `667` would require two-thirds +/// (or there abouts) of validators. pub fn set_approval_ppm_required(ppm: u32) { ppm.store(b"gov:apr"); } // INSPECTION API +/// The proportion of validators required for a propsal to be approved measured as the number out +/// of 1000. pub fn approval_ppm_required() -> u32 { Storable::lookup(b"gov:apr").unwrap_or(1000) } +/// The number of concrete validator approvals required for a proposal to pass. pub fn approvals_required() -> u32 { - approval_ppm_required() * staking::validator_count() as u32 / 1000 + approval_ppm_required() * session::validator_count() as u32 / 1000 } // PUBLIC API @@ -80,18 +92,270 @@ pub fn end_of_an_era() { // tally up votes for the current proposal, if any. enact if there are sufficient approvals. if let Some(proposal) = Proposal::lookup(b"gov:pro") { kill(b"gov:pro"); - let approved: u32 = session::validators().into_iter() - .map(|v| bool::take(&v.to_keyed_vec(b"gov:app:")).map(|_| 1).unwrap_or(0)) - .sum(); - if approved >= approvals_required() { + let approvals_required = approvals_required(); + let approved = session::validators().into_iter() + .filter_map(|v| bool::take(&v.to_keyed_vec(b"gov:app:"))) + .take(approvals_required as usize) + .count() as u32; + if approved == approvals_required { proposal.enact(); } } } -// PRIVATE API - #[cfg(test)] mod tests { - // TODO + use super::*; + use runtime_support::{with_externalities, twox_128}; + use keyedvec::KeyedVec; + use joiner::Joiner; + use testing::{one, two, TestExternalities}; + use primitives::AccountID; + use proposal::InternalFunction; + use runtime::{staking, session}; + use environment::with_env; + + fn new_test_ext() -> TestExternalities { + let one = one(); + let two = two(); + let three = [3u8; 32]; + + TestExternalities { storage: map![ + twox_128(b"gov:apr").to_vec() => vec![].join(&667u32), + twox_128(b"ses:len").to_vec() => vec![].join(&1u64), + twox_128(b"ses:val:len").to_vec() => vec![].join(&3u32), + twox_128(&0u32.to_keyed_vec(b"ses:val:")).to_vec() => one.to_vec(), + twox_128(&1u32.to_keyed_vec(b"ses:val:")).to_vec() => two.to_vec(), + twox_128(&2u32.to_keyed_vec(b"ses:val:")).to_vec() => three.to_vec(), + twox_128(b"sta:wil:len").to_vec() => vec![].join(&3u32), + twox_128(&0u32.to_keyed_vec(b"sta:wil:")).to_vec() => one.to_vec(), + twox_128(&1u32.to_keyed_vec(b"sta:wil:")).to_vec() => two.to_vec(), + twox_128(&2u32.to_keyed_vec(b"sta:wil:")).to_vec() => three.to_vec(), + twox_128(b"sta:spe").to_vec() => vec![].join(&1u64), + twox_128(b"sta:vac").to_vec() => vec![].join(&3u64), + twox_128(b"sta:era").to_vec() => vec![].join(&1u64) + ], } + } + + #[test] + fn majority_voting_should_work() { + let one = one(); + let two = two(); + let three = [3u8; 32]; + let mut t = new_test_ext(); + + with_externalities(&mut t, || { + assert_eq!(staking::era_length(), 1u64); + assert_eq!(staking::current_era(), 1u64); + assert_eq!(session::validator_count(), 3usize); + assert_eq!(session::validators(), vec![one.clone(), two.clone(), three.clone()]); + assert!(!session::validators().into_iter().position(|v| &v == &one).is_none()); + + // Block 1: Make proposal. Approve it. Era length changes. + with_env(|e| e.block_number = 1); + propose(&one, &Proposal { + function: InternalFunction::StakingSetSessionsPerEra, + input_data: vec![].join(&2u64), + }); + approve(&two, 1); + staking::check_new_era(); + assert_eq!(staking::era_length(), 2); + }); + } + + #[test] + fn majority_voting_should_work_after_unsuccessful_previous() { + let one = one(); + let two = two(); + let three = [3u8; 32]; + let mut t = new_test_ext(); + + with_externalities(&mut t, || { + assert_eq!(staking::era_length(), 1u64); + assert_eq!(staking::current_era(), 1u64); + assert_eq!(session::validator_count(), 3usize); + assert_eq!(session::validators(), vec![one.clone(), two.clone(), three.clone()]); + assert!(!session::validators().into_iter().position(|v| &v == &one).is_none()); + + // Block 1: Make proposal. Fail it. + with_env(|e| e.block_number = 1); + propose(&one, &Proposal { + function: InternalFunction::StakingSetSessionsPerEra, + input_data: vec![].join(&2u64), + }); + staking::check_new_era(); + assert_eq!(staking::era_length(), 1); + + // Block 2: Make proposal. Approve it. It should change era length. + with_env(|e| e.block_number = 2); + propose(&one, &Proposal { + function: InternalFunction::StakingSetSessionsPerEra, + input_data: vec![].join(&2u64), + }); + approve(&two, 2); + staking::check_new_era(); + assert_eq!(staking::era_length(), 2); + }); + } + + #[test] + fn minority_voting_should_not_succeed() { + let one = one(); + let two = two(); + let three = [3u8; 32]; + let mut t = new_test_ext(); + + with_externalities(&mut t, || { + assert_eq!(staking::era_length(), 1u64); + assert_eq!(staking::current_era(), 1u64); + assert_eq!(session::validator_count(), 3usize); + assert_eq!(session::validators(), vec![one.clone(), two.clone(), three.clone()]); + assert!(!session::validators().into_iter().position(|v| &v == &one).is_none()); + + // Block 1: Make proposal. Will have only 1 vote. No change. + with_env(|e| e.block_number = 1); + propose(&one, &Proposal { + function: InternalFunction::StakingSetSessionsPerEra, + input_data: vec![].join(&2u64), + }); + staking::check_new_era(); + assert_eq!(staking::era_length(), 1); + }); + } + + #[test] + #[should_panic] + fn old_voting_should_be_illegal() { + let one = one(); + let two = two(); + let three = [3u8; 32]; + let mut t = new_test_ext(); + + with_externalities(&mut t, || { + assert_eq!(staking::era_length(), 1u64); + assert_eq!(staking::current_era(), 1u64); + assert_eq!(session::validator_count(), 3usize); + assert_eq!(session::validators(), vec![one.clone(), two.clone(), three.clone()]); + assert!(!session::validators().into_iter().position(|v| &v == &one).is_none()); + + // Block 1: Make proposal. Will have only 1 vote. No change. + with_env(|e| e.block_number = 1); + propose(&one, &Proposal { + function: InternalFunction::StakingSetSessionsPerEra, + input_data: vec![].join(&2u64), + }); + approve(&two, 0); + staking::check_new_era(); + assert_eq!(staking::era_length(), 1); + }); + } + + #[test] + #[should_panic] + fn double_voting_should_be_illegal() { + let one = one(); + let two = two(); + let three = [3u8; 32]; + let mut t = new_test_ext(); + + with_externalities(&mut t, || { + assert_eq!(staking::era_length(), 1u64); + assert_eq!(staking::current_era(), 1u64); + assert_eq!(session::validator_count(), 3usize); + assert_eq!(session::validators(), vec![one.clone(), two.clone(), three.clone()]); + assert!(!session::validators().into_iter().position(|v| &v == &one).is_none()); + + // Block 1: Make proposal. Will have only 1 vote. No change. + with_env(|e| e.block_number = 1); + propose(&one, &Proposal { + function: InternalFunction::StakingSetSessionsPerEra, + input_data: vec![].join(&2u64), + }); + approve(&two, 1); + approve(&two, 1); + staking::check_new_era(); + assert_eq!(staking::era_length(), 1); + }); + } + + #[test] + #[should_panic] + fn over_proposing_should_be_illegal() { + let one = one(); + let two = two(); + let three = [3u8; 32]; + let mut t = new_test_ext(); + + with_externalities(&mut t, || { + assert_eq!(staking::era_length(), 1u64); + assert_eq!(staking::current_era(), 1u64); + assert_eq!(session::validator_count(), 3usize); + assert_eq!(session::validators(), vec![one.clone(), two.clone(), three.clone()]); + assert!(!session::validators().into_iter().position(|v| &v == &one).is_none()); + + // Block 1: Make proposal. Will have only 1 vote. No change. + with_env(|e| e.block_number = 1); + propose(&one, &Proposal { + function: InternalFunction::StakingSetSessionsPerEra, + input_data: vec![].join(&2u64), + }); + propose(&two, &Proposal { + function: InternalFunction::StakingSetSessionsPerEra, + input_data: vec![].join(&2u64), + }); + staking::check_new_era(); + assert_eq!(staking::era_length(), 1); + }); + } + + #[test] + #[should_panic] + fn approving_without_proposal_should_be_illegal() { + let one = one(); + let two = two(); + let three = [3u8; 32]; + let mut t = new_test_ext(); + + with_externalities(&mut t, || { + assert_eq!(staking::era_length(), 1u64); + assert_eq!(staking::current_era(), 1u64); + assert_eq!(session::validator_count(), 3usize); + assert_eq!(session::validators(), vec![one.clone(), two.clone(), three.clone()]); + assert!(!session::validators().into_iter().position(|v| &v == &one).is_none()); + + // Block 1: Make proposal. Will have only 1 vote. No change. + with_env(|e| e.block_number = 1); + approve(&two, 1); + staking::check_new_era(); + assert_eq!(staking::era_length(), 1); + }); + } + + #[test] + #[should_panic] + fn non_validator_approving_should_be_illegal() { + let one = one(); + let two = two(); + let three = [3u8; 32]; + let four = [4u8; 32]; + let mut t = new_test_ext(); + + with_externalities(&mut t, || { + assert_eq!(staking::era_length(), 1u64); + assert_eq!(staking::current_era(), 1u64); + assert_eq!(session::validator_count(), 3usize); + assert_eq!(session::validators(), vec![one.clone(), two.clone(), three.clone()]); + assert!(!session::validators().into_iter().position(|v| &v == &one).is_none()); + + // Block 1: Make proposal. Will have only 1 vote. No change. + with_env(|e| e.block_number = 1); + propose(&one, &Proposal { + function: InternalFunction::StakingSetSessionsPerEra, + input_data: vec![].join(&2u64), + }); + approve(&four, 1); + staking::check_new_era(); + assert_eq!(staking::era_length(), 1); + }); + } } diff --git a/substrate/wasm-runtime/polkadot/src/runtime/session.rs b/substrate/wasm-runtime/polkadot/src/runtime/session.rs index 907b3e16b162e0313952817054b4c937234198fc..2034ff316886b965424492625ffac0281e80d344 100644 --- a/substrate/wasm-runtime/polkadot/src/runtime/session.rs +++ b/substrate/wasm-runtime/polkadot/src/runtime/session.rs @@ -59,6 +59,11 @@ pub fn length() -> BlockNumber { Storable::lookup_default(b"ses:len") } +/// The number of validators currently. +pub fn validator_count() -> usize { + ValidatorStorageVec::count() as usize +} + /// The current era index. pub fn current_index() -> BlockNumber { Storable::lookup_default(b"ses:ind") diff --git a/substrate/wasm-runtime/polkadot/src/support/storable.rs b/substrate/wasm-runtime/polkadot/src/support/storable.rs index 61d9f080d9e0aa1d6ef9833104fe961f0a9c6f6f..719415b8e86643436796b9f5bcb0818bc5c9deee 100644 --- a/substrate/wasm-runtime/polkadot/src/support/storable.rs +++ b/substrate/wasm-runtime/polkadot/src/support/storable.rs @@ -56,7 +56,7 @@ pub fn kill(key: &[u8]) { impl<T: Sized + Slicable> Storable for T { fn lookup(key: &[u8]) -> Option<Self> { Slicable::set_as_slice(&|out, offset| - runtime_support::read_storage(&twox_128(key)[..], out, offset) == out.len() + runtime_support::read_storage(&twox_128(key)[..], out, offset) >= out.len() ) } fn store(&self, key: &[u8]) { @@ -104,3 +104,89 @@ pub trait StorageVec { Storable::lookup_default(&b"len".to_keyed_vec(Self::PREFIX)) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use runtime_support::with_externalities; + use testing::{TestExternalities, HexDisplay}; + use runtime_support::{storage, twox_128}; + + #[test] + fn integers_can_be_stored() { + let mut t = TestExternalities { storage: HashMap::new(), }; + with_externalities(&mut t, || { + let x = 69u32; + x.store(b":test"); + let y = u32::lookup(b":test").unwrap(); + assert_eq!(x, y); + }); + with_externalities(&mut t, || { + let x = 69426942i64; + x.store(b":test"); + let y = i64::lookup(b":test").unwrap(); + assert_eq!(x, y); + }); + } + + #[test] + fn bools_can_be_stored() { + let mut t = TestExternalities { storage: HashMap::new(), }; + with_externalities(&mut t, || { + let x = true; + x.store(b":test"); + let y = bool::lookup(b":test").unwrap(); + assert_eq!(x, y); + }); + + with_externalities(&mut t, || { + let x = false; + x.store(b":test"); + let y = bool::lookup(b":test").unwrap(); + assert_eq!(x, y); + }); + } + + #[test] + fn vecs_can_be_retrieved() { + let mut t = TestExternalities { storage: HashMap::new(), }; + with_externalities(&mut t, || { + runtime_support::set_storage(&twox_128(b":test"), b"\x0b\0\0\0Hello world"); + let x = b"Hello world".to_vec(); + println!("Hex: {}", HexDisplay::from(&storage(&twox_128(b":test")))); + let y = <Vec<u8>>::lookup(b":test").unwrap(); + assert_eq!(x, y); + + }); + } + + #[test] + fn vecs_can_be_stored() { + let mut t = TestExternalities { storage: HashMap::new(), }; + let x = b"Hello world".to_vec(); + + with_externalities(&mut t, || { + x.store(b":test"); + }); + + println!("Ext is {:?}", t); + with_externalities(&mut t, || { + println!("Hex: {}", HexDisplay::from(&storage(&twox_128(b":test")))); + let y = <Vec<u8>>::lookup(b":test").unwrap(); + assert_eq!(x, y); + }); + } + + #[test] + fn proposals_can_be_stored() { + use proposal::{Proposal, InternalFunction}; + let mut t = TestExternalities { storage: HashMap::new(), }; + with_externalities(&mut t, || { + let x = Proposal { function: InternalFunction::StakingSetSessionsPerEra, input_data: b"Hello world".to_vec() }; + x.store(b":test"); + let y = Proposal::lookup(b":test").unwrap(); + assert_eq!(x, y); + }); + } +}