From b3c89dd01226bf46d39ab47c636dc1d9811a9093 Mon Sep 17 00:00:00 2001 From: SeonpyoKim Date: Tue, 14 Apr 2020 18:00:24 +0900 Subject: [PATCH 1/8] Initialize the staking module --- Cargo.lock | 5 +++++ Cargo.toml | 4 +++- basic_module/staking/Cargo.toml | 8 ++++++++ basic_module/staking/src/lib.rs | 0 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 basic_module/staking/Cargo.toml create mode 100644 basic_module/staking/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d57100ce8b..002ec9499f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1220,6 +1220,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "staking", "tokio-core", "toml 0.4.6", "vergen", @@ -3178,6 +3179,10 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15132e0e364248108c5e2c02e3ab539be8d6f5d52a01ca9bbf27ed657316f02b" +[[package]] +name = "staking" +version = "0.1.0" + [[package]] name = "stream-cipher" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index c2baf561f9..65159890d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ rustc-serialize = "0.3" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" +staking = { path = "basic_module/staking" } tokio-core = "0.1.17" toml = "0.4" cidr = "0.0.4" @@ -81,5 +82,6 @@ members = [ "module-macros", "coordinator", "basesandbox", - "basic_module/account" + "basic_module/account", + "basic_module/staking" ] diff --git a/basic_module/staking/Cargo.toml b/basic_module/staking/Cargo.toml new file mode 100644 index 0000000000..8555bbd072 --- /dev/null +++ b/basic_module/staking/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "staking" +version = "0.1.0" +authors = ["CodeChain Team "] +edition = "2018" + +[dependencies] + diff --git a/basic_module/staking/src/lib.rs b/basic_module/staking/src/lib.rs new file mode 100644 index 0000000000..e69de29bb2 From a7bfce2606e8431db830dfec5c7fc907f0d590e3 Mon Sep 17 00:00:00 2001 From: SeonpyoKim Date: Thu, 16 Apr 2020 13:45:23 +0900 Subject: [PATCH 2/8] Implement the state and types of Staking module --- Cargo.lock | 24 +- basic_module/staking/Cargo.toml | 10 +- basic_module/staking/src/error.rs | 58 +++ basic_module/staking/src/lib.rs | 39 ++ basic_module/staking/src/runtime_error.rs | 36 ++ basic_module/staking/src/state.rs | 539 ++++++++++++++++++++++ basic_module/staking/src/syntax_error.rs | 42 ++ basic_module/staking/src/types.rs | 76 +++ 8 files changed, 818 insertions(+), 6 deletions(-) create mode 100644 basic_module/staking/src/error.rs create mode 100644 basic_module/staking/src/runtime_error.rs create mode 100644 basic_module/staking/src/state.rs create mode 100644 basic_module/staking/src/syntax_error.rs create mode 100644 basic_module/staking/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 002ec9499f..7fa864e96f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2983,6 +2983,9 @@ name = "serde" version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e707fbbf255b8fc8c3b99abb91e7257a622caeb20a9818cbadbeeede4e0932ff" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_cbor" @@ -2996,13 +2999,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.53" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7149ef7af607b09e0e7df38b1fd74264f08a29a67f604d5cb09d3fbdb1e256bc" +checksum = "ac5d00fc561ba2724df6758a17de23df5914f20e41cb00f94d5b7ae42fffaff8" dependencies = [ - "proc-macro2 0.3.8", - "quote 0.5.2", - "syn 0.13.11", + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn 1.0.18", ] [[package]] @@ -3182,6 +3185,17 @@ checksum = "15132e0e364248108c5e2c02e3ab539be8d6f5d52a01ca9bbf27ed657316f02b" [[package]] name = "staking" version = "0.1.0" +dependencies = [ + "codechain-crypto", + "codechain-key", + "codechain-types", + "coordinator", + "lazy_static 1.4.0", + "primitives", + "serde", + "serde_cbor", + "serde_derive", +] [[package]] name = "stream-cipher" diff --git a/basic_module/staking/Cargo.toml b/basic_module/staking/Cargo.toml index 8555bbd072..2a18b448d2 100644 --- a/basic_module/staking/Cargo.toml +++ b/basic_module/staking/Cargo.toml @@ -5,4 +5,12 @@ authors = ["CodeChain Team "] edition = "2018" [dependencies] - +primitives = { git = "https://github.com/CodeChain-io/rust-codechain-primitives.git", version = "0.4" } +coordinator = { path = "../../coordinator" } +lazy_static = "1.4" +serde_cbor = "0.11.1" +serde = { version = "1.0", features = ["derive"] } +serde_derive = "1.0.105" +codechain-crypto = { git = "https://github.com/CodeChain-io/rust-codechain-crypto.git", version = "0.2" } +ftypes = { path = "../../types", package = "codechain-types" } +fkey = { path = "../../key", package = "codechain-key" } diff --git a/basic_module/staking/src/error.rs b/basic_module/staking/src/error.rs new file mode 100644 index 0000000000..1820be7e50 --- /dev/null +++ b/basic_module/staking/src/error.rs @@ -0,0 +1,58 @@ +// Copyright 2020 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pub use crate::runtime_error::Error as RuntimeError; +use std::fmt::{Display, Formatter, Result as FormatResult}; + +#[derive(Debug)] +/// Error indicating an expected value was not found. +pub struct Mismatch { + /// Value expected. + pub expected: T, + /// Value found. + pub found: T, +} + +impl Display for Mismatch { + fn fmt(&self, f: &mut Formatter<'_>) -> FormatResult { + write!(f, "Expected {}, found {}", self.expected, self.found) + } +} + +#[derive(Debug)] +pub struct Insufficient { + /// Value to have at least + pub required: T, + /// Value found + pub actual: T, +} + +impl Display for Insufficient { + fn fmt(&self, f: &mut Formatter<'_>) -> FormatResult { + write!(f, "Required at least {}, found {}", self.required, self.actual) + } +} + +#[derive(Debug)] +pub enum Error { + Runtime(RuntimeError), +} + +impl From for Error { + fn from(error: RuntimeError) -> Error { + Error::Runtime(error) + } +} diff --git a/basic_module/staking/src/lib.rs b/basic_module/staking/src/lib.rs index e69de29bb2..3d254f803f 100644 --- a/basic_module/staking/src/lib.rs +++ b/basic_module/staking/src/lib.rs @@ -0,0 +1,39 @@ +// Copyright 2020 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate lazy_static; + +use coordinator::context::SubStorageAccess; + +mod error; +mod runtime_error; +mod state; +mod types; + +pub fn substorage() -> Box { + unimplemented!() +} + +pub fn deserialize(buffer: Vec) -> T { + serde_cbor::from_slice(&buffer).unwrap() +} + +pub fn serialize(data: T) -> Vec { + serde_cbor::to_vec(&data).unwrap() +} diff --git a/basic_module/staking/src/runtime_error.rs b/basic_module/staking/src/runtime_error.rs new file mode 100644 index 0000000000..4540e3396c --- /dev/null +++ b/basic_module/staking/src/runtime_error.rs @@ -0,0 +1,36 @@ +// Copyright 2020 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::error::Insufficient; +use crate::types::{Public, StakeQuantity}; +use std::fmt::{Display, Formatter, Result as FormatResult}; + +#[derive(Debug)] +pub enum Error { + InsufficientStakes(Insufficient), + DelegateeNotFoundInCandidates(Public), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> FormatResult { + match self { + Error::InsufficientStakes(insufficient) => write!(f, "Insufficient stakes: {}", insufficient), + Error::DelegateeNotFoundInCandidates(delegatee) => { + write!(f, "Delegatee {:?} is not in Candidates", delegatee) + } + } + } +} diff --git a/basic_module/staking/src/state.rs b/basic_module/staking/src/state.rs new file mode 100644 index 0000000000..d4de81890c --- /dev/null +++ b/basic_module/staking/src/state.rs @@ -0,0 +1,539 @@ +// Copyright 2020 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::error::{Insufficient, Mismatch}; +use crate::runtime_error::Error; +use crate::types::*; +use crate::{deserialize, serialize, substorage}; +use serde::{de::DeserializeOwned, ser::Serialize}; +use std::cmp::{max, Ordering}; +use std::collections::{ + btree_map::{self, Entry}, + BTreeMap, BTreeSet, HashMap, HashSet, +}; +use std::ops::Deref; + +type KEY = dyn AsRef<[u8]>; + +const STAKE_ACCOUNT_PREFIX: [u8; 1] = [0x1]; +const DELEGATION_PREFIX: [u8; 1] = [0x2]; + +const METADATA_KEY: &[u8; 8] = b"Metadata"; +const STAKEHOLDERS_KEY: &[u8; 12] = b"Stakeholders"; +const CANDIDATES_KEY: &[u8; 10] = b"Candidates"; +const NEXT_VALIDATORS_KEY: &[u8; 14] = b"NextValidators"; +const CURRENT_VALIDATORS_KEY: &[u8; 17] = b"CurrentValidators"; +const JAIL_KEY: &[u8; 4] = b"Jail"; +const BANNED_KEY: &[u8; 6] = b"Banned"; + +fn prefix_public_key(prefix: &[u8], key: &Public) -> Vec { + [prefix, key.as_ref()].concat() +} + +fn remove_key(key: &KEY) { + substorage().remove(key) +} + +fn load_with_key(key: &KEY) -> Option { + substorage().get(key).map(deserialize) +} + +fn write_with_key(key: &KEY, data: T) { + substorage().set(key, serialize(data)) +} + +#[derive(Serialize, Deserialize)] +pub struct Metadata { + pub seq: u64, + pub current_term_id: u64, + pub last_term_finished_block_num: u64, + pub params: Params, + pub term_params: Params, +} + +#[derive(Clone, Copy, Serialize, Deserialize)] +pub struct Params { + pub term_seconds: u64, + pub nomination_expiration: u64, + pub custody_period: u64, + pub release_period: u64, + pub max_num_of_validators: usize, + pub min_num_of_validators: usize, + pub delegation_threshold: StakeQuantity, + pub min_deposit: DepositQuantity, + pub max_candidate_metadata_size: usize, + + pub era: u64, +} + +impl Metadata { + pub fn load() -> Self { + load_with_key(METADATA_KEY).expect("Params must be exist") + } + + pub fn save(self) { + write_with_key(METADATA_KEY, self) + } +} + +pub struct StakeAccount<'a> { + pub public: &'a Public, + pub balance: StakeQuantity, +} + +impl<'a> StakeAccount<'a> { + pub fn load(public: &'a Public) -> Self { + StakeAccount { + public, + balance: load_with_key(&prefix_public_key(&STAKE_ACCOUNT_PREFIX, public)).unwrap_or_default(), + } + } + + pub fn save(self) { + write_with_key(&prefix_public_key(&STAKE_ACCOUNT_PREFIX, self.public), self.balance) + } + + pub fn subtract_balance(&mut self, quantity: StakeQuantity) -> Result<(), Error> { + if self.balance < quantity { + Err(Error::InsufficientStakes(Insufficient { + required: quantity, + actual: self.balance, + })) + } else { + self.balance -= quantity; + Ok(()) + } + } + + pub fn add_balance(&mut self, quantity: StakeQuantity) -> Result<(), Error> { + self.balance += quantity; + Ok(()) + } +} + +pub struct Delegation<'a> { + pub delegator: &'a Public, + delegatees: BTreeMap, +} + +impl<'a> Delegation<'a> { + pub fn load(delegator: &'a Public) -> Self { + Delegation { + delegator, + delegatees: load_with_key(&prefix_public_key(&DELEGATION_PREFIX, delegator)).unwrap_or_default(), + } + } + + pub fn save(self) { + let Delegation { + delegator, + delegatees, + } = self; + write_with_key(delegator, delegatees) + } + + pub fn add_quantity(&mut self, delegatee: Public, quantity: StakeQuantity) -> Result<(), Error> { + if quantity != 0 { + *self.delegatees.entry(delegatee).or_insert(0) += quantity; + } + Ok(()) + } + + pub fn sub_quantity(&mut self, delegatee: Public, quantity: StakeQuantity) -> Result<(), Error> { + if quantity != 0 { + if let Entry::Occupied(mut entry) = self.delegatees.entry(delegatee) { + let delegation = entry.get(); + match delegation.cmp(&quantity) { + Ordering::Greater => { + *entry.get_mut() -= quantity; + Ok(()) + } + Ordering::Equal => { + entry.remove(); + Ok(()) + } + Ordering::Less => Err(Error::InsufficientStakes(Insufficient { + required: quantity, + actual: *delegation, + })), + } + } else { + Err(Error::DelegateeNotFoundInCandidates(delegatee)) + } + } else { + Ok(()) + } + } + + pub fn get_quantity(&self, delegatee: &Public) -> StakeQuantity { + self.delegatees.get(delegatee).cloned().unwrap_or(0) + } + + pub fn into_iter(self) -> btree_map::IntoIter { + self.delegatees.into_iter() + } + + pub fn sum(&self) -> u64 { + self.delegatees.values().sum() + } +} + +pub struct Stakeholders(BTreeSet); + +impl Stakeholders { + pub fn load() -> Stakeholders { + Stakeholders(load_with_key(STAKEHOLDERS_KEY).unwrap_or_default()) + } + + pub fn save(self) { + let key = STAKEHOLDERS_KEY; + if !self.0.is_empty() { + write_with_key(key, self.0) + } else { + remove_key(key) + } + } + + pub fn delegatees() -> HashMap { + Stakeholders::load().0.into_iter().fold(HashMap::new(), |mut map, stakeholder| { + let delegation = Delegation::load(&stakeholder); + delegation.into_iter().for_each(|(delegatee, quantity)| { + *map.entry(delegatee).or_default() += quantity; + }); + map + }) + } + + pub fn update_by_increased_balance(&mut self, account: &StakeAccount) { + if account.balance > 0 { + self.0.insert(*account.public); + } + } + + pub fn update_by_decreased_balance(&mut self, account: &StakeAccount, delegation: &Delegation) { + assert!(account.public == delegation.delegator); + if account.balance == 0 && delegation.sum() == 0 { + self.0.remove(account.public); + } + } +} + +pub struct NextValidators(Vec); +impl NextValidators { + pub fn load() -> Self { + NextValidators(load_with_key(NEXT_VALIDATORS_KEY).unwrap_or_default()) + } + + pub fn save(self) { + write_with_key(NEXT_VALIDATORS_KEY, self.0) + } + + pub fn elect() -> Self { + let Params { + delegation_threshold, + max_num_of_validators, + min_num_of_validators, + min_deposit, + .. + } = Metadata::load().term_params; + assert!(max_num_of_validators >= min_num_of_validators); + let mut validators = Candidates::prepare_validators(min_deposit); + // validators are now sorted in descending order of (delegation, deposit, priority) + validators.reverse(); + + { + let banned = Banned::load(); + validators.iter().for_each(|validator| { + let public = &validator.pubkey(); + assert!(!banned.is_banned(&public), "{:?} is banned public", public); + }); + } + + validators.truncate(max_num_of_validators); + + if validators.len() < min_num_of_validators { + println!( + "There must be something wrong. validators.len() < min_num_of_validators, {} < {}", + validators.len(), + min_num_of_validators + ); + } + + let (minimum, rest) = validators.split_at(min_num_of_validators.min(validators.len())); + let over_threshold = rest.iter().filter(|c| c.delegation >= delegation_threshold); + + let mut result: Vec<_> = minimum.iter().chain(over_threshold).cloned().collect(); + result.reverse(); + NextValidators(result) + } + + pub fn update_weight(&mut self, block_author: &Public) { + let min_delegation = self.min_delegation(); + for Validator { + weight, + pubkey, + .. + } in self.0.iter_mut().rev() + { + if pubkey == block_author { + // block author + *weight = weight.saturating_sub(min_delegation); + break + } + // neglecting validators + *weight = weight.saturating_sub(min_delegation * 2); + } + if self.0.iter().all(|validator| validator.weight == 0) { + self.0.iter_mut().for_each(Validator::reset); + } + self.0.sort_unstable(); + } + + pub fn delegation(&self, pubkey: &Public) -> Option { + self.0.iter().find(|validator| validator.pubkey() == pubkey).map(|validator| validator.delegation) + } + + fn min_delegation(&self) -> StakeQuantity { + self.0.iter().map(|validator| validator.delegation).min().expect("There must be at least one validator") + } +} + +impl Deref for NextValidators { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Vec { + fn from(val: NextValidators) -> Self { + val.0 + } +} + +pub struct CurrentValidators(Vec); +impl CurrentValidators { + pub fn load() -> Self { + CurrentValidators(load_with_key(CURRENT_VALIDATORS_KEY).unwrap_or_default()) + } + + pub fn save(self) { + let key = CURRENT_VALIDATORS_KEY; + if !self.is_empty() { + write_with_key(key, self.0) + } else { + remove_key(key) + } + } + + pub fn update(&mut self, validators: Vec) { + self.0 = validators; + } + + pub fn publics(&self) -> Vec { + self.0.iter().rev().map(|v| *v.pubkey()).collect() + } +} + +impl Deref for CurrentValidators { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Vec { + fn from(val: CurrentValidators) -> Self { + val.0 + } +} + +pub struct Candidates(Vec); + +impl Candidates { + pub fn load() -> Self { + Candidates(load_with_key(CANDIDATES_KEY).unwrap_or_default()) + } + + pub fn save(self) { + write_with_key(CANDIDATES_KEY, self.0) + } + + fn prepare_validators(min_deposit: DepositQuantity) -> Vec { + let Candidates(candidates) = Self::load(); + let delegations = Stakeholders::delegatees(); + let mut result = + candidates.into_iter().filter(|c| c.deposit >= min_deposit).fold(Vec::new(), |mut vec, candidate| { + let public = &candidate.pubkey; + if let Some(&delegation) = delegations.get(public) { + vec.push(Validator::new(delegation, candidate.deposit, candidate.pubkey)); + } + vec + }); + result.sort_by_key(|v| (v.delegation, v.deposit)); + result + } + + pub fn get_candidate(&self, account: &Public) -> Option<&Candidate> { + self.0.iter().find(|&c| &c.pubkey == account) + } + + pub fn add_deposit( + &mut self, + pubkey: &Public, + quantity: DepositQuantity, + nomination_ends_at: u64, + metadata: Bytes, + ) { + if let Some(candidate) = self.0.iter_mut().find(|c| c.pubkey == *pubkey) { + candidate.deposit += quantity; + candidate.nomination_ends_at = max(candidate.nomination_ends_at, nomination_ends_at); + candidate.metadata = metadata; + } else { + self.0.push(Candidate { + pubkey: *pubkey, + deposit: quantity, + nomination_ends_at, + metadata, + }) + }; + } + + pub fn renew_candidates( + &mut self, + validators: &NextValidators, + nomination_ends_at: u64, + inactive_validators: &[Public], + banned: &Banned, + ) { + let to_renew: HashSet<_> = validators + .iter() + .map(|validator| validator.pubkey()) + .filter(|pubkey| !inactive_validators.contains(pubkey)) + .collect(); + + for candidate in self.0.iter_mut().filter(|c| to_renew.contains(&c.pubkey)) { + let public = &candidate.pubkey; + assert!(!banned.is_banned(public), "{:?} is banned public", public); + candidate.nomination_ends_at = nomination_ends_at; + } + + let to_reprioritize: Vec<_> = + self.0.iter().filter(|c| to_renew.contains(&c.pubkey)).map(|c| c.pubkey).collect(); + + self.reprioritize(to_reprioritize); + } + + pub fn drain_expired_candidates(&mut self, term_index: u64) -> Vec { + let (expired, retained): (Vec<_>, Vec<_>) = self.0.drain(..).partition(|c| c.nomination_ends_at <= term_index); + self.0 = retained; + expired + } + + pub fn remove(&mut self, public: &Public) -> Option { + if let Some(index) = self.0.iter().position(|c| &c.pubkey == public) { + Some(self.0.remove(index)) + } else { + None + } + } + + /// reprioritize candidates in the order of last updated time + fn reprioritize(&mut self, targets: Vec) { + let (mut old, mut renewed): (Vec<_>, Vec<_>) = self.0.drain(..).partition(|c| !targets.contains(&c.pubkey)); + old.append(&mut renewed); + self.0 = old; + } +} + +pub struct Jail(BTreeMap); + +impl Jail { + pub fn load() -> Self { + let prisoners: Vec = load_with_key(JAIL_KEY).unwrap_or_default(); + Jail(prisoners.into_iter().map(|p| (p.pubkey, p)).collect()) + } + + pub fn save(self) { + if !self.0.is_empty() { + let vectorized: Vec = self.0.into_iter().map(|(_, p)| p).collect(); + write_with_key(JAIL_KEY, vectorized) + } + } + + pub fn get_prisoner(&self, public: &Public) -> Option<&Prisoner> { + self.0.get(public) + } + + pub fn add(&mut self, candidate: Candidate, custody_until: u64, released_at: u64) { + assert!(custody_until <= released_at); + let pubkey = candidate.pubkey; + self.0.insert(pubkey, Prisoner { + pubkey, + deposit: candidate.deposit, + custody_until, + released_at, + }); + } + + pub fn remove(&mut self, public: &Public) -> Option { + self.0.remove(public) + } + + pub fn try_release(&mut self, public: &Public, term_index: u64) -> ReleaseResult { + match self.0.entry(*public) { + Entry::Occupied(entry) => { + if entry.get().custody_until < term_index { + ReleaseResult::Released(entry.remove()) + } else { + ReleaseResult::InCustody + } + } + _ => ReleaseResult::NotExists, + } + } + + pub fn drain_released_prisoners(&mut self, term_index: u64) -> Vec { + let (released, retained): (Vec<_>, Vec<_>) = + self.0.values().cloned().partition(|c| c.released_at <= term_index); + self.0 = retained.into_iter().map(|c| (c.pubkey, c)).collect(); + released + } +} + +pub struct Banned(BTreeSet); + +impl Banned { + pub fn load() -> Self { + Banned(load_with_key(BANNED_KEY).unwrap_or_default()) + } + + #[allow(dead_code)] + pub fn save(self) { + write_with_key(BANNED_KEY, self.0) + } + + #[allow(dead_code)] + pub fn add(&mut self, public: Public) { + self.0.insert(public); + } + + pub fn is_banned(&self, public: &Public) -> bool { + self.0.contains(public) + } +} diff --git a/basic_module/staking/src/syntax_error.rs b/basic_module/staking/src/syntax_error.rs new file mode 100644 index 0000000000..3dc46c57cc --- /dev/null +++ b/basic_module/staking/src/syntax_error.rs @@ -0,0 +1,42 @@ +// Copyright 2020 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::types::{NetworkId, Signature}; +use std::fmt; + +#[derive(Debug)] +pub enum Error { + InvalidSignature(Signature), + InvalidNetworkId(NetworkId), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::InvalidNetworkId(network_id) => write!(f, "{} is an invalid network id", network_id), + Error::InvalidSignature(sig) => write!(f, "Signature {:?} is invalid", sig), + } + } +} + +impl Error { + pub fn code(&self) -> i64 { + match self { + Error::InvalidSignature(_) => -1, + Error::InvalidNetworkId(_) => -2, + } + } +} diff --git a/basic_module/staking/src/types.rs b/basic_module/staking/src/types.rs new file mode 100644 index 0000000000..785ce1878a --- /dev/null +++ b/basic_module/staking/src/types.rs @@ -0,0 +1,76 @@ +// Copyright 2020 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pub use fkey::{sign, verify, Ed25519Private as Private, Ed25519Public as Public, Signature}; +pub use ftypes::Header; + +pub type Bytes = Vec; +pub type StakeQuantity = u64; +pub type DepositQuantity = u64; + +#[derive(Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Clone)] +pub struct Validator { + // Indicates weights in a round-robin proposer scheduling + pub weight: StakeQuantity, + pub delegation: StakeQuantity, + pub deposit: DepositQuantity, + pub pubkey: Public, +} + +impl Validator { + pub fn new(delegation: StakeQuantity, deposit: DepositQuantity, pubkey: Public) -> Self { + Self { + weight: delegation, + delegation, + deposit, + pubkey, + } + } + + pub fn reset(&mut self) { + self.weight = self.delegation; + } + + pub fn pubkey(&self) -> &Public { + &self.pubkey + } + + pub fn delegation(&self) -> StakeQuantity { + self.delegation + } +} + +#[derive(Serialize, Deserialize)] +pub struct Candidate { + pub pubkey: Public, + pub deposit: DepositQuantity, + pub nomination_ends_at: u64, + pub metadata: Bytes, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Prisoner { + pub pubkey: Public, + pub deposit: DepositQuantity, + pub custody_until: u64, + pub released_at: u64, +} + +pub enum ReleaseResult { + NotExists, + InCustody, + Released(Prisoner), +} From 6d2ce4cb1de4fe6ad1c36429517d7e9d035017ae Mon Sep 17 00:00:00 2001 From: SeonpyoKim Date: Thu, 16 Apr 2020 20:18:21 +0900 Subject: [PATCH 3/8] Implement transaction and related types for the staking module --- basic_module/staking/src/error.rs | 8 +++ basic_module/staking/src/lib.rs | 10 +++ basic_module/staking/src/transactions.rs | 86 ++++++++++++++++++++++++ basic_module/staking/src/types.rs | 24 +++++++ 4 files changed, 128 insertions(+) create mode 100644 basic_module/staking/src/transactions.rs diff --git a/basic_module/staking/src/error.rs b/basic_module/staking/src/error.rs index 1820be7e50..5c45c6a386 100644 --- a/basic_module/staking/src/error.rs +++ b/basic_module/staking/src/error.rs @@ -15,6 +15,7 @@ // along with this program. If not, see . pub use crate::runtime_error::Error as RuntimeError; +pub use crate::syntax_error::Error as SyntaxError; use std::fmt::{Display, Formatter, Result as FormatResult}; #[derive(Debug)] @@ -49,6 +50,7 @@ impl Display for Insufficient { #[derive(Debug)] pub enum Error { Runtime(RuntimeError), + Syntax(SyntaxError), } impl From for Error { @@ -56,3 +58,9 @@ impl From for Error { Error::Runtime(error) } } + +impl From for Error { + fn from(error: SyntaxError) -> Error { + Error::Syntax(error) + } +} diff --git a/basic_module/staking/src/lib.rs b/basic_module/staking/src/lib.rs index 3d254f803f..7b7eeb27f6 100644 --- a/basic_module/staking/src/lib.rs +++ b/basic_module/staking/src/lib.rs @@ -24,6 +24,8 @@ use coordinator::context::SubStorageAccess; mod error; mod runtime_error; mod state; +mod syntax_error; +mod transactions; mod types; pub fn substorage() -> Box { @@ -37,3 +39,11 @@ pub fn deserialize(buffer: Vec) -> T { pub fn serialize(data: T) -> Vec { serde_cbor::to_vec(&data).unwrap() } + +lazy_static! { + static ref NETWORK_ID: types::NetworkId = Default::default(); +} + +fn check_network_id(network_id: types::NetworkId) -> bool { + *NETWORK_ID == network_id +} diff --git a/basic_module/staking/src/transactions.rs b/basic_module/staking/src/transactions.rs new file mode 100644 index 0000000000..cc86158223 --- /dev/null +++ b/basic_module/staking/src/transactions.rs @@ -0,0 +1,86 @@ +// Copyright 2020 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::state::Params; +use crate::types::{verify, Approval, Bytes, DepositQuantity, NetworkId, Public, Signature, StakeQuantity}; +use codechain_crypto::blake256; +use primitives::H256; + +pub struct SignedTransaction { + pub signature: Signature, + pub signer_public: Public, + pub tx: Transaction, +} + +impl SignedTransaction { + pub fn verify(&self) -> bool { + let message = self.tx.hash(); + verify(&self.signature, &message, &self.signer_public) + } +} + +#[derive(Serialize)] +pub struct Transaction { + /// Seq + pub seq: u64, + /// Quantity of CCC to be paid as a cost for distributing this transaction to the network. + pub fee: u64, + // Network id + pub network_id: NetworkId, + pub action: Action, +} + +impl Transaction { + pub fn hash(&self) -> H256 { + let serialized = serde_cbor::to_vec(&self).unwrap(); + blake256(serialized) + } +} + +#[allow(dead_code)] +#[derive(Serialize)] +pub enum Action { + TransferCCS { + receiver_public: Public, + quantity: StakeQuantity, + }, + DelegateCCS { + delegatee_public: Public, + quantity: StakeQuantity, + }, + Revoke { + delegatee_public: Public, + quantity: StakeQuantity, + }, + Redelegate { + prev_delegatee: Public, + next_delegatee: Public, + quantity: StakeQuantity, + }, + SelfNominate { + deposit: DepositQuantity, + metadata: Bytes, + }, + ChangeParams { + metadata_seq: u64, + params: Params, + approvals: Vec, + }, + ReportDoubleVote { + message1: Bytes, + message2: Bytes, + }, +} diff --git a/basic_module/staking/src/types.rs b/basic_module/staking/src/types.rs index 785ce1878a..357e3a8506 100644 --- a/basic_module/staking/src/types.rs +++ b/basic_module/staking/src/types.rs @@ -16,11 +16,29 @@ pub use fkey::{sign, verify, Ed25519Private as Private, Ed25519Public as Public, Signature}; pub use ftypes::Header; +use std::fmt; +use std::str; pub type Bytes = Vec; pub type StakeQuantity = u64; pub type DepositQuantity = u64; +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)] +pub struct NetworkId([u8; 2]); + +impl fmt::Display for NetworkId { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + let s = str::from_utf8(&self.0).expect("network_id a valid utf8 string"); + write!(f, "{}", s) + } +} + +impl Default for NetworkId { + fn default() -> Self { + NetworkId([116, 99]) + } +} + #[derive(Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Clone)] pub struct Validator { // Indicates weights in a round-robin proposer scheduling @@ -74,3 +92,9 @@ pub enum ReleaseResult { InCustody, Released(Prisoner), } + +#[derive(Serialize)] +pub struct Approval { + pub signature: Signature, + pub signer_public: Public, +} From 80e7fb0efbe413d689a27451719f7e74c30cadd9 Mon Sep 17 00:00:00 2001 From: SeonpyoKim Date: Wed, 13 May 2020 16:49:50 +0900 Subject: [PATCH 4/8] Improve functionality of TransactionExecutionOutcome --- coordinator/src/types.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/coordinator/src/types.rs b/coordinator/src/types.rs index e0521c9998..916da963f4 100644 --- a/coordinator/src/types.rs +++ b/coordinator/src/types.rs @@ -230,10 +230,17 @@ pub enum VerifiedCrime { }, } +#[derive(Default)] pub struct TransactionExecutionOutcome { pub events: Vec, } +impl TransactionExecutionOutcome { + fn push_event(&mut self, event: Event) { + self.events.push(event); + } +} + pub type HeaderError = String; pub type ExecuteTransactionError = (); pub type CloseBlockError = String; From 611e35cf6868feeb4cb39f16aee6ea45cca9f230 Mon Sep 17 00:00:00 2001 From: SeonpyoKim Date: Thu, 16 Apr 2020 20:39:15 +0900 Subject: [PATCH 5/8] Implement abci requirements --- basic_module/staking/src/check.rs | 35 +++ basic_module/staking/src/core.rs | 42 ++++ basic_module/staking/src/execute.rs | 266 ++++++++++++++++++++++ basic_module/staking/src/impls.rs | 112 +++++++++ basic_module/staking/src/imported.rs | 38 ++++ basic_module/staking/src/lib.rs | 18 ++ basic_module/staking/src/runtime_error.rs | 16 +- basic_module/staking/src/state.rs | 35 ++- basic_module/staking/src/transactions.rs | 8 + basic_module/staking/src/types.rs | 20 +- 10 files changed, 587 insertions(+), 3 deletions(-) create mode 100644 basic_module/staking/src/check.rs create mode 100644 basic_module/staking/src/core.rs create mode 100644 basic_module/staking/src/execute.rs create mode 100644 basic_module/staking/src/impls.rs create mode 100644 basic_module/staking/src/imported.rs diff --git a/basic_module/staking/src/check.rs b/basic_module/staking/src/check.rs new file mode 100644 index 0000000000..f8e39013eb --- /dev/null +++ b/basic_module/staking/src/check.rs @@ -0,0 +1,35 @@ +// Copyright 2020 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::check_network_id; +use crate::syntax_error::Error; +use crate::transactions::{SignedTransaction, Transaction}; + +pub fn check(signed_tx: &SignedTransaction) -> Result<(), Error> { + if !signed_tx.verify() { + Err(Error::InvalidSignature(signed_tx.signature)) + } else { + check_inner(&signed_tx.tx) + } +} + +fn check_inner(tx: &Transaction) -> Result<(), Error> { + if !check_network_id(tx.network_id) { + Err(Error::InvalidNetworkId(tx.network_id)) + } else { + Ok(()) + } +} diff --git a/basic_module/staking/src/core.rs b/basic_module/staking/src/core.rs new file mode 100644 index 0000000000..3f1d44976b --- /dev/null +++ b/basic_module/staking/src/core.rs @@ -0,0 +1,42 @@ +// Copyright 2020 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::state::{Banned, Params}; +use crate::transactions::SignedTransaction; +use crate::types::{Header, Public, Validator}; +pub use coordinator::context::SubStorageAccess; +pub use coordinator::types::{ExecuteTransactionError, HeaderError, TransactionExecutionOutcome, VerifiedCrime}; +use std::collections::HashMap; + +pub trait Abci { + fn open_block(&self, header: &Header, verified_crime: &[VerifiedCrime]) -> Result<(), HeaderError>; + fn execute_transactions( + &self, + transactions: Vec, + ) -> Result, ExecuteTransactionError>; + fn check_transaction(&self, transaction: &SignedTransaction) -> Result<(), i64>; +} + +pub trait StakingView { + fn get_stakes(&self) -> HashMap; + fn get_validators(&self) -> Vec; + fn current_term_id(&self) -> u64; + fn get_term_common_params(&self) -> Params; + fn is_term_changed(&self) -> bool; + fn last_term_finished_block_num(&self) -> u64; + fn era(&self) -> u64; + fn get_banned_validators(&self) -> Banned; +} diff --git a/basic_module/staking/src/execute.rs b/basic_module/staking/src/execute.rs new file mode 100644 index 0000000000..0a872acc71 --- /dev/null +++ b/basic_module/staking/src/execute.rs @@ -0,0 +1,266 @@ +// Copyright 2020 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::core::TransactionExecutionOutcome; +use crate::error::{Insufficient, Mismatch}; +use crate::runtime_error::Error; +use crate::state::*; +use crate::transactions::{Action, Transaction}; +use crate::types::{Approval, Bytes, Public, ReleaseResult, ResultantFee, StakeQuantity}; +use crate::{account_manager, account_viewer, substorage}; + +fn check_before_fee_imposition(sender_public: &Public, fee: u64, seq: u64, min_fee: u64) -> Result<(), Error> { + let account_sequence = account_viewer().get_sequence(sender_public); + if account_sequence != seq { + Err(Error::InvalidSeq(Mismatch { + expected: seq, + found: account_sequence, + })) + } else if fee < min_fee { + Err(Error::InsufficientFee(Insufficient { + required: min_fee, + actual: fee, + })) + } else { + Ok(()) + } +} + +pub fn apply_internal( + tx: Transaction, + sender_public: &Public, + current_block_number: u64, +) -> Result<(TransactionExecutionOutcome, ResultantFee), Error> { + let Transaction { + action, + fee, + seq, + .. + } = tx; + + let min_fee = action.min_fee(); + check_before_fee_imposition(sender_public, fee, seq, min_fee)?; + + // Does not impose fee and increase sequence for a failed transaction + let substorage = substorage(); + substorage.create_checkpoint(); + + let account_manager = account_manager(); + account_manager.sub_balance(sender_public, fee).map_err(|_err| { + Error::InsufficientBalance(Insufficient { + required: fee, + actual: account_viewer().get_balance(sender_public), + }) + })?; + account_manager.increment_sequence(&sender_public); + + let result = execute(&sender_public, action, current_block_number); + match result { + Ok(_) => substorage.discard_checkpoint(), + Err(_) => substorage.revert_to_the_checkpoint(), + }; + + result.map(|outcome| { + (outcome, ResultantFee { + additional_fee: fee - min_fee, + min_fee, + }) + }) +} + +fn execute( + sender_public: &Public, + action: Action, + current_block_number: u64, +) -> Result { + match action { + Action::TransferCCS { + receiver_public, + quantity, + } => transfer_ccs(sender_public, &receiver_public, quantity), + Action::DelegateCCS { + delegatee_public, + quantity, + } => delegate_ccs(sender_public, &delegatee_public, quantity), + Action::Revoke { + delegatee_public, + quantity, + } => revoke(sender_public, &delegatee_public, quantity), + Action::Redelegate { + prev_delegatee, + next_delegatee, + quantity, + } => redelegate(sender_public, &prev_delegatee, &next_delegatee, quantity), + Action::SelfNominate { + deposit, + metadata, + } => self_nominate(sender_public, deposit, metadata), + Action::ChangeParams { + metadata_seq, + params, + approvals, + } => change_params(metadata_seq, params, approvals), + Action::ReportDoubleVote { + .. + } => unimplemented!(), + } +} + +fn transfer_ccs(from: &Public, to: &Public, quantity: StakeQuantity) -> Result { + let mut stakeholders = Stakeholders::load(); + let mut sender_account = StakeAccount::load(from); + let mut receiver_account = StakeAccount::load(to); + let sender_delegations = Delegation::load(from); + + sender_account.subtract_balance(quantity)?; + receiver_account.add_balance(quantity)?; + + stakeholders.update_by_decreased_balance(&sender_account, &sender_delegations); + stakeholders.update_by_increased_balance(&receiver_account); + + stakeholders.save(); + sender_account.save(); + receiver_account.save(); + + Ok(Default::default()) +} + +fn delegate_ccs(delegator: &Public, delegatee: &Public, quantity: u64) -> Result { + let candidates = Candidates::load(); + if candidates.get_candidate(delegatee).is_none() { + return Err(Error::DelegateeNotFoundInCandidates(*delegatee)) + } + + let banned = Banned::load(); + let jailed = Jail::load(); + assert!(!banned.is_banned(delegatee), "A candidate must not be banned"); + assert_eq!(None, jailed.get_prisoner(delegatee), "A candidate must not be jailed"); + + let mut delegator_account = StakeAccount::load(delegator); + let mut delegation = Delegation::load(delegator); + + delegator_account.subtract_balance(quantity)?; + delegation.add_quantity(*delegatee, quantity)?; + // delegation does not touch stakeholders + + delegation.save(); + delegator_account.save(); + + Ok(Default::default()) +} + +fn revoke(delegator: &Public, delegatee: &Public, quantity: u64) -> Result { + let mut delegator_account = StakeAccount::load(delegator); + let mut delegation = Delegation::load(delegator); + + delegator_account.add_balance(quantity)?; + delegation.sub_quantity(*delegatee, quantity)?; + // delegation does not touch stakeholders + + delegation.save(); + delegator_account.save(); + + Ok(Default::default()) +} + +fn redelegate( + delegator: &Public, + prev_delegatee: &Public, + next_delegatee: &Public, + quantity: u64, +) -> Result { + let candidates = Candidates::load(); + if candidates.get_candidate(next_delegatee).is_none() { + return Err(Error::DelegateeNotFoundInCandidates(*next_delegatee)) + } + + let banned = Banned::load(); + let jailed = Jail::load(); + assert!(!banned.is_banned(&next_delegatee), "A candidate must not be banned"); + assert_eq!(None, jailed.get_prisoner(next_delegatee), "A candidate must not be jailed"); + + let delegator_account = StakeAccount::load(delegator); + let mut delegation = Delegation::load(delegator); + + delegation.sub_quantity(*prev_delegatee, quantity)?; + delegation.add_quantity(*next_delegatee, quantity)?; + + delegation.save(); + delegator_account.save(); + + Ok(Default::default()) +} + +pub fn self_nominate( + nominee_public: &Public, + deposit: u64, + metadata: Bytes, +) -> Result { + let state_metadata = Metadata::load(); + let current_term = state_metadata.current_term_id; + let nomination_ends_at = current_term + state_metadata.term_params.nomination_expiration; + + let blacklist = Banned::load(); + if blacklist.is_banned(nominee_public) { + return Err(Error::BannedAccount(*nominee_public)) + } + + let mut jail = Jail::load(); + let total_deposit = match jail.try_release(nominee_public, current_term) { + ReleaseResult::InCustody => return Err(Error::AccountInCustody(*nominee_public)), + ReleaseResult::NotExists => deposit, + ReleaseResult::Released(prisoner) => { + assert_eq!(&prisoner.pubkey, nominee_public); + prisoner.deposit + deposit + } + }; + + let mut candidates = Candidates::load(); + // FIXME: Error handling is required + account_manager().sub_balance(nominee_public, deposit).unwrap(); + candidates.add_deposit(nominee_public, total_deposit, nomination_ends_at, metadata); + + jail.save(); + candidates.save(); + + Ok(Default::default()) +} + +pub fn change_params( + metadata_seq: u64, + params: Params, + approvals: Vec, +) -> Result { + // Update state first because the signature validation is more expensive. + let mut metadata = Metadata::load(); + metadata.update_params(metadata_seq, params)?; + let stakes = get_stakes(); + // Approvals are verified + let signed_stakes = approvals.iter().try_fold(0, |sum, approval| { + let public = approval.signer_public; + stakes.get(&public).map(|stake| sum + stake).ok_or_else(|| Error::SignatureOfInvalidAccount(public)) + })?; + let total_stakes: u64 = stakes.values().sum(); + if total_stakes / 2 >= signed_stakes { + return Err(Error::InsufficientStakes(Insufficient { + required: total_stakes, + actual: signed_stakes, + })) + } + + metadata.save(); + Ok(Default::default()) +} diff --git a/basic_module/staking/src/impls.rs b/basic_module/staking/src/impls.rs new file mode 100644 index 0000000000..f7d61016ad --- /dev/null +++ b/basic_module/staking/src/impls.rs @@ -0,0 +1,112 @@ +// Copyright 2020 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::check::check; +use crate::core::{ + Abci, ExecuteTransactionError, HeaderError, StakingView, TransactionExecutionOutcome, VerifiedCrime, +}; +use crate::error::Error; +use crate::execute::apply_internal; +use crate::fee_manager; +use crate::state::{get_stakes, Banned, CurrentValidators, Metadata, Params}; +use crate::transactions::SignedTransaction; +use crate::types::{Header, Public, ResultantFee, Validator}; +use std::cell::RefCell; +use std::collections::HashMap; + +struct ABCIHandle { + executing_block_header: RefCell
, +} + +impl Abci for ABCIHandle { + fn open_block(&self, header: &Header, _verified_crime: &[VerifiedCrime]) -> Result<(), HeaderError> { + *self.executing_block_header.borrow_mut() = header.clone(); + Ok(()) + } + + fn execute_transactions( + &self, + transactions: Vec, + ) -> Result, ExecuteTransactionError> { + let results: Result, _> = transactions + .into_iter() + .map(|signed_tx| { + check(&signed_tx).map_err(Error::Syntax).and({ + let SignedTransaction { + tx, + signer_public, + .. + } = signed_tx; + apply_internal(tx, &signer_public, self.executing_block_header.borrow().number()) + .map_err(Error::Runtime) + }) + }) + .collect(); + + // failed block does not accumulate fee and rejected + results + .map(|results| { + let (outcomes, fees): (_, Vec<_>) = results.into_iter().unzip(); + let ResultantFee { + additional_fee: total_additional_fee, + min_fee: total_min_fee, + } = fees.into_iter().fold(ResultantFee::default(), |fee_acc, fee| fee_acc + fee); + fee_manager().accumulate_block_fee(total_additional_fee, total_min_fee); + outcomes + }) + .map_err(|_| ()) + } + + fn check_transaction(&self, transaction: &SignedTransaction) -> Result<(), i64> { + check(transaction).map_err(|err| err.code()) + } +} + +struct StakingViewer {} + +impl StakingView for StakingViewer { + fn get_stakes(&self) -> HashMap { + get_stakes() + } + + fn last_term_finished_block_num(&self) -> u64 { + Metadata::load().last_term_finished_block_num + } + + fn get_term_common_params(&self) -> Params { + Metadata::load().term_params + } + + fn era(&self) -> u64 { + Metadata::load().term_params.era + } + + fn is_term_changed(&self) -> bool { + unimplemented!() + } + + fn current_term_id(&self) -> u64 { + Metadata::load().current_term_id + } + + fn get_validators(&self) -> Vec { + CurrentValidators::load().into() + } + + fn get_banned_validators(&self) -> Banned { + Banned::load() + } +} diff --git a/basic_module/staking/src/imported.rs b/basic_module/staking/src/imported.rs new file mode 100644 index 0000000000..93399070cc --- /dev/null +++ b/basic_module/staking/src/imported.rs @@ -0,0 +1,38 @@ +// Copyright 2020 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::types::{Public, Signature}; + +pub trait AccountManager { + fn add_balance(&self, public: &Public, val: u64); + fn sub_balance(&self, public: &Public, val: u64) -> Result<(), String>; + fn set_balance(&self, public: &Public, val: u64); + fn increment_sequence(&self, public: &Public); +} + +pub trait AccountView { + fn is_active(&self, public: &Public) -> bool; + fn get_balance(&self, public: &Public) -> u64; + fn get_sequence(&self, public: &Public) -> u64; +} + +pub trait SignatureManager { + fn verify(&self, signature: &Signature, message: &[u8], public: &Public) -> bool; +} + +pub trait FeeManager { + fn accumulate_block_fee(&self, total_additional_fee: u64, total_min_fee: u64); +} diff --git a/basic_module/staking/src/lib.rs b/basic_module/staking/src/lib.rs index 7b7eeb27f6..2a64bf590a 100644 --- a/basic_module/staking/src/lib.rs +++ b/basic_module/staking/src/lib.rs @@ -20,8 +20,14 @@ extern crate serde_derive; extern crate lazy_static; use coordinator::context::SubStorageAccess; +use imported::{AccountManager, AccountView, FeeManager}; +mod check; +mod core; mod error; +mod execute; +mod impls; +mod imported; mod runtime_error; mod state; mod syntax_error; @@ -47,3 +53,15 @@ lazy_static! { fn check_network_id(network_id: types::NetworkId) -> bool { *NETWORK_ID == network_id } + +pub fn account_manager() -> Box { + unimplemented!() +} + +pub fn account_viewer() -> Box { + unimplemented!() +} + +pub fn fee_manager() -> Box { + unimplemented!() +} diff --git a/basic_module/staking/src/runtime_error.rs b/basic_module/staking/src/runtime_error.rs index 4540e3396c..fbcea73aa2 100644 --- a/basic_module/staking/src/runtime_error.rs +++ b/basic_module/staking/src/runtime_error.rs @@ -14,23 +14,37 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use crate::error::Insufficient; +use crate::error::{Insufficient, Mismatch}; use crate::types::{Public, StakeQuantity}; use std::fmt::{Display, Formatter, Result as FormatResult}; #[derive(Debug)] pub enum Error { InsufficientStakes(Insufficient), + InsufficientBalance(Insufficient), DelegateeNotFoundInCandidates(Public), + BannedAccount(Public), + AccountInCustody(Public), + SignatureOfInvalidAccount(Public), + InvalidMetadataSeq(Mismatch), + InvalidSeq(Mismatch), + InsufficientFee(Insufficient), } impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> FormatResult { match self { Error::InsufficientStakes(insufficient) => write!(f, "Insufficient stakes: {}", insufficient), + Error::InsufficientBalance(insufficient) => write!(f, "Insufficient balance: {}", insufficient), Error::DelegateeNotFoundInCandidates(delegatee) => { write!(f, "Delegatee {:?} is not in Candidates", delegatee) } + Error::BannedAccount(nominee) => write!(f, "Public {:?} was blacklisted", nominee), + Error::AccountInCustody(nominee) => write!(f, "Public {:?} is still in custody", nominee), + Error::SignatureOfInvalidAccount(signer) => write!(f, "Public {:?} does not have any stake", signer), + Error::InvalidMetadataSeq(mismatch) => write!(f, "Metatdata sequence mismatched. {}", mismatch), + Error::InvalidSeq(mismatch) => write!(f, "Seq of the transaction mismatched. {}", mismatch), + Error::InsufficientFee(insufficient) => write!(f, "Insufficient fee: {}", insufficient), } } } diff --git a/basic_module/staking/src/state.rs b/basic_module/staking/src/state.rs index d4de81890c..6d4eb9eea7 100644 --- a/basic_module/staking/src/state.rs +++ b/basic_module/staking/src/state.rs @@ -22,7 +22,7 @@ use serde::{de::DeserializeOwned, ser::Serialize}; use std::cmp::{max, Ordering}; use std::collections::{ btree_map::{self, Entry}, - BTreeMap, BTreeSet, HashMap, HashSet, + btree_set, BTreeMap, BTreeSet, HashMap, HashSet, }; use std::ops::Deref; @@ -87,6 +87,23 @@ impl Metadata { pub fn save(self) { write_with_key(METADATA_KEY, self) } + + pub fn update_params(&mut self, metadata_seq: u64, new_params: Params) -> Result<(), Error> { + if self.seq != metadata_seq { + Err(Error::InvalidMetadataSeq(Mismatch { + found: metadata_seq, + expected: self.seq, + })) + } else { + self.params = new_params; + self.seq += 1; + Ok(()) + } + } + + pub fn update_term_params(&mut self) { + self.term_params = self.params; + } } pub struct StakeAccount<'a> { @@ -229,6 +246,10 @@ impl Stakeholders { self.0.remove(account.public); } } + + pub fn iter(&self) -> btree_set::Iter<'_, Public> { + self.0.iter() + } } pub struct NextValidators(Vec); @@ -537,3 +558,15 @@ impl Banned { self.0.contains(public) } } + +pub fn get_stakes() -> HashMap { + let stakeholders = Stakeholders::load(); + stakeholders + .iter() + .map(|stakeholder| { + let account = StakeAccount::load(stakeholder); + let delegation = Delegation::load(stakeholder); + (*stakeholder, account.balance + delegation.sum()) + }) + .collect() +} diff --git a/basic_module/staking/src/transactions.rs b/basic_module/staking/src/transactions.rs index cc86158223..4377aa8398 100644 --- a/basic_module/staking/src/transactions.rs +++ b/basic_module/staking/src/transactions.rs @@ -84,3 +84,11 @@ pub enum Action { message2: Bytes, }, } + +impl Action { + pub fn min_fee(&self) -> u64 { + // Where can we initialize the min fee + // We need both consensus-defined minimum fee and machine-defined minimum fee + unimplemented!() + } +} diff --git a/basic_module/staking/src/types.rs b/basic_module/staking/src/types.rs index 357e3a8506..c1cc27655f 100644 --- a/basic_module/staking/src/types.rs +++ b/basic_module/staking/src/types.rs @@ -17,6 +17,7 @@ pub use fkey::{sign, verify, Ed25519Private as Private, Ed25519Public as Public, Signature}; pub use ftypes::Header; use std::fmt; +use std::ops::Add; use std::str; pub type Bytes = Vec; @@ -79,7 +80,7 @@ pub struct Candidate { pub metadata: Bytes, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Prisoner { pub pubkey: Public, pub deposit: DepositQuantity, @@ -98,3 +99,20 @@ pub struct Approval { pub signature: Signature, pub signer_public: Public, } + +#[derive(Default, Serialize, Clone, Copy)] +pub struct ResultantFee { + pub additional_fee: u64, + pub min_fee: u64, +} + +impl Add for ResultantFee { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self { + additional_fee: self.additional_fee + other.additional_fee, + min_fee: self.min_fee + other.min_fee, + } + } +} From a5ac0e18997fbaeaf05b58164a83ab15959e8dd9 Mon Sep 17 00:00:00 2001 From: SeonpyoKim Date: Fri, 24 Apr 2020 12:32:55 +0900 Subject: [PATCH 6/8] Implement transactions related to term changes Term changes trigger the staking rule to elect validators and to modify states. --- basic_module/staking/src/check.rs | 4 +- basic_module/staking/src/core.rs | 6 +- basic_module/staking/src/execute.rs | 166 +++++++++++++++++++--- basic_module/staking/src/impls.rs | 25 ++-- basic_module/staking/src/runtime_error.rs | 2 + basic_module/staking/src/state.rs | 13 ++ basic_module/staking/src/transactions.rs | 39 +++-- 7 files changed, 216 insertions(+), 39 deletions(-) diff --git a/basic_module/staking/src/check.rs b/basic_module/staking/src/check.rs index f8e39013eb..d524084eb1 100644 --- a/basic_module/staking/src/check.rs +++ b/basic_module/staking/src/check.rs @@ -16,7 +16,7 @@ use crate::check_network_id; use crate::syntax_error::Error; -use crate::transactions::{SignedTransaction, Transaction}; +use crate::transactions::{SignedTransaction, UserTransaction}; pub fn check(signed_tx: &SignedTransaction) -> Result<(), Error> { if !signed_tx.verify() { @@ -26,7 +26,7 @@ pub fn check(signed_tx: &SignedTransaction) -> Result<(), Error> { } } -fn check_inner(tx: &Transaction) -> Result<(), Error> { +fn check_inner(tx: &UserTransaction) -> Result<(), Error> { if !check_network_id(tx.network_id) { Err(Error::InvalidNetworkId(tx.network_id)) } else { diff --git a/basic_module/staking/src/core.rs b/basic_module/staking/src/core.rs index 3f1d44976b..f1ccf5c49a 100644 --- a/basic_module/staking/src/core.rs +++ b/basic_module/staking/src/core.rs @@ -15,7 +15,7 @@ // along with this program. If not, see . use crate::state::{Banned, Params}; -use crate::transactions::SignedTransaction; +use crate::transactions::Transaction; use crate::types::{Header, Public, Validator}; pub use coordinator::context::SubStorageAccess; pub use coordinator::types::{ExecuteTransactionError, HeaderError, TransactionExecutionOutcome, VerifiedCrime}; @@ -25,9 +25,9 @@ pub trait Abci { fn open_block(&self, header: &Header, verified_crime: &[VerifiedCrime]) -> Result<(), HeaderError>; fn execute_transactions( &self, - transactions: Vec, + transactions: Vec, ) -> Result, ExecuteTransactionError>; - fn check_transaction(&self, transaction: &SignedTransaction) -> Result<(), i64>; + fn check_transaction(&self, transaction: &Transaction) -> Result<(), i64>; } pub trait StakingView { diff --git a/basic_module/staking/src/execute.rs b/basic_module/staking/src/execute.rs index 0a872acc71..321376d6f4 100644 --- a/basic_module/staking/src/execute.rs +++ b/basic_module/staking/src/execute.rs @@ -18,7 +18,7 @@ use crate::core::TransactionExecutionOutcome; use crate::error::{Insufficient, Mismatch}; use crate::runtime_error::Error; use crate::state::*; -use crate::transactions::{Action, Transaction}; +use crate::transactions::{AutoAction, UserAction, UserTransaction}; use crate::types::{Approval, Bytes, Public, ReleaseResult, ResultantFee, StakeQuantity}; use crate::{account_manager, account_viewer, substorage}; @@ -40,11 +40,10 @@ fn check_before_fee_imposition(sender_public: &Public, fee: u64, seq: u64, min_f } pub fn apply_internal( - tx: Transaction, + tx: UserTransaction, sender_public: &Public, - current_block_number: u64, ) -> Result<(TransactionExecutionOutcome, ResultantFee), Error> { - let Transaction { + let UserTransaction { action, fee, seq, @@ -67,7 +66,7 @@ pub fn apply_internal( })?; account_manager.increment_sequence(&sender_public); - let result = execute(&sender_public, action, current_block_number); + let result = execute_user_action(&sender_public, action); match result { Ok(_) => substorage.discard_checkpoint(), Err(_) => substorage.revert_to_the_checkpoint(), @@ -81,44 +80,77 @@ pub fn apply_internal( }) } -fn execute( - sender_public: &Public, - action: Action, - current_block_number: u64, -) -> Result { +fn execute_user_action(sender_public: &Public, action: UserAction) -> Result { match action { - Action::TransferCCS { + UserAction::TransferCCS { receiver_public, quantity, } => transfer_ccs(sender_public, &receiver_public, quantity), - Action::DelegateCCS { + UserAction::DelegateCCS { delegatee_public, quantity, } => delegate_ccs(sender_public, &delegatee_public, quantity), - Action::Revoke { + UserAction::Revoke { delegatee_public, quantity, } => revoke(sender_public, &delegatee_public, quantity), - Action::Redelegate { + UserAction::Redelegate { prev_delegatee, next_delegatee, quantity, } => redelegate(sender_public, &prev_delegatee, &next_delegatee, quantity), - Action::SelfNominate { + UserAction::SelfNominate { deposit, metadata, } => self_nominate(sender_public, deposit, metadata), - Action::ChangeParams { + UserAction::ChangeParams { metadata_seq, params, approvals, } => change_params(metadata_seq, params, approvals), - Action::ReportDoubleVote { + UserAction::ReportDoubleVote { .. } => unimplemented!(), } } +pub fn execute_auto_action( + action: AutoAction, + current_block_number: u64, +) -> Result { + match action { + AutoAction::UpdateValidators { + validators, + } => update_validators(validators), + AutoAction::CloseTerm { + inactive_validators, + next_validators, + released_addresses, + custody_until, + kick_at, + } => { + close_term(next_validators, &inactive_validators)?; + release_jailed_prisoners(&released_addresses)?; + jail(&inactive_validators, custody_until, kick_at); + increase_term_id(current_block_number); + Ok(Default::default()) + } + AutoAction::Elect => { + NextValidators::elect().save(); + let mut metadata = Metadata::load(); + metadata.update_term_params(); + metadata.save(); + Ok(Default::default()) + } + AutoAction::ChangeNextValidators { + validators, + } => { + NextValidators::from(validators).save(); + Ok(Default::default()) + } + } +} + fn transfer_ccs(from: &Public, to: &Public, quantity: StakeQuantity) -> Result { let mut stakeholders = Stakeholders::load(); let mut sender_account = StakeAccount::load(from); @@ -264,3 +296,103 @@ pub fn change_params( metadata.save(); Ok(Default::default()) } + +fn update_validators(validators: NextValidators) -> Result { + let next_validators_in_state = NextValidators::load(); + if validators != next_validators_in_state { + return Err(Error::InvalidValidators) + } + let mut current_validators = CurrentValidators::load(); + current_validators.update(validators.into()); + current_validators.save(); + Ok(Default::default()) +} + +fn close_term(next_validators: NextValidators, inactive_validators: &[Public]) -> Result<(), Error> { + let metadata = Metadata::load(); + let current_term_id = metadata.current_term_id; + let nomination_expiration = metadata.params.nomination_expiration; + assert_ne!(0, nomination_expiration); + + update_candidates(current_term_id, nomination_expiration, &next_validators, inactive_validators)?; + next_validators.save(); + Ok(()) +} + +fn update_candidates( + current_term: u64, + nomination_expiration: u64, + next_validators: &NextValidators, + inactive_validators: &[Public], +) -> Result<(), Error> { + let banned = Banned::load(); + let mut candidates = Candidates::load(); + let nomination_ends_at = current_term + nomination_expiration; + + candidates.renew_candidates(next_validators, nomination_ends_at, inactive_validators, &banned); + + let expired = candidates.drain_expired_candidates(current_term); + + let account_manager = account_manager(); + for candidate in &expired { + account_manager.add_balance(&candidate.pubkey, candidate.deposit); + } + candidates.save(); + let expired: Vec<_> = expired.into_iter().map(|c| c.pubkey).collect(); + revert_delegations(&expired)?; + Ok(()) +} + +fn revert_delegations(reverted_delegatees: &[Public]) -> Result<(), Error> { + let stakeholders = Stakeholders::load(); + for stakeholder in stakeholders.iter() { + let mut delegator = StakeAccount::load(stakeholder); + let mut delegation = Delegation::load(stakeholder); + + for delegatee in reverted_delegatees { + let quantity = delegation.get_quantity(delegatee); + if quantity > 0 { + delegation.sub_quantity(*delegatee, quantity)?; + delegator.add_balance(quantity)?; + } + } + delegation.save(); + delegator.save(); + } + Ok(()) +} + +fn release_jailed_prisoners(released: &[Public]) -> Result<(), Error> { + if released.is_empty() { + return Ok(()) + } + + let mut jailed = Jail::load(); + let account_manager = account_manager(); + for public in released { + let prisoner = jailed.remove(public).unwrap(); + account_manager.add_balance(&public, prisoner.deposit); + } + jailed.save(); + revert_delegations(released)?; + Ok(()) +} + +fn jail(publics: &[Public], custody_until: u64, kick_at: u64) { + let mut candidates = Candidates::load(); + let mut jail = Jail::load(); + + for public in publics { + let candidate = candidates.remove(public).expect("There should be a candidate to jail"); + jail.add(candidate, custody_until, kick_at); + } + + jail.save(); + candidates.save(); +} + +fn increase_term_id(last_term_finished_block_num: u64) { + let mut metadata = Metadata::load(); + metadata.increase_term_id(last_term_finished_block_num); + metadata.save(); +} diff --git a/basic_module/staking/src/impls.rs b/basic_module/staking/src/impls.rs index f7d61016ad..cd725505bd 100644 --- a/basic_module/staking/src/impls.rs +++ b/basic_module/staking/src/impls.rs @@ -19,10 +19,10 @@ use crate::core::{ Abci, ExecuteTransactionError, HeaderError, StakingView, TransactionExecutionOutcome, VerifiedCrime, }; use crate::error::Error; -use crate::execute::apply_internal; +use crate::execute::{apply_internal, execute_auto_action}; use crate::fee_manager; use crate::state::{get_stakes, Banned, CurrentValidators, Metadata, Params}; -use crate::transactions::SignedTransaction; +use crate::transactions::{SignedTransaction, Transaction}; use crate::types::{Header, Public, ResultantFee, Validator}; use std::cell::RefCell; use std::collections::HashMap; @@ -39,20 +39,24 @@ impl Abci for ABCIHandle { fn execute_transactions( &self, - transactions: Vec, + transactions: Vec, ) -> Result, ExecuteTransactionError> { let results: Result, _> = transactions .into_iter() - .map(|signed_tx| { - check(&signed_tx).map_err(Error::Syntax).and({ + .map(|tx| match tx { + Transaction::User(signed_tx) => check(&signed_tx).map_err(Error::Syntax).and({ let SignedTransaction { tx, signer_public, .. } = signed_tx; - apply_internal(tx, &signer_public, self.executing_block_header.borrow().number()) + apply_internal(tx, &signer_public).map_err(Error::Runtime) + }), + Transaction::Auto(auto_action) => { + execute_auto_action(auto_action, self.executing_block_header.borrow().number()) + .map(|execution_result| (execution_result, Default::default())) .map_err(Error::Runtime) - }) + } }) .collect(); @@ -70,8 +74,11 @@ impl Abci for ABCIHandle { .map_err(|_| ()) } - fn check_transaction(&self, transaction: &SignedTransaction) -> Result<(), i64> { - check(transaction).map_err(|err| err.code()) + fn check_transaction(&self, transaction: &Transaction) -> Result<(), i64> { + match transaction { + Transaction::User(signed_tx) => check(signed_tx).map_err(|err| err.code()), + Transaction::Auto(_) => Ok(()), + } } } diff --git a/basic_module/staking/src/runtime_error.rs b/basic_module/staking/src/runtime_error.rs index fbcea73aa2..b2eb36d410 100644 --- a/basic_module/staking/src/runtime_error.rs +++ b/basic_module/staking/src/runtime_error.rs @@ -29,6 +29,7 @@ pub enum Error { InvalidMetadataSeq(Mismatch), InvalidSeq(Mismatch), InsufficientFee(Insufficient), + InvalidValidators, } impl Display for Error { @@ -45,6 +46,7 @@ impl Display for Error { Error::InvalidMetadataSeq(mismatch) => write!(f, "Metatdata sequence mismatched. {}", mismatch), Error::InvalidSeq(mismatch) => write!(f, "Seq of the transaction mismatched. {}", mismatch), Error::InsufficientFee(insufficient) => write!(f, "Insufficient fee: {}", insufficient), + Error::InvalidValidators => write!(f, "Next validators do not match with the state's"), } } } diff --git a/basic_module/staking/src/state.rs b/basic_module/staking/src/state.rs index 6d4eb9eea7..7274ca21c9 100644 --- a/basic_module/staking/src/state.rs +++ b/basic_module/staking/src/state.rs @@ -104,6 +104,12 @@ impl Metadata { pub fn update_term_params(&mut self) { self.term_params = self.params; } + + pub fn increase_term_id(&mut self, last_term_finished_block_num: u64) { + assert!(self.last_term_finished_block_num < last_term_finished_block_num); + self.last_term_finished_block_num = last_term_finished_block_num; + self.current_term_id += 1; + } } pub struct StakeAccount<'a> { @@ -252,6 +258,7 @@ impl Stakeholders { } } +#[derive(Serialize, PartialEq, Eq)] pub struct NextValidators(Vec); impl NextValidators { pub fn load() -> Self { @@ -346,6 +353,12 @@ impl From for Vec { } } +impl From> for NextValidators { + fn from(val: Vec) -> Self { + Self(val) + } +} + pub struct CurrentValidators(Vec); impl CurrentValidators { pub fn load() -> Self { diff --git a/basic_module/staking/src/transactions.rs b/basic_module/staking/src/transactions.rs index 4377aa8398..5c66e181ef 100644 --- a/basic_module/staking/src/transactions.rs +++ b/basic_module/staking/src/transactions.rs @@ -14,15 +14,21 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use crate::state::Params; -use crate::types::{verify, Approval, Bytes, DepositQuantity, NetworkId, Public, Signature, StakeQuantity}; +use crate::state::{NextValidators, Params}; +use crate::types::{verify, Approval, Bytes, DepositQuantity, NetworkId, Public, Signature, StakeQuantity, Validator}; use codechain_crypto::blake256; use primitives::H256; +#[allow(dead_code)] +pub enum Transaction { + User(SignedTransaction), + Auto(AutoAction), +} + pub struct SignedTransaction { pub signature: Signature, pub signer_public: Public, - pub tx: Transaction, + pub tx: UserTransaction, } impl SignedTransaction { @@ -33,17 +39,17 @@ impl SignedTransaction { } #[derive(Serialize)] -pub struct Transaction { +pub struct UserTransaction { /// Seq pub seq: u64, /// Quantity of CCC to be paid as a cost for distributing this transaction to the network. pub fee: u64, // Network id pub network_id: NetworkId, - pub action: Action, + pub action: UserAction, } -impl Transaction { +impl UserTransaction { pub fn hash(&self) -> H256 { let serialized = serde_cbor::to_vec(&self).unwrap(); blake256(serialized) @@ -52,7 +58,7 @@ impl Transaction { #[allow(dead_code)] #[derive(Serialize)] -pub enum Action { +pub enum UserAction { TransferCCS { receiver_public: Public, quantity: StakeQuantity, @@ -85,7 +91,24 @@ pub enum Action { }, } -impl Action { +pub enum AutoAction { + UpdateValidators { + validators: NextValidators, + }, + CloseTerm { + inactive_validators: Vec, + next_validators: NextValidators, + released_addresses: Vec, + custody_until: u64, + kick_at: u64, + }, + Elect, + ChangeNextValidators { + validators: Vec, + }, +} + +impl UserAction { pub fn min_fee(&self) -> u64 { // Where can we initialize the min fee // We need both consensus-defined minimum fee and machine-defined minimum fee From 79d56e9727edc8552d61a66585991ab169eade4e Mon Sep 17 00:00:00 2001 From: SeonpyoKim Date: Wed, 13 May 2020 16:22:48 +0900 Subject: [PATCH 7/8] Implement AdditionalTxCreator --- basic_module/staking/src/core.rs | 4 + basic_module/staking/src/impls.rs | 16 +++- basic_module/staking/src/lib.rs | 11 ++- basic_module/staking/src/state.rs | 11 ++- basic_module/staking/src/transactions.rs | 99 +++++++++++++++++++++++- 5 files changed, 135 insertions(+), 6 deletions(-) diff --git a/basic_module/staking/src/core.rs b/basic_module/staking/src/core.rs index f1ccf5c49a..137376ec00 100644 --- a/basic_module/staking/src/core.rs +++ b/basic_module/staking/src/core.rs @@ -40,3 +40,7 @@ pub trait StakingView { fn era(&self) -> u64; fn get_banned_validators(&self) -> Banned; } + +pub trait AdditionalTxCreator { + fn create(&self) -> Vec; +} diff --git a/basic_module/staking/src/impls.rs b/basic_module/staking/src/impls.rs index cd725505bd..9c83d29469 100644 --- a/basic_module/staking/src/impls.rs +++ b/basic_module/staking/src/impls.rs @@ -16,13 +16,16 @@ use crate::check::check; use crate::core::{ - Abci, ExecuteTransactionError, HeaderError, StakingView, TransactionExecutionOutcome, VerifiedCrime, + Abci, AdditionalTxCreator, ExecuteTransactionError, HeaderError, StakingView, TransactionExecutionOutcome, + VerifiedCrime, }; use crate::error::Error; use crate::execute::{apply_internal, execute_auto_action}; use crate::fee_manager; use crate::state::{get_stakes, Banned, CurrentValidators, Metadata, Params}; -use crate::transactions::{SignedTransaction, Transaction}; +use crate::transactions::{ + create_close_block_transactions, create_open_block_transactions, SignedTransaction, Transaction, +}; use crate::types::{Header, Public, ResultantFee, Validator}; use std::cell::RefCell; use std::collections::HashMap; @@ -31,6 +34,15 @@ struct ABCIHandle { executing_block_header: RefCell
, } +impl AdditionalTxCreator for ABCIHandle { + fn create(&self) -> Vec { + create_open_block_transactions() + .into_iter() + .chain(create_close_block_transactions(&*self.executing_block_header.borrow()).into_iter()) + .collect() + } +} + impl Abci for ABCIHandle { fn open_block(&self, header: &Header, _verified_crime: &[VerifiedCrime]) -> Result<(), HeaderError> { *self.executing_block_header.borrow_mut() = header.clone(); diff --git a/basic_module/staking/src/lib.rs b/basic_module/staking/src/lib.rs index 2a64bf590a..c024d60769 100644 --- a/basic_module/staking/src/lib.rs +++ b/basic_module/staking/src/lib.rs @@ -19,7 +19,7 @@ extern crate serde_derive; #[macro_use] extern crate lazy_static; -use coordinator::context::SubStorageAccess; +use coordinator::context::{ChainHistoryAccess, SubStateHistoryAccess, SubStorageAccess}; use imported::{AccountManager, AccountView, FeeManager}; mod check; @@ -46,6 +46,7 @@ pub fn serialize(data: T) -> Vec { serde_cbor::to_vec(&data).unwrap() } +// FIXME: network_id should be mutable lazy_static! { static ref NETWORK_ID: types::NetworkId = Default::default(); } @@ -65,3 +66,11 @@ pub fn account_viewer() -> Box { pub fn fee_manager() -> Box { unimplemented!() } + +pub fn chain_history_manager() -> Box { + unimplemented!() +} + +pub fn state_history_manager() -> Box { + unimplemented!() +} diff --git a/basic_module/staking/src/state.rs b/basic_module/staking/src/state.rs index 7274ca21c9..f93ac44c34 100644 --- a/basic_module/staking/src/state.rs +++ b/basic_module/staking/src/state.rs @@ -17,7 +17,8 @@ use crate::error::{Insufficient, Mismatch}; use crate::runtime_error::Error; use crate::types::*; -use crate::{deserialize, serialize, substorage}; +use crate::{deserialize, serialize, state_history_manager, substorage}; +use ftypes::BlockId; use serde::{de::DeserializeOwned, ser::Serialize}; use std::cmp::{max, Ordering}; use std::collections::{ @@ -51,6 +52,10 @@ fn load_with_key(key: &KEY) -> Option { substorage().get(key).map(deserialize) } +fn load_with_key_from(key: &KEY, id: BlockId) -> Option { + state_history_manager().get_at(Some(id), key).map(deserialize) +} + fn write_with_key(key: &KEY, data: T) { substorage().set(key, serialize(data)) } @@ -84,6 +89,10 @@ impl Metadata { load_with_key(METADATA_KEY).expect("Params must be exist") } + pub fn load_from(state_id: BlockId) -> Option { + load_with_key_from(METADATA_KEY, state_id) + } + pub fn save(self) { write_with_key(METADATA_KEY, self) } diff --git a/basic_module/staking/src/transactions.rs b/basic_module/staking/src/transactions.rs index 5c66e181ef..87adbbbd89 100644 --- a/basic_module/staking/src/transactions.rs +++ b/basic_module/staking/src/transactions.rs @@ -14,10 +14,14 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use crate::state::{NextValidators, Params}; -use crate::types::{verify, Approval, Bytes, DepositQuantity, NetworkId, Public, Signature, StakeQuantity, Validator}; +use crate::chain_history_manager; +use crate::state::{Jail, Metadata, NextValidators, Params}; +use crate::types::{ + verify, Approval, Bytes, DepositQuantity, Header, NetworkId, Public, Signature, StakeQuantity, Validator, +}; use codechain_crypto::blake256; use primitives::H256; +use std::collections::HashSet; #[allow(dead_code)] pub enum Transaction { @@ -115,3 +119,94 @@ impl UserAction { unimplemented!() } } + +pub fn create_close_block_transactions(current_header: &Header) -> Vec { + let chain_history = chain_history_manager(); + let parent_hash = current_header.parent_hash(); + let parent_header = chain_history.get_block_header(parent_hash.clone().into()).expect("parent header must exist"); + let parent_metadata = Metadata::load_from(parent_hash.clone().into()).expect("parent metadata must exist"); + let metadata = Metadata::load(); + let term = metadata.current_term_id; + let term_seconds = match term { + 0 => parent_metadata.params.term_seconds, + _ => parent_metadata.term_params.term_seconds, + }; + + let mut next_validators = NextValidators::load(); + next_validators.update_weight(current_header.author()); + + if is_term_close(current_header, &parent_header, term_seconds) { + vec![Transaction::Auto(AutoAction::ChangeNextValidators { + validators: next_validators.into(), + })] + } else { + let inactive_validators = match term { + 0 => Vec::new(), + _ => { + let start_of_the_current_term = metadata.last_term_finished_block_num + 1; + let validators = next_validators.iter().map(|val| val.pubkey).collect(); + inactive_validators(current_header, start_of_the_current_term, validators) + } + }; + let current_term_id = metadata.current_term_id; + let (custody_until, kick_at) = { + let params = metadata.params; + let custody_period = params.custody_period; + assert_ne!(0, custody_period); + let release_period = params.release_period; + assert_ne!(0, release_period); + (current_term_id + custody_period, current_term_id + release_period) + }; + let released_addresses = Jail::load() + .drain_released_prisoners(current_term_id) + .into_iter() + .map(|prisoner| prisoner.pubkey) + .collect(); + vec![ + Transaction::Auto(AutoAction::CloseTerm { + inactive_validators, + next_validators, + released_addresses, + custody_until, + kick_at, + }), + Transaction::Auto(AutoAction::Elect {}), + ] + } +} + +fn is_term_close(header: &Header, parent: &Header, term_seconds: u64) -> bool { + // Because the genesis block has a fixed generation time, the first block should not change the term. + if header.number() == 1 { + return false + } + if term_seconds == 0 { + return false + } + + header.timestamp() / term_seconds != parent.timestamp() / term_seconds +} + +fn inactive_validators( + current_header: &Header, + start_of_the_current_term: u64, + mut validators: HashSet, +) -> Vec { + let chain_history = chain_history_manager(); + validators.remove(current_header.author()); + let hash = *current_header.parent_hash(); + let mut header = chain_history.get_block_header(hash.into()).expect("Header of the parent must exist"); + while start_of_the_current_term <= header.number() { + validators.remove(&header.author()); + header = + chain_history.get_block_header((*header.parent_hash()).into()).expect("Header of the parent must exist"); + } + + validators.into_iter().collect() +} + +pub fn create_open_block_transactions() -> Vec { + vec![Transaction::Auto(AutoAction::UpdateValidators { + validators: NextValidators::load(), + })] +} From 012a96b745e73f0a33e9011da709943780f2bb70 Mon Sep 17 00:00:00 2001 From: SeonpyoKim Date: Tue, 19 May 2020 19:25:40 +0900 Subject: [PATCH 8/8] Sort validators by public key It applies the changes in #388. NextValidators and CurrentValidators are sorted by public key. As a tiebreaker Candidate and Validator utilize the nomination start block number and transaction index. --- basic_module/staking/src/execute.rs | 17 ++++++++++++----- basic_module/staking/src/impls.rs | 10 ++++++++-- basic_module/staking/src/state.rs | 28 ++++++++++++++++++++-------- basic_module/staking/src/types.rs | 16 +++++++++++++--- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/basic_module/staking/src/execute.rs b/basic_module/staking/src/execute.rs index 321376d6f4..d0bb990526 100644 --- a/basic_module/staking/src/execute.rs +++ b/basic_module/staking/src/execute.rs @@ -19,7 +19,7 @@ use crate::error::{Insufficient, Mismatch}; use crate::runtime_error::Error; use crate::state::*; use crate::transactions::{AutoAction, UserAction, UserTransaction}; -use crate::types::{Approval, Bytes, Public, ReleaseResult, ResultantFee, StakeQuantity}; +use crate::types::{Approval, Bytes, Public, ReleaseResult, ResultantFee, StakeQuantity, Tiebreaker}; use crate::{account_manager, account_viewer, substorage}; fn check_before_fee_imposition(sender_public: &Public, fee: u64, seq: u64, min_fee: u64) -> Result<(), Error> { @@ -42,6 +42,7 @@ fn check_before_fee_imposition(sender_public: &Public, fee: u64, seq: u64, min_f pub fn apply_internal( tx: UserTransaction, sender_public: &Public, + tiebreaker: Tiebreaker, ) -> Result<(TransactionExecutionOutcome, ResultantFee), Error> { let UserTransaction { action, @@ -66,7 +67,7 @@ pub fn apply_internal( })?; account_manager.increment_sequence(&sender_public); - let result = execute_user_action(&sender_public, action); + let result = execute_user_action(&sender_public, action, tiebreaker); match result { Ok(_) => substorage.discard_checkpoint(), Err(_) => substorage.revert_to_the_checkpoint(), @@ -80,7 +81,11 @@ pub fn apply_internal( }) } -fn execute_user_action(sender_public: &Public, action: UserAction) -> Result { +fn execute_user_action( + sender_public: &Public, + action: UserAction, + tiebreaker: Tiebreaker, +) -> Result { match action { UserAction::TransferCCS { receiver_public, @@ -102,7 +107,7 @@ fn execute_user_action(sender_public: &Public, action: UserAction) -> Result self_nominate(sender_public, deposit, metadata), + } => self_nominate(sender_public, deposit, metadata, tiebreaker), UserAction::ChangeParams { metadata_seq, params, @@ -240,6 +245,7 @@ pub fn self_nominate( nominee_public: &Public, deposit: u64, metadata: Bytes, + tiebreaker: Tiebreaker, ) -> Result { let state_metadata = Metadata::load(); let current_term = state_metadata.current_term_id; @@ -263,7 +269,7 @@ pub fn self_nominate( let mut candidates = Candidates::load(); // FIXME: Error handling is required account_manager().sub_balance(nominee_public, deposit).unwrap(); - candidates.add_deposit(nominee_public, total_deposit, nomination_ends_at, metadata); + candidates.add_deposit(nominee_public, total_deposit, nomination_ends_at, metadata, tiebreaker); jail.save(); candidates.save(); @@ -299,6 +305,7 @@ pub fn change_params( fn update_validators(validators: NextValidators) -> Result { let next_validators_in_state = NextValidators::load(); + // NextValidators should be sorted by public key. if validators != next_validators_in_state { return Err(Error::InvalidValidators) } diff --git a/basic_module/staking/src/impls.rs b/basic_module/staking/src/impls.rs index 9c83d29469..17ff37945b 100644 --- a/basic_module/staking/src/impls.rs +++ b/basic_module/staking/src/impls.rs @@ -26,7 +26,7 @@ use crate::state::{get_stakes, Banned, CurrentValidators, Metadata, Params}; use crate::transactions::{ create_close_block_transactions, create_open_block_transactions, SignedTransaction, Transaction, }; -use crate::types::{Header, Public, ResultantFee, Validator}; +use crate::types::{Header, Public, ResultantFee, Tiebreaker, Validator}; use std::cell::RefCell; use std::collections::HashMap; @@ -53,16 +53,22 @@ impl Abci for ABCIHandle { &self, transactions: Vec, ) -> Result, ExecuteTransactionError> { + let mut user_tx_idx = 0; let results: Result, _> = transactions .into_iter() .map(|tx| match tx { Transaction::User(signed_tx) => check(&signed_tx).map_err(Error::Syntax).and({ + user_tx_idx += 1; let SignedTransaction { tx, signer_public, .. } = signed_tx; - apply_internal(tx, &signer_public).map_err(Error::Runtime) + let tiebreaker = Tiebreaker { + nominated_at_block_number: self.executing_block_header.borrow().number(), + nominated_at_transaction_index: user_tx_idx, + }; + apply_internal(tx, &signer_public, tiebreaker).map_err(Error::Runtime) }), Transaction::Auto(auto_action) => { execute_auto_action(auto_action, self.executing_block_header.borrow().number()) diff --git a/basic_module/staking/src/state.rs b/basic_module/staking/src/state.rs index f93ac44c34..26d6fb458d 100644 --- a/basic_module/staking/src/state.rs +++ b/basic_module/staking/src/state.rs @@ -20,7 +20,7 @@ use crate::types::*; use crate::{deserialize, serialize, state_history_manager, substorage}; use ftypes::BlockId; use serde::{de::DeserializeOwned, ser::Serialize}; -use std::cmp::{max, Ordering}; +use std::cmp::{max, Ordering, Reverse}; use std::collections::{ btree_map::{self, Entry}, btree_set, BTreeMap, BTreeSet, HashMap, HashSet, @@ -287,9 +287,8 @@ impl NextValidators { .. } = Metadata::load().term_params; assert!(max_num_of_validators >= min_num_of_validators); + // Sorted by (delegation DESC, deposit DESC, tiebreaker ASC) let mut validators = Candidates::prepare_validators(min_deposit); - // validators are now sorted in descending order of (delegation, deposit, priority) - validators.reverse(); { let banned = Banned::load(); @@ -313,17 +312,20 @@ impl NextValidators { let over_threshold = rest.iter().filter(|c| c.delegation >= delegation_threshold); let mut result: Vec<_> = minimum.iter().chain(over_threshold).cloned().collect(); - result.reverse(); + result.sort_unstable_by_key(|v| v.pubkey); + NextValidators(result) } pub fn update_weight(&mut self, block_author: &Public) { let min_delegation = self.min_delegation(); + let mut sorted_validators_view: Vec<&mut Validator> = self.0.iter_mut().collect(); + sorted_validators_view.sort_unstable_by_key(|val| (Reverse(val.weight), Reverse(val.deposit), val.tiebreaker)); for Validator { weight, pubkey, .. - } in self.0.iter_mut().rev() + } in sorted_validators_view.iter_mut() { if pubkey == block_author { // block author @@ -336,7 +338,6 @@ impl NextValidators { if self.0.iter().all(|validator| validator.weight == 0) { self.0.iter_mut().for_each(Validator::reset); } - self.0.sort_unstable(); } pub fn delegation(&self, pubkey: &Public) -> Option { @@ -384,6 +385,15 @@ impl CurrentValidators { } pub fn update(&mut self, validators: Vec) { + debug_assert_eq!( + validators, + { + let mut cloned = validators.clone(); + cloned.sort_unstable_by_key(|v| v.pubkey); + cloned + }, + "CurrentValidators is always sorted by public key" + ); self.0 = validators; } @@ -424,11 +434,11 @@ impl Candidates { candidates.into_iter().filter(|c| c.deposit >= min_deposit).fold(Vec::new(), |mut vec, candidate| { let public = &candidate.pubkey; if let Some(&delegation) = delegations.get(public) { - vec.push(Validator::new(delegation, candidate.deposit, candidate.pubkey)); + vec.push(Validator::new(delegation, candidate.deposit, candidate.pubkey, candidate.tiebreaker)); } vec }); - result.sort_by_key(|v| (v.delegation, v.deposit)); + result.sort_unstable_by_key(|v| (Reverse(v.delegation), Reverse(v.deposit), v.tiebreaker)); result } @@ -442,6 +452,7 @@ impl Candidates { quantity: DepositQuantity, nomination_ends_at: u64, metadata: Bytes, + tiebreaker: Tiebreaker, ) { if let Some(candidate) = self.0.iter_mut().find(|c| c.pubkey == *pubkey) { candidate.deposit += quantity; @@ -453,6 +464,7 @@ impl Candidates { deposit: quantity, nomination_ends_at, metadata, + tiebreaker, }) }; } diff --git a/basic_module/staking/src/types.rs b/basic_module/staking/src/types.rs index c1cc27655f..91d02012b7 100644 --- a/basic_module/staking/src/types.rs +++ b/basic_module/staking/src/types.rs @@ -15,7 +15,7 @@ // along with this program. If not, see . pub use fkey::{sign, verify, Ed25519Private as Private, Ed25519Public as Public, Signature}; -pub use ftypes::Header; +pub use ftypes::{BlockNumber, Header}; use std::fmt; use std::ops::Add; use std::str; @@ -40,22 +40,31 @@ impl Default for NetworkId { } } -#[derive(Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Clone)] +#[derive(Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Clone, Copy, Debug)] +pub struct Tiebreaker { + pub nominated_at_block_number: BlockNumber, + // User transaction index in a block + pub nominated_at_transaction_index: usize, +} + +#[derive(Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, Clone, Debug)] pub struct Validator { // Indicates weights in a round-robin proposer scheduling pub weight: StakeQuantity, pub delegation: StakeQuantity, pub deposit: DepositQuantity, pub pubkey: Public, + pub tiebreaker: Tiebreaker, } impl Validator { - pub fn new(delegation: StakeQuantity, deposit: DepositQuantity, pubkey: Public) -> Self { + pub fn new(delegation: StakeQuantity, deposit: DepositQuantity, pubkey: Public, tiebreaker: Tiebreaker) -> Self { Self { weight: delegation, delegation, deposit, pubkey, + tiebreaker, } } @@ -78,6 +87,7 @@ pub struct Candidate { pub deposit: DepositQuantity, pub nomination_ends_at: u64, pub metadata: Bytes, + pub tiebreaker: Tiebreaker, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]