diff --git a/Cargo.lock b/Cargo.lock index d57100ce8b..7fa864e96f 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", @@ -2982,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" @@ -2995,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]] @@ -3178,6 +3182,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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" 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..2a18b448d2 --- /dev/null +++ b/basic_module/staking/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "staking" +version = "0.1.0" +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/check.rs b/basic_module/staking/src/check.rs new file mode 100644 index 0000000000..d524084eb1 --- /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, UserTransaction}; + +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: &UserTransaction) -> 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..137376ec00 --- /dev/null +++ b/basic_module/staking/src/core.rs @@ -0,0 +1,46 @@ +// 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::Transaction; +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: &Transaction) -> 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; +} + +pub trait AdditionalTxCreator { + fn create(&self) -> Vec; +} diff --git a/basic_module/staking/src/error.rs b/basic_module/staking/src/error.rs new file mode 100644 index 0000000000..5c45c6a386 --- /dev/null +++ b/basic_module/staking/src/error.rs @@ -0,0 +1,66 @@ +// 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; +pub use crate::syntax_error::Error as SyntaxError; +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), + Syntax(SyntaxError), +} + +impl From for Error { + fn from(error: RuntimeError) -> Error { + Error::Runtime(error) + } +} + +impl From for Error { + fn from(error: SyntaxError) -> Error { + Error::Syntax(error) + } +} diff --git a/basic_module/staking/src/execute.rs b/basic_module/staking/src/execute.rs new file mode 100644 index 0000000000..d0bb990526 --- /dev/null +++ b/basic_module/staking/src/execute.rs @@ -0,0 +1,405 @@ +// 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::{AutoAction, UserAction, UserTransaction}; +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> { + 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: UserTransaction, + sender_public: &Public, + tiebreaker: Tiebreaker, +) -> Result<(TransactionExecutionOutcome, ResultantFee), Error> { + let UserTransaction { + 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_user_action(&sender_public, action, tiebreaker); + 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_user_action( + sender_public: &Public, + action: UserAction, + tiebreaker: Tiebreaker, +) -> Result { + match action { + UserAction::TransferCCS { + receiver_public, + quantity, + } => transfer_ccs(sender_public, &receiver_public, quantity), + UserAction::DelegateCCS { + delegatee_public, + quantity, + } => delegate_ccs(sender_public, &delegatee_public, quantity), + UserAction::Revoke { + delegatee_public, + quantity, + } => revoke(sender_public, &delegatee_public, quantity), + UserAction::Redelegate { + prev_delegatee, + next_delegatee, + quantity, + } => redelegate(sender_public, &prev_delegatee, &next_delegatee, quantity), + UserAction::SelfNominate { + deposit, + metadata, + } => self_nominate(sender_public, deposit, metadata, tiebreaker), + UserAction::ChangeParams { + metadata_seq, + params, + approvals, + } => change_params(metadata_seq, params, approvals), + 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); + 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, + tiebreaker: Tiebreaker, +) -> 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, tiebreaker); + + 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()) +} + +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) + } + 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 new file mode 100644 index 0000000000..17ff37945b --- /dev/null +++ b/basic_module/staking/src/impls.rs @@ -0,0 +1,137 @@ +// 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, 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::{ + create_close_block_transactions, create_open_block_transactions, SignedTransaction, Transaction, +}; +use crate::types::{Header, Public, ResultantFee, Tiebreaker, Validator}; +use std::cell::RefCell; +use std::collections::HashMap; + +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(); + Ok(()) + } + + fn execute_transactions( + &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; + 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()) + .map(|execution_result| (execution_result, Default::default())) + .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: &Transaction) -> Result<(), i64> { + match transaction { + Transaction::User(signed_tx) => check(signed_tx).map_err(|err| err.code()), + Transaction::Auto(_) => Ok(()), + } + } +} + +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 new file mode 100644 index 0000000000..c024d60769 --- /dev/null +++ b/basic_module/staking/src/lib.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 . + +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate lazy_static; + +use coordinator::context::{ChainHistoryAccess, SubStateHistoryAccess, 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; +mod transactions; +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() +} + +// FIXME: network_id should be mutable +lazy_static! { + static ref NETWORK_ID: types::NetworkId = Default::default(); +} + +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!() +} + +pub fn chain_history_manager() -> Box { + unimplemented!() +} + +pub fn state_history_manager() -> Box { + unimplemented!() +} diff --git a/basic_module/staking/src/runtime_error.rs b/basic_module/staking/src/runtime_error.rs new file mode 100644 index 0000000000..b2eb36d410 --- /dev/null +++ b/basic_module/staking/src/runtime_error.rs @@ -0,0 +1,52 @@ +// 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::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), + InvalidValidators, +} + +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), + 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 new file mode 100644 index 0000000000..26d6fb458d --- /dev/null +++ b/basic_module/staking/src/state.rs @@ -0,0 +1,606 @@ +// 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, state_history_manager, substorage}; +use ftypes::BlockId; +use serde::{de::DeserializeOwned, ser::Serialize}; +use std::cmp::{max, Ordering, Reverse}; +use std::collections::{ + btree_map::{self, Entry}, + btree_set, 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 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)) +} + +#[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 load_from(state_id: BlockId) -> Option { + load_with_key_from(METADATA_KEY, state_id) + } + + 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 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> { + 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 fn iter(&self) -> btree_set::Iter<'_, Public> { + self.0.iter() + } +} + +#[derive(Serialize, PartialEq, Eq)] +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); + // Sorted by (delegation DESC, deposit DESC, tiebreaker ASC) + let mut validators = Candidates::prepare_validators(min_deposit); + + { + 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.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 sorted_validators_view.iter_mut() + { + 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); + } + } + + 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 + } +} + +impl From> for NextValidators { + fn from(val: Vec) -> Self { + Self(val) + } +} + +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) { + 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; + } + + 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, candidate.tiebreaker)); + } + vec + }); + result.sort_unstable_by_key(|v| (Reverse(v.delegation), Reverse(v.deposit), v.tiebreaker)); + 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, + tiebreaker: Tiebreaker, + ) { + 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, + tiebreaker, + }) + }; + } + + 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) + } +} + +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/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/transactions.rs b/basic_module/staking/src/transactions.rs new file mode 100644 index 0000000000..87adbbbd89 --- /dev/null +++ b/basic_module/staking/src/transactions.rs @@ -0,0 +1,212 @@ +// 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::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 { + User(SignedTransaction), + Auto(AutoAction), +} + +pub struct SignedTransaction { + pub signature: Signature, + pub signer_public: Public, + pub tx: UserTransaction, +} + +impl SignedTransaction { + pub fn verify(&self) -> bool { + let message = self.tx.hash(); + verify(&self.signature, &message, &self.signer_public) + } +} + +#[derive(Serialize)] +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: UserAction, +} + +impl UserTransaction { + pub fn hash(&self) -> H256 { + let serialized = serde_cbor::to_vec(&self).unwrap(); + blake256(serialized) + } +} + +#[allow(dead_code)] +#[derive(Serialize)] +pub enum UserAction { + 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, + }, +} + +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 + 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(), + })] +} diff --git a/basic_module/staking/src/types.rs b/basic_module/staking/src/types.rs new file mode 100644 index 0000000000..91d02012b7 --- /dev/null +++ b/basic_module/staking/src/types.rs @@ -0,0 +1,128 @@ +// 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::{BlockNumber, Header}; +use std::fmt; +use std::ops::Add; +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, 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, tiebreaker: Tiebreaker) -> Self { + Self { + weight: delegation, + delegation, + deposit, + pubkey, + tiebreaker, + } + } + + 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, + pub tiebreaker: Tiebreaker, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct Prisoner { + pub pubkey: Public, + pub deposit: DepositQuantity, + pub custody_until: u64, + pub released_at: u64, +} + +pub enum ReleaseResult { + NotExists, + InCustody, + Released(Prisoner), +} + +#[derive(Serialize)] +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, + } + } +} 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;