diff --git a/.changelog/unreleased/features/687-remove-staking-address.md b/.changelog/unreleased/features/687-remove-staking-address.md new file mode 100644 index 0000000000..39d4def2aa --- /dev/null +++ b/.changelog/unreleased/features/687-remove-staking-address.md @@ -0,0 +1,2 @@ +- PoS: Removed staking reward addresses in preparation of auto-staked rewards + system. ([#687](https://github.com/anoma/namada/pull/687)) \ No newline at end of file diff --git a/.changelog/unreleased/features/695-validator-commission-rates.md b/.changelog/unreleased/features/695-validator-commission-rates.md new file mode 100644 index 0000000000..086227b595 --- /dev/null +++ b/.changelog/unreleased/features/695-validator-commission-rates.md @@ -0,0 +1,4 @@ +- Allow to set validator's commission rates and a limit on change of commission + rate per epoch. Commission rate can be changed via a transaction authorized + by the validator, but the limit is immutable value, set when the validator's + account is initialized. ([#695](https://github.com/anoma/namada/pull/695)) \ No newline at end of file diff --git a/.changelog/unreleased/features/707-refactor-voting-powers.md b/.changelog/unreleased/features/707-refactor-voting-powers.md new file mode 100644 index 0000000000..76c26cab67 --- /dev/null +++ b/.changelog/unreleased/features/707-refactor-voting-powers.md @@ -0,0 +1,5 @@ +- Optimize the PoS code to depend only on bonded stake, removing + the VotingPower(Delta) structs. This mitigates some previous + information loss in PoS calculations. Instead, the notion of + voting power is only relevant when communicating with Tendermint. + ([#707](https://github.com/anoma/namada/pull/707)) \ No newline at end of file diff --git a/.changelog/unreleased/features/708-update-pos-params.md b/.changelog/unreleased/features/708-update-pos-params.md new file mode 100644 index 0000000000..2941c5fc4e --- /dev/null +++ b/.changelog/unreleased/features/708-update-pos-params.md @@ -0,0 +1,4 @@ +- Update the set of parameters in the PoS system according to the + latest spec and standardizes the use of the rust_decimal crate + for parameters and calculations that require fractional numbers. + ([#708](https://github.com/anoma/namada/pull/708)) \ No newline at end of file diff --git a/.changelog/unreleased/improvements/436-remove-f64.md b/.changelog/unreleased/improvements/436-remove-f64.md new file mode 100644 index 0000000000..e55af7ee8f --- /dev/null +++ b/.changelog/unreleased/improvements/436-remove-f64.md @@ -0,0 +1,2 @@ +- Refactored token decimal formatting. + ([#436](https://github.com/anoma/namada/pull/436)) \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3d7588b5b7..02485425dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2949,6 +2949,7 @@ dependencies = [ "rand_core 0.6.4", "rayon", "rust_decimal", + "rust_decimal_macros", "serde 1.0.145", "serde_json", "sha2 0.9.9", @@ -3026,6 +3027,8 @@ dependencies = [ "rlimit", "rocksdb", "rpassword", + "rust_decimal", + "rust_decimal_macros", "serde 1.0.145", "serde_bytes", "serde_json", @@ -3085,6 +3088,8 @@ dependencies = [ "borsh", "derivative", "proptest", + "rust_decimal", + "rust_decimal_macros", "thiserror", ] @@ -3113,6 +3118,8 @@ dependencies = [ "proptest", "prost", "rand 0.8.5", + "rust_decimal", + "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -3130,6 +3137,7 @@ dependencies = [ "namada", "namada_macros", "namada_vm_env", + "rust_decimal", "sha2 0.10.6", "thiserror", ] @@ -4332,10 +4340,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" dependencies = [ "arrayvec 0.7.2", + "borsh", "num-traits 0.2.15", "serde 1.0.145", ] +[[package]] +name = "rust_decimal_macros" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4903d8db81d2321699ca8318035d6ff805c548868df435813968795a802171b2" +dependencies = [ + "quote", + "rust_decimal", +] + [[package]] name = "rustc-demangle" version = "0.1.21" diff --git a/apps/Cargo.toml b/apps/Cargo.toml index 87ad660beb..f11e1cd151 100644 --- a/apps/Cargo.toml +++ b/apps/Cargo.toml @@ -142,6 +142,8 @@ tracing-subscriber = {version = "0.3.7", features = ["env-filter"]} websocket = "0.26.2" winapi = "0.3.9" bimap = {version = "0.6.2", features = ["serde"]} +rust_decimal = "1.26.1" +rust_decimal_macros = "1.26.1" [dev-dependencies] namada = {path = "../shared", features = ["testing", "wasm-runtime"]} diff --git a/apps/src/bin/anoma-client/cli.rs b/apps/src/bin/anoma-client/cli.rs index 841e4b56ae..734946aa9d 100644 --- a/apps/src/bin/anoma-client/cli.rs +++ b/apps/src/bin/anoma-client/cli.rs @@ -55,8 +55,11 @@ pub async fn main() -> Result<()> { Sub::QueryBonds(QueryBonds(args)) => { rpc::query_bonds(ctx, args).await; } - Sub::QueryVotingPower(QueryVotingPower(args)) => { - rpc::query_voting_power(ctx, args).await; + Sub::QueryBondedStake(QueryBondedStake(args)) => { + rpc::query_bonded_stake(ctx, args).await; + } + Sub::QueryCommissionRate(QueryCommissionRate(args)) => { + rpc::query_commission_rate(ctx, args).await; } Sub::QuerySlashes(QuerySlashes(args)) => { rpc::query_slashes(ctx, args).await; diff --git a/apps/src/lib/cli.rs b/apps/src/lib/cli.rs index a3d39ed73e..8a8588f1d6 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -159,7 +159,7 @@ pub mod cmds { .subcommand(QueryBlock::def().display_order(3)) .subcommand(QueryBalance::def().display_order(3)) .subcommand(QueryBonds::def().display_order(3)) - .subcommand(QueryVotingPower::def().display_order(3)) + .subcommand(QueryBondedStake::def().display_order(3)) .subcommand(QuerySlashes::def().display_order(3)) .subcommand(QueryResult::def().display_order(3)) .subcommand(QueryRawBytes::def().display_order(3)) @@ -189,8 +189,8 @@ pub mod cmds { let query_block = Self::parse_with_ctx(matches, QueryBlock); let query_balance = Self::parse_with_ctx(matches, QueryBalance); let query_bonds = Self::parse_with_ctx(matches, QueryBonds); - let query_voting_power = - Self::parse_with_ctx(matches, QueryVotingPower); + let query_bonded_stake = + Self::parse_with_ctx(matches, QueryBondedStake); let query_slashes = Self::parse_with_ctx(matches, QuerySlashes); let query_result = Self::parse_with_ctx(matches, QueryResult); let query_raw_bytes = Self::parse_with_ctx(matches, QueryRawBytes); @@ -214,7 +214,7 @@ pub mod cmds { .or(query_block) .or(query_balance) .or(query_bonds) - .or(query_voting_power) + .or(query_bonded_stake) .or(query_slashes) .or(query_result) .or(query_raw_bytes) @@ -272,7 +272,8 @@ pub mod cmds { QueryBlock(QueryBlock), QueryBalance(QueryBalance), QueryBonds(QueryBonds), - QueryVotingPower(QueryVotingPower), + QueryBondedStake(QueryBondedStake), + QueryCommissionRate(QueryCommissionRate), QuerySlashes(QuerySlashes), QueryRawBytes(QueryRawBytes), QueryProposal(QueryProposal), @@ -840,8 +841,8 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about( - "Send a signed transaction to create a new validator and \ - its staking reward account.", + "Send a signed transaction to create a new validator \ + account.", ) .add_args::() } @@ -981,21 +982,40 @@ pub mod cmds { } #[derive(Clone, Debug)] - pub struct QueryVotingPower(pub args::QueryVotingPower); + pub struct QueryBondedStake(pub args::QueryBondedStake); - impl SubCmd for QueryVotingPower { - const CMD: &'static str = "voting-power"; + impl SubCmd for QueryBondedStake { + const CMD: &'static str = "bonded-stake"; fn parse(matches: &ArgMatches) -> Option { matches.subcommand_matches(Self::CMD).map(|matches| { - QueryVotingPower(args::QueryVotingPower::parse(matches)) + QueryBondedStake(args::QueryBondedStake::parse(matches)) }) } fn def() -> App { App::new(Self::CMD) - .about("Query PoS voting power.") - .add_args::() + .about("Query PoS bonded stake.") + .add_args::() + } + } + + #[derive(Clone, Debug)] + pub struct QueryCommissionRate(pub args::QueryCommissionRate); + + impl SubCmd for QueryCommissionRate { + const CMD: &'static str = "commission-rate"; + + fn parse(matches: &ArgMatches) -> Option { + matches.subcommand_matches(Self::CMD).map(|matches| { + QueryCommissionRate(args::QueryCommissionRate::parse(matches)) + }) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Query commission rate.") + .add_args::() } } @@ -1194,9 +1214,9 @@ pub mod cmds { fn def() -> App { App::new(Self::CMD) .about( - "Initialize genesis validator's address, staking reward \ - address, consensus key, validator account key and \ - staking rewards key and use it in the ledger's node.", + "Initialize genesis validator's address, consensus key \ + and validator account key and use it in the ledger's \ + node.", ) .add_args::() } @@ -1217,6 +1237,7 @@ pub mod args { use namada::types::storage::{self, Epoch}; use namada::types::token; use namada::types::transaction::GasLimit; + use rust_decimal::Decimal; use super::context::{WalletAddress, WalletKeypair, WalletPublicKey}; use super::utils::*; @@ -1245,6 +1266,7 @@ pub mod args { const CHAIN_ID_PREFIX: Arg = arg("chain-prefix"); const CODE_PATH: Arg = arg("code-path"); const CODE_PATH_OPT: ArgOpt = CODE_PATH.opt(); + const COMMISSION_RATE: Arg = arg("commission-rate"); const CONSENSUS_TIMEOUT_COMMIT: ArgDefault = arg_default( "consensus-timeout-commit", DefaultFn(|| Timeout::from_str("1s").unwrap()), @@ -1276,6 +1298,8 @@ pub mod args { const LEDGER_ADDRESS: Arg = arg("ledger-address"); const LOCALHOST: ArgFlag = flag("localhost"); + const MAX_COMMISSION_RATE_CHANGE: Arg = + arg("max-commission-rate-change"); const MODE: ArgOpt = arg_opt("mode"); const NET_ADDRESS: Arg = arg("net-address"); const OWNER: ArgOpt = arg_opt("owner"); @@ -1289,8 +1313,6 @@ pub mod args { const RAW_ADDRESS: Arg
= arg("address"); const RAW_ADDRESS_OPT: ArgOpt
= RAW_ADDRESS.opt(); const RAW_PUBLIC_KEY_OPT: ArgOpt = arg_opt("public-key"); - const REWARDS_CODE_PATH: ArgOpt = arg_opt("rewards-code-path"); - const REWARDS_KEY: ArgOpt = arg_opt("rewards-key"); const SCHEME: ArgDefault = arg_default("scheme", DefaultFn(|| SchemeType::Ed25519)); const SIGNER: ArgOpt = arg_opt("signer"); @@ -1528,10 +1550,10 @@ pub mod args { pub scheme: SchemeType, pub account_key: Option, pub consensus_key: Option, - pub rewards_account_key: Option, pub protocol_key: Option, + pub commission_rate: Decimal, + pub max_commission_rate_change: Decimal, pub validator_vp_code_path: Option, - pub rewards_vp_code_path: Option, pub unsafe_dont_encrypt: bool, } @@ -1542,10 +1564,11 @@ pub mod args { let scheme = SCHEME.parse(matches); let account_key = VALIDATOR_ACCOUNT_KEY.parse(matches); let consensus_key = VALIDATOR_CONSENSUS_KEY.parse(matches); - let rewards_account_key = REWARDS_KEY.parse(matches); let protocol_key = PROTOCOL_KEY.parse(matches); + let commission_rate = COMMISSION_RATE.parse(matches); + let max_commission_rate_change = + MAX_COMMISSION_RATE_CHANGE.parse(matches); let validator_vp_code_path = VALIDATOR_CODE_PATH.parse(matches); - let rewards_vp_code_path = REWARDS_CODE_PATH.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); Self { tx, @@ -1553,10 +1576,10 @@ pub mod args { scheme, account_key, consensus_key, - rewards_account_key, protocol_key, + commission_rate, + max_commission_rate_change, validator_vp_code_path, - rewards_vp_code_path, unsafe_dont_encrypt, } } @@ -1578,24 +1601,26 @@ pub mod args { "A consensus key for the validator account. A new one \ will be generated if none given.", )) - .arg(REWARDS_KEY.def().about( - "A public key for the staking reward account. A new one \ - will be generated if none given.", - )) .arg(PROTOCOL_KEY.def().about( "A public key for signing protocol transactions. A new \ one will be generated if none given.", )) + .arg(COMMISSION_RATE.def().about( + "The commission rate charged by the validator for \ + delegation rewards. Expressed as a decimal between 0 and \ + 1. This is a required parameter.", + )) + .arg(MAX_COMMISSION_RATE_CHANGE.def().about( + "The maximum change per epoch in the commission rate \ + charged by the validator for delegation rewards. \ + Expressed as a decimal between 0 and 1. This is a \ + required parameter.", + )) .arg(VALIDATOR_CODE_PATH.def().about( "The path to the validity predicate WASM code to be used \ for the validator account. Uses the default validator VP \ if none specified.", )) - .arg(REWARDS_CODE_PATH.def().about( - "The path to the validity predicate WASM code to be used \ - for the staking reward account. Uses the default staking \ - reward VP if none specified.", - )) .arg(UNSAFE_DONT_ENCRYPT.def().about( "UNSAFE: Do not encrypt the generated keypairs. Do not \ use this for keys used in a live network.", @@ -2035,18 +2060,18 @@ pub mod args { } } - /// Query PoS voting power + /// Query PoS bonded stake #[derive(Clone, Debug)] - pub struct QueryVotingPower { + pub struct QueryBondedStake { /// Common query args pub query: Query, /// Address of a validator pub validator: Option, - /// Epoch in which to find voting power + /// Epoch in which to find bonded stake pub epoch: Option, } - impl Args for QueryVotingPower { + impl Args for QueryBondedStake { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); let validator = VALIDATOR_OPT.parse(matches); @@ -2061,7 +2086,78 @@ pub mod args { fn def(app: App) -> App { app.add_args::() .arg(VALIDATOR_OPT.def().about( - "The validator's address whose voting power to query.", + "The validator's address whose bonded stake to query.", + )) + .arg(EPOCH.def().about( + "The epoch at which to query (last committed, if not \ + specified).", + )) + } + } + + #[derive(Clone, Debug)] + /// Commission rate change args + pub struct TxCommissionRateChange { + /// Common tx arguments + pub tx: Tx, + /// Validator address (should be self) + pub validator: WalletAddress, + /// Value to which the tx changes the commission rate + pub rate: Decimal, + } + + impl Args for TxCommissionRateChange { + fn parse(matches: &ArgMatches) -> Self { + let tx = Tx::parse(matches); + let validator = VALIDATOR.parse(matches); + let rate = COMMISSION_RATE.parse(matches); + Self { + tx, + validator, + rate, + } + } + + fn def(app: App) -> App { + app.add_args::() + .arg(VALIDATOR.def().about( + "The validator's address whose commission rate to change.", + )) + .arg( + COMMISSION_RATE + .def() + .about("The desired new commission rate."), + ) + } + } + + /// Query PoS commission rate + #[derive(Clone, Debug)] + pub struct QueryCommissionRate { + /// Common query args + pub query: Query, + /// Address of a validator + pub validator: WalletAddress, + /// Epoch in which to find commission rate + pub epoch: Option, + } + + impl Args for QueryCommissionRate { + fn parse(matches: &ArgMatches) -> Self { + let query = Query::parse(matches); + let validator = VALIDATOR.parse(matches); + let epoch = EPOCH.parse(matches); + Self { + query, + validator, + epoch, + } + } + + fn def(app: App) -> App { + app.add_args::() + .arg(VALIDATOR.def().about( + "The validator's address whose commission rate to query.", )) .arg(EPOCH.def().about( "The epoch at which to query (last committed, if not \ @@ -2569,6 +2665,8 @@ pub mod args { #[derive(Clone, Debug)] pub struct InitGenesisValidator { pub alias: String, + pub commission_rate: Decimal, + pub max_commission_rate_change: Decimal, pub net_address: SocketAddr, pub unsafe_dont_encrypt: bool, pub key_scheme: SchemeType, @@ -2577,6 +2675,9 @@ pub mod args { impl Args for InitGenesisValidator { fn parse(matches: &ArgMatches) -> Self { let alias = ALIAS.parse(matches); + let commission_rate = COMMISSION_RATE.parse(matches); + let max_commission_rate_change = + MAX_COMMISSION_RATE_CHANGE.parse(matches); let net_address = NET_ADDRESS.parse(matches); let unsafe_dont_encrypt = UNSAFE_DONT_ENCRYPT.parse(matches); let key_scheme = SCHEME.parse(matches); @@ -2585,6 +2686,8 @@ pub mod args { net_address, unsafe_dont_encrypt, key_scheme, + commission_rate, + max_commission_rate_change, } } @@ -2595,6 +2698,15 @@ pub mod args { Anoma uses port `26656` for P2P connections by default, \ but you can configure a different value.", )) + .arg(COMMISSION_RATE.def().about( + "The commission rate charged by the validator for \ + delegation rewards. This is a required parameter.", + )) + .arg(MAX_COMMISSION_RATE_CHANGE.def().about( + "The maximum change per epoch in the commission rate \ + charged by the validator for delegation rewards. This is \ + a required parameter.", + )) .arg(UNSAFE_DONT_ENCRYPT.def().about( "UNSAFE: Do not encrypt the generated keypairs. Do not \ use this for keys used in a live network.", diff --git a/apps/src/lib/client/rpc.rs b/apps/src/lib/client/rpc.rs index ce6a542897..5152d41fdc 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -18,7 +18,7 @@ use namada::ledger::governance::storage as gov_storage; use namada::ledger::governance::utils::Votes; use namada::ledger::parameters::{storage as param_storage, EpochDuration}; use namada::ledger::pos::types::{ - Epoch as PosEpoch, VotingPower, WeightedValidator, + decimal_mult_u64, Epoch as PosEpoch, WeightedValidator, }; use namada::ledger::pos::{ self, is_validator_slashes_key, BondId, Bonds, PosParams, Slash, Unbonds, @@ -33,6 +33,7 @@ use namada::types::key::*; use namada::types::storage::{Epoch, Key, KeySeg, PrefixValue}; use namada::types::token::{balance_key, Amount}; use namada::types::{address, storage, token}; +use rust_decimal::prelude::Decimal; use crate::cli::{self, args, Context}; use crate::client::tendermint_rpc_types::TxResponse; @@ -469,7 +470,7 @@ pub async fn query_protocol_parameters( println!("Governance Parameters\n {:4}", gov_parameters); println!("Protocol parameters"); - let key = param_storage::get_epoch_storage_key(); + let key = param_storage::get_epoch_duration_storage_key(); let epoch_duration = query_storage_value::(&client, &key) .await .expect("Parameter should be definied."); @@ -514,12 +515,12 @@ pub async fn query_protocol_parameters( "", pos_params.block_vote_reward ); println!( - "{:4}Duplicate vote slash rate: {}", - "", pos_params.duplicate_vote_slash_rate + "{:4}Duplicate vote minimum slash rate: {}", + "", pos_params.duplicate_vote_min_slash_rate ); println!( - "{:4}Light client attack slash rate: {}", - "", pos_params.light_client_attack_slash_rate + "{:4}Light client attack minimum slash rate: {}", + "", pos_params.light_client_attack_min_slash_rate ); println!( "{:4}Max. validator slots: {}", @@ -527,7 +528,7 @@ pub async fn query_protocol_parameters( ); println!("{:4}Pipeline length: {}", "", pos_params.pipeline_len); println!("{:4}Unbonding length: {}", "", pos_params.unbonding_len); - println!("{:4}Votes per token: {}", "", pos_params.votes_per_token); + println!("{:4}Votes per token: {}", "", pos_params.tm_votes_per_token); } /// Query PoS bond(s) @@ -877,8 +878,8 @@ pub async fn query_bonds(ctx: Context, args: args::QueryBonds) { } } -/// Query PoS voting power -pub async fn query_voting_power(ctx: Context, args: args::QueryVotingPower) { +/// Query PoS bonded stake +pub async fn query_bonded_stake(ctx: Context, args: args::QueryBondedStake) { let epoch = match args.epoch { Some(epoch) => epoch, None => query_epoch(args.query.clone()).await, @@ -894,26 +895,26 @@ pub async fn query_voting_power(ctx: Context, args: args::QueryVotingPower) { let validator_set = validator_sets .get(epoch) .expect("Validator set should be always set in the current epoch"); + match args.validator { Some(validator) => { let validator = ctx.get(&validator); - // Find voting power for the given validator - let voting_power_key = pos::validator_voting_power_key(&validator); - let voting_powers = - query_storage_value::( - &client, - &voting_power_key, - ) - .await; - match voting_powers.and_then(|data| data.get(epoch)) { - Some(voting_power_delta) => { - let voting_power: VotingPower = - voting_power_delta.try_into().expect( - "The sum voting power deltas shouldn't be negative", - ); + // Find bonded stake for the given validator + let validator_deltas_key = pos::validator_deltas_key(&validator); + let validator_deltas = query_storage_value::( + &client, + &validator_deltas_key, + ) + .await; + match validator_deltas.and_then(|data| data.get(epoch)) { + Some(val_stake) => { + let bonded_stake: u64 = val_stake.try_into().expect( + "The sum of the bonded stake deltas shouldn't be \ + negative", + ); let weighted = WeightedValidator { address: validator.clone(), - voting_power, + bonded_stake, }; let is_active = validator_set.active.contains(&weighted); if !is_active { @@ -922,14 +923,14 @@ pub async fn query_voting_power(ctx: Context, args: args::QueryVotingPower) { ); } println!( - "Validator {} is {}, voting power: {}", + "Validator {} is {}, bonded stake: {}", validator.encode(), if is_active { "active" } else { "inactive" }, - voting_power + bonded_stake, ) } None => { - println!("No voting power found for {}", validator.encode()) + println!("No bonded stake found for {}", validator.encode()) } } } @@ -944,7 +945,7 @@ pub async fn query_voting_power(ctx: Context, args: args::QueryVotingPower) { w, " {}: {}", active.address.encode(), - active.voting_power + active.bonded_stake ) .unwrap(); } @@ -955,24 +956,82 @@ pub async fn query_voting_power(ctx: Context, args: args::QueryVotingPower) { w, " {}: {}", inactive.address.encode(), - inactive.voting_power + inactive.bonded_stake ) .unwrap(); } } } } - let total_voting_power_key = pos::total_voting_power_key(); - let total_voting_powers = query_storage_value::( - &client, - &total_voting_power_key, - ) - .await - .expect("Total voting power should always be set"); - let total_voting_power = total_voting_powers + let total_deltas_key = pos::total_deltas_key(); + let total_deltas = + query_storage_value::(&client, &total_deltas_key) + .await + .expect("Total bonded stake should always be set"); + let total_bonded_stake = total_deltas .get(epoch) - .expect("Total voting power should be always set in the current epoch"); - println!("Total voting power: {}", total_voting_power); + .expect("Total bonded stake should be always set in the current epoch"); + let total_bonded_stake: u64 = total_bonded_stake + .try_into() + .expect("total_bonded_stake should be a positive value"); + + println!("Total bonded stake: {}", total_bonded_stake); +} + +/// Query PoS validator's commission rate +pub async fn query_commission_rate( + ctx: Context, + args: args::QueryCommissionRate, +) { + let epoch = match args.epoch { + Some(epoch) => epoch, + None => query_epoch(args.query.clone()).await, + }; + let client = HttpClient::new(args.query.ledger_address.clone()).unwrap(); + let validator = ctx.get(&args.validator); + let is_validator = + is_validator(&validator, args.query.ledger_address).await; + + if is_validator { + let validator_commission_key = + pos::validator_commission_rate_key(&validator); + let validator_max_commission_change_key = + pos::validator_max_commission_rate_change_key(&validator); + let commission_rates = query_storage_value::( + &client, + &validator_commission_key, + ) + .await; + let max_rate_change = query_storage_value::( + &client, + &validator_max_commission_change_key, + ) + .await; + let max_rate_change = + max_rate_change.expect("No max rate change found"); + let commission_rates = + commission_rates.expect("No commission rate found "); + match commission_rates.get(epoch) { + Some(rate) => { + println!( + "Validator {} commission rate: {}, max change per epoch: \ + {}", + validator.encode(), + *rate, + max_rate_change, + ) + } + None => { + println!( + "No commission rate found for {} in epoch {}", + validator.encode(), + epoch + ) + } + } + } else { + println!("Cannot find validator with address {}", validator); + } } /// Query PoS slashes @@ -1145,7 +1204,8 @@ fn apply_slashes( .unwrap(); } let raw_delta: u64 = delta.into(); - let current_slashed = token::Amount::from(slash.rate * raw_delta); + let current_slashed = + token::Amount::from(decimal_mult_u64(slash.rate, raw_delta)); slashed += current_slashed; delta -= current_slashed; } @@ -1836,17 +1896,16 @@ async fn get_validator_stake( epoch: Epoch, validator: &Address, ) -> VotePower { - let total_voting_power_key = pos::validator_total_deltas_key(validator); - let total_voting_power = query_storage_value::( + let validator_deltas_key = pos::validator_deltas_key(validator); + let validator_deltas = query_storage_value::( client, - &total_voting_power_key, + &validator_deltas_key, ) .await .expect("Total deltas should be defined"); - let epoched_total_voting_power = total_voting_power.get(epoch); + let validator_stake = validator_deltas.get(epoch); - VotePower::try_from(epoched_total_voting_power.unwrap_or_default()) - .unwrap_or_default() + VotePower::try_from(validator_stake.unwrap_or_default()).unwrap_or_default() } pub async fn get_delegators_delegation( diff --git a/apps/src/lib/client/tx.rs b/apps/src/lib/client/tx.rs index 0d369ba6b7..56c53ecd5c 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -9,7 +9,7 @@ use async_std::io::{self}; use borsh::BorshSerialize; use itertools::Either::*; use namada::ledger::governance::storage as gov_storage; -use namada::ledger::pos::{BondId, Bonds, Unbonds}; +use namada::ledger::pos::{BondId, Bonds, CommissionRates, Unbonds}; use namada::proto::Tx; use namada::types::address::{nam, Address}; use namada::types::governance::{ @@ -23,6 +23,7 @@ use namada::types::transaction::governance::{ use namada::types::transaction::{pos, InitAccount, InitValidator, UpdateVp}; use namada::types::{address, storage, token}; use namada::{ledger, vm}; +use rust_decimal::Decimal; use super::rpc; use crate::cli::context::WalletAddress; @@ -51,6 +52,7 @@ const VP_USER_WASM: &str = "vp_user.wasm"; const TX_BOND_WASM: &str = "tx_bond.wasm"; const TX_UNBOND_WASM: &str = "tx_unbond.wasm"; const TX_WITHDRAW_WASM: &str = "tx_withdraw.wasm"; +const TX_CHANGE_COMMISSION_WASM: &str = "tx_change_validator_commission.wasm"; const ENV_VAR_ANOMA_TENDERMINT_WEBSOCKET_TIMEOUT: &str = "ANOMA_TENDERMINT_WEBSOCKET_TIMEOUT"; @@ -154,10 +156,10 @@ pub async fn submit_init_validator( scheme, account_key, consensus_key, - rewards_account_key, protocol_key, + commission_rate, + max_commission_rate_change, validator_vp_code_path, - rewards_vp_code_path, unsafe_dont_encrypt, }: args::TxInitValidator, ) { @@ -169,7 +171,6 @@ pub async fn submit_init_validator( let validator_key_alias = format!("{}-key", alias); let consensus_key_alias = format!("{}-consensus-key", alias); - let rewards_key_alias = format!("{}-rewards-key", alias); let account_key = ctx.get_opt_cached(&account_key).unwrap_or_else(|| { println!("Generating validator account key..."); ctx.wallet @@ -203,18 +204,6 @@ pub async fn submit_init_validator( .1 }); - let rewards_account_key = - ctx.get_opt_cached(&rewards_account_key).unwrap_or_else(|| { - println!("Generating staking reward account key..."); - ctx.wallet - .gen_key( - scheme, - Some(rewards_key_alias.clone()), - unsafe_dont_encrypt, - ) - .1 - .ref_to() - }); let protocol_key = ctx.get_opt_cached(&protocol_key); if protocol_key.is_none() { @@ -235,24 +224,32 @@ pub async fn submit_init_validator( let validator_vp_code = validator_vp_code_path .map(|path| ctx.read_wasm(path)) .unwrap_or_else(|| ctx.read_wasm(VP_USER_WASM)); - // Validate the validator VP code - if let Err(err) = vm::validate_untrusted_wasm(&validator_vp_code) { + + // Validate the commission rate data + if commission_rate > Decimal::ONE || commission_rate < Decimal::ZERO { eprintln!( - "Validator validity predicate code validation failed with {}", - err + "The validator commission rate must not exceed 1.0 or 100%, and \ + it must be 0 or positive" ); if !tx_args.force { safe_exit(1) } } - let rewards_vp_code = rewards_vp_code_path - .map(|path| ctx.read_wasm(path)) - .unwrap_or_else(|| ctx.read_wasm(VP_USER_WASM)); - // Validate the rewards VP code - if let Err(err) = vm::validate_untrusted_wasm(&rewards_vp_code) { + if max_commission_rate_change > Decimal::ONE + || max_commission_rate_change < Decimal::ZERO + { eprintln!( - "Staking reward account validity predicate code validation failed \ - with {}", + "The validator maximum change in commission rate per epoch must \ + not exceed 1.0 or 100%" + ); + if !tx_args.force { + safe_exit(1) + } + } + // Validate the validator VP code + if let Err(err) = vm::validate_untrusted_wasm(&validator_vp_code) { + eprintln!( + "Validator validity predicate code validation failed with {}", err ); if !tx_args.force { @@ -264,32 +261,21 @@ pub async fn submit_init_validator( let data = InitValidator { account_key, consensus_key: consensus_key.ref_to(), - rewards_account_key, protocol_key, dkg_key, + commission_rate, + max_commission_rate_change, validator_vp_code, - rewards_vp_code, }; let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); let tx = Tx::new(tx_code, Some(data)); let (mut ctx, initialized_accounts) = process_tx(ctx, &tx_args, tx, Some(&source)).await; if !tx_args.dry_run { - let (validator_address_alias, validator_address, rewards_address_alias) = + let (validator_address_alias, validator_address) = match &initialized_accounts[..] { - // There should be 2 accounts, one for the validator itself, one - // for its staking reward address. - [account_1, account_2] => { - // We need to find out which address is which - let (validator_address, rewards_address) = - if rpc::is_validator(account_1, tx_args.ledger_address) - .await - { - (account_1, account_2) - } else { - (account_2, account_1) - }; - + // There should be 1 account for the validator itself + [validator_address] => { let validator_address_alias = match tx_args .initialized_account_alias { @@ -324,23 +310,7 @@ pub async fn submit_init_validator( validator_address.encode() ); } - let rewards_address_alias = - format!("{}-rewards", validator_address_alias); - if let Some(new_alias) = ctx.wallet.add_address( - rewards_address_alias.clone(), - rewards_address.clone(), - ) { - println!( - "Added alias {} for address {}.", - new_alias, - rewards_address.encode() - ); - } - ( - validator_address_alias, - validator_address.clone(), - rewards_address_alias, - ) + (validator_address_alias, validator_address.clone()) } _ => { eprintln!("Expected two accounts to be created"); @@ -361,10 +331,8 @@ pub async fn submit_init_validator( "The validator's addresses and keys were stored in the wallet:" ); println!(" Validator address \"{}\"", validator_address_alias); - println!(" Staking reward address \"{}\"", rewards_address_alias); println!(" Validator account key \"{}\"", validator_key_alias); println!(" Consensus key \"{}\"", consensus_key_alias); - println!(" Staking reward key \"{}\"", rewards_key_alias); println!( "The ledger node has been setup to use this validator's address \ and consensus key." @@ -1008,6 +976,82 @@ pub async fn submit_withdraw(ctx: Context, args: args::Withdraw) { process_tx(ctx, &args.tx, tx, Some(default_signer)).await; } +pub async fn submit_validator_commission_change( + ctx: Context, + args: args::TxCommissionRateChange, +) { + let epoch = rpc::query_epoch(args::Query { + ledger_address: args.tx.ledger_address.clone(), + }) + .await; + + let tx_code = ctx.read_wasm(TX_CHANGE_COMMISSION_WASM); + let client = HttpClient::new(args.tx.ledger_address.clone()).unwrap(); + + let validator = ctx.get(&args.validator); + if rpc::is_validator(&validator, args.tx.ledger_address.clone()).await { + if args.rate < Decimal::ZERO || args.rate > Decimal::ONE { + eprintln!("Invalid new commission rate, received {}", args.rate); + if !args.tx.force { + safe_exit(1) + } + } + + let commission_rate_key = + ledger::pos::validator_commission_rate_key(&validator); + let max_commission_rate_change_key = + ledger::pos::validator_max_commission_rate_change_key(&validator); + let commission_rates = rpc::query_storage_value::( + &client, + &commission_rate_key, + ) + .await; + let max_change = rpc::query_storage_value::( + &client, + &max_commission_rate_change_key, + ) + .await; + + match (commission_rates, max_change) { + (Some(rates), Some(max_change)) => { + // Assuming that pipeline length = 2 + let rate_next_epoch = rates.get(epoch + 1).unwrap(); + if (args.rate - rate_next_epoch).abs() > max_change { + eprintln!( + "New rate is too large of a change with respect to \ + the predecessor epoch in which the rate will take \ + effect." + ); + if !args.tx.force { + safe_exit(1) + } + } + } + _ => { + eprintln!("Error retrieving from storage"); + if !args.tx.force { + safe_exit(1) + } + } + } + } else { + eprintln!("The given address {validator} is not a validator."); + if !args.tx.force { + safe_exit(1) + } + } + + let data = pos::CommissionChange { + validator: ctx.get(&args.validator), + new_rate: args.rate, + }; + let data = data.try_to_vec().expect("Encoding tx data shouldn't fail"); + + let tx = Tx::new(tx_code, Some(data)); + let default_signer = &args.validator; + process_tx(ctx, &args.tx, tx, Some(default_signer)).await; +} + /// Submit transaction and wait for result. Returns a list of addresses /// initialized in the transaction if any. In dry run, this is always empty. async fn process_tx( diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 8848726792..5f6a81897b 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -16,6 +16,7 @@ use namada::types::key::*; use prost::bytes::Bytes; use rand::prelude::ThreadRng; use rand::thread_rng; +use rust_decimal::Decimal; use serde_json::json; use sha2::{Digest, Sha256}; @@ -475,10 +476,7 @@ pub fn init_network( // Generate account and reward addresses let address = address::gen_established_address("validator account"); - let reward_address = - address::gen_established_address("validator reward account"); config.address = Some(address.to_string()); - config.staking_reward_address = Some(reward_address.to_string()); // Generate the consensus, account and reward keys, unless they're // pre-defined. @@ -518,24 +516,6 @@ pub fn init_network( keypair.ref_to() }); - let staking_reward_pk = try_parse_public_key( - format!("validator {name} staking reward key"), - &config.staking_reward_public_key, - ) - .unwrap_or_else(|| { - let alias = format!("{}-reward-key", name); - println!( - "Generating validator {} staking reward account key...", - name - ); - let (_alias, keypair) = wallet.gen_key( - SchemeType::Ed25519, - Some(alias), - unsafe_dont_encrypt, - ); - keypair.ref_to() - }); - let protocol_pk = try_parse_public_key( format!("validator {name} protocol key"), &config.protocol_public_key, @@ -583,8 +563,6 @@ pub fn init_network( Some(genesis_config::HexString(consensus_pk.to_string())); config.account_public_key = Some(genesis_config::HexString(account_pk.to_string())); - config.staking_reward_public_key = - Some(genesis_config::HexString(staking_reward_pk.to_string())); config.protocol_public_key = Some(genesis_config::HexString(protocol_pk.to_string())); @@ -593,7 +571,6 @@ pub fn init_network( // Write keypairs to wallet wallet.add_address(name.clone(), address); - wallet.add_address(format!("{}-reward", &name), reward_address); wallet.save().unwrap(); }); @@ -898,18 +875,36 @@ fn init_established_account( } } -/// Initialize genesis validator's address, staking reward address, -/// consensus key, validator account key and staking rewards key and use -/// it in the ledger's node. +/// Initialize genesis validator's address, consensus key and validator account +/// key and use it in the ledger's node. pub fn init_genesis_validator( global_args: args::Global, args::InitGenesisValidator { alias, + commission_rate, + max_commission_rate_change, net_address, unsafe_dont_encrypt, key_scheme, }: args::InitGenesisValidator, ) { + // Validate the commission rate data + if commission_rate > Decimal::ONE || commission_rate < Decimal::ZERO { + eprintln!( + "The validator commission rate must not exceed 1.0 or 100%, and \ + it must be 0 or positive" + ); + cli::safe_exit(1) + } + if max_commission_rate_change > Decimal::ONE + || max_commission_rate_change < Decimal::ZERO + { + eprintln!( + "The validator maximum change in commission rate per epoch must \ + not exceed 1.0 or 100%" + ); + cli::safe_exit(1) + } let pre_genesis_dir = validator_pre_genesis_dir(&global_args.base_dir, &alias); println!("Generating validator keys..."); @@ -940,9 +935,6 @@ pub fn init_genesis_validator( account_public_key: Some(HexString( pre_genesis.account_key.ref_to().to_string(), )), - staking_reward_public_key: Some(HexString( - pre_genesis.rewards_key.ref_to().to_string(), - )), protocol_public_key: Some(HexString( pre_genesis .store @@ -961,6 +953,8 @@ pub fn init_genesis_validator( .public() .to_string(), )), + commission_rate: Some(commission_rate), + max_commission_rate_change: Some(max_commission_rate_change), tendermint_node_key: Some(HexString( pre_genesis.tendermint_node_key.ref_to().to_string(), )), diff --git a/apps/src/lib/config/genesis.rs b/apps/src/lib/config/genesis.rs index 9425e3b019..7a062eff79 100644 --- a/apps/src/lib/config/genesis.rs +++ b/apps/src/lib/config/genesis.rs @@ -29,13 +29,13 @@ pub mod genesis_config { use eyre::Context; use namada::ledger::governance::parameters::GovParams; use namada::ledger::parameters::{EpochDuration, Parameters}; - use namada::ledger::pos::types::BasisPoints; use namada::ledger::pos::{GenesisValidator, PosParams}; use namada::types::address::Address; use namada::types::key::dkg_session_keys::DkgPublicKey; use namada::types::key::*; use namada::types::time::Rfc3339String; use namada::types::{storage, token}; + use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -159,8 +159,6 @@ pub mod genesis_config { pub consensus_public_key: Option, // Public key for validator account. (default: generate) pub account_public_key: Option, - // Public key for staking reward account. (default: generate) - pub staking_reward_public_key: Option, // Public protocol signing key for validator account. (default: // generate) pub protocol_public_key: Option, @@ -168,18 +166,19 @@ pub mod genesis_config { pub dkg_public_key: Option, // Validator address (default: generate). pub address: Option, - // Staking reward account address (default: generate). - pub staking_reward_address: Option, // Total number of tokens held at genesis. // XXX: u64 doesn't work with toml-rs! pub tokens: Option, // Unstaked balance at genesis. // XXX: u64 doesn't work with toml-rs! pub non_staked_balance: Option, + /// Commission rate charged on rewards for delegators (bounded inside + /// 0-1) + pub commission_rate: Option, + /// Maximum change in commission rate permitted per epoch + pub max_commission_rate_change: Option, // Filename of validator VP. (default: default validator VP) pub validator_vp: Option, - // Filename of staking reward account VP. (default: user VP) - pub staking_reward_vp: Option, // IP:port of the validator. (used in generation only) pub net_address: Option, /// Tendermint node key is used to derive Tendermint node ID for node @@ -221,9 +220,6 @@ pub mod genesis_config { // Minimum number of blocks per epoch. // XXX: u64 doesn't work with toml-rs! pub min_num_of_blocks: u64, - // Minimum duration of an epoch (in seconds). - // TODO: this is i64 because datetime wants it - pub min_duration: i64, // Maximum duration per block (in seconds). // TODO: this is i64 because datetime wants it pub max_expected_time_per_block: i64, @@ -233,6 +229,12 @@ pub mod genesis_config { // Hashes of whitelisted txs array. `None` value or an empty array // disables whitelisting. pub tx_whitelist: Option>, + /// Expected number of epochs per year + pub epochs_per_year: u64, + /// PoS gain p + pub pos_gain_p: Decimal, + /// PoS gain d + pub pos_gain_d: Decimal, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -246,23 +248,28 @@ pub mod genesis_config { // Unbonding length (in epochs). // XXX: u64 doesn't work with toml-rs! pub unbonding_len: u64, - // Votes per token (in basis points). + // Votes per token. // XXX: u64 doesn't work with toml-rs! - pub votes_per_token: u64, + pub tm_votes_per_token: Decimal, // Reward for proposing a block. // XXX: u64 doesn't work with toml-rs! - pub block_proposer_reward: u64, + pub block_proposer_reward: Decimal, // Reward for voting on a block. // XXX: u64 doesn't work with toml-rs! - pub block_vote_reward: u64, + pub block_vote_reward: Decimal, + // Maximum staking APY + // XXX: u64 doesn't work with toml-rs! + pub max_inflation_rate: Decimal, + // Target ratio of staked NAM tokens to total NAM tokens + pub target_staked_ratio: Decimal, // Portion of a validator's stake that should be slashed on a - // duplicate vote (in basis points). + // duplicate vote. // XXX: u64 doesn't work with toml-rs! - pub duplicate_vote_slash_rate: u64, + pub duplicate_vote_min_slash_rate: Decimal, // Portion of a validator's stake that should be slashed on a - // light client attack (in basis points). + // light client attack. // XXX: u64 doesn't work with toml-rs! - pub light_client_attack_slash_rate: u64, + pub light_client_attack_min_slash_rate: Decimal, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -277,17 +284,11 @@ pub mod genesis_config { ) -> Validator { let validator_vp_name = config.validator_vp.as_ref().unwrap(); let validator_vp_config = wasm.get(validator_vp_name).unwrap(); - let reward_vp_name = config.staking_reward_vp.as_ref().unwrap(); - let reward_vp_config = wasm.get(reward_vp_name).unwrap(); Validator { pos_data: GenesisValidator { address: Address::decode(&config.address.as_ref().unwrap()) .unwrap(), - staking_reward_address: Address::decode( - &config.staking_reward_address.as_ref().unwrap(), - ) - .unwrap(), tokens: token::Amount::whole(config.tokens.unwrap_or_default()), consensus_key: config .consensus_public_key @@ -295,12 +296,29 @@ pub mod genesis_config { .unwrap() .to_public_key() .unwrap(), - staking_reward_key: config - .staking_reward_public_key - .as_ref() - .unwrap() - .to_public_key() - .unwrap(), + commission_rate: config + .commission_rate + .and_then(|rate| { + if rate >= Decimal::ZERO && rate <= Decimal::ONE { + Some(rate) + } else { + None + } + }) + .expect("Commission rate must be between 0.0 and 1.0"), + max_commission_rate_change: config + .max_commission_rate_change + .and_then(|rate| { + if rate >= Decimal::ZERO && rate <= Decimal::ONE { + Some(rate) + } else { + None + } + }) + .expect( + "Max commission rate change must be between 0.0 and \ + 1.0", + ), }, account_key: config .account_public_key @@ -330,16 +348,6 @@ pub mod genesis_config { .unwrap() .to_sha256_bytes() .unwrap(), - reward_vp_code_path: reward_vp_config.filename.to_owned(), - reward_vp_sha256: reward_vp_config - .sha256 - .clone() - .unwrap_or_else(|| { - eprintln!("Unknown validator VP WASM sha256"); - cli::safe_exit(1); - }) - .to_sha256_bytes() - .unwrap(), } } @@ -512,11 +520,13 @@ pub mod genesis_config { }) .collect(); + let min_duration: i64 = + 60 * 60 * 24 * 365 / (config.parameters.epochs_per_year as i64); let parameters = Parameters { epoch_duration: EpochDuration { min_num_of_blocks: config.parameters.min_num_of_blocks, min_duration: namada::types::time::Duration::seconds( - config.parameters.min_duration, + min_duration, ) .into(), }, @@ -527,6 +537,11 @@ pub mod genesis_config { .into(), vp_whitelist: config.parameters.vp_whitelist.unwrap_or_default(), tx_whitelist: config.parameters.tx_whitelist.unwrap_or_default(), + epochs_per_year: config.parameters.epochs_per_year, + pos_gain_p: config.parameters.pos_gain_p, + pos_gain_d: config.parameters.pos_gain_d, + staked_ratio: Decimal::ZERO, + pos_inflation_amount: 0, }; let gov_params = GovParams { @@ -546,17 +561,17 @@ pub mod genesis_config { max_validator_slots: config.pos_params.max_validator_slots, pipeline_len: config.pos_params.pipeline_len, unbonding_len: config.pos_params.unbonding_len, - votes_per_token: BasisPoints::new( - config.pos_params.votes_per_token, - ), + tm_votes_per_token: config.pos_params.tm_votes_per_token, block_proposer_reward: config.pos_params.block_proposer_reward, block_vote_reward: config.pos_params.block_vote_reward, - duplicate_vote_slash_rate: BasisPoints::new( - config.pos_params.duplicate_vote_slash_rate, - ), - light_client_attack_slash_rate: BasisPoints::new( - config.pos_params.light_client_attack_slash_rate, - ), + max_inflation_rate: config.pos_params.max_inflation_rate, + target_staked_ratio: config.pos_params.target_staked_ratio, + duplicate_vote_min_slash_rate: config + .pos_params + .duplicate_vote_min_slash_rate, + light_client_attack_min_slash_rate: config + .pos_params + .light_client_attack_min_slash_rate, }; let mut genesis = Genesis { @@ -651,17 +666,13 @@ pub struct Validator { pub protocol_key: common::PublicKey, /// The public DKG session key used during the DKG protocol pub dkg_public_key: DkgPublicKey, - /// These tokens are no staked and hence do not contribute to the + /// These tokens are not staked and hence do not contribute to the /// validator's voting power pub non_staked_balance: token::Amount, /// Validity predicate code WASM pub validator_vp_code_path: String, /// Expected SHA-256 hash of the validator VP pub validator_vp_sha256: [u8; 32], - /// Staking reward account code WASM - pub reward_vp_code_path: String, - /// Expected SHA-256 hash of the staking reward VP - pub reward_vp_sha256: [u8; 32], } #[derive( @@ -725,6 +736,7 @@ pub fn genesis(base_dir: impl AsRef, chain_id: &ChainId) -> Genesis { pub fn genesis() -> Genesis { use namada::ledger::parameters::EpochDuration; use namada::types::address; + use rust_decimal_macros::dec; use crate::wallet; @@ -736,23 +748,15 @@ pub fn genesis() -> Genesis { // `tests::gen_genesis_validator` below. let consensus_keypair = wallet::defaults::validator_keypair(); let account_keypair = wallet::defaults::validator_keypair(); - let ed_staking_reward_keypair = ed25519::SecretKey::try_from_slice(&[ - 61, 198, 87, 204, 44, 94, 234, 228, 217, 72, 245, 27, 40, 2, 151, 174, - 24, 247, 69, 6, 9, 30, 44, 16, 88, 238, 77, 162, 243, 125, 240, 206, - ]) - .unwrap(); - let staking_reward_keypair = - common::SecretKey::try_from_sk(&ed_staking_reward_keypair).unwrap(); let address = wallet::defaults::validator_address(); - let staking_reward_address = Address::decode("atest1v4ehgw36xcersvee8qerxd35x9prsw2xg5erxv6pxfpygd2x89z5xsf5xvmnysejgv6rwd2rnj2avt").unwrap(); let (protocol_keypair, dkg_keypair) = wallet::defaults::validator_keys(); let validator = Validator { pos_data: GenesisValidator { address, - staking_reward_address, tokens: token::Amount::whole(200_000), consensus_key: consensus_keypair.ref_to(), - staking_reward_key: staking_reward_keypair.ref_to(), + commission_rate: dec!(0.05), + max_commission_rate_change: dec!(0.01), }, account_key: account_keypair.ref_to(), protocol_key: protocol_keypair.ref_to(), @@ -761,8 +765,6 @@ pub fn genesis() -> Genesis { // TODO replace with https://github.com/anoma/anoma/issues/25) validator_vp_code_path: vp_user_path.into(), validator_vp_sha256: Default::default(), - reward_vp_code_path: vp_user_path.into(), - reward_vp_sha256: Default::default(), }; let parameters = Parameters { epoch_duration: EpochDuration { @@ -772,6 +774,12 @@ pub fn genesis() -> Genesis { max_expected_time_per_block: namada::types::time::DurationSecs(30), vp_whitelist: vec![], tx_whitelist: vec![], + epochs_per_year: 525_600, /* seconds in yr (60*60*24*365) div seconds + * per epoch (60 = min_duration) */ + pos_gain_p: dec!(0.1), + pos_gain_d: dec!(0.1), + staked_ratio: dec!(0.0), + pos_inflation_amount: 0, }; let albert = EstablishedAccount { address: wallet::defaults::albert_address(), @@ -853,24 +861,18 @@ pub mod tests { use crate::wallet; /// Run `cargo test gen_genesis_validator -- --nocapture` to generate a - /// new genesis validator address, staking reward address and keypair. + /// new genesis validator address and keypair. #[test] fn gen_genesis_validator() { let address = gen_established_address(); - let staking_reward_address = gen_established_address(); let mut rng: ThreadRng = thread_rng(); let keypair: common::SecretKey = ed25519::SigScheme::generate(&mut rng).try_to_sk().unwrap(); let kp_arr = keypair.try_to_vec().unwrap(); - let staking_reward_keypair: common::SecretKey = - ed25519::SigScheme::generate(&mut rng).try_to_sk().unwrap(); - let srkp_arr = staking_reward_keypair.try_to_vec().unwrap(); let (protocol_keypair, dkg_keypair) = wallet::defaults::validator_keys(); println!("address: {}", address); - println!("staking_reward_address: {}", staking_reward_address); println!("keypair: {:?}", kp_arr); - println!("staking_reward_keypair: {:?}", srkp_arr); println!("protocol_keypair: {:?}", protocol_keypair); println!("dkg_keypair: {:?}", dkg_keypair.try_to_vec().unwrap()); } diff --git a/apps/src/lib/node/ledger/shell/finalize_block.rs b/apps/src/lib/node/ledger/shell/finalize_block.rs index 22ab9661c7..f3d4642736 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1,5 +1,6 @@ //! Implementation of the `FinalizeBlock` ABCI++ method for the Shell +use namada::ledger::pos::types::into_tm_voting_power; use namada::ledger::protocol; use namada::types::storage::{BlockHash, Header}; @@ -286,18 +287,19 @@ where fn update_epoch(&self, response: &mut shim::response::FinalizeBlock) { // Apply validator set update let (current_epoch, _gas) = self.storage.get_current_epoch(); + let pos_params = self.storage.read_pos_params(); // TODO ABCI validator updates on block H affects the validator set // on block H+2, do we need to update a block earlier? self.storage.validator_set_update(current_epoch, |update| { let (consensus_key, power) = match update { ValidatorSetUpdate::Active(ActiveValidator { consensus_key, - voting_power, + bonded_stake, }) => { - let power: u64 = voting_power.into(); - let power: i64 = power - .try_into() - .expect("unexpected validator's voting power"); + let power: i64 = into_tm_voting_power( + pos_params.tm_votes_per_token, + bonded_stake, + ); (consensus_key, power) } ValidatorSetUpdate::Deactivated(consensus_key) => { diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index d556fc1afd..f3510e5705 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::hash::Hash; +use namada::ledger::pos::into_tm_voting_power; use namada::types::key::*; #[cfg(not(feature = "dev"))] use sha2::{Digest, Sha256}; @@ -272,11 +273,10 @@ where sum: Some(key_to_tendermint(&consensus_key).unwrap()), }; abci_validator.pub_key = Some(pub_key); - let power: u64 = - validator.pos_data.voting_power(&genesis.pos_params).into(); - abci_validator.power = power - .try_into() - .expect("unexpected validator's voting power"); + abci_validator.power = into_tm_voting_power( + genesis.pos_params.tm_votes_per_token, + validator.pos_data.tokens, + ); response.validators.push(abci_validator); } Ok(response) diff --git a/apps/src/lib/node/ledger/shell/queries.rs b/apps/src/lib/node/ledger/shell/queries.rs index e53ea91417..53f9485d15 100644 --- a/apps/src/lib/node/ledger/shell/queries.rs +++ b/apps/src/lib/node/ledger/shell/queries.rs @@ -2,6 +2,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use ferveo_common::TendermintValidator; +use namada::ledger::pos::into_tm_voting_power; use namada::ledger::queries::{RequestCtx, ResponseQuery}; use namada::ledger::storage_api; use namada::types::address::Address; @@ -92,6 +93,8 @@ where .expect("Serializing public key should not fail"); // get the current epoch let (current_epoch, _) = self.storage.get_current_epoch(); + // get the PoS params + let pos_params = self.storage.read_pos_params(); // get the active validator set self.storage .read_validator_set() @@ -123,7 +126,10 @@ where "DKG public key in storage should be deserializable", ); TendermintValidator { - power: validator.voting_power.into(), + power: into_tm_voting_power( + pos_params.tm_votes_per_token, + validator.bonded_stake, + ) as u64, address: validator.address.to_string(), public_key: dkg_publickey.into(), } diff --git a/apps/src/lib/wallet/alias.rs b/apps/src/lib/wallet/alias.rs index 25fcf03d11..13d977b852 100644 --- a/apps/src/lib/wallet/alias.rs +++ b/apps/src/lib/wallet/alias.rs @@ -97,11 +97,6 @@ pub fn validator_consensus_key(validator_alias: &Alias) -> Alias { format!("{validator_alias}-consensus-key").into() } -/// Default alias of a validator's staking rewards key -pub fn validator_rewards_key(validator_alias: &Alias) -> Alias { - format!("{validator_alias}-rewards-key").into() -} - /// Default alias of a validator's Tendermint node key pub fn validator_tendermint_node_key(validator_alias: &Alias) -> Alias { format!("{validator_alias}-tendermint-node-key").into() diff --git a/apps/src/lib/wallet/pre_genesis.rs b/apps/src/lib/wallet/pre_genesis.rs index f28be00d1b..fb47fb9f88 100644 --- a/apps/src/lib/wallet/pre_genesis.rs +++ b/apps/src/lib/wallet/pre_genesis.rs @@ -40,8 +40,6 @@ pub struct ValidatorWallet { pub account_key: Rc, /// Cryptographic keypair for consensus key pub consensus_key: Rc, - /// Cryptographic keypair for rewards key - pub rewards_key: Rc, /// Cryptographic keypair for Tendermint node key pub tendermint_node_key: Rc, } @@ -54,8 +52,6 @@ pub struct ValidatorStore { pub account_key: wallet::StoredKeypair, /// Cryptographic keypair for consensus key pub consensus_key: wallet::StoredKeypair, - /// Cryptographic keypair for rewards key - pub rewards_key: wallet::StoredKeypair, /// Cryptographic keypair for Tendermint node key pub tendermint_node_key: wallet::StoredKeypair, /// Special validator keys @@ -107,7 +103,6 @@ impl ValidatorWallet { let password = if store.account_key.is_encrypted() || store.consensus_key.is_encrypted() - || store.rewards_key.is_encrypted() || store.account_key.is_encrypted() { Some(wallet::read_password("Enter decryption password: ")) @@ -119,8 +114,6 @@ impl ValidatorWallet { store.account_key.get(true, password.clone())?; let consensus_key = store.consensus_key.get(true, password.clone())?; - let rewards_key = - store.rewards_key.get(true, password.clone())?; let tendermint_node_key = store.tendermint_node_key.get(true, password)?; @@ -128,7 +121,6 @@ impl ValidatorWallet { store, account_key, consensus_key, - rewards_key, tendermint_node_key, }) } @@ -149,7 +141,6 @@ impl ValidatorWallet { SchemeType::Ed25519, &password, ); - let (rewards_key, rewards_sk) = gen_key_to_store(scheme, &password); let (tendermint_node_key, tendermint_node_sk) = gen_key_to_store( // Note that TM only allows ed25519 for node IDs SchemeType::Ed25519, @@ -159,7 +150,6 @@ impl ValidatorWallet { let store = ValidatorStore { account_key, consensus_key, - rewards_key, tendermint_node_key, validator_keys, }; @@ -167,7 +157,6 @@ impl ValidatorWallet { store, account_key: account_sk, consensus_key: consensus_sk, - rewards_key: rewards_sk, tendermint_node_key: tendermint_node_sk, } } diff --git a/apps/src/lib/wallet/store.rs b/apps/src/lib/wallet/store.rs index e189255355..8668b6ed1b 100644 --- a/apps/src/lib/wallet/store.rs +++ b/apps/src/lib/wallet/store.rs @@ -392,7 +392,6 @@ impl Store { other: pre_genesis::ValidatorWallet, ) { let account_key_alias = alias::validator_key(&validator_alias); - let rewards_key_alias = alias::validator_rewards_key(&validator_alias); let consensus_key_alias = alias::validator_consensus_key(&validator_alias); let tendermint_node_key_alias = @@ -400,7 +399,6 @@ impl Store { let keys = [ (account_key_alias.clone(), other.store.account_key), - (rewards_key_alias.clone(), other.store.rewards_key), (consensus_key_alias.clone(), other.store.consensus_key), ( tendermint_node_key_alias.clone(), @@ -410,12 +408,10 @@ impl Store { self.keys.extend(keys.into_iter()); let account_pk = other.account_key.ref_to(); - let rewards_pk = other.rewards_key.ref_to(); let consensus_pk = other.consensus_key.ref_to(); let tendermint_node_pk = other.tendermint_node_key.ref_to(); let addresses = [ (account_key_alias.clone(), (&account_pk).into()), - (rewards_key_alias.clone(), (&rewards_pk).into()), (consensus_key_alias.clone(), (&consensus_pk).into()), ( tendermint_node_key_alias.clone(), @@ -426,7 +422,6 @@ impl Store { let pkhs = [ ((&account_pk).into(), account_key_alias), - ((&rewards_pk).into(), rewards_key_alias), ((&consensus_pk).into(), consensus_key_alias), ((&tendermint_node_pk).into(), tendermint_node_key_alias), ]; diff --git a/genesis/dev.toml b/genesis/dev.toml index fc95244e14..46e3421ea4 100644 --- a/genesis/dev.toml +++ b/genesis/dev.toml @@ -7,20 +7,18 @@ genesis_time = "2021-09-30:10:00.00Z" consensus_public_key = "5e704c4e46265e1ccc87505149f79b9d2e414d01a4e3806dfc65f0a73901c1d0" # Public key of the validator's Anoma account. account_public_key = "5e704c4e46265e1ccc87505149f79b9d2e414d01a4e3806dfc65f0a73901c1d0" -# Public key of the Anoma account for this validator's staking rewards. -staking_reward_public_key = "6f5c421769d321ec05d01158b170649a01848f43a27988f71443041be23f2f39" # Address of the validator. address = "a1qq5qqqqqgfqnsd6pxse5zdj9g5crzsf5x4zyzv6yxerr2d2rxpryzwp5g5m5zvfjxv6ygsekjmraj0" -# Staking reward address of the validator. -staking_reward_address = "a1qq5qqqqqxaz5vven8yu5gdpng9zrys6ygvurwv3sgsmrvd6xgdzrys6yg4pnwd6z89rrqv2xvjcy9t" # Validator's token balance at genesis. tokens = 200000 # Amount of the validator's genesis token balance which is not staked. non_staked_balance = 100000 # VP for the validator account -validator_vp = "vp_user" -# VP for the staking reward account -staking_reward_vp = "vp_user" +validator_vp = "vp_validator" +# Commission rate for rewards +commission_rate = 0.05 +# Maximum change per epoch in the commission rate +max_commission_rate_change = 0.01 # Public IP:port address net_address = "127.0.0.1:26656" @@ -129,6 +127,11 @@ filename = "vp_user.wasm" # SHA-256 hash of the wasm file sha256 = "dc7b97f0448f2369bd2401c3c1d8898f53cac8c464a8c1b1f7f81415a658625d" +# Default validator VP +[wasm.vp_validator] +# filename (relative to wasm path used by the node) +filename = "vp_validator.wasm" + # Token VP [wasm.vp_token] filename = "vp_token.wasm" @@ -138,10 +141,10 @@ sha256 = "e428a11f570d21dd3c871f5d35de6fe18098eb8ee0456b3e11a72ccdd8685cd0" [parameters] # Minimum number of blocks in an epoch. min_num_of_blocks = 10 -# Minimum duration of an epoch (in seconds). -min_duration = 60 # Maximum expected time per block (in seconds). max_expected_time_per_block = 30 +# Expected epochs per year (also sets the minimum duration of an epoch in seconds) +epochs_per_year = 525_600 # Proof of stake parameters. [pos_params] @@ -152,19 +155,23 @@ max_validator_slots = 128 pipeline_len = 2 # Unbonding length (in epochs). Validators may have their stake slashed # for a fault in epoch 'n' up through epoch 'n + unbonding_len'. -unbonding_len = 6 -# Votes per token (in basis points, i.e., per 10,000 tokens) -votes_per_token = 10 +unbonding_len = 21 +# Votes per fundamental staking token (namnam) +tm_votes_per_token = 1 # Reward for proposing a block. -block_proposer_reward = 100 +block_proposer_reward = 0.125 # Reward for voting on a block. -block_vote_reward = 1 +block_vote_reward = 0.1 +# Maximum inflation rate per annum (10%) +max_inflation_rate = 0.1 +# Targeted ratio of staked tokens to total tokens in the supply +target_staked_ratio = 0.6667 # Portion of a validator's stake that should be slashed on a duplicate -# vote (in basis points, i.e., 500 = 5%). -duplicate_vote_slash_rate = 500 +# vote. +duplicate_vote_min_slash_rate = 0.001 # Portion of a validator's stake that should be slashed on a light -# client attack (in basis points, i.e., 500 = 5%). -light_client_attack_slash_rate = 500 +# client attack. +light_client_attack_min_slash_rate = 0.001 # Governance parameters. [gov_params] diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index 95e51173c6..f92117d7ff 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -10,9 +10,11 @@ tokens = 200000 # Amount of the validator's genesis token balance which is not staked. non_staked_balance = 1000000000000 # VP for the validator account -validator_vp = "vp_user" -# VP for the staking reward account -staking_reward_vp = "vp_user" +validator_vp = "vp_validator" +# Commission rate for rewards +commission_rate = 0.05 +# Maximum change per epoch in the commission rate +max_commission_rate_change = 0.01 # Public IP:port address. # We set the port to be the default+1000, so that if a local node was running at # the same time as the E2E tests, it wouldn't affect them. @@ -120,6 +122,11 @@ filename = "vp_user.wasm" # SHA-256 hash of the wasm file sha256 = "dc7b97f0448f2369bd2401c3c1d8898f53cac8c464a8c1b1f7f81415a658625d" +# Default validator VP +[wasm.vp_validator] +# filename (relative to wasm path used by the node) +filename = "vp_validator.wasm" + # Token VP [wasm.vp_token] filename = "vp_token.wasm" @@ -134,14 +141,18 @@ sha256 = "2038d93afd456a77c45123811b671627f488c8d2a72b714d82dd494cbbd552bc" [parameters] # Minimum number of blocks in an epoch. min_num_of_blocks = 4 -# Minimum duration of an epoch (in seconds). -min_duration = 1 # Maximum expected time per block (in seconds). max_expected_time_per_block = 30 # vp whitelist vp_whitelist = [] # tx whitelist tx_whitelist = [] +# Expected number of epochs per year (also sets the min duration of an epoch in seconds) +epochs_per_year = 31_536_000 +# The P gain factor in the Proof of Stake rewards controller +pos_gain_p = 0.1 +# The D gain factor in the Proof of Stake rewards controller +pos_gain_d = 0.1 # Proof of stake parameters. [pos_params] @@ -153,18 +164,22 @@ pipeline_len = 2 # Unbonding length (in epochs). Validators may have their stake slashed # for a fault in epoch 'n' up through epoch 'n + unbonding_len'. unbonding_len = 3 -# Votes per token (in basis points, i.e., per 10,000 tokens) -votes_per_token = 10 +# Votes per fundamental staking token (namnam) +tm_votes_per_token = 1 # Reward for proposing a block. -block_proposer_reward = 100 +block_proposer_reward = 0.125 # Reward for voting on a block. -block_vote_reward = 1 +block_vote_reward = 0.1 +# Maximum inflation rate per annum (10%) +max_inflation_rate = 0.1 +# Targeted ratio of staked tokens to total tokens in the supply +target_staked_ratio = 0.6667 # Portion of a validator's stake that should be slashed on a duplicate -# vote (in basis points, i.e., 500 = 5%). -duplicate_vote_slash_rate = 500 +# vote. +duplicate_vote_min_slash_rate = 0.001 # Portion of a validator's stake that should be slashed on a light -# client attack (in basis points, i.e., 500 = 5%). -light_client_attack_slash_rate = 500 +# client attack. +light_client_attack_min_slash_rate = 0.001 # Governance parameters. [gov_params] diff --git a/proof_of_stake/Cargo.toml b/proof_of_stake/Cargo.toml index d6ee686121..c680a3229a 100644 --- a/proof_of_stake/Cargo.toml +++ b/proof_of_stake/Cargo.toml @@ -19,5 +19,7 @@ thiserror = "1.0.30" # A fork with state machine testing proptest = {git = "https://github.com/heliaxdev/proptest", branch = "tomas/sm", optional = true} derivative = "2.2.0" +rust_decimal = { version = "1.26.1", features = ["borsh"] } +rust_decimal_macros = "1.26.1" [dev-dependencies] diff --git a/proof_of_stake/src/epoched.rs b/proof_of_stake/src/epoched.rs index f13bec3ee0..cc2f24f1a8 100644 --- a/proof_of_stake/src/epoched.rs +++ b/proof_of_stake/src/epoched.rs @@ -128,6 +128,8 @@ pub enum DynEpochOffset { PipelineLen, /// Offset at unbonding length. UnbondingLen, + /// Offset at pipeline length - 1. + PipelineLenMinusOne, } impl DynEpochOffset { /// Find the value of a given offset from PoS parameters. @@ -135,6 +137,7 @@ impl DynEpochOffset { match self { DynEpochOffset::PipelineLen => params.pipeline_len, DynEpochOffset::UnbondingLen => params.unbonding_len, + DynEpochOffset::PipelineLenMinusOne => params.pipeline_len - 1, } } } @@ -1223,7 +1226,9 @@ mod tests { Some(DynEpochOffset::PipelineLen) => { Just(DynEpochOffset::PipelineLen).boxed() } - Some(DynEpochOffset::UnbondingLen) | None => prop_oneof![ + Some(DynEpochOffset::UnbondingLen) + | Some(DynEpochOffset::PipelineLenMinusOne) + | None => prop_oneof![ Just(DynEpochOffset::PipelineLen), Just(DynEpochOffset::UnbondingLen), ] diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 6e5f4e2196..f80bb55caa 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -31,16 +31,19 @@ use epoched::{ DynEpochOffset, EpochOffset, Epoched, EpochedDelta, OffsetPipelineLen, }; use parameters::PosParams; +use rust_decimal::Decimal; use thiserror::Error; use types::{ - ActiveValidator, Bonds, Epoch, GenesisValidator, Slash, SlashType, Slashes, - TotalVotingPowers, Unbond, Unbonds, ValidatorConsensusKeys, ValidatorSet, - ValidatorSetUpdate, ValidatorSets, ValidatorState, ValidatorStates, - ValidatorTotalDeltas, ValidatorVotingPowers, VotingPower, VotingPowerDelta, + ActiveValidator, Bonds, CommissionRates, Epoch, GenesisValidator, Slash, + SlashType, Slashes, TotalDeltas, Unbond, Unbonds, ValidatorConsensusKeys, + ValidatorDeltas, ValidatorSet, ValidatorSetUpdate, ValidatorSets, + ValidatorState, ValidatorStates, }; use crate::btree_set::BTreeSetShims; -use crate::types::{Bond, BondId, WeightedValidator}; +use crate::types::{ + decimal_mult_i128, decimal_mult_u64, Bond, BondId, WeightedValidator, +}; /// Read-only part of the PoS system pub trait PosReadOnly { @@ -107,11 +110,6 @@ pub trait PosReadOnly { /// Read PoS parameters. fn read_pos_params(&self) -> Result; - /// Read PoS validator's staking reward address. - fn read_validator_staking_reward_address( - &self, - key: &Self::Address, - ) -> Result, Self::Error>; /// Read PoS validator's consensus key (used for signing block votes). fn read_validator_consensus_key( &self, @@ -124,20 +122,27 @@ pub trait PosReadOnly { ) -> Result, Self::Error>; /// Read PoS validator's total deltas of their bonds (validator self-bonds /// and delegations). - fn read_validator_total_deltas( + fn read_validator_deltas( &self, key: &Self::Address, - ) -> Result>, Self::Error>; - /// Read PoS validator's voting power. - fn read_validator_voting_power( - &self, - key: &Self::Address, - ) -> Result, Self::Error>; + ) -> Result>, Self::Error>; + /// Read PoS slashes applied to a validator. fn read_validator_slashes( &self, key: &Self::Address, ) -> Result, Self::Error>; + /// Read PoS validator's commission rate for delegation rewards + fn read_validator_commission_rate( + &self, + key: &Self::Address, + ) -> Result, Self::Error>; + /// Read PoS validator's maximum change in the commission rate for + /// delegation rewards + fn read_validator_max_commission_rate_change( + &self, + key: &Self::Address, + ) -> Result, Self::Error>; /// Read PoS bond (validator self-bond or a delegation). fn read_bond( &self, @@ -153,9 +158,10 @@ pub trait PosReadOnly { fn read_validator_set( &self, ) -> Result, Self::Error>; - /// Read PoS total voting power of all validators (active and inactive). - fn read_total_voting_power(&self) - -> Result; + /// Read PoS total deltas for all validators (active and inactive) + fn read_total_deltas( + &self, + ) -> Result, Self::Error>; } /// PoS system trait to be implemented in integration that can read and write @@ -175,6 +181,10 @@ pub trait PosActions: PosReadOnly { /// Error in `PosActions::withdraw_tokens` type WithdrawError: From + From>; + /// Error in `PosActions::change_commission_rate` + type CommissionRateChangeError: From + + From>; + /// Write PoS parameters. fn write_pos_params( &mut self, @@ -186,13 +196,6 @@ pub trait PosActions: PosReadOnly { address: &Self::Address, consensus_key: &Self::PublicKey, ) -> Result<(), Self::Error>; - /// Write PoS validator's staking reward address, into which staking rewards - /// will be credited. - fn write_validator_staking_reward_address( - &mut self, - key: &Self::Address, - value: Self::Address, - ) -> Result<(), Self::Error>; /// Write PoS validator's consensus key (used for signing block votes). fn write_validator_consensus_key( &mut self, @@ -205,19 +208,26 @@ pub trait PosActions: PosReadOnly { key: &Self::Address, value: ValidatorStates, ) -> Result<(), Self::Error>; - /// Write PoS validator's total deltas of their bonds (validator self-bonds - /// and delegations). - fn write_validator_total_deltas( + /// Write PoS validator's commission rate for delegator rewards + fn write_validator_commission_rate( &mut self, key: &Self::Address, - value: ValidatorTotalDeltas, + value: CommissionRates, ) -> Result<(), Self::Error>; - /// Write PoS validator's voting power. - fn write_validator_voting_power( + /// Write PoS validator's maximum change in the commission rate per epoch + fn write_validator_max_commission_rate_change( + &mut self, + key: &Self::Address, + value: Decimal, + ) -> Result<(), Self::Error>; + /// Write PoS validator's total deltas of their bonds (validator self-bonds + /// and delegations). + fn write_validator_deltas( &mut self, key: &Self::Address, - value: ValidatorVotingPowers, + value: ValidatorDeltas, ) -> Result<(), Self::Error>; + /// Write PoS bond (validator self-bond or a delegation). fn write_bond( &mut self, @@ -236,12 +246,11 @@ pub trait PosActions: PosReadOnly { &mut self, value: ValidatorSets, ) -> Result<(), Self::Error>; - /// Write PoS total voting power of all validators (active and inactive). - fn write_total_voting_power( + /// Write PoS total deltas of all validators (active and inactive). + fn write_total_deltas( &mut self, - value: TotalVotingPowers, + value: TotalDeltas, ) -> Result<(), Self::Error>; - /// Delete an emptied PoS bond (validator self-bond or a delegation). fn delete_bond( &mut self, @@ -267,9 +276,10 @@ pub trait PosActions: PosReadOnly { fn become_validator( &mut self, address: &Self::Address, - staking_reward_address: &Self::Address, consensus_key: &Self::PublicKey, current_epoch: impl Into, + commission_rate: Decimal, + max_commission_rate_change: Decimal, ) -> Result<(), Self::BecomeValidatorError> { let current_epoch = current_epoch.into(); let params = self.read_pos_params()?; @@ -277,36 +287,37 @@ pub trait PosActions: PosReadOnly { if self.is_validator(address)? { Err(BecomeValidatorError::AlreadyValidator(address.clone()))?; } - if address == staking_reward_address { - Err( - BecomeValidatorError::StakingRewardAddressEqValidatorAddress( - address.clone(), - ), - )?; - } let consensus_key_clone = consensus_key.clone(); let BecomeValidatorData { consensus_key, state, - total_deltas, - voting_power, + deltas, + commission_rate, + max_commission_rate_change, } = become_validator( ¶ms, address, consensus_key, &mut validator_set, current_epoch, + commission_rate, + max_commission_rate_change, ); - self.write_validator_staking_reward_address( - address, - staking_reward_address.clone(), - )?; self.write_validator_consensus_key(address, consensus_key)?; self.write_validator_state(address, state)?; self.write_validator_set(validator_set)?; self.write_validator_address_raw_hash(address, &consensus_key_clone)?; - self.write_validator_total_deltas(address, total_deltas)?; - self.write_validator_voting_power(address, voting_power)?; + self.write_validator_deltas(address, deltas)?; + self.write_validator_max_commission_rate_change( + address, + max_commission_rate_change, + )?; + + let commission_rates = + Epoched::init(commission_rate, current_epoch, ¶ms); + self.write_validator_commission_rate(address, commission_rates)?; + + // Do we need to write the total deltas of all validators? Ok(()) } @@ -344,33 +355,27 @@ pub trait PosActions: PosReadOnly { validator: validator.clone(), }; let bond = self.read_bond(&bond_id)?; - let validator_total_deltas = - self.read_validator_total_deltas(validator)?; - let validator_voting_power = - self.read_validator_voting_power(validator)?; - let mut total_voting_power = self.read_total_voting_power()?; + let validator_deltas = self.read_validator_deltas(validator)?; + let mut total_deltas = self.read_total_deltas()?; let mut validator_set = self.read_validator_set()?; let BondData { bond, - validator_total_deltas, - validator_voting_power, + validator_deltas, } = bond_tokens( ¶ms, validator_state, &bond_id, bond, amount, - validator_total_deltas, - validator_voting_power, - &mut total_voting_power, + validator_deltas, + &mut total_deltas, &mut validator_set, current_epoch, )?; self.write_bond(&bond_id, bond)?; - self.write_validator_total_deltas(validator, validator_total_deltas)?; - self.write_validator_voting_power(validator, validator_voting_power)?; - self.write_total_voting_power(total_voting_power)?; + self.write_validator_deltas(validator, validator_deltas)?; + self.write_total_deltas(total_deltas)?; self.write_validator_set(validator_set)?; // Transfer the bonded tokens from the source to PoS @@ -402,21 +407,15 @@ pub trait PosActions: PosReadOnly { }; let mut bond = match self.read_bond(&bond_id)? { Some(val) => val, - None => Err(UnbondError::NoBondFound)?, + None => return Err(UnbondError::NoBondFound.into()), }; let unbond = self.read_unbond(&bond_id)?; - let mut validator_total_deltas = self - .read_validator_total_deltas(validator)? - .ok_or_else(|| { + let mut validator_deltas = + self.read_validator_deltas(validator)?.ok_or_else(|| { UnbondError::ValidatorHasNoBonds(validator.clone()) })?; - let mut validator_voting_power = self - .read_validator_voting_power(validator)? - .ok_or_else(|| { - UnbondError::ValidatorHasNoVotingPower(validator.clone()) - })?; let slashes = self.read_validator_slashes(validator)?; - let mut total_voting_power = self.read_total_voting_power()?; + let mut total_deltas = self.read_total_deltas()?; let mut validator_set = self.read_validator_set()?; let UnbondData { unbond } = unbond_tokens( @@ -426,9 +425,8 @@ pub trait PosActions: PosReadOnly { unbond, amount, slashes, - &mut validator_total_deltas, - &mut validator_voting_power, - &mut total_voting_power, + &mut validator_deltas, + &mut total_deltas, &mut validator_set, current_epoch, )?; @@ -448,9 +446,8 @@ pub trait PosActions: PosReadOnly { } } self.write_unbond(&bond_id, unbond)?; - self.write_validator_total_deltas(validator, validator_total_deltas)?; - self.write_validator_voting_power(validator, validator_voting_power)?; - self.write_total_voting_power(total_voting_power)?; + self.write_validator_deltas(validator, validator_deltas)?; + self.write_total_deltas(total_deltas)?; self.write_validator_set(validator_set)?; Ok(()) @@ -513,6 +510,82 @@ pub trait PosActions: PosReadOnly { Ok(slashed) } + + /// Change the commission rate of a validator + fn change_validator_commission_rate( + &mut self, + validator: &Self::Address, + new_rate: Decimal, + current_epoch: impl Into, + ) -> Result<(), Self::CommissionRateChangeError> { + if new_rate < Decimal::ZERO { + return Err(CommissionRateChangeError::NegativeRate( + new_rate, + validator.clone(), + ) + .into()); + } + let current_epoch = current_epoch.into(); + let max_change = self + .read_validator_max_commission_rate_change(validator) + .map_err(|_| { + CommissionRateChangeError::NoMaxSetInStorage(validator.clone()) + })? + .ok_or_else(|| { + CommissionRateChangeError::CannotRead(validator.clone()) + })?; + let mut commission_rates = + match self.read_validator_commission_rate(validator) { + Ok(Some(rates)) => rates, + _ => { + return Err(CommissionRateChangeError::CannotRead( + validator.clone(), + ) + .into()); + } + }; + let params = self.read_pos_params()?; + + let rate_at_pipeline = *commission_rates + .get_at_offset(current_epoch, DynEpochOffset::PipelineLen, ¶ms) + .expect("Could not find a rate in given epoch"); + if new_rate == rate_at_pipeline { + return Err(CommissionRateChangeError::ChangeIsZero( + validator.clone(), + ) + .into()); + } + + let rate_before_pipeline = *commission_rates + .get_at_offset( + current_epoch, + DynEpochOffset::PipelineLenMinusOne, + ¶ms, + ) + .expect("Could not find a rate in given epoch"); + let change_from_prev = new_rate - rate_before_pipeline; + if change_from_prev.abs() > max_change { + return Err(CommissionRateChangeError::RateChangeTooLarge( + change_from_prev, + validator.clone(), + ) + .into()); + } + commission_rates.update_from_offset( + |val, _epoch| { + *val = new_rate; + }, + current_epoch, + DynEpochOffset::PipelineLen, + ¶ms, + ); + self.write_validator_commission_rate(validator, commission_rates) + .map_err(|_| { + CommissionRateChangeError::CannotWrite(validator.clone()) + })?; + + Ok(()) + } } /// PoS system base trait for system initialization on genesis block, updating @@ -602,21 +675,27 @@ pub trait PosBase { ) -> Option; /// Read PoS validator's total deltas of their bonds (validator self-bonds /// and delegations). - fn read_validator_total_deltas( + fn read_validator_deltas( &self, key: &Self::Address, - ) -> Option>; - /// Read PoS validator's voting power. - fn read_validator_voting_power( - &self, - key: &Self::Address, - ) -> Option; + ) -> Option>; + /// Read PoS slashes applied to a validator. fn read_validator_slashes(&self, key: &Self::Address) -> Slashes; + /// Read PoS validator's commission rate + fn read_validator_commission_rate( + &self, + key: &Self::Address, + ) -> CommissionRates; + /// Read PoS validator's maximum commission rate change per epoch + fn read_validator_max_commission_rate_change( + &self, + key: &Self::Address, + ) -> Decimal; /// Read PoS validator set (active and inactive). fn read_validator_set(&self) -> ValidatorSets; - /// Read PoS total voting power of all validators (active and inactive). - fn read_total_voting_power(&self) -> TotalVotingPowers; + /// Read PoS total deltas of all validators (active and inactive). + fn read_total_deltas(&self) -> TotalDeltas; /// Write PoS parameters. fn write_pos_params(&mut self, params: &PosParams); @@ -626,13 +705,6 @@ pub trait PosBase { address: &Self::Address, consensus_key: &Self::PublicKey, ); - /// Write PoS validator's staking reward address, into which staking rewards - /// will be credited. - fn write_validator_staking_reward_address( - &mut self, - key: &Self::Address, - value: &Self::Address, - ); /// Write PoS validator's consensus key (used for signing block votes). fn write_validator_consensus_key( &mut self, @@ -647,16 +719,22 @@ pub trait PosBase { ); /// Write PoS validator's total deltas of their bonds (validator self-bonds /// and delegations). - fn write_validator_total_deltas( + fn write_validator_deltas( &mut self, key: &Self::Address, - value: &ValidatorTotalDeltas, + value: &ValidatorDeltas, ); - /// Write PoS validator's voting power. - fn write_validator_voting_power( + /// Write PoS validator's commission rate. + fn write_validator_commission_rate( &mut self, key: &Self::Address, - value: &ValidatorVotingPowers, + value: &CommissionRates, + ); + /// Write PoS validator's maximum change in the commission rate. + fn write_validator_max_commission_rate_change( + &mut self, + key: &Self::Address, + value: &Decimal, ); /// Write (append) PoS slash applied to a validator. fn write_validator_slash( @@ -672,14 +750,8 @@ pub trait PosBase { ); /// Write PoS validator set (active and inactive). fn write_validator_set(&mut self, value: &ValidatorSets); - /// Read PoS total voting power of all validators (active and inactive). - fn write_total_voting_power(&mut self, value: &TotalVotingPowers); - /// Initialize staking reward account with the given public key. - fn init_staking_reward_account( - &mut self, - address: &Self::Address, - pk: &Self::PublicKey, - ); + /// Write total deltas in PoS for all validators (active and inactive) + fn write_total_deltas(&mut self, value: &TotalDeltas); /// Credit tokens to the `target` account. This should only be used at /// genesis. fn credit_tokens( @@ -720,19 +792,18 @@ pub trait PosBase { let GenesisData { validators, validator_set, - total_voting_power, + total_deltas, total_bonded_balance, } = init_genesis(params, validators, current_epoch)?; for res in validators { let GenesisValidatorData { ref address, - staking_reward_address, consensus_key, - staking_reward_key, + commission_rate, + max_commission_rate_change, state, - total_deltas, - voting_power, + deltas, bond: (bond_id, bond), } = res?; self.write_validator_address_raw_hash( @@ -741,22 +812,21 @@ pub trait PosBase { .get(current_epoch) .expect("Consensus key must be set"), ); - self.write_validator_staking_reward_address( - address, - &staking_reward_address, - ); self.write_validator_consensus_key(address, &consensus_key); self.write_validator_state(address, &state); - self.write_validator_total_deltas(address, &total_deltas); - self.write_validator_voting_power(address, &voting_power); + self.write_validator_deltas(address, &deltas); self.write_bond(&bond_id, &bond); - self.init_staking_reward_account( - &staking_reward_address, - &staking_reward_key, + self.write_validator_commission_rate(address, &commission_rate); + self.write_validator_max_commission_rate_change( + address, + &max_commission_rate_change, ); } self.write_validator_set(&validator_set); - self.write_total_voting_power(&total_voting_power); + self.write_total_deltas(&total_deltas); + + // TODO: write total_staked_tokens (Amount) to storage? + // Credit the bonded tokens to the PoS account self.credit_tokens( &Self::staking_token_address(), @@ -812,7 +882,7 @@ pub trait PosBase { ); return None; } - if validator.voting_power == 0.into() { + if validator.bonded_stake == 0 { // If the validator was `Pending` in the previous epoch, // it means that it just was just added to validator // set. We have to skip it, because it's 0. @@ -839,7 +909,7 @@ pub trait PosBase { .clone(); Some(ValidatorSetUpdate::Active(ActiveValidator { consensus_key, - voting_power: validator.voting_power, + bonded_stake: validator.bonded_stake, })) }, ); @@ -854,7 +924,7 @@ pub trait PosBase { if prev_validators.inactive.contains(validator) { return None; } - if validator.voting_power == 0.into() { + if validator.bonded_stake == 0 { // If the validator was `Pending` in the previous epoch, // it means that it just was just added to validator // set. We have to skip it, because it's 0. @@ -901,37 +971,34 @@ pub trait PosBase { block_height: evidence_block_height.into(), }; - let mut total_deltas = - self.read_validator_total_deltas(validator).ok_or_else(|| { + let mut deltas = + self.read_validator_deltas(validator).ok_or_else(|| { SlashError::ValidatorHasNoTotalDeltas(validator.clone()) })?; - let mut voting_power = - self.read_validator_voting_power(validator).ok_or_else(|| { - SlashError::ValidatorHasNoVotingPower(validator.clone()) - })?; let mut validator_set = self.read_validator_set(); - let mut total_voting_power = self.read_total_voting_power(); + let mut total_deltas = self.read_total_deltas(); let slashed_change = slash( params, current_epoch, validator, &validator_slash, - &mut total_deltas, - &mut voting_power, + &mut deltas, &mut validator_set, - &mut total_voting_power, + &mut total_deltas, )?; let slashed_change: i128 = slashed_change.into(); let slashed_amount = u64::try_from(slashed_change) .map_err(|_err| SlashError::InvalidSlashChange(slashed_change))?; let slashed_amount = Self::TokenAmount::from(slashed_amount); - self.write_validator_total_deltas(validator, &total_deltas); - self.write_validator_voting_power(validator, &voting_power); + self.write_validator_deltas(validator, &deltas); self.write_validator_slash(validator, validator_slash); self.write_validator_set(&validator_set); - self.write_total_voting_power(&total_voting_power); + self.write_total_deltas(&total_deltas); + + // TODO: write total staked tokens (Amount) to storage? + // Transfer the slashed tokens to the PoS slash pool self.transfer( &Self::staking_token_address(), @@ -955,11 +1022,6 @@ pub enum GenesisError { pub enum BecomeValidatorError { #[error("The given address {0} is already a validator")] AlreadyValidator(Address), - #[error( - "The staking reward address must be different from the validator's \ - address {0}" - )] - StakingRewardAddressEqValidatorAddress(Address), } #[allow(missing_docs)] @@ -1037,6 +1099,36 @@ where NegativeStake(i128, Address), } +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum CommissionRateChangeError
+where + Address: Display + + Debug + + Clone + + PartialOrd + + Ord + + Hash + + BorshDeserialize + + BorshSerialize + + BorshSchema, +{ + #[error("Unexpected negative commission rate {0} for validator {1}")] + NegativeRate(Decimal, Address), + #[error("Rate change of {0} is too large for validator {1}")] + RateChangeTooLarge(Decimal, Address), + #[error("The rate change is 0 for validator {0}")] + ChangeIsZero(Address), + #[error( + "There is no maximum rate change written in storage for validator {0}" + )] + NoMaxSetInStorage(Address), + #[error("Cannot write to storage for validator {0}")] + CannotWrite(Address), + #[error("Cannot read storage for validator {0}")] + CannotRead(Address), +} + struct GenesisData where Validators: Iterator< @@ -1072,8 +1164,8 @@ where validators: Validators, /// Active and inactive validator sets validator_set: ValidatorSets
, - /// The sum of all active and inactive validators' voting power - total_voting_power: TotalVotingPowers, + /// The sum of all active and inactive validators' bonded deltas + total_deltas: TotalDeltas, /// The sum of all active and inactive validators' bonded tokens total_bonded_balance: TokenAmount, } @@ -1104,12 +1196,11 @@ where PK: Debug + Clone + BorshDeserialize + BorshSerialize + BorshSchema, { address: Address, - staking_reward_address: Address, consensus_key: ValidatorConsensusKeys, - staking_reward_key: PK, + commission_rate: CommissionRates, + max_commission_rate_change: Decimal, state: ValidatorStates, - total_deltas: ValidatorTotalDeltas, - voting_power: ValidatorVotingPowers, + deltas: ValidatorDeltas, bond: (BondId
, Bonds), } @@ -1158,6 +1249,7 @@ where + BorshSchema, TokenChange: 'a + Debug + + Default + Copy + Add + From @@ -1166,21 +1258,21 @@ where + BorshSchema, PK: 'a + Debug + Clone + BorshDeserialize + BorshSerialize + BorshSchema, { - // Accumulate the validator set and total voting power + // Accumulate the validator set and total bonded token balance let mut active: BTreeSet> = BTreeSet::default(); - let mut total_voting_power = VotingPowerDelta::default(); + let mut total_bonded_delta = TokenChange::default(); let mut total_bonded_balance = TokenAmount::default(); for GenesisValidator { address, tokens, .. } in validators.clone() { total_bonded_balance += *tokens; - let delta = VotingPowerDelta::try_from_tokens(*tokens, params) - .map_err(GenesisError::VotingPowerOverflow)?; - total_voting_power += delta; - let voting_power = VotingPower::from_tokens(*tokens, params); + // is some extra error handling needed here for casting the delta as + // i64? (TokenChange) + let delta = TokenChange::from(*tokens); + total_bonded_delta = total_bonded_delta + delta; active.insert(WeightedValidator { - voting_power, + bonded_stake: (*tokens).into(), address: address.clone(), }); } @@ -1198,33 +1290,29 @@ where } let validator_set = ValidatorSet { active, inactive }; let validator_set = Epoched::init_at_genesis(validator_set, current_epoch); - let total_voting_power = - EpochedDelta::init_at_genesis(total_voting_power, current_epoch); + let total_bonded_delta = + EpochedDelta::init_at_genesis(total_bonded_delta, current_epoch); // Adapt the genesis validators data to PoS data let validators = validators.map( move |GenesisValidator { address, - staking_reward_address, - tokens, consensus_key, - staking_reward_key, + commission_rate, + max_commission_rate_change, }| { let consensus_key = Epoched::init_at_genesis(consensus_key.clone(), current_epoch); + let commission_rate = + Epoched::init_at_genesis(*commission_rate, current_epoch); let state = Epoched::init_at_genesis( ValidatorState::Candidate, current_epoch, ); let token_delta = TokenChange::from(*tokens); - let total_deltas = + let deltas = EpochedDelta::init_at_genesis(token_delta, current_epoch); - let voting_power = - VotingPowerDelta::try_from_tokens(*tokens, params) - .map_err(GenesisError::VotingPowerOverflow)?; - let voting_power = - EpochedDelta::init_at_genesis(voting_power, current_epoch); let bond_id = BondId { source: address.clone(), validator: address.clone(), @@ -1240,21 +1328,21 @@ where ); Ok(GenesisValidatorData { address: address.clone(), - staking_reward_address: staking_reward_address.clone(), consensus_key, - staking_reward_key: staking_reward_key.clone(), + commission_rate, + max_commission_rate_change: *max_commission_rate_change, state, - total_deltas, - voting_power, + deltas, bond: (bond_id, bond), }) }, ); + // TODO: include total_tokens here, think abt where to write to storage Ok(GenesisData { validators, validator_set, - total_voting_power, + total_deltas: total_bonded_delta, total_bonded_balance, }) } @@ -1266,10 +1354,9 @@ fn slash( current_epoch: Epoch, validator: &Address, slash: &Slash, - total_deltas: &mut ValidatorTotalDeltas, - voting_power: &mut ValidatorVotingPowers, + validator_deltas: &mut ValidatorDeltas, validator_set: &mut ValidatorSets
, - total_voting_power: &mut TotalVotingPowers, + total_deltas: &mut TotalDeltas, ) -> Result> where Address: Display @@ -1295,7 +1382,7 @@ where + BorshSchema, { let current_stake: TokenChange = - total_deltas.get(current_epoch).unwrap_or_default(); + validator_deltas.get(current_epoch).unwrap_or_default(); if current_stake < TokenChange::default() { return Err(SlashError::NegativeStake( current_stake.into(), @@ -1303,14 +1390,15 @@ where )); } let raw_current_stake: i128 = current_stake.into(); - let slashed_amount: TokenChange = (slash.rate * raw_current_stake).into(); + let slashed_amount: TokenChange = + decimal_mult_i128(slash.rate, raw_current_stake).into(); let token_change = -slashed_amount; // Apply slash at pipeline offset let update_offset = DynEpochOffset::PipelineLen; // Update validator set. This has to be done before we update the - // `validator_total_deltas`, because we need to look-up the validator with + // `validator_deltas`, because we need to look-up the validator with // its voting power before the change. update_validator_set( params, @@ -1318,28 +1406,25 @@ where token_change, update_offset, validator_set, - Some(total_deltas), + Some(validator_deltas), current_epoch, ); - // Update validator's total deltas - total_deltas.add_at_offset( + // Update validator's deltas + validator_deltas.add_at_offset( token_change, current_epoch, update_offset, params, ); - // Update the validator's and the total voting power. - update_voting_powers( - params, - update_offset, - total_deltas, - voting_power, - total_voting_power, + // Update total deltas of all validators + total_deltas.add_at_offset( + token_change, current_epoch, - ) - .map_err(SlashError::VotingPowerOverflow)?; + update_offset, + params, + ); Ok(slashed_amount) } @@ -1358,8 +1443,9 @@ where { consensus_key: ValidatorConsensusKeys, state: ValidatorStates, - total_deltas: ValidatorTotalDeltas, - voting_power: ValidatorVotingPowers, + deltas: ValidatorDeltas, + commission_rate: Decimal, + max_commission_rate_change: Decimal, } /// A function that initialized data for a new validator. @@ -1369,6 +1455,8 @@ fn become_validator( consensus_key: &PK, validator_set: &mut ValidatorSets
, current_epoch: Epoch, + commission_rate: Decimal, + max_commission_rate_change: Decimal, ) -> BecomeValidatorData where Address: Debug @@ -1395,13 +1483,7 @@ where Epoched::init_at_genesis(ValidatorState::Pending, current_epoch); state.set(ValidatorState::Candidate, current_epoch, params); - let total_deltas = EpochedDelta::init_at_offset( - Default::default(), - current_epoch, - DynEpochOffset::PipelineLen, - params, - ); - let voting_power = EpochedDelta::init_at_offset( + let deltas = EpochedDelta::init_at_offset( Default::default(), current_epoch, DynEpochOffset::PipelineLen, @@ -1411,7 +1493,7 @@ where validator_set.update_from_offset( |validator_set, _epoch| { let validator = WeightedValidator { - voting_power: VotingPower::default(), + bonded_stake: 0, address: address.clone(), }; if validator_set.active.len() < params.max_validator_slots as usize @@ -1429,8 +1511,9 @@ where BecomeValidatorData { consensus_key, state, - total_deltas, - voting_power, + deltas, + commission_rate, + max_commission_rate_change, } } @@ -1454,8 +1537,7 @@ where + BorshSchema, { pub bond: Bonds, - pub validator_total_deltas: ValidatorTotalDeltas, - pub validator_voting_power: ValidatorVotingPowers, + pub validator_deltas: ValidatorDeltas, } /// Bond tokens to a validator (self-bond or delegation). @@ -1466,9 +1548,8 @@ fn bond_tokens( bond_id: &BondId
, current_bond: Option>, amount: TokenAmount, - validator_total_deltas: Option>, - validator_voting_power: Option, - total_voting_power: &mut TotalVotingPowers, + validator_deltas: Option>, + total_deltas: &mut TotalDeltas, validator_set: &mut ValidatorSets
, current_epoch: Epoch, ) -> Result, BondError
> @@ -1535,13 +1616,14 @@ where } } - let update_offset = DynEpochOffset::PipelineLen; - // Update or create the bond + // let mut value = Bond { pos_deltas: HashMap::default(), neg_deltas: TokenAmount::default(), }; + // Initialize the bond at the pipeline offset + let update_offset = DynEpochOffset::PipelineLen; value .pos_deltas .insert(current_epoch + update_offset.value(params), amount); @@ -1559,7 +1641,7 @@ where }; // Update validator set. This has to be done before we update the - // `validator_total_deltas`, because we need to look-up the validator with + // `validator_deltas`, because we need to look-up the validator with // its voting power before the change. let token_change = TokenChange::from(amount); update_validator_set( @@ -1568,21 +1650,21 @@ where token_change, update_offset, validator_set, - validator_total_deltas.as_ref(), + validator_deltas.as_ref(), current_epoch, ); - // Update validator's total deltas + // Update validator's total deltas and total staked token deltas let delta = TokenChange::from(amount); - let validator_total_deltas = match validator_total_deltas { - Some(mut validator_total_deltas) => { - validator_total_deltas.add_at_offset( + let validator_deltas = match validator_deltas { + Some(mut validator_deltas) => { + validator_deltas.add_at_offset( delta, current_epoch, update_offset, params, ); - validator_total_deltas + validator_deltas } None => EpochedDelta::init_at_offset( delta, @@ -1592,30 +1674,11 @@ where ), }; - // Update the validator's and the total voting power. - let mut validator_voting_power = match validator_voting_power { - Some(voting_power) => voting_power, - None => EpochedDelta::init_at_offset( - VotingPowerDelta::default(), - current_epoch, - update_offset, - params, - ), - }; - update_voting_powers( - params, - update_offset, - &validator_total_deltas, - &mut validator_voting_power, - total_voting_power, - current_epoch, - ) - .map_err(BondError::VotingPowerOverflow)?; + total_deltas.add_at_offset(delta, current_epoch, update_offset, params); Ok(BondData { bond, - validator_total_deltas, - validator_voting_power, + validator_deltas, }) } @@ -1643,9 +1706,8 @@ fn unbond_tokens( unbond: Option>, amount: TokenAmount, slashes: Slashes, - validator_total_deltas: &mut ValidatorTotalDeltas, - validator_voting_power: &mut ValidatorVotingPowers, - total_voting_power: &mut TotalVotingPowers, + validator_deltas: &mut ValidatorDeltas, + total_deltas: &mut TotalDeltas, validator_set: &mut ValidatorSets
, current_epoch: Epoch, ) -> Result, UnbondError> @@ -1745,7 +1807,8 @@ where for slash in &slashes { if slash.epoch >= *epoch_start { let raw_delta: u64 = slashed_bond_delta.into(); - let raw_slashed_delta = slash.rate * raw_delta; + let raw_slashed_delta = + decimal_mult_u64(slash.rate, raw_delta); let slashed_delta = TokenAmount::from(raw_slashed_delta); slashed_bond_delta -= slashed_delta; @@ -1774,7 +1837,7 @@ where ); // Update validator set. This has to be done before we update the - // `validator_total_deltas`, because we need to look-up the validator with + // `validator_deltas`, because we need to look-up the validator with // its voting power before the change. let token_change = -TokenChange::from(slashed_amount); update_validator_set( @@ -1783,23 +1846,16 @@ where token_change, update_offset, validator_set, - Some(validator_total_deltas), + Some(validator_deltas), current_epoch, ); - // Update validator's total deltas - validator_total_deltas.add(token_change, current_epoch, params); + // Update validator's deltas + validator_deltas.add(token_change, current_epoch, params); - // Update the validator's and the total voting power. - update_voting_powers( - params, - update_offset, - validator_total_deltas, - validator_voting_power, - total_voting_power, - current_epoch, - ) - .map_err(UnbondError::VotingPowerOverflow)?; + // Update the total deltas of all validators. + // TODO: provide some error handling that was maybe here before? + total_deltas.add(token_change, current_epoch, params); Ok(UnbondData { unbond }) } @@ -1812,7 +1868,7 @@ fn update_validator_set( token_change: TokenChange, change_offset: DynEpochOffset, validator_set: &mut ValidatorSets
, - validator_total_deltas: Option<&ValidatorTotalDeltas>, + validator_deltas: Option<&ValidatorDeltas>, current_epoch: Epoch, ) where Address: Display @@ -1840,9 +1896,9 @@ fn update_validator_set( { validator_set.update_from_offset( |validator_set, epoch| { - // Find the validator's voting power at the epoch that's being + // Find the validator's bonded stake at the epoch that's being // updated from its total deltas - let tokens_pre = validator_total_deltas + let tokens_pre = validator_deltas .and_then(|d| d.get(epoch)) .unwrap_or_default(); let tokens_post = tokens_pre + token_change; @@ -1850,26 +1906,24 @@ fn update_validator_set( let tokens_post: i128 = tokens_post.into(); let tokens_pre: u64 = TryFrom::try_from(tokens_pre).unwrap(); let tokens_post: u64 = TryFrom::try_from(tokens_post).unwrap(); - let voting_power_pre = VotingPower::from_tokens(tokens_pre, params); - let voting_power_post = - VotingPower::from_tokens(tokens_post, params); - if voting_power_pre != voting_power_post { + + if tokens_pre != tokens_post { let validator_pre = WeightedValidator { - voting_power: voting_power_pre, + bonded_stake: tokens_pre, address: validator.clone(), }; let validator_post = WeightedValidator { - voting_power: voting_power_post, + bonded_stake: tokens_post, address: validator.clone(), }; if validator_set.inactive.contains(&validator_pre) { let min_active_validator = validator_set.active.first_shim(); - let min_voting_power = min_active_validator - .map(|v| v.voting_power) + let min_bonded_stake = min_active_validator + .map(|v| v.bonded_stake) .unwrap_or_default(); - if voting_power_post > min_voting_power { + if tokens_post > min_bonded_stake { let deactivate_min = validator_set.active.pop_first_shim(); let popped = @@ -1889,10 +1943,10 @@ fn update_validator_set( ); let max_inactive_validator = validator_set.inactive.last_shim(); - let max_voting_power = max_inactive_validator - .map(|v| v.voting_power) + let max_bonded_stake = max_inactive_validator + .map(|v| v.bonded_stake) .unwrap_or_default(); - if voting_power_post < max_voting_power { + if tokens_post < max_bonded_stake { let activate_max = validator_set.inactive.pop_last_shim(); let popped = @@ -1915,67 +1969,6 @@ fn update_validator_set( ) } -/// Update the validator's voting power and the total voting power. -fn update_voting_powers( - params: &PosParams, - change_offset: DynEpochOffset, - validator_total_deltas: &ValidatorTotalDeltas, - validator_voting_power: &mut ValidatorVotingPowers, - total_voting_power: &mut TotalVotingPowers, - current_epoch: Epoch, -) -> Result<(), TryFromIntError> -where - TokenChange: Display - + Debug - + Default - + Clone - + Copy - + Add - + Sub - + Into - + BorshDeserialize - + BorshSerialize - + BorshSchema, -{ - let change_offset = change_offset.value(params); - let start_epoch = current_epoch + change_offset; - // Update voting powers from the change offset to the the last epoch of - // voting powers data (unbonding epoch) - let epochs = start_epoch.iter_range( - DynEpochOffset::UnbondingLen.value(params) - change_offset + 1, - ); - for epoch in epochs { - // Recalculate validator's voting power from validator's total deltas - let total_deltas_at_pipeline = - validator_total_deltas.get(epoch).unwrap_or_default(); - let total_deltas_at_pipeline: i128 = total_deltas_at_pipeline.into(); - let total_deltas_at_pipeline: u64 = - TryFrom::try_from(total_deltas_at_pipeline).unwrap(); - let voting_power_at_pipeline = - validator_voting_power.get(epoch).unwrap_or_default(); - let voting_power_delta = VotingPowerDelta::try_from_tokens( - total_deltas_at_pipeline, - params, - )? - voting_power_at_pipeline; - - validator_voting_power.add_at_epoch( - voting_power_delta, - current_epoch, - epoch, - params, - ); - - // Update total voting power - total_voting_power.add_at_epoch( - voting_power_delta, - current_epoch, - epoch, - params, - ); - } - Ok(()) -} - struct WithdrawData where TokenAmount: Debug @@ -2042,8 +2035,9 @@ where for slash in &slashes { if slash.epoch >= *epoch_start && slash.epoch <= *epoch_end { let raw_delta: u64 = delta.into(); - let current_slashed = - TokenAmount::from(slash.rate * raw_delta); + let current_slashed = TokenAmount::from(decimal_mult_u64( + slash.rate, raw_delta, + )); slashed += current_slashed; delta -= current_slashed; } diff --git a/proof_of_stake/src/parameters.rs b/proof_of_stake/src/parameters.rs index 7ee0abdf98..5e0ee682a6 100644 --- a/proof_of_stake/src/parameters.rs +++ b/proof_of_stake/src/parameters.rs @@ -1,11 +1,13 @@ //! Proof-of-Stake system parameters use borsh::{BorshDeserialize, BorshSerialize}; +use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; +use rust_decimal_macros::dec; use thiserror::Error; -use crate::types::BasisPoints; - -/// Proof-of-Stake system parameters +/// Proof-of-Stake system parameters, set at genesis and can only be changed via +/// governance #[derive(Debug, Clone, BorshDeserialize, BorshSerialize)] pub struct PosParams { /// A maximum number of active validators @@ -18,36 +20,46 @@ pub struct PosParams { /// `n + slashable_period_len` epoch. /// The value must be greater or equal to `pipeline_len`. pub unbonding_len: u64, - /// Used in validators' voting power calculation. Given in basis points - /// (voting power per ten thousand tokens). - pub votes_per_token: BasisPoints, + /// The voting power per fundamental unit of the staking token (namnam). + /// Used in validators' voting power calculation to interface with + /// tendermint. + pub tm_votes_per_token: Decimal, /// Amount of tokens rewarded to a validator for proposing a block - pub block_proposer_reward: u64, + pub block_proposer_reward: Decimal, /// Amount of tokens rewarded to each validator that voted on a block /// proposal - pub block_vote_reward: u64, + pub block_vote_reward: Decimal, + /// Maximum staking rewards rate per annum + pub max_inflation_rate: Decimal, + /// Target ratio of staked NAM tokens to total NAM tokens + pub target_staked_ratio: Decimal, /// Portion of validator's stake that should be slashed on a duplicate - /// vote. Given in basis points (slashed amount per ten thousand tokens). - pub duplicate_vote_slash_rate: BasisPoints, + /// vote. + pub duplicate_vote_min_slash_rate: Decimal, /// Portion of validator's stake that should be slashed on a light client - /// attack. Given in basis points (slashed amount per ten thousand tokens). - pub light_client_attack_slash_rate: BasisPoints, + /// attack. + pub light_client_attack_min_slash_rate: Decimal, } impl Default for PosParams { fn default() -> Self { Self { - max_validator_slots: 128, + max_validator_slots: 100, pipeline_len: 2, - unbonding_len: 6, - // 1 voting power per 1000 tokens - votes_per_token: BasisPoints::new(10), - block_proposer_reward: 100, - block_vote_reward: 1, - // slash 5% - duplicate_vote_slash_rate: BasisPoints::new(500), - // slash 5% - light_client_attack_slash_rate: BasisPoints::new(500), + unbonding_len: 21, + // 1 voting power per 1 fundamental token (10^6 per NAM or 1 per + // namnam) + tm_votes_per_token: dec!(1.0), + block_proposer_reward: dec!(0.125), + block_vote_reward: dec!(0.1), + // PoS inflation of 10% + max_inflation_rate: dec!(0.1), + // target staked ratio of 2/3 + target_staked_ratio: dec!(0.6667), + // slash 0.1% + duplicate_vote_min_slash_rate: dec!(0.001), + // slash 0.1% + light_client_attack_min_slash_rate: dec!(0.001), } } } @@ -61,7 +73,7 @@ pub enum ValidationError { )] TotalVotingPowerTooLarge(u64), #[error("Votes per token cannot be greater than 1, got {0}")] - VotesPerTokenGreaterThanOne(BasisPoints), + VotesPerTokenGreaterThanOne(Decimal), #[error("Pipeline length must be >= 2, got {0}")] PipelineLenTooShort(u64), #[error( @@ -71,11 +83,14 @@ pub enum ValidationError { UnbondingLenTooShort(u64, u64), } +/// The number of fundamental units per whole token of the native staking token +pub const TOKENS_PER_NAM: u64 = 1_000_000; + /// From Tendermint: const MAX_TOTAL_VOTING_POWER: i64 = i64::MAX / 8; /// Assuming token amount is `u64` in micro units. -const TOKEN_MAX_AMOUNT: u64 = u64::MAX / 1_000_000; +const TOKEN_MAX_AMOUNT: u64 = u64::MAX / TOKENS_PER_NAM; impl PosParams { /// Validate PoS parameters values. Returns an empty list if the values are @@ -98,25 +113,26 @@ impl PosParams { // Check maximum total voting power cannot get larger than what // Tendermint allows - let max_total_voting_power = self.max_validator_slots - * (self.votes_per_token * TOKEN_MAX_AMOUNT); + let max_total_voting_power = Decimal::from(self.max_validator_slots) + * self.tm_votes_per_token + * Decimal::from(TOKEN_MAX_AMOUNT); match i64::try_from(max_total_voting_power) { Ok(max_total_voting_power_i64) => { if max_total_voting_power_i64 > MAX_TOTAL_VOTING_POWER { errors.push(ValidationError::TotalVotingPowerTooLarge( - max_total_voting_power, + max_total_voting_power.to_u64().unwrap(), )) } } Err(_) => errors.push(ValidationError::TotalVotingPowerTooLarge( - max_total_voting_power, + max_total_voting_power.to_u64().unwrap(), )), } // Check that there is no more than 1 vote per token - if self.votes_per_token > BasisPoints::new(10_000) { + if self.tm_votes_per_token > dec!(1.0) { errors.push(ValidationError::VotesPerTokenGreaterThanOne( - self.votes_per_token, + self.tm_votes_per_token, )) } @@ -150,6 +166,7 @@ mod tests { #[cfg(any(test, feature = "testing"))] pub mod testing { use proptest::prelude::*; + use rust_decimal::Decimal; use super::*; @@ -161,17 +178,24 @@ pub mod testing { // `unbonding_len` > `pipeline_len` unbonding_len in pipeline_len + 1..pipeline_len + 8, pipeline_len in Just(pipeline_len), - votes_per_token in 1..10_001_u64) + tm_votes_per_token in 1..10_001_u64) -> PosParams { PosParams { max_validator_slots, pipeline_len, unbonding_len, - votes_per_token: BasisPoints::new(votes_per_token), + tm_votes_per_token: Decimal::from(tm_votes_per_token) / dec!(10_000), // The rest of the parameters that are not being used in the PoS // VP are constant for now ..Default::default() } } } + + /// Get an arbitrary rate - a Decimal value between 0 and 1 inclusive, with + /// some fixed precision + pub fn arb_rate() -> impl Strategy { + (0..=100_000_u64) + .prop_map(|num| Decimal::from(num) / Decimal::from(100_000_u64)) + } } diff --git a/proof_of_stake/src/rewards.rs b/proof_of_stake/src/rewards.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index 5f5ac6846d..b8e47193bd 100644 --- a/proof_of_stake/src/types.rs +++ b/proof_of_stake/src/types.rs @@ -5,10 +5,10 @@ use std::collections::{BTreeSet, HashMap}; use std::convert::TryFrom; use std::fmt::Display; use std::hash::Hash; -use std::num::TryFromIntError; -use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; +use std::ops::{Add, AddAssign, Sub}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use rust_decimal::prelude::{Decimal, ToPrimitive}; use crate::epoched::{ Epoched, EpochedDelta, OffsetPipelineLen, OffsetUnbondingLen, @@ -21,11 +21,9 @@ pub type ValidatorConsensusKeys = /// Epoched validator's state. pub type ValidatorStates = Epoched; /// Epoched validator's total deltas. -pub type ValidatorTotalDeltas = +pub type ValidatorDeltas = EpochedDelta; -/// Epoched validator's voting power. -pub type ValidatorVotingPowers = - EpochedDelta; + /// Epoched bond. pub type Bonds = EpochedDelta, OffsetUnbondingLen>; @@ -35,8 +33,11 @@ pub type Unbonds = /// Epoched validator set. pub type ValidatorSets
= Epoched, OffsetUnbondingLen>; -/// Epoched total voting power. -pub type TotalVotingPowers = EpochedDelta; +/// Epoched total deltas. +pub type TotalDeltas = + EpochedDelta; +/// Epoched validator commission rate +pub type CommissionRates = Epoched; /// Epoch identifier. Epochs are identified by consecutive natural numbers. /// @@ -59,40 +60,6 @@ pub type TotalVotingPowers = EpochedDelta; )] pub struct Epoch(u64); -/// Voting power is calculated from staked tokens. -#[derive( - Debug, - Default, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - BorshDeserialize, - BorshSerialize, - BorshSchema, -)] -pub struct VotingPower(u64); - -/// A change of voting power. -#[derive( - Debug, - Default, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - BorshDeserialize, - BorshSerialize, - BorshSchema, -)] -pub struct VotingPowerDelta(i64); - /// A genesis validator definition. #[derive( Debug, @@ -108,15 +75,14 @@ pub struct VotingPowerDelta(i64); pub struct GenesisValidator { /// Validator's address pub address: Address, - /// An address to which any staking rewards will be credited, must be - /// different from the `address` - pub staking_reward_address: Address, /// Staked tokens are put into a self-bond pub tokens: Token, /// A public key used for signing validator's consensus actions pub consensus_key: PK, - /// An public key associated with the staking reward address - pub staking_reward_key: PK, + /// Commission rate charged on rewards for delegators (bounded inside 0-1) + pub commission_rate: Decimal, + /// Maximum change in commission rate permitted per epoch + pub max_commission_rate_change: Decimal, } /// An update of the active and inactive validator set. @@ -128,13 +94,13 @@ pub enum ValidatorSetUpdate { Deactivated(PK), } -/// Active validator's consensus key and its voting power. +/// Active validator's consensus key and its bonded stake. #[derive(Debug, Clone)] pub struct ActiveValidator { /// A public key used for signing validator's consensus actions pub consensus_key: PK, - /// Voting power - pub voting_power: VotingPower, + /// Total bonded stake of the validator + pub bonded_stake: u64, } /// ID of a bond and/or an unbond. @@ -195,11 +161,11 @@ where + BorshSchema + BorshSerialize, { - /// The `voting_power` field must be on top, because lexicographic ordering + /// The `total_stake` field must be on top, because lexicographic ordering /// is based on the top-to-bottom declaration order and in the /// `ValidatorSet` the `WeightedValidator`s these need to be sorted by - /// the `voting_power`. - pub voting_power: VotingPower, + /// the `total_stake`. + pub bonded_stake: u64, /// Validator's address pub address: Address, } @@ -221,8 +187,8 @@ where fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "{} with voting power {}", - self.address, self.voting_power + "{} with bonded stake {}", + self.address, self.bonded_stake ) } } @@ -324,7 +290,7 @@ pub struct Slash { /// A type of slashsable event. pub r#type: SlashType, /// A rate is the portion of staked tokens that are slashed. - pub rate: BasisPoints, + pub rate: Decimal, } /// Slashes applied to validator, to punish byzantine behavior by removing @@ -340,110 +306,12 @@ pub enum SlashType { LightClientAttack, } -/// ‱ (Parts per ten thousand). This can be multiplied by any type that -/// implements [`Into`] or [`Into`]. -#[derive( - Debug, - Clone, - Copy, - BorshDeserialize, - BorshSerialize, - BorshSchema, - PartialOrd, - Ord, - PartialEq, - Eq, - Hash, -)] -pub struct BasisPoints(u64); - /// Derive Tendermint raw hash from the public key pub trait PublicKeyTmRawHash { /// Derive Tendermint raw hash from the public key fn tm_raw_hash(&self) -> String; } -impl VotingPower { - /// Convert token amount into a voting power. - pub fn from_tokens(tokens: impl Into, params: &PosParams) -> Self { - // The token amount is expected to be in micro units - let whole_tokens = tokens.into() / 1_000_000; - Self(params.votes_per_token * whole_tokens) - } -} - -impl Add for VotingPower { - type Output = VotingPower; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl Sub for VotingPower { - type Output = VotingPower; - - fn sub(self, rhs: Self) -> Self::Output { - Self(self.0 - rhs.0) - } -} - -impl VotingPowerDelta { - /// Try to convert token change into a voting power change. - pub fn try_from_token_change( - change: impl Into, - params: &PosParams, - ) -> Result { - // The token amount is expected to be in micro units - let whole_tokens = change.into() / 1_000_000; - let delta: i128 = params.votes_per_token * whole_tokens; - let delta: i64 = TryFrom::try_from(delta)?; - Ok(Self(delta)) - } - - /// Try to convert token amount into a voting power change. - pub fn try_from_tokens( - tokens: impl Into, - params: &PosParams, - ) -> Result { - // The token amount is expected to be in micro units - let whole_tokens = tokens.into() / 1_000_000; - let delta: i64 = - TryFrom::try_from(params.votes_per_token * whole_tokens)?; - Ok(Self(delta)) - } -} - -impl TryFrom for VotingPowerDelta { - type Error = TryFromIntError; - - fn try_from(value: VotingPower) -> Result { - let delta: i64 = TryFrom::try_from(value.0)?; - Ok(Self(delta)) - } -} - -impl TryFrom for VotingPower { - type Error = TryFromIntError; - - fn try_from(value: VotingPowerDelta) -> Result { - let vp: u64 = TryFrom::try_from(value.0)?; - Ok(Self(vp)) - } -} - -impl Display for VotingPower { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Display for VotingPowerDelta { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - impl Epoch { /// Iterate a range of consecutive epochs starting from `self` of a given /// length. Work-around for `Step` implementation pending on stabilization of . @@ -639,90 +507,14 @@ where } } -impl From for VotingPower { - fn from(voting_power: u64) -> Self { - Self(voting_power) - } -} - -impl From for u64 { - fn from(vp: VotingPower) -> Self { - vp.0 - } -} - -impl AddAssign for VotingPower { - fn add_assign(&mut self, rhs: Self) { - self.0 += rhs.0 - } -} - -impl SubAssign for VotingPower { - fn sub_assign(&mut self, rhs: Self) { - self.0 -= rhs.0 - } -} - -impl From for VotingPowerDelta { - fn from(delta: i64) -> Self { - Self(delta) - } -} - -impl From for i64 { - fn from(vp: VotingPowerDelta) -> Self { - vp.0 - } -} - -impl Add for VotingPowerDelta { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl AddAssign for VotingPowerDelta { - fn add_assign(&mut self, rhs: Self) { - self.0 += rhs.0 - } -} - -impl Sub for VotingPowerDelta { - type Output = Self; - - fn sub(self, rhs: Self) -> Self::Output { - Self(self.0 - rhs.0) - } -} - -impl Sub for VotingPowerDelta { - type Output = Self; - - fn sub(self, rhs: i64) -> Self::Output { - Self(self.0 - rhs) - } -} - -impl GenesisValidator -where - Token: Copy + Into, -{ - /// Calculate validator's voting power - pub fn voting_power(&self, params: &PosParams) -> VotingPower { - VotingPower::from_tokens(self.tokens, params) - } -} - impl SlashType { /// Get the slash rate applicable to the given slash type from the PoS /// parameters. - pub fn get_slash_rate(&self, params: &PosParams) -> BasisPoints { + pub fn get_slash_rate(&self, params: &PosParams) -> Decimal { match self { - SlashType::DuplicateVote => params.duplicate_vote_slash_rate, + SlashType::DuplicateVote => params.duplicate_vote_min_slash_rate, SlashType::LightClientAttack => { - params.light_client_attack_slash_rate + params.light_client_attack_min_slash_rate } } } @@ -737,35 +529,30 @@ impl Display for SlashType { } } -impl BasisPoints { - /// Initialize basis points from an integer. - pub fn new(value: u64) -> Self { - Self(value) - } -} - -impl Display for BasisPoints { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}‱", self.0) - } +/// Multiply a value of type Decimal with one of type u64 and then return the +/// truncated u64 +pub fn decimal_mult_u64(dec: Decimal, int: u64) -> u64 { + let prod = dec * Decimal::from(int); + // truncate the number to the floor + prod.to_u64().expect("Product is out of bounds") } -impl Mul for BasisPoints { - type Output = u64; - - fn mul(self, rhs: u64) -> Self::Output { - // TODO checked arithmetics - rhs * self.0 / 10_000 - } +/// Multiply a value of type Decimal with one of type i128 and then return the +/// truncated i128 +pub fn decimal_mult_i128(dec: Decimal, int: i128) -> i128 { + let prod = dec * Decimal::from(int); + // truncate the number to the floor + prod.to_i128().expect("Product is out of bounds") } -impl Mul for BasisPoints { - type Output = i128; - - fn mul(self, rhs: i128) -> Self::Output { - // TODO checked arithmetics - rhs * self.0 as i128 / 10_000 - } +/// Calculate voting power in the tendermint context (which is stored as i64) +/// from the number of tokens +pub fn into_tm_voting_power( + votes_per_token: Decimal, + tokens: impl Into, +) -> i64 { + let prod = decimal_mult_u64(votes_per_token, tokens.into()); + i64::try_from(prod).expect("Invalid voting power") } #[cfg(test)] diff --git a/proof_of_stake/src/validation.rs b/proof_of_stake/src/validation.rs index faef13f457..79749f9ab1 100644 --- a/proof_of_stake/src/validation.rs +++ b/proof_of_stake/src/validation.rs @@ -10,16 +10,17 @@ use std::ops::{Add, AddAssign, Neg, Sub, SubAssign}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use derivative::Derivative; +use rust_decimal::Decimal; use thiserror::Error; use crate::btree_set::BTreeSetShims; use crate::epoched::DynEpochOffset; use crate::parameters::PosParams; use crate::types::{ - BondId, Bonds, Epoch, PublicKeyTmRawHash, Slash, Slashes, - TotalVotingPowers, Unbonds, ValidatorConsensusKeys, ValidatorSets, - ValidatorState, ValidatorStates, ValidatorTotalDeltas, - ValidatorVotingPowers, VotingPower, VotingPowerDelta, WeightedValidator, + decimal_mult_i128, decimal_mult_u64, BondId, Bonds, CommissionRates, Epoch, + PublicKeyTmRawHash, Slash, Slashes, TotalDeltas, Unbonds, + ValidatorConsensusKeys, ValidatorDeltas, ValidatorSets, ValidatorState, + ValidatorStates, WeightedValidator, }; #[allow(missing_docs)] @@ -46,21 +47,22 @@ where InvalidNewValidatorState(u64), #[error("Invalid validator state update in epoch {0}")] InvalidValidatorStateUpdate(u64), + #[error("Unexpectedly missing consensus key value for validator {0}")] + ValidatorConsensusKeyIsRequired(Address), #[error("Missing new validator consensus key in epoch {0}")] MissingNewValidatorConsensusKey(u64), #[error("Invalid validator consensus key update in epoch {0}")] InvalidValidatorConsensusKeyUpdate(u64), - #[error("Validator staking reward address is required for validator {0}")] - StakingRewardAddressIsRequired(Address), - #[error( - "Staking reward address must be different from the validator's \ - address {0}" - )] - StakingRewardAddressEqValidator(Address), - #[error("Unexpectedly missing total deltas value for validator {0}")] - MissingValidatorTotalDeltas(Address), - #[error("The sum of total deltas for validator {0} are negative")] - NegativeValidatorTotalDeltasSum(Address), + #[error("Unexpectedly missing commission rate value for validator {0}")] + ValidatorCommissionRateIsRequired(Address), + #[error("Missing new validator commission rate in epoch {0}")] + MissingNewValidatorCommissionRate(u64), + #[error("Invalid validator commission rate update in epoch {0}")] + InvalidValidatorCommissionRateUpdate(u64), + #[error("Unexpectedly missing deltas value for validator {0}")] + MissingValidatorDeltas(Address), + #[error("The sum of deltas for validator {0} is negative")] + NegativeValidatorDeltasSum(Address), #[error("Unexpectedly missing balance value")] MissingBalance, #[error("Last update should be equal to the current epoch")] @@ -149,24 +151,10 @@ where ValidatorSetNotUpdated, #[error("Invalid voting power changes")] InvalidVotingPowerChanges, - #[error( - "Invalid validator {0} voting power changes. Expected {1}, but got \ - {2:?}" - )] - InvalidValidatorVotingPowerChange( - Address, - VotingPower, - Option, - ), #[error("Unexpectedly missing total voting power")] MissingTotalVotingPower, #[error("Total voting power should be updated when voting powers change")] TotalVotingPowerNotUpdated, - #[error( - "Invalid total voting power change in epoch {0}. Expected {1}, but \ - got {2}" - )] - InvalidTotalVotingPowerChange(u64, VotingPowerDelta, VotingPowerDelta), #[error("Invalid address raw hash, got {0}, expected {1}")] InvalidAddressRawHash(String, String), #[error("Invalid address raw hash update")] @@ -177,6 +165,16 @@ where NewValidatorMissingInValidatorSet(Address), #[error("Validator set has not been updated for new validators.")] MissingValidatorSetUpdate, + #[error( + "Changing the maximum commission rate change per epoch for validator \ + {0} is forbidden." + )] + ValidatorMaxCommissionRateChangeForbidden(Address), + #[error( + "Invalid value of maximum commission rate change per epoch for \ + validator {0}, got {1}." + )] + InvalidMaxCommissionRateChange(Address, Decimal), } /// An update of PoS data. @@ -245,12 +243,12 @@ where /// Validator's address address: Address, /// Validator's data update - update: ValidatorUpdate, + update: ValidatorUpdate, }, /// Validator set update ValidatorSet(Data>), - /// Total voting power update - TotalVotingPower(Data), + /// Total deltas update + TotalDeltas(Data>), /// Validator's address raw hash ValidatorAddressRawHash { /// Raw hash value @@ -262,9 +260,8 @@ where /// An update of a validator's data. #[derive(Clone, Debug)] -pub enum ValidatorUpdate +pub enum ValidatorUpdate where - Address: Clone + Debug, TokenChange: Display + Debug + Default @@ -283,12 +280,12 @@ where State(Data), /// Consensus key update ConsensusKey(Data>), - /// Staking reward address update - StakingRewardAddress(Data
), - /// Total deltas update - TotalDeltas(Data>), - /// Voting power update - VotingPowerUpdate(Data), + /// Validator deltas update + ValidatorDeltas(Data>), + /// Commission rate update + CommissionRate(Data, Option), + /// Maximum commission rate change update + MaxCommissionRateChange(Data), } /// Data update with prior and posterior state. @@ -312,10 +309,10 @@ pub struct NewValidator { has_state: bool, has_consensus_key: Option, has_total_deltas: bool, - has_voting_power: bool, - has_staking_reward_address: bool, has_address_raw_hash: Option, - voting_power: VotingPower, + bonded_stake: u64, + has_commission_rate: bool, + has_max_commission_rate_change: bool, } /// Validation constants @@ -407,20 +404,19 @@ where let mut errors = vec![]; let Accumulator { - balance_delta, - bond_delta, - unbond_delta, - total_deltas, - total_stake_by_epoch, - expected_total_voting_power_delta_by_epoch, - voting_power_by_epoch, - validator_set_pre, - validator_set_post, - total_voting_power_delta_by_epoch, - new_validators, - } = Validate::::accumulate_changes( - changes, params, &constants, &mut errors - ); + balance_delta, + bond_delta, + unbond_delta, + total_deltas, + total_stake_by_epoch, + validator_set_pre, + validator_set_post, + total_deltas_by_epoch: _, + bonded_stake_by_epoch, + new_validators, + } = Validate::::accumulate_changes( + changes, params, &constants, &mut errors + ); // Check total deltas against bonds for (validator, total_delta) in total_deltas.iter() { @@ -437,7 +433,7 @@ where // Check that all bonds also have a total deltas update for validator in bond_delta.keys() { if !total_deltas.contains_key(validator) { - errors.push(Error::MissingValidatorTotalDeltas(validator.clone())) + errors.push(Error::MissingValidatorDeltas(validator.clone())) } } // Check that all positive unbond deltas also have a total deltas update. @@ -447,7 +443,7 @@ where if *delta > TokenChange::default() && !total_deltas.contains_key(validator) { - errors.push(Error::MissingValidatorTotalDeltas(validator.clone())); + errors.push(Error::MissingValidatorDeltas(validator.clone())); } } @@ -466,8 +462,8 @@ where Some(min_active_validator), ) = (post.inactive.last_shim(), post.active.first_shim()) { - if max_inactive_validator.voting_power - > min_active_validator.voting_power + if max_inactive_validator.bonded_stake + > min_active_validator.bonded_stake { errors.push(Error::ValidatorSetOutOfOrder( max_inactive_validator.clone(), @@ -487,14 +483,12 @@ where for validator in &post.active { match total_stakes.get(&validator.address) { Some((_stake_pre, stake_post)) => { - let voting_power = VotingPower::from_tokens( - *stake_post, - params, - ); // Any validator who's total deltas changed, // should // be up-to-date - if validator.voting_power != voting_power { + if validator.bonded_stake + != Into::::into(*stake_post) + { errors.push( Error::InvalidActiveValidator( validator.clone(), @@ -534,14 +528,11 @@ where .get(&validator.address) }) { - let voting_power = - VotingPower::from_tokens( + is_valid = validator + .bonded_stake + == Into::::into( *last_total_stake, - params, ); - is_valid = validator - .voting_power - == voting_power; break; } else { search_epoch -= 1; @@ -564,11 +555,9 @@ where // be up-to-date match total_stakes.get(&validator.address) { Some((_stake_pre, stake_post)) => { - let voting_power = VotingPower::from_tokens( - *stake_post, - params, - ); - if validator.voting_power != voting_power { + if validator.bonded_stake + != Into::::into(*stake_post) + { errors.push( Error::InvalidInactiveValidator( validator.clone(), @@ -608,14 +597,11 @@ where .get(&validator.address) }) { - let voting_power = - VotingPower::from_tokens( + is_valid = validator + .bonded_stake + == Into::::into( *last_total_stake, - params, ); - is_valid = validator - .voting_power - == voting_power; break; } else { search_epoch -= 1; @@ -645,12 +631,8 @@ where for (validator, (_stake_pre, tokens_at_epoch)) in total_stake { - let voting_power = VotingPower::from_tokens( - *tokens_at_epoch, - params, - ); let weighted_validator = WeightedValidator { - voting_power, + bonded_stake: (*tokens_at_epoch).into(), address: validator.clone(), }; if !post.active.contains(&weighted_validator) { @@ -679,121 +661,10 @@ where } } } - } else if !voting_power_by_epoch.is_empty() { + } else if !bonded_stake_by_epoch.is_empty() { errors.push(Error::ValidatorSetNotUpdated) } - // Check voting power changes against validator total stakes - for (epoch, voting_powers) in &voting_power_by_epoch { - let mut epoch = *epoch; - let mut total_stakes; - // Try to find the stakes for this epoch - loop { - total_stakes = total_stake_by_epoch.get(&epoch); - // If there's no stake values in this epoch, it means it hasn't - // changed, so we can try to find it from predecessor epochs - if total_stakes.is_none() && epoch > current_epoch { - epoch = epoch - 1; - } else { - break; - } - } - if let Some(total_stakes) = total_stakes { - for (validator, voting_power) in voting_powers { - if let Some((_stake_pre, stake_post)) = - total_stakes.get(validator) - { - let voting_power_from_stake = - VotingPower::from_tokens(*stake_post, params); - if *voting_power != voting_power_from_stake { - errors.push(Error::InvalidVotingPowerChanges) - } - } else { - errors.push(Error::InvalidVotingPowerChanges) - } - } - } else { - errors.push(Error::InvalidVotingPowerChanges); - } - } - - let mut prev_epoch = None; - // Check expected voting power changes at each epoch - for (epoch, expected_total_stakes) in total_stake_by_epoch { - for (validator, (stake_pre, stake_post)) in expected_total_stakes { - let voting_power_pre = VotingPower::from_tokens(stake_pre, params); - let expected_voting_power = - VotingPower::from_tokens(stake_post, params); - match voting_power_by_epoch - .get(&epoch) - .and_then(|voting_powers| voting_powers.get(&validator)) - { - Some(actual_voting_power) => { - if *actual_voting_power != expected_voting_power { - errors.push(Error::InvalidValidatorVotingPowerChange( - validator, - expected_voting_power, - Some(*actual_voting_power), - )); - } - } - None => { - // If there's no voting power change, it's expected that - // there should be no record in `voting_power_by_epoch`. - if voting_power_pre == expected_voting_power { - continue; - } - - // If there's no actual voting power change present in this - // epoch, it might have been unbond that - // didn't affect the voting power bundled - // together with a bond with the same ID. - if let Some(prev_epoch) = prev_epoch.as_ref() { - if let Some(actual_voting_power) = - voting_power_by_epoch.get(prev_epoch) - { - // This is the case when there's some voting power - // change at the previous epoch that is equal to - // the expected value, because then the voting power - // at this epoch is the same. - if actual_voting_power.get(&validator) - == Some(&expected_voting_power) - { - continue; - } - } - } - errors.push(Error::InvalidValidatorVotingPowerChange( - validator, - expected_voting_power, - None, - )) - } - } - } - prev_epoch = Some(epoch); - } - - // Check expected total voting power change - for (epoch, expected_delta) in expected_total_voting_power_delta_by_epoch { - match total_voting_power_delta_by_epoch.get(&epoch) { - Some(actual_delta) => { - if *actual_delta != expected_delta { - errors.push(Error::InvalidTotalVotingPowerChange( - epoch.into(), - expected_delta, - *actual_delta, - )); - } - } - None => { - if expected_delta != VotingPowerDelta::default() { - errors.push(Error::TotalVotingPowerNotUpdated) - } - } - } - } - // Check new validators are initialized with all the required fields if !new_validators.is_empty() { match &validator_set_post { @@ -805,16 +676,16 @@ where has_state, has_consensus_key, has_total_deltas, - has_voting_power, - has_staking_reward_address, has_address_raw_hash, - voting_power, + bonded_stake, + has_commission_rate, + has_max_commission_rate_change, } = &new_validator; // The new validator must have set all the required fields if !(*has_state && *has_total_deltas - && *has_voting_power - && *has_staking_reward_address) + && *has_commission_rate + && *has_max_commission_rate_change) { errors.push(Error::InvalidNewValidator( address.clone(), @@ -837,7 +708,7 @@ where )), } let weighted_validator = WeightedValidator { - voting_power: *voting_power, + bonded_stake: *bonded_stake, address: address.clone(), }; match validator_sets { @@ -944,13 +815,10 @@ where total_stake_by_epoch: HashMap>, /// Total voting power delta calculated from validators' total deltas - expected_total_voting_power_delta_by_epoch: - HashMap, - /// Changes of validators' voting power data - voting_power_by_epoch: HashMap>, + total_deltas_by_epoch: HashMap, + bonded_stake_by_epoch: HashMap>, validator_set_pre: Option>, validator_set_post: Option>, - total_voting_power_delta_by_epoch: HashMap, new_validators: HashMap>, } @@ -1013,11 +881,10 @@ where unbond_delta: Default::default(), total_deltas: Default::default(), total_stake_by_epoch: Default::default(), - expected_total_voting_power_delta_by_epoch: Default::default(), - voting_power_by_epoch: Default::default(), + total_deltas_by_epoch: Default::default(), + bonded_stake_by_epoch: Default::default(), validator_set_pre: Default::default(), validator_set_post: Default::default(), - total_voting_power_delta_by_epoch: Default::default(), new_validators: Default::default(), } } @@ -1090,7 +957,7 @@ where { fn accumulate_changes( changes: Vec>, - params: &PosParams, + _params: &PosParams, constants: &Constants, errors: &mut Vec>, ) -> Accumulator { @@ -1104,11 +971,10 @@ where unbond_delta, total_deltas, total_stake_by_epoch, - expected_total_voting_power_delta_by_epoch, - voting_power_by_epoch, + total_deltas_by_epoch, + bonded_stake_by_epoch: _, validator_set_pre, validator_set_post, - total_voting_power_delta_by_epoch, new_validators, } = &mut accumulator; @@ -1129,16 +995,7 @@ where address, data, ), - StakingRewardAddress(data) => { - Self::validator_staking_reward_address( - errors, - new_validators, - address, - data, - ) - } - - TotalDeltas(data) => Self::validator_total_deltas( + ValidatorDeltas(data) => Self::validator_deltas( constants, errors, total_deltas, @@ -1147,16 +1004,24 @@ where address, data, ), - VotingPowerUpdate(data) => Self::validator_voting_power( - params, - constants, - errors, - voting_power_by_epoch, - expected_total_voting_power_delta_by_epoch, - new_validators, - address, - data, - ), + CommissionRate(data, max_change) => { + Self::validator_commission_rate( + constants, + errors, + new_validators, + address, + data, + max_change, + ) + } + MaxCommissionRateChange(data) => { + Self::validator_max_commission_rate_change( + errors, + new_validators, + address, + data, + ) + } }, Balance(data) => Self::balance(errors, balance_delta, data), Bond { id, data, slashes } => { @@ -1177,10 +1042,10 @@ where validator_set_post, data, ), - TotalVotingPower(data) => Self::total_voting_power( + TotalDeltas(data) => Self::total_deltas( constants, errors, - total_voting_power_delta_by_epoch, + total_deltas_by_epoch, data, ), ValidatorAddressRawHash { raw_hash, data } => { @@ -1321,39 +1186,13 @@ where } } (Some(_), None) => { - errors.push(Error::ValidatorStateIsRequired(address)) + errors.push(Error::ValidatorConsensusKeyIsRequired(address)) } (None, None) => {} } } - fn validator_staking_reward_address( - errors: &mut Vec>, - new_validators: &mut HashMap>, - address: Address, - data: Data
, - ) { - match (data.pre, data.post) { - (Some(_), Some(post)) => { - if post == address { - errors - .push(Error::StakingRewardAddressEqValidator(address)); - } - } - (None, Some(post)) => { - if post == address { - errors.push(Error::StakingRewardAddressEqValidator( - address.clone(), - )); - } - let validator = new_validators.entry(address).or_default(); - validator.has_staking_reward_address = true; - } - _ => errors.push(Error::StakingRewardAddressIsRequired(address)), - } - } - - fn validator_total_deltas( + fn validator_deltas( constants: &Constants, errors: &mut Vec>, total_deltas: &mut HashMap, @@ -1363,7 +1202,7 @@ where >, new_validators: &mut HashMap>, address: Address, - data: Data>, + data: Data>, ) { match (data.pre, data.post) { (Some(pre), Some(post)) => { @@ -1450,7 +1289,7 @@ where } } if post_deltas_sum < TokenChange::default() { - errors.push(Error::NegativeValidatorTotalDeltasSum( + errors.push(Error::NegativeValidatorDeltasSum( address.clone(), )) } @@ -1501,7 +1340,7 @@ where } } if deltas < TokenChange::default() { - errors.push(Error::NegativeValidatorTotalDeltasSum( + errors.push(Error::NegativeValidatorDeltasSum( address.clone(), )) } @@ -1512,112 +1351,121 @@ where } let validator = new_validators.entry(address).or_default(); validator.has_total_deltas = true; + validator.bonded_stake = + u64::try_from(Into::::into(deltas)) + .unwrap_or_default(); } (Some(_), None) => { - errors.push(Error::MissingValidatorTotalDeltas(address)) + errors.push(Error::MissingValidatorDeltas(address)) } (None, None) => {} } } - #[allow(clippy::too_many_arguments)] - fn validator_voting_power( - params: &PosParams, + fn validator_commission_rate( constants: &Constants, errors: &mut Vec>, - voting_power_by_epoch: &mut HashMap< - Epoch, - HashMap, - >, - expected_total_voting_power_delta_by_epoch: &mut HashMap< - Epoch, - VotingPowerDelta, - >, new_validators: &mut HashMap>, address: Address, - data: Data, + data: Data, + max_change: Option, ) { - match (&data.pre, data.post) { - (Some(_), Some(post)) | (None, Some(post)) => { + match (data.pre, data.post) { + (None, Some(post)) => { if post.last_update() != constants.current_epoch { errors.push(Error::InvalidLastUpdate) } - let mut voting_power = VotingPowerDelta::default(); - // Iter from the current epoch to the last epoch of - // `post` + // The value must be known at the pipeline epoch + match post.get(constants.pipeline_epoch) { + Some(_) => { + let validator = + new_validators.entry(address).or_default(); + validator.has_commission_rate = true; + } + _ => errors.push(Error::MissingNewValidatorCommissionRate( + constants.pipeline_epoch.into(), + )), + } + } + (Some(pre), Some(post)) => { + if post.last_update() != constants.current_epoch { + errors.push(Error::InvalidLastUpdate) + } + if max_change.is_none() { + errors.push(Error::InvalidLastUpdate) + } + // Before the pipeline epoch, the commission rate must not + // change for epoch in Epoch::iter_range( constants.current_epoch, - constants.unbonding_offset + 1, + constants.pipeline_offset, ) { - if let Some(delta_post) = post.get_delta_at_epoch(epoch) { - voting_power += *delta_post; - - // If the delta is not the same as in pre-state, - // accumulate the expected total voting power - // change - let delta_pre = data - .pre - .as_ref() - .and_then(|data| { - if epoch == constants.current_epoch { - // On the first epoch, we have to - // get the sum of all deltas at and - // before that epoch as the `pre` - // could have been set in an older - // epoch - data.get(epoch) - } else { - data.get_delta_at_epoch(epoch).copied() - } - }) - .unwrap_or_default(); - if delta_pre != *delta_post { - let current_delta = - expected_total_voting_power_delta_by_epoch - .entry(epoch) - .or_insert_with(Default::default); - *current_delta += *delta_post - delta_pre; - } - - let vp: i64 = Into::into(voting_power); - match u64::try_from(vp) { - Ok(vp) => { - let vp = VotingPower::from(vp); - voting_power_by_epoch - .entry(epoch) - .or_insert_with(HashMap::default) - .insert(address.clone(), vp); - } - Err(_) => { - errors.push(Error::InvalidValidatorVotingPower( - address.clone(), - vp, - )) - } + match (pre.get(epoch), post.get(epoch)) { + (Some(rate_pre), Some(rate_post)) + if rate_pre == rate_post => + { + continue; } + _ => errors.push( + Error::InvalidValidatorCommissionRateUpdate( + epoch.into(), + ), + ), } } - if data.pre.is_none() { - let validator = new_validators.entry(address).or_default(); - validator.has_voting_power = true; - validator.voting_power = post - .get_at_offset( - constants.current_epoch, - DynEpochOffset::PipelineLen, - params, - ) - .unwrap_or_default() - .try_into() - .unwrap_or_default() + // At the pipeline epoch, the rate must change by no larger than + // `max_change` relative to the previous epoch + match ( + pre.get(constants.pipeline_epoch - 1), + post.get(constants.pipeline_epoch), + ) { + (Some(prev_rate), Some(new_rate)) => { + if (new_rate - prev_rate).abs() + > max_change.unwrap_or_default() + { + errors.push( + Error::InvalidValidatorCommissionRateUpdate( + constants.pipeline_epoch.into(), + ), + ) + } + } + _ => errors.push(Error::ValidatorCommissionRateIsRequired( + address, + )), } } (Some(_), None) => { - errors.push(Error::MissingValidatorVotingPower(address)) + errors.push(Error::ValidatorCommissionRateIsRequired(address)) } (None, None) => {} } } + fn validator_max_commission_rate_change( + errors: &mut Vec>, + new_validators: &mut HashMap>, + address: Address, + data: Data, + ) { + match (data.pre, data.post) { + (None, Some(post)) => { + if post < Decimal::ZERO || post > Decimal::ONE { + errors.push(Error::InvalidMaxCommissionRateChange( + address.clone(), + post, + )) + } + + let validator = new_validators.entry(address).or_default(); + validator.has_max_commission_rate_change = true; + } + _ => errors.push(Error::ValidatorMaxCommissionRateChangeForbidden( + address, + )), + } + } + fn balance( errors: &mut Vec>, balance_delta: &mut TokenChange, @@ -1805,8 +1653,9 @@ where for slash in &slashes { if slash.epoch >= *start_epoch { let raw_delta: i128 = (*delta).into(); - let current_slashed = - TokenChange::from(slash.rate * raw_delta); + let current_slashed = TokenChange::from( + decimal_mult_i128(slash.rate, raw_delta), + ); *delta -= current_slashed; } } @@ -1871,7 +1720,7 @@ where if slash.epoch >= *start_epoch { let raw_delta: u64 = delta.into(); let current_slashed = TokenAmount::from( - slash.rate * raw_delta, + decimal_mult_u64(slash.rate, raw_delta), ); delta -= current_slashed; } @@ -1903,7 +1752,7 @@ where if slash.epoch >= *start_epoch { let raw_delta: u64 = delta.into(); let current_slashed = TokenAmount::from( - slash.rate * raw_delta, + decimal_mult_u64(slash.rate, raw_delta), ); delta -= current_slashed; } @@ -1997,8 +1846,9 @@ where && slash.epoch <= *end_epoch { let raw_delta: i128 = (*delta).into(); - let current_slashed = - TokenChange::from(slash.rate * raw_delta); + let current_slashed = TokenChange::from( + decimal_mult_i128(slash.rate, raw_delta), + ); *delta -= current_slashed; } } @@ -2034,7 +1884,7 @@ where { let raw_delta: u64 = delta.into(); let current_slashed = TokenAmount::from( - slash.rate * raw_delta, + decimal_mult_u64(slash.rate, raw_delta), ); delta -= current_slashed; } @@ -2067,7 +1917,7 @@ where { let raw_delta: u64 = delta.into(); let current_slashed = TokenAmount::from( - slash.rate * raw_delta, + decimal_mult_u64(slash.rate, raw_delta), ); delta -= current_slashed; } @@ -2104,14 +1954,11 @@ where } } - fn total_voting_power( + fn total_deltas( constants: &Constants, errors: &mut Vec>, - total_voting_power_delta_by_epoch: &mut HashMap< - Epoch, - VotingPowerDelta, - >, - data: Data, + total_delta_by_epoch: &mut HashMap, + data: Data>, ) { match (data.pre, data.post) { (Some(pre), Some(post)) => { @@ -2140,7 +1987,7 @@ where .copied() .unwrap_or_default(); if delta_pre != delta_post { - total_voting_power_delta_by_epoch + total_delta_by_epoch .insert(epoch, delta_post - delta_pre); } } diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 87bae5ef94..984849f49b 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -115,7 +115,8 @@ rand = {version = "0.8", optional = true} # TODO proptest rexports the RngCore trait but the re-implementations only work for version `0.8`. *sigh* rand_core = {version = "0.6", optional = true} rayon = {version = "=1.5.3", optional = true} -rust_decimal = "1.14.3" +rust_decimal = "1.26.1" +rust_decimal_macros = "1.26.1" serde = {version = "1.0.125", features = ["derive"]} serde_json = "1.0.62" sha2 = "0.9.3" diff --git a/shared/src/ledger/governance/utils.rs b/shared/src/ledger/governance/utils.rs index 152a629575..be66e098de 100644 --- a/shared/src/ledger/governance/utils.rs +++ b/shared/src/ledger/governance/utils.rs @@ -8,7 +8,8 @@ use thiserror::Error; use crate::ledger::governance::storage as gov_storage; use crate::ledger::pos; -use crate::ledger::pos::{BondId, Bonds, ValidatorSets, ValidatorTotalDeltas}; +use crate::ledger::pos::types::decimal_mult_u64; +use crate::ledger::pos::{BondId, Bonds, ValidatorDeltas, ValidatorSets}; use crate::ledger::storage::{DBIter, Storage, StorageHasher, DB}; use crate::types::address::Address; use crate::types::governance::{ProposalVote, TallyResult, VotePower}; @@ -198,7 +199,8 @@ fn apply_slashes( for slash in slashes { if Epoch::from(slash.epoch) >= epoch_start { let raw_delta: u64 = delta.into(); - let current_slashed = token::Amount::from(slash.rate * raw_delta); + let current_slashed = + token::Amount::from(decimal_mult_u64(slash.rate, raw_delta)); delta -= current_slashed; } } @@ -349,13 +351,13 @@ where D: DB + for<'iter> DBIter<'iter> + Sync + 'static, H: StorageHasher + Sync + 'static, { - let total_delta_key = pos::validator_total_deltas_key(validator); + let total_delta_key = pos::validator_deltas_key(validator); let (total_delta_bytes, _) = storage .read(&total_delta_key) .expect("Validator delta should be defined."); if let Some(total_delta_bytes) = total_delta_bytes { let total_delta = - ValidatorTotalDeltas::try_from_slice(&total_delta_bytes[..]).ok(); + ValidatorDeltas::try_from_slice(&total_delta_bytes[..]).ok(); if let Some(total_delta) = total_delta { let epoched_total_delta = total_delta.get(epoch); if let Some(epoched_total_delta) = epoched_total_delta { diff --git a/shared/src/ledger/inflation.rs b/shared/src/ledger/inflation.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/shared/src/ledger/parameters/mod.rs b/shared/src/ledger/parameters/mod.rs index fdc2a110d0..207a9008dd 100644 --- a/shared/src/ledger/parameters/mod.rs +++ b/shared/src/ledger/parameters/mod.rs @@ -4,6 +4,7 @@ pub mod storage; use std::collections::BTreeSet; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use rust_decimal::Decimal; use thiserror::Error; use self::storage as parameter_storage; @@ -114,14 +115,24 @@ pub enum WriteError { BorshSchema, )] pub struct Parameters { - /// Epoch duration + /// Epoch duration (read only) pub epoch_duration: EpochDuration, - /// Maximum expected time per block + /// Maximum expected time per block (read only) pub max_expected_time_per_block: DurationSecs, - /// Whitelisted validity predicate hashes + /// Whitelisted validity predicate hashes (read only) pub vp_whitelist: Vec, - /// Whitelisted tx hashes + /// Whitelisted tx hashes (read only) pub tx_whitelist: Vec, + /// Expected number of epochs per year (read only) + pub epochs_per_year: u64, + /// PoS gain p (read only) + pub pos_gain_p: Decimal, + /// PoS gain d (read only) + pub pos_gain_d: Decimal, + /// PoS staked ratio (read + write for every epoch) + pub staked_ratio: Decimal, + /// PoS inflation amount from the last epoch (read + write for every epoch) + pub pos_inflation_amount: u64, } /// Epoch duration. A new epoch begins as soon as both the `min_num_of_blocks` @@ -153,7 +164,7 @@ impl Parameters { H: ledger_storage::StorageHasher, { // write epoch parameters - let epoch_key = storage::get_epoch_storage_key(); + let epoch_key = storage::get_epoch_duration_storage_key(); let epoch_value = encode(&self.epoch_duration); storage.write(&epoch_key, epoch_value).expect( "Epoch parameters must be initialized in the genesis block", @@ -187,6 +198,41 @@ impl Parameters { "Max expected time per block parameters must be initialized \ in the genesis block", ); + + let epochs_per_year_key = storage::get_epochs_per_year_key(); + let epochs_per_year_value = encode(&self.epochs_per_year); + storage + .write(&epochs_per_year_key, epochs_per_year_value) + .expect( + "Epochs per year parameter must be initialized in the genesis \ + block", + ); + + let pos_gain_p_key = storage::get_pos_gain_p_key(); + let pos_gain_p_value = encode(&self.pos_gain_p); + storage.write(&pos_gain_p_key, pos_gain_p_value).expect( + "PoS P-gain parameter must be initialized in the genesis block", + ); + + let pos_gain_d_key = storage::get_pos_gain_d_key(); + let pos_gain_d_value = encode(&self.pos_gain_d); + storage.write(&pos_gain_d_key, pos_gain_d_value).expect( + "PoS D-gain parameter must be initialized in the genesis block", + ); + + let staked_ratio_key = storage::get_staked_ratio_key(); + let staked_ratio_val = encode(&self.staked_ratio); + storage.write(&staked_ratio_key, staked_ratio_val).expect( + "PoS staked ratio parameter must be initialized in the genesis \ + block", + ); + + let pos_inflation_key = storage::get_pos_inflation_amount_key(); + let pos_inflation_val = encode(&self.pos_inflation_amount); + storage.write(&pos_inflation_key, pos_inflation_val).expect( + "PoS inflation rate parameter must be initialized in the genesis \ + block", + ); } } @@ -242,7 +288,77 @@ where DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, H: ledger_storage::StorageHasher, { - let key = storage::get_epoch_storage_key(); + let key = storage::get_epoch_duration_storage_key(); + update(storage, value, key) +} + +/// Update the epochs_per_year parameter in storage. Returns the parameters and +/// gas cost. +pub fn update_epochs_per_year_parameter( + storage: &mut Storage, + value: &EpochDuration, +) -> std::result::Result +where + DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, + H: ledger_storage::StorageHasher, +{ + let key = storage::get_epochs_per_year_key(); + update(storage, value, key) +} + +/// Update the PoS P-gain parameter in storage. Returns the parameters and gas +/// cost. +pub fn update_pos_gain_p_parameter( + storage: &mut Storage, + value: &EpochDuration, +) -> std::result::Result +where + DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, + H: ledger_storage::StorageHasher, +{ + let key = storage::get_pos_gain_p_key(); + update(storage, value, key) +} + +/// Update the PoS D-gain parameter in storage. Returns the parameters and gas +/// cost. +pub fn update_pos_gain_d_parameter( + storage: &mut Storage, + value: &EpochDuration, +) -> std::result::Result +where + DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, + H: ledger_storage::StorageHasher, +{ + let key = storage::get_pos_gain_d_key(); + update(storage, value, key) +} + +/// Update the PoS staked ratio parameter in storage. Returns the parameters and +/// gas cost. +pub fn update_staked_ratio_parameter( + storage: &mut Storage, + value: &EpochDuration, +) -> std::result::Result +where + DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, + H: ledger_storage::StorageHasher, +{ + let key = storage::get_staked_ratio_key(); + update(storage, value, key) +} + +/// Update the PoS inflation rate parameter in storage. Returns the parameters +/// and gas cost. +pub fn update_pos_inflation_amount_parameter( + storage: &mut Storage, + value: &EpochDuration, +) -> std::result::Result +where + DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, + H: ledger_storage::StorageHasher, +{ + let key = storage::get_pos_inflation_amount_key(); update(storage, value, key) } @@ -268,7 +384,7 @@ where } /// Read the the epoch duration parameter from store -pub fn read_epoch_parameter( +pub fn read_epoch_duration_parameter( storage: &Storage, ) -> std::result::Result<(EpochDuration, u64), ReadError> where @@ -276,7 +392,7 @@ where H: ledger_storage::StorageHasher, { // read epoch - let epoch_key = storage::get_epoch_storage_key(); + let epoch_key = storage::get_epoch_duration_storage_key(); let (value, gas) = storage.read(&epoch_key).map_err(ReadError::StorageError)?; let epoch_duration: EpochDuration = @@ -295,8 +411,8 @@ where DB: ledger_storage::DB + for<'iter> ledger_storage::DBIter<'iter>, H: ledger_storage::StorageHasher, { - // read epoch - let (epoch_duration, gas_epoch) = read_epoch_parameter(storage) + // read epoch duration + let (epoch_duration, gas_epoch) = read_epoch_duration_parameter(storage) .expect("Couldn't read epoch duration parameters"); // read vp whitelist @@ -326,14 +442,72 @@ where decode(value.ok_or(ReadError::ParametersMissing)?) .map_err(ReadError::StorageTypeError)?; + // read epochs per year + let epochs_per_year_key = storage::get_epochs_per_year_key(); + let (value, gas_epy) = storage + .read(&epochs_per_year_key) + .map_err(ReadError::StorageError)?; + let epochs_per_year: u64 = + decode(value.ok_or(ReadError::ParametersMissing)?) + .map_err(ReadError::StorageTypeError)?; + + // read PoS gain P + let pos_gain_p_key = storage::get_pos_gain_p_key(); + let (value, gas_gain_p) = storage + .read(&pos_gain_p_key) + .map_err(ReadError::StorageError)?; + let pos_gain_p: Decimal = + decode(value.ok_or(ReadError::ParametersMissing)?) + .map_err(ReadError::StorageTypeError)?; + + // read PoS gain D + let pos_gain_d_key = storage::get_pos_gain_d_key(); + let (value, gas_gain_d) = storage + .read(&pos_gain_d_key) + .map_err(ReadError::StorageError)?; + let pos_gain_d: Decimal = + decode(value.ok_or(ReadError::ParametersMissing)?) + .map_err(ReadError::StorageTypeError)?; + + // read staked ratio + let staked_ratio_key = storage::get_staked_ratio_key(); + let (value, gas_staked) = storage + .read(&staked_ratio_key) + .map_err(ReadError::StorageError)?; + let staked_ratio: Decimal = + decode(value.ok_or(ReadError::ParametersMissing)?) + .map_err(ReadError::StorageTypeError)?; + + // read PoS inflation rate + let pos_inflation_key = storage::get_pos_inflation_amount_key(); + let (value, gas_reward) = storage + .read(&pos_inflation_key) + .map_err(ReadError::StorageError)?; + let pos_inflation_amount: u64 = + decode(value.ok_or(ReadError::ParametersMissing)?) + .map_err(ReadError::StorageTypeError)?; + Ok(( Parameters { epoch_duration, max_expected_time_per_block, vp_whitelist, tx_whitelist, + epochs_per_year, + pos_gain_p, + pos_gain_d, + staked_ratio, + pos_inflation_amount, }, - gas_epoch + gas_tx + gas_vp + gas_time, + gas_epoch + + gas_tx + + gas_vp + + gas_time + + gas_epy + + gas_gain_p + + gas_gain_d + + gas_staked + + gas_reward, )) } diff --git a/shared/src/ledger/parameters/storage.rs b/shared/src/ledger/parameters/storage.rs index 4041b82f2f..e2e25b92f5 100644 --- a/shared/src/ledger/parameters/storage.rs +++ b/shared/src/ledger/parameters/storage.rs @@ -6,6 +6,11 @@ const EPOCH_DURATION_KEY: &str = "epoch_duration"; const VP_WHITELIST_KEY: &str = "vp_whitelist"; const TX_WHITELIST_KEY: &str = "tx_whitelist"; const MAX_EXPECTED_TIME_PER_BLOCK_KEY: &str = "max_expected_time_per_block"; +const EPOCHS_PER_YEAR_KEY: &str = "epochs_per_year"; +const POS_GAIN_P_KEY: &str = "pos_gain_p"; +const POS_GAIN_D_KEY: &str = "pos_gain_d"; +const STAKED_RATIO_KEY: &str = "staked_ratio_key"; +const POS_INFLATION_AMOUNT_KEY: &str = "pos_inflation_amount_key"; /// Returns if the key is a parameter key. pub fn is_parameter_key(key: &Key) -> bool { @@ -14,14 +19,14 @@ pub fn is_parameter_key(key: &Key) -> bool { /// Returns if the key is a protocol parameter key. pub fn is_protocol_parameter_key(key: &Key) -> bool { - is_epoch_storage_key(key) + is_epoch_duration_storage_key(key) || is_max_expected_time_per_block_key(key) || is_tx_whitelist_key(key) || is_vp_whitelist_key(key) } /// Returns if the key is an epoch storage key. -pub fn is_epoch_storage_key(key: &Key) -> bool { +pub fn is_epoch_duration_storage_key(key: &Key) -> bool { matches!(&key.segments[..], [ DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(epoch_duration), @@ -52,8 +57,48 @@ pub fn is_vp_whitelist_key(key: &Key) -> bool { ] if addr == &ADDRESS && vp_whitelist == VP_WHITELIST_KEY) } +/// Returns if the key is the epoch_per_year key. +pub fn is_epochs_per_year_key(key: &Key) -> bool { + matches!(&key.segments[..], [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(epochs_per_year), + ] if addr == &ADDRESS && epochs_per_year == EPOCHS_PER_YEAR_KEY) +} + +/// Returns if the key is the pos_gain_p key. +pub fn is_pos_gain_p_key(key: &Key) -> bool { + matches!(&key.segments[..], [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(pos_gain_p), + ] if addr == &ADDRESS && pos_gain_p == POS_GAIN_P_KEY) +} + +/// Returns if the key is the pos_gain_d key. +pub fn is_pos_gain_d_key(key: &Key) -> bool { + matches!(&key.segments[..], [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(pos_gain_d), + ] if addr == &ADDRESS && pos_gain_d == POS_GAIN_D_KEY) +} + +/// Returns if the key is the staked ratio key. +pub fn is_staked_ratio_key(key: &Key) -> bool { + matches!(&key.segments[..], [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(staked_ratio), + ] if addr == &ADDRESS && staked_ratio == STAKED_RATIO_KEY) +} + +/// Returns if the key is the PoS reward rate key. +pub fn is_pos_inflation_amount_key(key: &Key) -> bool { + matches!(&key.segments[..], [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(pos_inflation_amount), + ] if addr == &ADDRESS && pos_inflation_amount == POS_INFLATION_AMOUNT_KEY) +} + /// Storage key used for epoch parameter. -pub fn get_epoch_storage_key() -> Key { +pub fn get_epoch_duration_storage_key() -> Key { Key { segments: vec![ DbKeySeg::AddressSeg(ADDRESS), @@ -82,7 +127,7 @@ pub fn get_tx_whitelist_storage_key() -> Key { } } -/// Storage key used for tx whitelist parameter. +/// Storage key used for max_epected_time_per_block parameter. pub fn get_max_expected_time_per_block_key() -> Key { Key { segments: vec![ @@ -91,3 +136,53 @@ pub fn get_max_expected_time_per_block_key() -> Key { ], } } + +/// Storage key used for epochs_per_year parameter. +pub fn get_epochs_per_year_key() -> Key { + Key { + segments: vec![ + DbKeySeg::AddressSeg(ADDRESS), + DbKeySeg::StringSeg(EPOCHS_PER_YEAR_KEY.to_string()), + ], + } +} + +/// Storage key used for pos_gain_p parameter. +pub fn get_pos_gain_p_key() -> Key { + Key { + segments: vec![ + DbKeySeg::AddressSeg(ADDRESS), + DbKeySeg::StringSeg(POS_GAIN_P_KEY.to_string()), + ], + } +} + +/// Storage key used for pos_gain_d parameter. +pub fn get_pos_gain_d_key() -> Key { + Key { + segments: vec![ + DbKeySeg::AddressSeg(ADDRESS), + DbKeySeg::StringSeg(POS_GAIN_D_KEY.to_string()), + ], + } +} + +/// Storage key used for staked ratio parameter. +pub fn get_staked_ratio_key() -> Key { + Key { + segments: vec![ + DbKeySeg::AddressSeg(ADDRESS), + DbKeySeg::StringSeg(STAKED_RATIO_KEY.to_string()), + ], + } +} + +/// Storage key used for the inflation amount parameter. +pub fn get_pos_inflation_amount_key() -> Key { + Key { + segments: vec![ + DbKeySeg::AddressSeg(ADDRESS), + DbKeySeg::StringSeg(POS_INFLATION_AMOUNT_KEY.to_string()), + ], + } +} diff --git a/shared/src/ledger/pos/mod.rs b/shared/src/ledger/pos/mod.rs index 0b1617c7c3..b8defc6adb 100644 --- a/shared/src/ledger/pos/mod.rs +++ b/shared/src/ledger/pos/mod.rs @@ -6,10 +6,10 @@ pub mod vp; pub use namada_proof_of_stake; pub use namada_proof_of_stake::parameters::PosParams; pub use namada_proof_of_stake::types::{ - self, Slash, Slashes, TotalVotingPowers, ValidatorStates, - ValidatorVotingPowers, + self, decimal_mult_u64, Slash, Slashes, ValidatorStates, }; use namada_proof_of_stake::PosBase; +use rust_decimal::Decimal; pub use storage::*; pub use vp::PosVP; @@ -31,6 +31,16 @@ pub fn staking_token_address() -> Address { address::nam() } +/// Calculate voting power in the tendermint context (which is stored as i64) +/// from the number of tokens +pub fn into_tm_voting_power( + votes_per_token: Decimal, + tokens: impl Into, +) -> i64 { + let prod = decimal_mult_u64(votes_per_token, tokens.into()); + i64::try_from(prod).expect("Invalid validator voting power (i64)") +} + /// Initialize storage in the genesis block. pub fn init_genesis_storage<'a, DB, H>( storage: &mut Storage, @@ -53,8 +63,8 @@ pub type ValidatorConsensusKeys = >; /// Alias for a PoS type with the same name with concrete type parameters -pub type ValidatorTotalDeltas = - namada_proof_of_stake::types::ValidatorTotalDeltas; +pub type ValidatorDeltas = + namada_proof_of_stake::types::ValidatorDeltas; /// Alias for a PoS type with the same name with concrete type parameters pub type Bonds = namada_proof_of_stake::types::Bonds; @@ -75,6 +85,11 @@ pub type GenesisValidator = namada_proof_of_stake::types::GenesisValidator< key::common::PublicKey, >; +/// Alias for a PoS type with the same name with concrete type parameters +pub type CommissionRates = namada_proof_of_stake::types::CommissionRates; +/// Alias for a PoS type with the same name with concrete type parameters +pub type TotalDeltas = namada_proof_of_stake::types::TotalDeltas; + impl From for namada_proof_of_stake::types::Epoch { fn from(epoch: Epoch) -> Self { let epoch: u64 = epoch.into(); @@ -123,6 +138,16 @@ impl From> } } +impl From> + for storage_api::Error +{ + fn from( + err: namada_proof_of_stake::CommissionRateChangeError
, + ) -> Self { + Self::new(err) + } +} + #[macro_use] mod macros { /// Implement `PosReadOnly` for a type that implements @@ -166,49 +191,47 @@ mod macros { Ok($crate::ledger::storage::types::decode(value).unwrap()) } - fn read_validator_staking_reward_address( + fn read_validator_consensus_key( &self, key: &Self::Address, - ) -> std::result::Result, Self::Error> { - let value = $crate::ledger::storage_api::StorageRead::read_bytes( - self, - &validator_staking_reward_address_key(key), - )?; + ) -> std::result::Result, Self::Error> { + let value = + $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_consensus_key_key(key))?; Ok(value.map(|value| $crate::ledger::storage::types::decode(value).unwrap())) } - fn read_validator_consensus_key( + fn read_validator_commission_rate( &self, key: &Self::Address, - ) -> std::result::Result, Self::Error> { + ) -> std::result::Result, Self::Error> { let value = - $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_consensus_key_key(key))?; + $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_commission_rate_key(key))?; Ok(value.map(|value| $crate::ledger::storage::types::decode(value).unwrap())) } - fn read_validator_state( + fn read_validator_max_commission_rate_change( &self, key: &Self::Address, - ) -> std::result::Result, Self::Error> { - let value = $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_state_key(key))?; + ) -> std::result::Result, Self::Error> { + let value = + $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_max_commission_rate_change_key(key))?; Ok(value.map(|value| $crate::ledger::storage::types::decode(value).unwrap())) } - fn read_validator_total_deltas( + fn read_validator_state( &self, key: &Self::Address, - ) -> std::result::Result, Self::Error> { - let value = - $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_total_deltas_key(key))?; + ) -> std::result::Result, Self::Error> { + let value = $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_state_key(key))?; Ok(value.map(|value| $crate::ledger::storage::types::decode(value).unwrap())) } - fn read_validator_voting_power( + fn read_validator_deltas( &self, key: &Self::Address, - ) -> std::result::Result, Self::Error> { + ) -> std::result::Result, Self::Error> { let value = - $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_voting_power_key(key))?; + $crate::ledger::storage_api::StorageRead::read_bytes(self, &validator_deltas_key(key))?; Ok(value.map(|value| $crate::ledger::storage::types::decode(value).unwrap())) } @@ -246,11 +269,11 @@ mod macros { Ok($crate::ledger::storage::types::decode(value).unwrap()) } - fn read_total_voting_power( + fn read_total_deltas( &self, - ) -> std::result::Result { + ) -> std::result::Result, Self::Error> { let value = - $crate::ledger::storage_api::StorageRead::read_bytes(self, &total_voting_power_key())?.unwrap(); + $crate::ledger::storage_api::StorageRead::read_bytes(self, &total_deltas_key())?.unwrap(); Ok($crate::ledger::storage::types::decode(value).unwrap()) } } diff --git a/shared/src/ledger/pos/storage.rs b/shared/src/ledger/pos/storage.rs index 366ce489b5..2aec4122cb 100644 --- a/shared/src/ledger/pos/storage.rs +++ b/shared/src/ledger/pos/storage.rs @@ -1,14 +1,12 @@ //! Proof-of-Stake storage keys and storage integration via [`PosBase`] trait. use namada_proof_of_stake::parameters::PosParams; -use namada_proof_of_stake::types::{ - TotalVotingPowers, ValidatorStates, ValidatorVotingPowers, -}; +use namada_proof_of_stake::types::ValidatorStates; use namada_proof_of_stake::{types, PosBase}; use super::{ - BondId, Bonds, ValidatorConsensusKeys, ValidatorSets, ValidatorTotalDeltas, - ADDRESS, + BondId, Bonds, CommissionRates, TotalDeltas, ValidatorConsensusKeys, + ValidatorDeltas, ValidatorSets, ADDRESS, }; use crate::ledger::storage::types::{decode, encode}; use crate::ledger::storage::{self, Storage, StorageHasher}; @@ -19,17 +17,17 @@ use crate::types::{key, token}; const PARAMS_STORAGE_KEY: &str = "params"; const VALIDATOR_STORAGE_PREFIX: &str = "validator"; const VALIDATOR_ADDRESS_RAW_HASH: &str = "address_raw_hash"; -const VALIDATOR_STAKING_REWARD_ADDRESS_STORAGE_KEY: &str = - "staking_reward_address"; const VALIDATOR_CONSENSUS_KEY_STORAGE_KEY: &str = "consensus_key"; const VALIDATOR_STATE_STORAGE_KEY: &str = "state"; -const VALIDATOR_TOTAL_DELTAS_STORAGE_KEY: &str = "total_deltas"; -const VALIDATOR_VOTING_POWER_STORAGE_KEY: &str = "voting_power"; +const VALIDATOR_DELTAS_STORAGE_KEY: &str = "validator_deltas"; +const VALIDATOR_COMMISSION_RATE_STORAGE_KEY: &str = "commission_rate"; +const VALIDATOR_MAX_COMMISSION_CHANGE_STORAGE_KEY: &str = + "max_commission_rate_change"; const SLASHES_PREFIX: &str = "slash"; const BOND_STORAGE_KEY: &str = "bond"; const UNBOND_STORAGE_KEY: &str = "unbond"; const VALIDATOR_SET_STORAGE_KEY: &str = "validator_set"; -const TOTAL_VOTING_POWER_STORAGE_KEY: &str = "total_voting_power"; +const TOTAL_DELTAS_STORAGE_KEY: &str = "total_deltas"; /// Is the given key a PoS storage key? pub fn is_pos_key(key: &Key) -> bool { @@ -92,15 +90,15 @@ pub fn is_validator_address_raw_hash_key(key: &Key) -> Option<&str> { } } -/// Storage key for validator's staking reward address. -pub fn validator_staking_reward_address_key(validator: &Address) -> Key { +/// Storage key for validator's consensus key. +pub fn validator_consensus_key_key(validator: &Address) -> Key { validator_prefix(validator) - .push(&VALIDATOR_STAKING_REWARD_ADDRESS_STORAGE_KEY.to_owned()) + .push(&VALIDATOR_CONSENSUS_KEY_STORAGE_KEY.to_owned()) .expect("Cannot obtain a storage key") } -/// Is storage key for validator's staking reward address? -pub fn is_validator_staking_reward_address_key(key: &Key) -> Option<&Address> { +/// Is storage key for validator's consensus key? +pub fn is_validator_consensus_key_key(key: &Key) -> Option<&Address> { match &key.segments[..] { [ DbKeySeg::AddressSeg(addr), @@ -109,7 +107,7 @@ pub fn is_validator_staking_reward_address_key(key: &Key) -> Option<&Address> { DbKeySeg::StringSeg(key), ] if addr == &ADDRESS && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_STAKING_REWARD_ADDRESS_STORAGE_KEY => + && key == VALIDATOR_CONSENSUS_KEY_STORAGE_KEY => { Some(validator) } @@ -117,15 +115,15 @@ pub fn is_validator_staking_reward_address_key(key: &Key) -> Option<&Address> { } } -/// Storage key for validator's consensus key. -pub fn validator_consensus_key_key(validator: &Address) -> Key { +/// Storage key for validator's commission rate. +pub fn validator_commission_rate_key(validator: &Address) -> Key { validator_prefix(validator) - .push(&VALIDATOR_CONSENSUS_KEY_STORAGE_KEY.to_owned()) + .push(&VALIDATOR_COMMISSION_RATE_STORAGE_KEY.to_owned()) .expect("Cannot obtain a storage key") } -/// Is storage key for validator's consensus key? -pub fn is_validator_consensus_key_key(key: &Key) -> Option<&Address> { +/// Is storage key for validator's commissionr ate? +pub fn is_validator_commission_rate_key(key: &Key) -> Option<&Address> { match &key.segments[..] { [ DbKeySeg::AddressSeg(addr), @@ -134,7 +132,7 @@ pub fn is_validator_consensus_key_key(key: &Key) -> Option<&Address> { DbKeySeg::StringSeg(key), ] if addr == &ADDRESS && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_CONSENSUS_KEY_STORAGE_KEY => + && key == VALIDATOR_COMMISSION_RATE_STORAGE_KEY => { Some(validator) } @@ -142,15 +140,17 @@ pub fn is_validator_consensus_key_key(key: &Key) -> Option<&Address> { } } -/// Storage key for validator's state. -pub fn validator_state_key(validator: &Address) -> Key { +/// Storage key for validator's maximum commission rate change per epoch. +pub fn validator_max_commission_rate_change_key(validator: &Address) -> Key { validator_prefix(validator) - .push(&VALIDATOR_STATE_STORAGE_KEY.to_owned()) + .push(&VALIDATOR_MAX_COMMISSION_CHANGE_STORAGE_KEY.to_owned()) .expect("Cannot obtain a storage key") } -/// Is storage key for validator's state? -pub fn is_validator_state_key(key: &Key) -> Option<&Address> { +/// Is storage key for validator's maximum commission rate change per epoch? +pub fn is_validator_max_commission_rate_change_key( + key: &Key, +) -> Option<&Address> { match &key.segments[..] { [ DbKeySeg::AddressSeg(addr), @@ -159,7 +159,7 @@ pub fn is_validator_state_key(key: &Key) -> Option<&Address> { DbKeySeg::StringSeg(key), ] if addr == &ADDRESS && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_STATE_STORAGE_KEY => + && key == VALIDATOR_MAX_COMMISSION_CHANGE_STORAGE_KEY => { Some(validator) } @@ -167,15 +167,15 @@ pub fn is_validator_state_key(key: &Key) -> Option<&Address> { } } -/// Storage key for validator's total deltas. -pub fn validator_total_deltas_key(validator: &Address) -> Key { +/// Storage key for validator's consensus key. +pub fn validator_state_key(validator: &Address) -> Key { validator_prefix(validator) - .push(&VALIDATOR_TOTAL_DELTAS_STORAGE_KEY.to_owned()) + .push(&VALIDATOR_STATE_STORAGE_KEY.to_owned()) .expect("Cannot obtain a storage key") } -/// Is storage key for validator's total deltas? -pub fn is_validator_total_deltas_key(key: &Key) -> Option<&Address> { +/// Is storage key for validator's state? +pub fn is_validator_state_key(key: &Key) -> Option<&Address> { match &key.segments[..] { [ DbKeySeg::AddressSeg(addr), @@ -184,7 +184,7 @@ pub fn is_validator_total_deltas_key(key: &Key) -> Option<&Address> { DbKeySeg::StringSeg(key), ] if addr == &ADDRESS && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_TOTAL_DELTAS_STORAGE_KEY => + && key == VALIDATOR_STATE_STORAGE_KEY => { Some(validator) } @@ -192,15 +192,15 @@ pub fn is_validator_total_deltas_key(key: &Key) -> Option<&Address> { } } -/// Storage key for validator's voting power. -pub fn validator_voting_power_key(validator: &Address) -> Key { +/// Storage key for validator's deltas. +pub fn validator_deltas_key(validator: &Address) -> Key { validator_prefix(validator) - .push(&VALIDATOR_VOTING_POWER_STORAGE_KEY.to_owned()) + .push(&VALIDATOR_DELTAS_STORAGE_KEY.to_owned()) .expect("Cannot obtain a storage key") } -/// Is storage key for validator's voting power? -pub fn is_validator_voting_power_key(key: &Key) -> Option<&Address> { +/// Is storage key for validator's total deltas? +pub fn is_validator_deltas_key(key: &Key) -> Option<&Address> { match &key.segments[..] { [ DbKeySeg::AddressSeg(addr), @@ -209,7 +209,7 @@ pub fn is_validator_voting_power_key(key: &Key) -> Option<&Address> { DbKeySeg::StringSeg(key), ] if addr == &ADDRESS && prefix == VALIDATOR_STORAGE_PREFIX - && key == VALIDATOR_VOTING_POWER_STORAGE_KEY => + && key == VALIDATOR_DELTAS_STORAGE_KEY => { Some(validator) } @@ -336,18 +336,18 @@ pub fn is_validator_set_key(key: &Key) -> bool { } } -/// Storage key for total voting power. -pub fn total_voting_power_key() -> Key { +/// Storage key for total deltas of all validators. +pub fn total_deltas_key() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&TOTAL_VOTING_POWER_STORAGE_KEY.to_owned()) + .push(&TOTAL_DELTAS_STORAGE_KEY.to_owned()) .expect("Cannot obtain a storage key") } -/// Is storage key for total voting power? -pub fn is_total_voting_power_key(key: &Key) -> bool { +/// Is storage key for total deltas of all validators? +pub fn is_total_deltas_key(key: &Key) -> bool { match &key.segments[..] { [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key)] - if addr == &ADDRESS && key == TOTAL_VOTING_POWER_STORAGE_KEY => + if addr == &ADDRESS && key == TOTAL_DELTAS_STORAGE_KEY => { true } @@ -415,21 +415,11 @@ where value.map(|value| decode(value).unwrap()) } - fn read_validator_total_deltas( + fn read_validator_deltas( &self, key: &Self::Address, - ) -> Option> { - let (value, _gas) = - self.read(&validator_total_deltas_key(key)).unwrap(); - value.map(|value| decode(value).unwrap()) - } - - fn read_validator_voting_power( - &self, - key: &Self::Address, - ) -> Option { - let (value, _gas) = - self.read(&validator_voting_power_key(key)).unwrap(); + ) -> Option> { + let (value, _gas) = self.read(&validator_deltas_key(key)).unwrap(); value.map(|value| decode(value).unwrap()) } @@ -440,13 +430,32 @@ where .unwrap_or_default() } + fn read_validator_commission_rate( + &self, + key: &Self::Address, + ) -> CommissionRates { + let (value, _gas) = + self.read(&validator_commission_rate_key(key)).unwrap(); + decode(value.unwrap()).unwrap() + } + + fn read_validator_max_commission_rate_change( + &self, + key: &Self::Address, + ) -> rust_decimal::Decimal { + let (value, _gas) = self + .read(&validator_max_commission_rate_change_key(key)) + .unwrap(); + decode(value.unwrap()).unwrap() + } + fn read_validator_set(&self) -> ValidatorSets { let (value, _gas) = self.read(&validator_set_key()).unwrap(); decode(value.unwrap()).unwrap() } - fn read_total_voting_power(&self) -> TotalVotingPowers { - let (value, _gas) = self.read(&total_voting_power_key()).unwrap(); + fn read_total_deltas(&self) -> TotalDeltas { + let (value, _gas) = self.read(&total_deltas_key()).unwrap(); decode(value.unwrap()).unwrap() } @@ -464,15 +473,27 @@ where .unwrap(); } - fn write_validator_staking_reward_address( + fn write_validator_commission_rate( &mut self, key: &Self::Address, - value: &Self::Address, + value: &CommissionRates, ) { - self.write(&validator_staking_reward_address_key(key), encode(value)) + self.write(&validator_commission_rate_key(key), encode(value)) .unwrap(); } + fn write_validator_max_commission_rate_change( + &mut self, + key: &Self::Address, + value: &rust_decimal::Decimal, + ) { + self.write( + &validator_max_commission_rate_change_key(key), + encode(value), + ) + .unwrap(); + } + fn write_validator_consensus_key( &mut self, key: &Self::Address, @@ -491,21 +512,12 @@ where .unwrap(); } - fn write_validator_total_deltas( + fn write_validator_deltas( &mut self, key: &Self::Address, - value: &ValidatorTotalDeltas, + value: &ValidatorDeltas, ) { - self.write(&validator_total_deltas_key(key), encode(value)) - .unwrap(); - } - - fn write_validator_voting_power( - &mut self, - key: &Self::Address, - value: &ValidatorVotingPowers, - ) { - self.write(&validator_voting_power_key(key), encode(value)) + self.write(&validator_deltas_key(key), encode(value)) .unwrap(); } @@ -528,25 +540,8 @@ where self.write(&validator_set_key(), encode(value)).unwrap(); } - fn write_total_voting_power(&mut self, value: &TotalVotingPowers) { - self.write(&total_voting_power_key(), encode(value)) - .unwrap(); - } - - fn init_staking_reward_account( - &mut self, - address: &Self::Address, - pk: &Self::PublicKey, - ) { - // let user_vp = - // std::fs::read("wasm/vp_user.wasm").expect("cannot load user VP"); - // // The staking reward accounts are setup with a user VP - // self.write(&Key::validity_predicate(address), user_vp.to_vec()) - // .unwrap(); - - // Write the public key - let pk_key = key::pk_key(address); - self.write(&pk_key, encode(pk)).unwrap(); + fn write_total_deltas(&mut self, value: &TotalDeltas) { + self.write(&total_deltas_key(), encode(value)).unwrap(); } fn credit_tokens( diff --git a/shared/src/ledger/pos/vp.rs b/shared/src/ledger/pos/vp.rs index 60264e4926..9b5293329d 100644 --- a/shared/src/ledger/pos/vp.rs +++ b/shared/src/ledger/pos/vp.rs @@ -7,24 +7,21 @@ use borsh::BorshDeserialize; use itertools::Itertools; pub use namada_proof_of_stake; pub use namada_proof_of_stake::parameters::PosParams; -pub use namada_proof_of_stake::types::{ - self, Slash, Slashes, TotalVotingPowers, ValidatorStates, - ValidatorVotingPowers, -}; +pub use namada_proof_of_stake::types::{self, Slash, Slashes, ValidatorStates}; use namada_proof_of_stake::validation::validate; use namada_proof_of_stake::{validation, PosReadOnly}; +use rust_decimal::Decimal; use thiserror::Error; use super::{ - bond_key, is_bond_key, is_params_key, is_total_voting_power_key, - is_unbond_key, is_validator_set_key, - is_validator_staking_reward_address_key, is_validator_total_deltas_key, - is_validator_voting_power_key, params_key, staking_token_address, - total_voting_power_key, unbond_key, validator_consensus_key_key, - validator_set_key, validator_slashes_key, - validator_staking_reward_address_key, validator_state_key, - validator_total_deltas_key, validator_voting_power_key, BondId, Bonds, - Unbonds, ValidatorConsensusKeys, ValidatorSets, ValidatorTotalDeltas, + bond_key, is_bond_key, is_params_key, is_total_deltas_key, is_unbond_key, + is_validator_deltas_key, is_validator_set_key, params_key, + staking_token_address, total_deltas_key, unbond_key, + validator_commission_rate_key, validator_consensus_key_key, + validator_deltas_key, validator_max_commission_rate_change_key, + validator_set_key, validator_slashes_key, validator_state_key, BondId, + Bonds, CommissionRates, Unbonds, ValidatorConsensusKeys, ValidatorDeltas, + ValidatorSets, }; use crate::impl_pos_read_only; use crate::ledger::governance::vp::is_proposal_accepted; @@ -32,8 +29,9 @@ use crate::ledger::native_vp::{ self, Ctx, CtxPostStorageRead, CtxPreStorageRead, NativeVp, }; use crate::ledger::pos::{ - is_validator_address_raw_hash_key, is_validator_consensus_key_key, - is_validator_state_key, + is_validator_address_raw_hash_key, is_validator_commission_rate_key, + is_validator_consensus_key_key, + is_validator_max_commission_rate_change_key, is_validator_state_key, }; use crate::ledger::storage::{self as ledger_storage, StorageHasher}; use crate::ledger::storage_api::{self, StorageRead}; @@ -149,21 +147,6 @@ where address: validator.clone(), update: State(Data { pre, post }), }); - } else if let Some(validator) = - is_validator_staking_reward_address_key(key) - { - let pre = - self.ctx.pre().read_bytes(key)?.and_then(|bytes| { - Address::try_from_slice(&bytes[..]).ok() - }); - let post = - self.ctx.post().read_bytes(key)?.and_then(|bytes| { - Address::try_from_slice(&bytes[..]).ok() - }); - changes.push(Validator { - address: validator.clone(), - update: StakingRewardAddress(Data { pre, post }), - }); } else if let Some(validator) = is_validator_consensus_key_key(key) { let pre = self.ctx.pre().read_bytes(key)?.and_then(|bytes| { @@ -176,27 +159,16 @@ where address: validator.clone(), update: ConsensusKey(Data { pre, post }), }); - } else if let Some(validator) = is_validator_total_deltas_key(key) { - let pre = self.ctx.pre().read_bytes(key)?.and_then(|bytes| { - ValidatorTotalDeltas::try_from_slice(&bytes[..]).ok() - }); - let post = self.ctx.post().read_bytes(key)?.and_then(|bytes| { - ValidatorTotalDeltas::try_from_slice(&bytes[..]).ok() - }); - changes.push(Validator { - address: validator.clone(), - update: TotalDeltas(Data { pre, post }), - }); - } else if let Some(validator) = is_validator_voting_power_key(key) { + } else if let Some(validator) = is_validator_deltas_key(key) { let pre = self.ctx.pre().read_bytes(key)?.and_then(|bytes| { - ValidatorVotingPowers::try_from_slice(&bytes[..]).ok() + namada_proof_of_stake::types::ValidatorDeltas::try_from_slice(&bytes[..]).ok() }); let post = self.ctx.post().read_bytes(key)?.and_then(|bytes| { - ValidatorVotingPowers::try_from_slice(&bytes[..]).ok() + namada_proof_of_stake::types::ValidatorDeltas::try_from_slice(&bytes[..]).ok() }); changes.push(Validator { address: validator.clone(), - update: VotingPowerUpdate(Data { pre, post }), + update: ValidatorDeltas(Data { pre, post }), }); } else if let Some(raw_hash) = is_validator_address_raw_hash_key(key) @@ -268,21 +240,54 @@ where data: Data { pre, post }, slashes, }); - } else if is_total_voting_power_key(key) { + } else if is_total_deltas_key(key) { + let pre = self.ctx.pre().read_bytes(key)?.and_then(|bytes| { + super::TotalDeltas::try_from_slice(&bytes[..]).ok() + }); + let post = self.ctx.post().read_bytes(key)?.and_then(|bytes| { + super::TotalDeltas::try_from_slice(&bytes[..]).ok() + }); + changes.push(TotalDeltas(Data { pre, post })); + } else if let Some(address) = is_validator_commission_rate_key(key) + { + let max_change = self + .ctx + .pre() + .read_bytes(&validator_max_commission_rate_change_key( + address, + ))? + .and_then(|bytes| Decimal::try_from_slice(&bytes[..]).ok()); let pre = self.ctx.pre().read_bytes(key)?.and_then(|bytes| { - TotalVotingPowers::try_from_slice(&bytes[..]).ok() + CommissionRates::try_from_slice(&bytes[..]).ok() }); let post = self.ctx.post().read_bytes(key)?.and_then(|bytes| { - TotalVotingPowers::try_from_slice(&bytes[..]).ok() + CommissionRates::try_from_slice(&bytes[..]).ok() + }); + changes.push(Validator { + address: address.clone(), + update: CommissionRate(Data { pre, post }, max_change), + }); + } else if let Some(address) = + is_validator_max_commission_rate_change_key(key) + { + let pre = + self.ctx.pre().read_bytes(key)?.and_then(|bytes| { + Decimal::try_from_slice(&bytes[..]).ok() + }); + let post = + self.ctx.post().read_bytes(key)?.and_then(|bytes| { + Decimal::try_from_slice(&bytes[..]).ok() + }); + changes.push(Validator { + address: address.clone(), + update: MaxCommissionRateChange(Data { pre, post }), }); - changes.push(TotalVotingPower(Data { pre, post })); } else if key.segments.get(0) == Some(&addr.to_db_key()) { // Unknown changes to this address space are disallowed tracing::info!("PoS unrecognized key change {} rejected", key); return Ok(false); } else { // Unknown changes anywhere else are permitted - return Ok(true); } } diff --git a/shared/src/ledger/queries/router.rs b/shared/src/ledger/queries/router.rs index e4823e5ad7..67e76c55bf 100644 --- a/shared/src/ledger/queries/router.rs +++ b/shared/src/ledger/queries/router.rs @@ -410,7 +410,6 @@ macro_rules! pattern_and_handler_to_method { ::Error > where CLIENT: $crate::ledger::queries::Client + std::marker::Sync { - println!("IMMA VEC!!!!!!"); let path = self.storage_value_path( $( $param ),* ); let $crate::ledger::queries::ResponseQuery { diff --git a/shared/src/ledger/storage/mod.rs b/shared/src/ledger/storage/mod.rs index 16c3ecf180..bde43d5826 100644 --- a/shared/src/ledger/storage/mod.rs +++ b/shared/src/ledger/storage/mod.rs @@ -916,6 +916,7 @@ pub mod testing { mod tests { use chrono::{TimeZone, Utc}; use proptest::prelude::*; + use rust_decimal_macros::dec; use super::testing::*; use super::*; @@ -986,7 +987,13 @@ mod tests { epoch_duration: epoch_duration.clone(), max_expected_time_per_block: Duration::seconds(max_expected_time_per_block).into(), vp_whitelist: vec![], - tx_whitelist: vec![] + tx_whitelist: vec![], + epochs_per_year: 100, + pos_gain_p: dec!(0.1), + pos_gain_d: dec!(0.1), + staked_ratio: dec!(0.1), + pos_inflation_amount: 0, + }; parameters.init_storage(&mut storage); diff --git a/shared/src/types/token.rs b/shared/src/types/token.rs index 787a8855dc..b19642b85a 100644 --- a/shared/src/types/token.rs +++ b/shared/src/types/token.rs @@ -6,6 +6,7 @@ use std::ops::{Add, AddAssign, Mul, Sub, SubAssign}; use std::str::FromStr; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use rust_decimal::prelude::{Decimal, ToPrimitive}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -37,7 +38,6 @@ pub struct Amount { pub const MAX_DECIMAL_PLACES: u32 = 6; /// Decimal scale of token [`Amount`] and [`Change`]. pub const SCALE: u64 = 1_000_000; -const SCALE_F64: f64 = SCALE as f64; /// A change in tokens amount pub type Change = i128; @@ -109,21 +109,16 @@ impl<'de> serde::Deserialize<'de> for Amount { } } -impl From for f64 { - /// Warning: `f64` loses precision and it should not be used when exact - /// values are required. +impl From for Decimal { fn from(amount: Amount) -> Self { - amount.micro as f64 / SCALE_F64 + Into::::into(amount.micro) / Into::::into(SCALE) } } -impl From for Amount { - /// Warning: `f64` loses precision and it should not be used when exact - /// values are required. - fn from(micro: f64) -> Self { - Self { - micro: (micro * SCALE_F64).round() as u64, - } +impl From for Amount { + fn from(micro: Decimal) -> Self { + let res = (micro * Into::::into(SCALE)).to_u64().unwrap(); + Self { micro: res } } } @@ -205,7 +200,7 @@ impl FromStr for Amount { match rust_decimal::Decimal::from_str(s) { Ok(decimal) => { let scale = decimal.scale(); - if scale > 6 { + if scale > MAX_DECIMAL_PLACES { return Err(AmountParseError::ScaleTooLarge(scale)); } let whole = @@ -440,11 +435,11 @@ mod tests { /// The upper limit is set to `2^51`, because then the float is /// starting to lose precision. #[test] - fn test_token_amount_f64_conversion(raw_amount in 0..2_u64.pow(51)) { + fn test_token_amount_decimal_conversion(raw_amount in 0..2_u64.pow(51)) { let amount = Amount::from(raw_amount); - // A round-trip conversion to and from f64 should be an identity - let float = f64::from(amount); - let identity = Amount::from(float); + // A round-trip conversion to and from Decimal should be an identity + let decimal = Decimal::from(amount); + let identity = Amount::from(decimal); assert_eq!(amount, identity); } } diff --git a/shared/src/types/transaction/mod.rs b/shared/src/types/transaction/mod.rs index 071c3df2a5..3ee6ebd218 100644 --- a/shared/src/types/transaction/mod.rs +++ b/shared/src/types/transaction/mod.rs @@ -21,6 +21,7 @@ pub use decrypted::*; #[cfg(feature = "ferveo-tpke")] pub use encrypted::EncryptionKey; pub use protocol::UpdateDkgSessionKey; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; pub use wrapper::*; @@ -166,8 +167,7 @@ pub struct InitAccount { pub vp_code: Vec, } -/// A tx data type to initialize a new validator account and its staking reward -/// account. +/// A tx data type to initialize a new validator account. #[derive( Debug, Clone, @@ -185,18 +185,17 @@ pub struct InitValidator { pub account_key: common::PublicKey, /// A key to be used for signing blocks and votes on blocks. pub consensus_key: common::PublicKey, - /// Public key to be written into the staking reward account's storage. - /// This can be used for signature verification of transactions for the - /// newly created account. - pub rewards_account_key: common::PublicKey, /// Public key used to sign protocol transactions pub protocol_key: common::PublicKey, /// Serialization of the public session key used in the DKG pub dkg_key: DkgPublicKey, + /// The initial commission rate charged for delegation rewards + pub commission_rate: Decimal, + /// The maximum change allowed per epoch to the commission rate. This is + /// immutable once set here. + pub max_commission_rate_change: Decimal, /// The VP code for validator account pub validator_vp_code: Vec, - /// The VP code for validator's staking reward account - pub rewards_vp_code: Vec, } /// Module that includes helper functions for classifying diff --git a/shared/src/types/transaction/pos.rs b/shared/src/types/transaction/pos.rs index b6cba21df3..8119eb2310 100644 --- a/shared/src/types/transaction/pos.rs +++ b/shared/src/types/transaction/pos.rs @@ -1,6 +1,7 @@ //! Types used for PoS system transactions use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use crate::types::address::Address; @@ -53,3 +54,23 @@ pub struct Withdraw { /// from self-bonds, the validator is also the source pub source: Option
, } + +/// A change to the validator commission rate. +#[derive( + Debug, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + BorshSchema, + Hash, + Eq, + Serialize, + Deserialize, +)] +pub struct CommissionChange { + /// Validator address + pub validator: Address, + /// The new commission rate + pub new_rate: Decimal, +} diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 74056ba00b..f47133e216 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -25,6 +25,8 @@ tempfile = "3.2.0" tracing = "0.1.30" tracing-subscriber = {version = "0.3.7", default-features = false, features = ["env-filter", "fmt"]} derivative = "2.2.0" +rust_decimal = "1.26.1" +rust_decimal_macros = "1.26.1" [dev-dependencies] namada_apps = {path = "../apps", default-features = false, features = ["testing"]} diff --git a/tests/src/e2e/helpers.rs b/tests/src/e2e/helpers.rs index d0dd60a688..4cdecc4f9b 100644 --- a/tests/src/e2e/helpers.rs +++ b/tests/src/e2e/helpers.rs @@ -94,8 +94,8 @@ pub fn find_keypair( }) } -/// Find the address of an account by its alias from the wallet -pub fn find_voting_power( +/// Find the bonded stake of an account by its alias from the wallet +pub fn find_bonded_stake( test: &Test, alias: impl AsRef, ledger_address: &str, @@ -104,7 +104,7 @@ pub fn find_voting_power( test, Bin::Client, &[ - "voting-power", + "bonded-stake", "--validator", alias.as_ref(), "--ledger-address", @@ -112,16 +112,16 @@ pub fn find_voting_power( ], Some(10) )?; - let (unread, matched) = find.exp_regex("voting power: .*")?; - let voting_power_str = strip_trailing_newline(&matched) + let (unread, matched) = find.exp_regex("bonded stake: .*")?; + let bonded_stake_str = strip_trailing_newline(&matched) .trim() .rsplit_once(' ') .unwrap() .1; - u64::from_str(voting_power_str).map_err(|e| { + u64::from_str(bonded_stake_str).map_err(|e| { eyre!(format!( - "Voting power: {} parsed from {}, Error: {}\n\nOutput: {}", - voting_power_str, matched, e, unread + "Bonded stake: {} parsed from {}, Error: {}\n\nOutput: {}", + bonded_stake_str, matched, e, unread )) }) } diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index 8a7427a9fd..612537d9dc 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -20,13 +20,14 @@ use namada::types::token; use namada_apps::config::genesis::genesis_config::{ GenesisConfig, ParametersConfig, PosParamsConfig, }; +use rust_decimal_macros::dec; use serde_json::json; use setup::constants::*; use super::helpers::{get_height, wait_for_block_height}; use super::setup::get_all_wasms_hashes; use crate::e2e::helpers::{ - find_address, find_voting_power, get_actor_rpc, get_epoch, + find_address, find_bonded_stake, get_actor_rpc, get_epoch, }; use crate::e2e::setup::{self, sleep, Bin, Who}; use crate::{run, run_as}; @@ -572,8 +573,8 @@ fn pos_bonds() -> Result<()> { |genesis| { let parameters = ParametersConfig { min_num_of_blocks: 2, - min_duration: 1, max_expected_time_per_block: 1, + epochs_per_year: 31_536_000, ..genesis.parameters }; let pos_params = PosParamsConfig { @@ -757,7 +758,7 @@ fn pos_bonds() -> Result<()> { /// 4. Transfer some NAM to the new validator /// 5. Submit a self-bond for the new validator /// 6. Wait for the pipeline epoch -/// 7. Check the new validator's voting power +/// 7. Check the new validator's bonded stake #[test] fn pos_init_validator() -> Result<()> { let pipeline_len = 1; @@ -765,7 +766,7 @@ fn pos_init_validator() -> Result<()> { |genesis| { let parameters = ParametersConfig { min_num_of_blocks: 2, - min_duration: 1, + epochs_per_year: 31_536_000, max_expected_time_per_block: 1, ..genesis.parameters }; @@ -808,6 +809,10 @@ fn pos_init_validator() -> Result<()> { "0", "--fee-token", NAM, + "--commission-rate", + "0.05", + "--max-commission-rate-change", + "0.01", "--ledger-address", &validator_one_rpc, ]; @@ -905,12 +910,12 @@ fn pos_init_validator() -> Result<()> { client.exp_string("Transaction is valid.")?; client.assert_success(); - // 6. Wait for the pipeline epoch when the validator's voting power should + // 6. Wait for the pipeline epoch when the validator's bonded stake should // be non-zero let epoch = get_epoch(&test, &validator_one_rpc)?; let earliest_update_epoch = epoch + pipeline_len; println!( - "Current epoch: {}, earliest epoch with updated voting power: {}", + "Current epoch: {}, earliest epoch with updated bonded stake: {}", epoch, earliest_update_epoch ); let start = Instant::now(); @@ -925,10 +930,10 @@ fn pos_init_validator() -> Result<()> { } } - // 7. Check the new validator's voting power - let voting_power = - find_voting_power(&test, new_validator, &validator_one_rpc)?; - assert_eq!(voting_power, 11); + // 7. Check the new validator's bonded stake + let bonded_stake = + find_bonded_stake(&test, new_validator, &validator_one_rpc)?; + assert_eq!(bonded_stake, 11_000_500_000); Ok(()) } @@ -1031,7 +1036,6 @@ fn proposal_submission() -> Result<()> { |genesis| { let parameters = ParametersConfig { min_num_of_blocks: 1, - min_duration: 1, max_expected_time_per_block: 1, vp_whitelist: Some(get_all_wasms_hashes( &working_dir, @@ -1043,6 +1047,9 @@ fn proposal_submission() -> Result<()> { &working_dir, Some("tx_"), )), + epochs_per_year: 31_536_000, + pos_gain_p: dec!(0.1), + pos_gain_d: dec!(0.1), }; GenesisConfig { @@ -1598,6 +1605,10 @@ fn test_genesis_validators() -> Result<()> { validator_0_alias, "--scheme", "ed25519", + "--commission-rate", + "0.05", + "--max-commission-rate-change", + "0.01", "--net-address", &format!("127.0.0.1:{}", get_first_port(0)), ], @@ -1636,6 +1647,10 @@ fn test_genesis_validators() -> Result<()> { validator_1_alias, "--scheme", "secp256k1", + "--commission-rate", + "0.05", + "--max-commission-rate-change", + "0.01", "--net-address", &format!("127.0.0.1:{}", get_first_port(1)), ], @@ -1674,7 +1689,6 @@ fn test_genesis_validators() -> Result<()> { config.tokens = Some(200000); config.non_staked_balance = Some(1000000000000); config.validator_vp = Some("vp_user".into()); - config.staking_reward_vp = Some("vp_user".into()); // Setup the validator ports same as what // `setup::add_validators` would do let mut net_address = net_address_0; diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index ec6f75a15f..bc19e99b67 100644 --- a/tests/src/native_vp/pos.rs +++ b/tests/src/native_vp/pos.rs @@ -71,7 +71,6 @@ //! address in Tendermint) //! - `#{PoS}/validator_set` //! - `#{PoS}/validator/#{validator}/consensus_key` -//! - `#{PoS}/validator/#{validator}/staking_reward_address` //! - `#{PoS}/validator/#{validator}/state` //! - `#{PoS}/validator/#{validator}/total_deltas` //! - `#{PoS}/validator/#{validator}/voting_power` @@ -121,10 +120,7 @@ pub fn init_pos( // addresses exist tx_env.spawn_accounts([&staking_token_address()]); for validator in genesis_validators { - tx_env.spawn_accounts([ - &validator.address, - &validator.staking_reward_address, - ]); + tx_env.spawn_accounts([&validator.address]); } tx_env.storage.block.epoch = start_epoch; // Initialize PoS storage @@ -404,6 +400,8 @@ mod tests { ValidPosAction::InitValidator { address, consensus_key, + commission_rate: _, + max_commission_rate_change: _, } => { !state.is_validator(address) && !state.is_used_key(consensus_key) @@ -582,22 +580,22 @@ pub mod testing { use namada_tx_prelude::proof_of_stake::epoched::{ DynEpochOffset, Epoched, EpochedDelta, }; + use namada_tx_prelude::proof_of_stake::parameters::testing::arb_rate; use namada_tx_prelude::proof_of_stake::types::{ - Bond, Unbond, ValidatorState, VotingPower, VotingPowerDelta, - WeightedValidator, + Bond, Unbond, ValidatorState, WeightedValidator, }; use namada_tx_prelude::proof_of_stake::{ staking_token_address, BondId, Bonds, PosParams, Unbonds, }; use namada_tx_prelude::{Address, StorageRead, StorageWrite}; use proptest::prelude::*; + use rust_decimal::Decimal; use crate::tx::{self, tx_host_env}; #[derive(Clone, Debug, Default)] pub struct TestValidator { pub address: Option
, - pub staking_reward_address: Option
, pub stake: Option, /// Balance is a pair of token address and its amount pub unstaked_balances: Vec<(Address, token::Amount)>, @@ -612,6 +610,8 @@ pub mod testing { InitValidator { address: Address, consensus_key: PublicKey, + commission_rate: Decimal, + max_commission_rate_change: Decimal, }, Bond { amount: token::Amount, @@ -665,8 +665,8 @@ pub mod testing { owner: Address, validator: Address, }, - TotalVotingPower { - vp_delta: i128, + TotalDeltas { + delta: i128, offset: Either, }, ValidatorSet { @@ -679,20 +679,11 @@ pub mod testing { #[derivative(Debug = "ignore")] pk: PublicKey, }, - ValidatorStakingRewardsAddress { - validator: Address, - address: Address, - }, - ValidatorTotalDeltas { + ValidatorDeltas { validator: Address, delta: i128, offset: DynEpochOffset, }, - ValidatorVotingPower { - validator: Address, - vp_delta: i64, - offset: Either, - }, ValidatorState { validator: Address, state: ValidatorState, @@ -705,6 +696,14 @@ pub mod testing { #[derivative(Debug = "ignore")] consensus_key: PublicKey, }, + ValidatorCommissionRate { + address: Address, + rate: Decimal, + }, + ValidatorMaxCommissionRateChange { + address: Address, + change: Decimal, + }, } pub fn arb_valid_pos_action( @@ -722,13 +721,24 @@ pub mod testing { let init_validator = ( address::testing::arb_established_address(), key::testing::arb_common_keypair(), + arb_rate(), + arb_rate(), ) - .prop_map(|(addr, consensus_key)| { - ValidPosAction::InitValidator { - address: Address::Established(addr), - consensus_key: consensus_key.ref_to(), - } - }); + .prop_map( + |( + addr, + consensus_key, + commission_rate, + max_commission_rate_change, + )| { + ValidPosAction::InitValidator { + address: Address::Established(addr), + consensus_key: consensus_key.ref_to(), + commission_rate, + max_commission_rate_change, + } + }, + ); if validators.is_empty() { // When there is no validator, we can only initialize new ones @@ -851,7 +861,7 @@ pub mod testing { }); println!("Current epoch {}", current_epoch); - let changes = self.into_storage_changes(¶ms, current_epoch); + let changes = self.into_storage_changes(current_epoch); for change in changes { apply_pos_storage_change( change, @@ -865,7 +875,6 @@ pub mod testing { /// Convert a valid PoS action to PoS storage changes pub fn into_storage_changes( self, - params: &PosParams, current_epoch: Epoch, ) -> PosStorageChanges { use namada_tx_prelude::PosRead; @@ -874,6 +883,8 @@ pub mod testing { ValidPosAction::InitValidator { address, consensus_key, + commission_rate, + max_commission_rate_change, } => { let offset = DynEpochOffset::PipelineLen; vec![ @@ -893,10 +904,6 @@ pub mod testing { validator: address.clone(), pk: consensus_key, }, - PosStorageChange::ValidatorStakingRewardsAddress { - validator: address.clone(), - address: address::testing::established_address_1(), - }, PosStorageChange::ValidatorState { validator: address.clone(), state: ValidatorState::Pending, @@ -905,15 +912,18 @@ pub mod testing { validator: address.clone(), state: ValidatorState::Candidate, }, - PosStorageChange::ValidatorTotalDeltas { + PosStorageChange::ValidatorDeltas { validator: address.clone(), delta: 0, offset, }, - PosStorageChange::ValidatorVotingPower { - validator: address, - vp_delta: 0, - offset: Either::Left(offset), + PosStorageChange::ValidatorCommissionRate { + address: address.clone(), + rate: commission_rate, + }, + PosStorageChange::ValidatorMaxCommissionRateChange { + address, + change: max_commission_rate_change, }, ] } @@ -923,129 +933,34 @@ pub mod testing { validator, } => { let offset = DynEpochOffset::PipelineLen; - // We first need to find if the validator's voting power - // is affected let token_delta = amount.change(); - // Read the validator's current total deltas (this may be - // updated by previous transition(s) within the same - // transaction via write log) - let validator_total_deltas = tx::ctx() - .read_validator_total_deltas(&validator) - .unwrap() - .unwrap(); - let total_delta = validator_total_deltas - .get_at_offset(current_epoch, offset, params) - .unwrap_or_default(); - // We convert the tokens from micro units to whole tokens - // with division by 10^6 - let vp_before = - params.votes_per_token * ((total_delta) / 1_000_000); - let vp_after = params.votes_per_token - * ((total_delta + token_delta) / 1_000_000); - // voting power delta - let vp_delta = vp_after - vp_before; - let mut changes = Vec::with_capacity(10); // ensure that the owner account exists changes.push(PosStorageChange::SpawnAccount { address: owner.clone(), }); - // If the bond increases the voting power, more storage - // updates are needed - if vp_delta != 0 { - // IMPORTANT: we have to update `ValidatorSet` and - // `TotalVotingPower` before we update - // `ValidatorTotalDeltas`, because they needs to - // read the total deltas before they change. - changes.extend([ - PosStorageChange::ValidatorSet { - validator: validator.clone(), - token_delta, - offset, - }, - PosStorageChange::TotalVotingPower { - vp_delta, - offset: Either::Left(offset), - }, - PosStorageChange::ValidatorVotingPower { - validator: validator.clone(), - vp_delta: vp_delta.try_into().unwrap(), - offset: Either::Left(offset), - }, - ]); - } - - // Check and if necessary recalculate voting power change at - // every epoch after pipeline offset until the last epoch of - // validator total deltas - let num_of_epochs = (DynEpochOffset::UnbondingLen - .value(params) - - DynEpochOffset::PipelineLen.value(params) - + u64::from(validator_total_deltas.last_update())) - .checked_sub(u64::from(current_epoch)) - .unwrap_or_default(); - - // We have to accumulate the total delta to find the delta - // for each epoch that we iterate, less the deltas of the - // predecessor epochs - let mut total_vp_delta = 0_i128; - for epoch in namada::ledger::pos::namada_proof_of_stake::types::Epoch::iter_range( - (current_epoch.0 + DynEpochOffset::PipelineLen.value(params) + 1).into(), - num_of_epochs, - ) { - // Read the validator's current total deltas (this may - // be updated by previous transition(s) within the same - // transaction via write log) - let total_delta = validator_total_deltas - .get(epoch) - .unwrap_or_default(); - // We convert the tokens from micro units to whole - // tokens with division by 10^6 - let vp_before = params.votes_per_token - * ((total_delta) / 1_000_000); - let vp_after = params.votes_per_token - * ((total_delta + token_delta) / 1_000_000); - // voting power delta - let vp_delta_at_unbonding = - vp_after - vp_before - vp_delta - total_vp_delta; - total_vp_delta += vp_delta_at_unbonding; - - // If the bond increases the voting power, we also need - // to check if that affects updates at unbonding offset - // and if so, update these again. We don't have to - // update validator sets as those are already updated - // from the bond offset to the unbonding offset. - if vp_delta_at_unbonding != 0 { - // IMPORTANT: we have to update `TotalVotingPower` - // before we update `ValidatorTotalDeltas`, because - // it needs to read the total deltas before they - // change. - changes.extend([ - PosStorageChange::TotalVotingPower { - vp_delta: vp_delta_at_unbonding, - offset: Either::Right(epoch.into()), - }, - PosStorageChange::ValidatorVotingPower { - validator: validator.clone(), - vp_delta: vp_delta_at_unbonding - .try_into() - .unwrap(), - offset: Either::Right(epoch.into()), - }, - ]); - } - } - changes.extend([ - PosStorageChange::Bond { - owner, + PosStorageChange::ValidatorSet { + validator: validator.clone(), + token_delta, + offset, + }, + PosStorageChange::TotalDeltas { + delta: token_delta, + offset: Either::Left(offset), + }, + PosStorageChange::ValidatorDeltas { validator: validator.clone(), delta: token_delta, offset, }, - PosStorageChange::ValidatorTotalDeltas { + ]); + + changes.extend([ + PosStorageChange::Bond { + owner, validator, delta: token_delta, offset, @@ -1063,55 +978,26 @@ pub mod testing { validator, } => { let offset = DynEpochOffset::UnbondingLen; - // We first need to find if the validator's voting power - // is affected let token_delta = -amount.change(); - // Read the validator's current total deltas (this may be - // updated by previous transition(s) within the same - // transaction via write log) - let validator_total_deltas_cur = tx::ctx() - .read_validator_total_deltas(&validator) - .unwrap() - .unwrap(); - let total_delta_cur = validator_total_deltas_cur - .get_at_offset(current_epoch, offset, params) - .unwrap_or_default(); - // We convert the tokens from micro units to whole tokens - // with division by 10^6 - let vp_before = params.votes_per_token - * ((total_delta_cur) / 1_000_000); - let vp_after = params.votes_per_token - * ((total_delta_cur + token_delta) / 1_000_000); - // voting power delta - let vp_delta = vp_after - vp_before; - let mut changes = Vec::with_capacity(6); - // If the bond increases the voting power, more storage - // updates are needed - if vp_delta != 0 { - // IMPORTANT: we have to update `ValidatorSet` and - // `TotalVotingPower` before we update - // `ValidatorTotalDeltas`, because they needs to - // read the total deltas before they change. - changes.extend([ - PosStorageChange::ValidatorSet { - validator: validator.clone(), - token_delta, - offset, - }, - PosStorageChange::TotalVotingPower { - vp_delta, - offset: Either::Left(offset), - }, - PosStorageChange::ValidatorVotingPower { - validator: validator.clone(), - vp_delta: vp_delta.try_into().unwrap(), - offset: Either::Left(offset), - }, - ]); - } + changes.extend([ + PosStorageChange::ValidatorSet { + validator: validator.clone(), + token_delta, + offset, + }, + PosStorageChange::TotalDeltas { + delta: token_delta, + offset: Either::Left(offset), + }, + PosStorageChange::ValidatorDeltas { + validator: validator.clone(), + delta: token_delta, + offset, + }, + ]); changes.extend([ // IMPORTANT: we have to update `Unbond` before we @@ -1124,11 +1010,6 @@ pub mod testing { }, PosStorageChange::Bond { owner, - validator: validator.clone(), - delta: token_delta, - offset, - }, - PosStorageChange::ValidatorTotalDeltas { validator, delta: token_delta, offset, @@ -1323,31 +1204,27 @@ pub mod testing { }; tx::ctx().write_unbond(&bond_id, unbonds).unwrap(); } - PosStorageChange::TotalVotingPower { vp_delta, offset } => { - let mut total_voting_powers = - tx::ctx().read_total_voting_power().unwrap(); - let vp_delta: i64 = vp_delta.try_into().unwrap(); + PosStorageChange::TotalDeltas { delta, offset } => { + let mut total_deltas = tx::ctx().read_total_deltas().unwrap(); match offset { Either::Left(offset) => { - total_voting_powers.add_at_offset( - VotingPowerDelta::from(vp_delta), + total_deltas.add_at_offset( + delta, current_epoch, offset, params, ); } Either::Right(epoch) => { - total_voting_powers.add_at_epoch( - VotingPowerDelta::from(vp_delta), + total_deltas.add_at_epoch( + delta, current_epoch, epoch, params, ); } } - tx::ctx() - .write_total_voting_power(total_voting_powers) - .unwrap() + tx::ctx().write_total_deltas(total_deltas).unwrap() } PosStorageChange::ValidatorAddressRawHash { address, @@ -1385,30 +1262,22 @@ pub mod testing { .write_validator_consensus_key(&validator, consensus_key) .unwrap(); } - PosStorageChange::ValidatorStakingRewardsAddress { - validator, - address, - } => { - tx::ctx() - .write_validator_staking_reward_address(&validator, address) - .unwrap(); - } - PosStorageChange::ValidatorTotalDeltas { + PosStorageChange::ValidatorDeltas { validator, delta, offset, } => { - let total_deltas = tx::ctx() - .read_validator_total_deltas(&validator) + let validator_deltas = tx::ctx() + .read_validator_deltas(&validator) .unwrap() - .map(|mut total_deltas| { - total_deltas.add_at_offset( + .map(|mut validator_deltas| { + validator_deltas.add_at_offset( delta, current_epoch, offset, params, ); - total_deltas + validator_deltas }) .unwrap_or_else(|| { EpochedDelta::init_at_offset( @@ -1419,48 +1288,7 @@ pub mod testing { ) }); tx::ctx() - .write_validator_total_deltas(&validator, total_deltas) - .unwrap(); - } - PosStorageChange::ValidatorVotingPower { - validator, - vp_delta: delta, - offset, - } => { - let voting_power = tx::ctx() - .read_validator_voting_power(&validator) - .unwrap() - .map(|mut voting_powers| { - match offset { - Either::Left(offset) => { - voting_powers.add_at_offset( - delta.into(), - current_epoch, - offset, - params, - ); - } - Either::Right(epoch) => { - voting_powers.add_at_epoch( - delta.into(), - current_epoch, - epoch, - params, - ); - } - } - voting_powers - }) - .unwrap_or_else(|| { - EpochedDelta::init_at_offset( - delta.into(), - current_epoch, - DynEpochOffset::PipelineLen, - params, - ) - }); - tx::ctx() - .write_validator_voting_power(&validator, voting_power) + .write_validator_deltas(&validator, validator_deltas) .unwrap(); } PosStorageChange::ValidatorState { validator, state } => { @@ -1504,6 +1332,35 @@ pub mod testing { unbonds.delete_current(current_epoch, params); tx::ctx().write_unbond(&bond_id, unbonds).unwrap(); } + PosStorageChange::ValidatorCommissionRate { address, rate } => { + let rates = tx::ctx() + .read_validator_commission_rate(&address) + .unwrap() + .map(|mut rates| { + rates.set(rate, current_epoch, params); + rates + }) + .unwrap_or_else(|| { + Epoched::init_at_genesis(rate, current_epoch) + }); + tx::ctx() + .write_validator_commission_rate(&address, rates) + .unwrap(); + } + PosStorageChange::ValidatorMaxCommissionRateChange { + address, + change, + } => { + let max_change = tx::ctx() + .read_validator_max_commission_rate_change(&address) + .unwrap() + .unwrap_or(change); + tx::ctx() + .write_validator_max_commission_rate_change( + &address, max_change, + ) + .unwrap(); + } } } @@ -1516,41 +1373,36 @@ pub mod testing { ) { use namada_tx_prelude::{PosRead, PosWrite}; - let validator_total_deltas = - tx::ctx().read_validator_total_deltas(&validator).unwrap(); - // println!("Read validator set"); + let validator_deltas = + tx::ctx().read_validator_deltas(&validator).unwrap(); let mut validator_set = tx::ctx().read_validator_set().unwrap(); - // println!("Read validator set: {:#?}", validator_set); validator_set.update_from_offset( |validator_set, epoch| { - let total_delta = validator_total_deltas + let validator_stake = validator_deltas .as_ref() - .and_then(|delta| delta.get(epoch)); - match total_delta { - Some(total_delta) => { - let tokens_pre: u64 = total_delta.try_into().unwrap(); - let voting_power_pre = - VotingPower::from_tokens(tokens_pre, params); + .and_then(|deltas| deltas.get(epoch)); + match validator_stake { + Some(validator_stake) => { + let tokens_pre: u64 = + validator_stake.try_into().unwrap(); let tokens_post: u64 = - (total_delta + token_delta).try_into().unwrap(); - let voting_power_post = - VotingPower::from_tokens(tokens_post, params); + (validator_stake + token_delta).try_into().unwrap(); let weighed_validator_pre = WeightedValidator { - voting_power: voting_power_pre, + bonded_stake: tokens_pre, address: validator.clone(), }; let weighed_validator_post = WeightedValidator { - voting_power: voting_power_post, + bonded_stake: tokens_post, address: validator.clone(), }; if validator_set.active.contains(&weighed_validator_pre) { let max_inactive_validator = validator_set.inactive.last_shim(); - let max_voting_power = max_inactive_validator - .map(|v| v.voting_power) + let max_bonded_stake = max_inactive_validator + .map(|v| v.bonded_stake) .unwrap_or_default(); - if voting_power_post < max_voting_power { + if tokens_post < max_bonded_stake { let activate_max = validator_set.inactive.pop_last_shim(); let popped = validator_set @@ -1574,10 +1426,10 @@ pub mod testing { } else { let min_active_validator = validator_set.active.first_shim(); - let min_voting_power = min_active_validator - .map(|v| v.voting_power) + let min_bonded_stake = min_active_validator + .map(|v| v.bonded_stake) .unwrap_or_default(); - if voting_power_post > min_voting_power { + if tokens_post > min_bonded_stake { let deactivate_min = validator_set.active.pop_first_shim(); let popped = validator_set @@ -1605,9 +1457,7 @@ pub mod testing { None => { let tokens: u64 = token_delta.try_into().unwrap(); let weighed_validator = WeightedValidator { - voting_power: VotingPower::from_tokens( - tokens, params, - ), + bonded_stake: tokens, address: validator.clone(), }; if has_vacant_active_validator_slots( diff --git a/tx_prelude/Cargo.toml b/tx_prelude/Cargo.toml index 11449b9460..b5b8df7f51 100644 --- a/tx_prelude/Cargo.toml +++ b/tx_prelude/Cargo.toml @@ -16,3 +16,4 @@ namada_macros = {path = "../macros"} borsh = "0.9.0" sha2 = "0.10.1" thiserror = "1.0.30" +rust_decimal = "1.26.1" diff --git a/tx_prelude/src/proof_of_stake.rs b/tx_prelude/src/proof_of_stake.rs index c11b035495..d2f38903f2 100644 --- a/tx_prelude/src/proof_of_stake.rs +++ b/tx_prelude/src/proof_of_stake.rs @@ -2,11 +2,11 @@ pub use namada::ledger::pos::*; use namada::ledger::pos::{ - bond_key, namada_proof_of_stake, params_key, total_voting_power_key, - unbond_key, validator_address_raw_hash_key, validator_consensus_key_key, - validator_set_key, validator_slashes_key, - validator_staking_reward_address_key, validator_state_key, - validator_total_deltas_key, validator_voting_power_key, + bond_key, namada_proof_of_stake, params_key, unbond_key, + validator_address_raw_hash_key, validator_commission_rate_key, + validator_consensus_key_key, validator_deltas_key, + validator_max_commission_rate_change_key, validator_set_key, + validator_slashes_key, validator_state_key, }; use namada::types::address::Address; use namada::types::transaction::InitValidator; @@ -14,6 +14,7 @@ use namada::types::{key, token}; pub use namada_proof_of_stake::{ epoched, parameters, types, PosActions as PosWrite, PosReadOnly as PosRead, }; +use rust_decimal::Decimal; use super::*; @@ -73,20 +74,35 @@ impl Ctx { ) } + /// Change validator commission rate. + pub fn change_validator_commission_rate( + &mut self, + validator: &Address, + rate: &Decimal, + ) -> TxResult { + let current_epoch = self.get_block_epoch()?; + namada_proof_of_stake::PosActions::change_validator_commission_rate( + self, + validator, + *rate, + current_epoch, + ) + } + /// Attempt to initialize a validator account. On success, returns the - /// initialized validator account's address and its staking reward address. + /// initialized validator account's address. pub fn init_validator( &mut self, InitValidator { account_key, consensus_key, - rewards_account_key, protocol_key, dkg_key, + commission_rate, + max_commission_rate_change, validator_vp_code, - rewards_vp_code, }: InitValidator, - ) -> EnvResult<(Address, Address)> { + ) -> EnvResult
{ let current_epoch = self.get_block_epoch()?; // Init validator account let validator_address = self.init_account(&validator_vp_code)?; @@ -97,19 +113,15 @@ impl Ctx { let dkg_pk_key = key::dkg_session_keys::dkg_pk_key(&validator_address); self.write(&dkg_pk_key, &dkg_key)?; - // Init staking reward account - let rewards_address = self.init_account(&rewards_vp_code)?; - let pk_key = key::pk_key(&rewards_address); - self.write(&pk_key, &rewards_account_key)?; - self.become_validator( &validator_address, - &rewards_address, &consensus_key, current_epoch, + commission_rate, + max_commission_rate_change, )?; - Ok((validator_address, rewards_address)) + Ok(validator_address) } } @@ -121,6 +133,7 @@ namada::impl_pos_read_only! { impl namada_proof_of_stake::PosActions for Ctx { type BecomeValidatorError = crate::Error; type BondError = crate::Error; + type CommissionRateChangeError = crate::Error; type UnbondError = crate::Error; type WithdrawError = crate::Error; @@ -140,14 +153,6 @@ impl namada_proof_of_stake::PosActions for Ctx { self.write(&validator_address_raw_hash_key(raw_hash), address) } - fn write_validator_staking_reward_address( - &mut self, - key: &Self::Address, - value: Self::Address, - ) -> Result<(), Self::Error> { - self.write(&validator_staking_reward_address_key(key), &value) - } - fn write_validator_consensus_key( &mut self, key: &Self::Address, @@ -164,20 +169,28 @@ impl namada_proof_of_stake::PosActions for Ctx { self.write(&validator_state_key(key), &value) } - fn write_validator_total_deltas( + fn write_validator_commission_rate( + &mut self, + key: &Self::Address, + value: CommissionRates, + ) -> Result<(), Self::Error> { + self.write(&validator_commission_rate_key(key), &value) + } + + fn write_validator_max_commission_rate_change( &mut self, key: &Self::Address, - value: ValidatorTotalDeltas, + value: Decimal, ) -> Result<(), Self::Error> { - self.write(&validator_total_deltas_key(key), &value) + self.write(&validator_max_commission_rate_change_key(key), &value) } - fn write_validator_voting_power( + fn write_validator_deltas( &mut self, key: &Self::Address, - value: ValidatorVotingPowers, + value: ValidatorDeltas, ) -> Result<(), Self::Error> { - self.write(&validator_voting_power_key(key), &value) + self.write(&validator_deltas_key(key), &value) } fn write_bond( @@ -203,11 +216,11 @@ impl namada_proof_of_stake::PosActions for Ctx { self.write(&validator_set_key(), &value) } - fn write_total_voting_power( + fn write_total_deltas( &mut self, - value: TotalVotingPowers, + value: TotalDeltas, ) -> Result<(), Self::Error> { - self.write(&total_voting_power_key(), &value) + self.write(&total_deltas_key(), &value) } fn delete_bond(&mut self, key: &BondId) -> Result<(), Self::Error> { diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 9d63d52b84..9ac4d0df4e 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1383,6 +1383,7 @@ dependencies = [ "rand_core 0.6.4", "rayon", "rust_decimal", + "rust_decimal_macros", "serde", "serde_json", "sha2 0.9.9", @@ -1418,6 +1419,8 @@ dependencies = [ "borsh", "derivative", "proptest", + "rust_decimal", + "rust_decimal_macros", "thiserror", ] @@ -1432,6 +1435,8 @@ dependencies = [ "namada_tx_prelude", "namada_vp_prelude", "prost", + "rust_decimal", + "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -1448,6 +1453,7 @@ dependencies = [ "namada", "namada_macros", "namada_vm_env", + "rust_decimal", "sha2 0.10.6", "thiserror", ] @@ -1987,10 +1993,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" dependencies = [ "arrayvec", + "borsh", "num-traits", "serde", ] +[[package]] +name = "rust_decimal_macros" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4903d8db81d2321699ca8318035d6ff805c548868df435813968795a802171b2" +dependencies = [ + "quote", + "rust_decimal", +] + [[package]] name = "rustc-demangle" version = "0.1.21" diff --git a/wasm/checksums.json b/wasm/checksums.json index 496d1c7a0f..6a58f4a336 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,15 +1,17 @@ { - "tx_bond.wasm": "tx_bond.04d6847800dad11990b42e8f2981a4a79d06d6d0c981c3d70c929e5b6a4f348b.wasm", - "tx_ibc.wasm": "tx_ibc.6ab530398ed8e276a8af7f231edbfae984b7e84eeb854714ba9339c5bed9d330.wasm", - "tx_init_account.wasm": "tx_init_account.578d987351e6ae42baa7849ae167e3ba33f3a62dba51cd47b0fa6d3ea6e4f128.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.71e27610210622fa53c3de58351761cca839681a4f450d4eff6b46bde3ae85a5.wasm", - "tx_init_validator.wasm": "tx_init_validator.269f065ff683782db2fdcac6e2485e80cbebb98929671a42eeb01703e0bbd8f5.wasm", - "tx_transfer.wasm": "tx_transfer.784325cf7763faf8d75797960cda6fbabbd343f3c6f7e6785f60f5e0911a6bb5.wasm", - "tx_unbond.wasm": "tx_unbond.ed13fa636d138ac4e35f2b4f31a6b4d3bed67e6b998dc6325f90711a2aca3704.wasm", - "tx_update_vp.wasm": "tx_update_vp.c4050e597116203eba5afde946a014afb067bdeaaae417377214a80c38a3786b.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.ece325881aad1c8a29f715c2f435c3335e08e51eed837c00ce0f7bbaddbefe50.wasm", - "tx_withdraw.wasm": "tx_withdraw.408fc10b3744c398258124e5e48e3449f6baf82a263df26911586a3382fbceb9.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.ae9a681dc2c1bd244b0575474fa4a364af56fa75833950693ca52ab25018c97d.wasm", - "vp_token.wasm": "vp_token.468de153dc5ce3af208bd762de3e85be48bc631012ec5f0947af95168da6cb93.wasm", - "vp_user.wasm": "vp_user.c101016a85a72f40da7f33e5d9061cfd2e3274eaac75a71c59c9ab4ed9896ffd.wasm" + "tx_bond.wasm": "tx_bond.fd65fa5ab8d36c70d92d48f451b9366b640c2e41cc741d44a99ae8534959a5f6.wasm", + "tx_change_validator_commission.wasm": "tx_change_validator_commission.a0721f500502c2c6be96d5959c15dfcee949be47f759d7887b50c14db2059663.wasm", + "tx_ibc.wasm": "tx_ibc.3eaa2f0e04f7236e2bda5a9085c72ff3c551967e7f74ed579ef84ac35da77261.wasm", + "tx_init_account.wasm": "tx_init_account.dbdf93a4bc047d605c62f9820b790b97d45fb31227450bc002ec3c9911f1ef0c.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.4e236bd4638fbf1e4cf9c95666088a2f1f9ce40965618e451a53f0e98df7c94e.wasm", + "tx_init_validator.wasm": "tx_init_validator.4a23272a0b3317893ffa1375bb216bcdfc3d8aac08ae57cbc9962919113852e1.wasm", + "tx_transfer.wasm": "tx_transfer.aee9a43a61f3ead0d98bf2408d4ecd9fa79540ed3e936849a77297c23c0864c5.wasm", + "tx_unbond.wasm": "tx_unbond.d49203da07696c5767dc7bc35f15b4176061ba590e5379bae632b7034b092712.wasm", + "tx_update_vp.wasm": "tx_update_vp.6f8c336936df72e8fdf2906e7f4da4b7ec2651fd0c2f89eaf4dc9af23d3211a5.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.0cfb4d0bb75bc48e6d5864e88d14d50aaaa77bd18efeafb902f35876c6d8b9bd.wasm", + "tx_withdraw.wasm": "tx_withdraw.fd5a77505e0bd3d2c4b860d749e1a7b42bb03fab484017ed329fa86bdf74de53.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.66a3c2307679f4db3e10d0f24cc9ac882ae8e693796a486995bdbf4dca786f0b.wasm", + "vp_token.wasm": "vp_token.ac5cba1238db7a18e51324063229b73d6c6b56d426be8acb4d2e2534e5a3b8e8.wasm", + "vp_user.wasm": "vp_user.8472c70c15095ffb3abcc69853553c4ca8b420f33d485788800558e1ba75a671.wasm", + "vp_validator.wasm": "vp_validator.a018d2597de6f78c44ab90cc7e884e3b8bacc359968c61566429f34c2fefab7e.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index f306196c39..a9682c5d9f 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -23,16 +23,18 @@ tx_unbond = ["namada_tx_prelude"] tx_update_vp = ["namada_tx_prelude"] tx_vote_proposal = ["namada_tx_prelude"] tx_withdraw = ["namada_tx_prelude"] +tx_change_validator_commission = ["namada_tx_prelude"] vp_testnet_faucet = ["namada_vp_prelude", "once_cell"] vp_token = ["namada_vp_prelude"] vp_user = ["namada_vp_prelude", "once_cell", "rust_decimal"] +vp_validator = ["namada_vp_prelude", "once_cell", "rust_decimal"] [dependencies] namada_tx_prelude = {path = "../../tx_prelude", optional = true} namada_vp_prelude = {path = "../../vp_prelude", optional = true} borsh = "0.9.0" once_cell = {version = "1.8.0", optional = true} -rust_decimal = {version = "1.14.3", optional = true} +rust_decimal = {version = "1.26.1", optional = true} wee_alloc = "0.4.5" getrandom = { version = "0.2", features = ["custom"] } @@ -45,3 +47,4 @@ namada_vp_prelude = {path = "../../vp_prelude"} proptest = {git = "https://github.com/heliaxdev/proptest", branch = "tomas/sm"} tracing = "0.1.30" tracing-subscriber = {version = "0.3.7", default-features = false, features = ["env-filter", "fmt"]} +rust_decimal = "1.26.1" \ No newline at end of file diff --git a/wasm/wasm_source/Makefile b/wasm/wasm_source/Makefile index ce4655c39a..b7842bd9f0 100644 --- a/wasm/wasm_source/Makefile +++ b/wasm/wasm_source/Makefile @@ -15,9 +15,11 @@ wasms += tx_transfer wasms += tx_unbond wasms += tx_update_vp wasms += tx_withdraw +wasms += tx_change_validator_commission wasms += vp_testnet_faucet wasms += vp_token wasms += vp_user +wasms += vp_validator # Build all wasms in release mode all: $(wasms) diff --git a/wasm/wasm_source/src/lib.rs b/wasm/wasm_source/src/lib.rs index 9075c60153..1afd097a45 100644 --- a/wasm/wasm_source/src/lib.rs +++ b/wasm/wasm_source/src/lib.rs @@ -1,5 +1,7 @@ #[cfg(feature = "tx_bond")] pub mod tx_bond; +#[cfg(feature = "tx_change_validator_commission")] +pub mod tx_change_validator_commission; #[cfg(feature = "tx_ibc")] pub mod tx_ibc; #[cfg(feature = "tx_init_account")] @@ -24,3 +26,6 @@ pub mod vp_testnet_faucet; pub mod vp_token; #[cfg(feature = "vp_user")] pub mod vp_user; + +#[cfg(feature = "vp_validator")] +pub mod vp_validator; diff --git a/wasm/wasm_source/src/tx_bond.rs b/wasm/wasm_source/src/tx_bond.rs index 6718988657..b90bcafb97 100644 --- a/wasm/wasm_source/src/tx_bond.rs +++ b/wasm/wasm_source/src/tx_bond.rs @@ -32,23 +32,22 @@ mod tests { use namada_tx_prelude::key::RefTo; use namada_tx_prelude::proof_of_stake::parameters::testing::arb_pos_params; use namada_tx_prelude::token; - use namada_vp_prelude::proof_of_stake::types::{ - Bond, VotingPower, VotingPowerDelta, - }; + use namada_vp_prelude::proof_of_stake::types::Bond; use namada_vp_prelude::proof_of_stake::{ staking_token_address, BondId, GenesisValidator, PosVP, }; use proptest::prelude::*; + use rust_decimal; use super::*; proptest! { - /// In this test we setup the ledger and PoS system with an arbitrary - /// initial state with 1 genesis validator and arbitrary PoS parameters. We then + /// In this test, we setup the ledger and PoS system with an arbitrary + /// initial stake with 1 genesis validator and arbitrary PoS parameters. We then /// generate an arbitrary bond that we'd like to apply. /// /// After we apply the bond, we check that all the storage values - /// in PoS system have been updated as expected and then we also check + /// in the PoS system have been updated as expected, and then we check /// that this transaction is accepted by the PoS validity predicate. #[test] fn test_tx_bond( @@ -68,16 +67,16 @@ mod tests { ) -> TxResult { let is_delegation = matches!( &bond.source, Some(source) if *source != bond.validator); - let staking_reward_address = address::testing::established_address_1(); let consensus_key = key::testing::keypair_1().ref_to(); - let staking_reward_key = key::testing::keypair_2().ref_to(); + let commission_rate = rust_decimal::Decimal::new(5, 2); + let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); let genesis_validators = [GenesisValidator { address: bond.validator.clone(), - staking_reward_address, tokens: initial_stake, consensus_key, - staking_reward_key, + commission_rate, + max_commission_rate_change, }]; init_pos(&genesis_validators[..], &pos_params, Epoch(0)); @@ -98,7 +97,8 @@ mod tests { let signed_tx = tx.sign(&key); let tx_data = signed_tx.data.unwrap(); - // Read the data before the tx is executed + // Ensure that the initial stake of the sole validator is equal to the + // PoS account balance let pos_balance_key = token::balance_key( &staking_token_address(), &Address::Internal(InternalAddress::PoS), @@ -107,41 +107,84 @@ mod tests { .read(&pos_balance_key)? .expect("PoS must have balance"); assert_eq!(pos_balance_pre, initial_stake); - let total_voting_powers_pre = ctx().read_total_voting_power()?; + + // Read some data before the tx is executed + let total_deltas_pre = ctx().read_total_deltas()?; + let validator_deltas_pre = + ctx().read_validator_deltas(&bond.validator)?.unwrap(); let validator_sets_pre = ctx().read_validator_set()?; - let validator_voting_powers_pre = - ctx().read_validator_voting_power(&bond.validator)?.unwrap(); apply_tx(ctx(), tx_data)?; - // Read the data after the tx is executed + // Read the data after the tx is executed. + let validator_deltas_post = + ctx().read_validator_deltas(&bond.validator)?.unwrap(); + let total_deltas_post = ctx().read_total_deltas()?; + let validator_sets_post = ctx().read_validator_set()?; // The following storage keys should be updated: - // - `#{PoS}/validator/#{validator}/total_deltas` - let total_delta_post = - ctx().read_validator_total_deltas(&bond.validator)?; - for epoch in 0..pos_params.pipeline_len { - assert_eq!( - total_delta_post.as_ref().unwrap().get(epoch), - Some(initial_stake.into()), - "The total deltas before the pipeline offset must not change \ - - checking in epoch: {epoch}" - ); - } - for epoch in pos_params.pipeline_len..=pos_params.unbonding_len { - let expected_stake = - i128::from(initial_stake) + i128::from(bond.amount); - assert_eq!( - total_delta_post.as_ref().unwrap().get(epoch), - Some(expected_stake), - "The total deltas at and after the pipeline offset epoch must \ - be incremented by the bonded amount - checking in epoch: \ - {epoch}" - ); + // - `#{PoS}/validator/#{validator}/deltas` + // - `#{PoS}/total_deltas` + // - `#{PoS}/validator_set` + + // Check that the validator set and deltas are unchanged before pipeline + // length and that they are updated between the pipeline and + // unbonding lengths + if bond.amount == token::Amount::from(0) { + // None of the optional storage fields should have been updated + assert_eq!(validator_sets_pre, validator_sets_post); + assert_eq!(validator_deltas_pre, validator_deltas_post); + assert_eq!(total_deltas_pre, total_deltas_post); + } else { + for epoch in 0..pos_params.pipeline_len { + assert_eq!( + validator_deltas_post.get(epoch), + Some(initial_stake.into()), + "The validator deltas before the pipeline offset must not \ + change - checking in epoch: {epoch}" + ); + assert_eq!( + total_deltas_post.get(epoch), + Some(initial_stake.into()), + "The total deltas before the pipeline offset must not \ + change - checking in epoch: {epoch}" + ); + assert_eq!( + validator_sets_pre.get(epoch), + validator_sets_post.get(epoch), + "Validator set before pipeline offset must not change - \ + checking epoch {epoch}" + ); + } + for epoch in pos_params.pipeline_len..=pos_params.unbonding_len { + let expected_stake = + i128::from(initial_stake) + i128::from(bond.amount); + assert_eq!( + validator_deltas_post.get(epoch), + Some(expected_stake), + "The total deltas at and after the pipeline offset epoch \ + must be incremented by the bonded amount - checking in \ + epoch: {epoch}" + ); + assert_eq!( + total_deltas_post.get(epoch), + Some(expected_stake), + "The total deltas at and after the pipeline offset epoch \ + must be incremented by the bonded amount - checking in \ + epoch: {epoch}" + ); + assert_ne!( + validator_sets_pre.get(epoch), + validator_sets_post.get(epoch), + "Validator set at and after pipeline offset must have \ + changed - checking epoch {epoch}" + ); + } } // - `#{staking_token}/balance/#{PoS}` + // Check that PoS balance is updated let pos_balance_post: token::Amount = ctx().read(&pos_balance_key)?.unwrap(); assert_eq!(pos_balance_pre + bond.amount, pos_balance_post); @@ -159,6 +202,7 @@ mod tests { if is_delegation { // A delegation is applied at pipeline offset + // Check that bond is empty before pipeline offset for epoch in 0..pos_params.pipeline_len { let bond: Option> = bonds_post.get(epoch); assert!( @@ -167,6 +211,7 @@ mod tests { checking epoch {epoch}, got {bond:#?}" ); } + // Check that bond is updated after the pipeline length for epoch in pos_params.pipeline_len..=pos_params.unbonding_len { let start_epoch = namada_tx_prelude::proof_of_stake::types::Epoch::from( @@ -182,9 +227,11 @@ mod tests { ); } } else { + // This is a self-bond + // Check that a bond already exists from genesis with initial stake + // for the validator let genesis_epoch = namada_tx_prelude::proof_of_stake::types::Epoch::from(0); - // It was a self-bond for epoch in 0..pos_params.pipeline_len { let expected_bond = HashMap::from_iter([(genesis_epoch, initial_stake)]); @@ -197,6 +244,7 @@ mod tests { genesis initial stake - checking epoch {epoch}" ); } + // Check that the bond is updated after the pipeline length for epoch in pos_params.pipeline_len..=pos_params.unbonding_len { let start_epoch = namada_tx_prelude::proof_of_stake::types::Epoch::from( @@ -216,99 +264,6 @@ mod tests { } } - // If the voting power from validator's initial stake is different - // from the voting power after the bond is applied, we expect the - // following 3 fields to be updated: - // - `#{PoS}/total_voting_power` (optional) - // - `#{PoS}/validator_set` (optional) - // - `#{PoS}/validator/#{validator}/voting_power` (optional) - let total_voting_powers_post = ctx().read_total_voting_power()?; - let validator_sets_post = ctx().read_validator_set()?; - let validator_voting_powers_post = - ctx().read_validator_voting_power(&bond.validator)?.unwrap(); - - let voting_power_pre = - VotingPower::from_tokens(initial_stake, &pos_params); - let voting_power_post = - VotingPower::from_tokens(initial_stake + bond.amount, &pos_params); - if voting_power_pre == voting_power_post { - // None of the optional storage fields should have been updated - assert_eq!(total_voting_powers_pre, total_voting_powers_post); - assert_eq!(validator_sets_pre, validator_sets_post); - assert_eq!( - validator_voting_powers_pre, - validator_voting_powers_post - ); - } else { - for epoch in 0..pos_params.pipeline_len { - let total_voting_power_pre = total_voting_powers_pre.get(epoch); - let total_voting_power_post = - total_voting_powers_post.get(epoch); - assert_eq!( - total_voting_power_pre, total_voting_power_post, - "Total voting power before pipeline offset must not \ - change - checking epoch {epoch}" - ); - - let validator_set_pre = validator_sets_pre.get(epoch); - let validator_set_post = validator_sets_post.get(epoch); - assert_eq!( - validator_set_pre, validator_set_post, - "Validator set before pipeline offset must not change - \ - checking epoch {epoch}" - ); - - let validator_voting_power_pre = - validator_voting_powers_pre.get(epoch); - let validator_voting_power_post = - validator_voting_powers_post.get(epoch); - assert_eq!( - validator_voting_power_pre, validator_voting_power_post, - "Validator's voting power before pipeline offset must not \ - change - checking epoch {epoch}" - ); - } - for epoch in pos_params.pipeline_len..=pos_params.unbonding_len { - let total_voting_power_pre = - total_voting_powers_pre.get(epoch).unwrap(); - let total_voting_power_post = - total_voting_powers_post.get(epoch).unwrap(); - assert_ne!( - total_voting_power_pre, total_voting_power_post, - "Total voting power at and after pipeline offset must \ - have changed - checking epoch {epoch}" - ); - - let validator_set_pre = validator_sets_pre.get(epoch).unwrap(); - let validator_set_post = - validator_sets_post.get(epoch).unwrap(); - assert_ne!( - validator_set_pre, validator_set_post, - "Validator set at and after pipeline offset must have \ - changed - checking epoch {epoch}" - ); - - let validator_voting_power_pre = - validator_voting_powers_pre.get(epoch).unwrap(); - let validator_voting_power_post = - validator_voting_powers_post.get(epoch).unwrap(); - assert_ne!( - validator_voting_power_pre, validator_voting_power_post, - "Validator's voting power at and after pipeline offset \ - must have changed - checking epoch {epoch}" - ); - - // Expected voting power from the model ... - let expected_validator_voting_power: VotingPowerDelta = - voting_power_post.try_into().unwrap(); - // ... must match the voting power read from storage - assert_eq!( - validator_voting_power_post, - expected_validator_voting_power - ); - } - } - // Use the tx_env to run PoS VP let tx_env = tx_host_env::take(); let vp_env = TestNativeVpEnv::from_tx_env(tx_env, address::POS); @@ -328,9 +283,9 @@ mod tests { /// overflow. fn arb_initial_stake_and_bond() // Generate initial stake - (initial_stake in token::testing::arb_amount()) + (initial_stake in token::testing::arb_amount_ceiled((i64::MAX/8) as u64)) // Use the initial stake to limit the bond amount - (bond in arb_bond(u64::MAX - u64::from(initial_stake)), + (bond in arb_bond(((i64::MAX/8) as u64) - u64::from(initial_stake)), // Use the generated initial stake too initial_stake in Just(initial_stake), ) -> (token::Amount, transaction::pos::Bond) { diff --git a/wasm/wasm_source/src/tx_change_validator_commission.rs b/wasm/wasm_source/src/tx_change_validator_commission.rs new file mode 100644 index 0000000000..0f72355af5 --- /dev/null +++ b/wasm/wasm_source/src/tx_change_validator_commission.rs @@ -0,0 +1,192 @@ +//! A tx for a validator to change their commission rate for PoS rewards. + +use namada_tx_prelude::transaction::pos::CommissionChange; +use namada_tx_prelude::*; + +#[transaction] +fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { + let signed = SignedTxData::try_from_slice(&tx_data[..]) + .wrap_err("failed to decode SignedTxData")?; + let data = signed.data.ok_or_err_msg("Missing data")?; + let CommissionChange { + validator, + new_rate, + } = transaction::pos::CommissionChange::try_from_slice(&data[..]) + .wrap_err("failed to decode Decimal value")?; + ctx.change_validator_commission_rate(&validator, &new_rate) +} + +#[cfg(test)] +mod tests { + use namada::ledger::pos::PosParams; + use namada::proto::Tx; + use namada::types::storage::Epoch; + use namada_tests::log::test; + use namada_tests::native_vp::pos::init_pos; + use namada_tests::native_vp::TestNativeVpEnv; + use namada_tests::tx::*; + use namada_tx_prelude::address::testing::arb_established_address; + use namada_tx_prelude::key::testing::arb_common_keypair; + use namada_tx_prelude::key::RefTo; + use namada_tx_prelude::proof_of_stake::parameters::testing::arb_pos_params; + use namada_tx_prelude::token; + use namada_vp_prelude::proof_of_stake::{ + CommissionRates, GenesisValidator, PosVP, + }; + use proptest::prelude::*; + use rust_decimal::prelude::ToPrimitive; + use rust_decimal::Decimal; + + use super::*; + + proptest! { + /// In this test we setup the ledger and PoS system with an arbitrary + /// initial state with 1 genesis validator and arbitrary PoS parameters. We then + /// generate an arbitrary bond that we'd like to apply. + /// + /// After we apply the bond, we check that all the storage values + /// in PoS system have been updated as expected and then we also check + /// that this transaction is accepted by the PoS validity predicate. + #[test] + fn test_tx_change_validator_commissions( + commission_state_change in arb_commission_info(), + // A key to sign the transaction + key in arb_common_keypair(), + pos_params in arb_pos_params()) { + test_tx_change_validator_commission_aux(commission_state_change.2, commission_state_change.0, commission_state_change.1, key, pos_params).unwrap() + } + } + + fn test_tx_change_validator_commission_aux( + commission_change: transaction::pos::CommissionChange, + initial_rate: Decimal, + max_change: Decimal, + key: key::common::SecretKey, + pos_params: PosParams, + ) -> TxResult { + let consensus_key = key::testing::keypair_1().ref_to(); + let genesis_validators = [GenesisValidator { + address: commission_change.validator.clone(), + tokens: token::Amount::from(1_000_000), + consensus_key, + commission_rate: initial_rate, + max_commission_rate_change: max_change, + }]; + + init_pos(&genesis_validators[..], &pos_params, Epoch(0)); + + let tx_code = vec![]; + let tx_data = commission_change.try_to_vec().unwrap(); + let tx = Tx::new(tx_code, Some(tx_data)); + let signed_tx = tx.sign(&key); + let tx_data = signed_tx.data.unwrap(); + + // Read the data before the tx is executed + let commission_rates_pre: CommissionRates = ctx() + .read_validator_commission_rate(&commission_change.validator)? + .expect("PoS validator must have commission rates"); + let commission_rate = *commission_rates_pre + .get(0) + .expect("PoS validator must have commission rate at genesis"); + assert_eq!(commission_rate, initial_rate); + + apply_tx(ctx(), tx_data)?; + + // Read the data after the tx is executed + + // The following storage keys should be updated: + + // - `#{PoS}/validator/#{validator}/commission_rate` + + let commission_rates_post: CommissionRates = ctx() + .read_validator_commission_rate(&commission_change.validator)? + .unwrap(); + + dbg!(&commission_rates_pre); + dbg!(&commission_rates_post); + + // Before pipeline, the commission rates should not change + for epoch in 0..pos_params.pipeline_len { + assert_eq!( + commission_rates_pre.get(epoch), + commission_rates_post.get(epoch), + "The commission rates before the pipeline offset must not \ + change - checking in epoch: {epoch}" + ); + assert_eq!( + Some(&initial_rate), + commission_rates_post.get(epoch), + "The commission rates before the pipeline offset must not \ + change - checking in epoch: {epoch}" + ); + } + + // After pipeline, the commission rates should have changed + for epoch in pos_params.pipeline_len..=pos_params.unbonding_len { + assert_ne!( + commission_rates_pre.get(epoch), + commission_rates_post.get(epoch), + "The commission rate after the pipeline offset must have \ + changed - checking in epoch: {epoch}" + ); + assert_eq!( + Some(&commission_change.new_rate), + commission_rates_post.get(epoch), + "The commission rate after the pipeline offset must be the \ + new_rate - checking in epoch: {epoch}" + ); + } + + // Use the tx_env to run PoS VP + let tx_env = tx_host_env::take(); + let vp_env = TestNativeVpEnv::from_tx_env(tx_env, address::POS); + let result = vp_env.validate_tx(PosVP::new); + let result = + result.expect("Validation of valid changes must not fail!"); + assert!( + result, + "PoS Validity predicate must accept this transaction" + ); + + Ok(()) + } + + fn arb_rate(min: Decimal, max: Decimal) -> impl Strategy { + let int_min: u64 = (min * Decimal::from(100_000_u64)) + .to_u64() + .unwrap_or_default(); + let int_max: u64 = (max * Decimal::from(100_000_u64)).to_u64().unwrap(); + (int_min..=int_max) + .prop_map(|num| Decimal::from(num) / Decimal::from(100_000_u64)) + } + + fn arb_commission_change( + rate_pre: Decimal, + max_change: Decimal, + ) -> impl Strategy { + let min = rate_pre - max_change; + let max = rate_pre + max_change; + (arb_established_address(), arb_rate(min, max)).prop_map( + |(validator, new_rate)| transaction::pos::CommissionChange { + validator: Address::Established(validator), + new_rate, + }, + ) + } + + fn arb_commission_info() + -> impl Strategy + { + let min = Decimal::ZERO; + let max = Decimal::ONE; + (arb_rate(min, max), arb_rate(min, max)).prop_flat_map( + |(rate, change)| { + ( + Just(rate), + Just(change), + arb_commission_change(rate, change), + ) + }, + ) + } +} diff --git a/wasm/wasm_source/src/tx_init_validator.rs b/wasm/wasm_source/src/tx_init_validator.rs index 2d5f1a6256..6a823faf3f 100644 --- a/wasm/wasm_source/src/tx_init_validator.rs +++ b/wasm/wasm_source/src/tx_init_validator.rs @@ -1,5 +1,5 @@ -//! A tx to initialize a new validator account and staking reward account with a -//! given public keys and a validity predicates. +//! A tx to initialize a new validator account with a given public keys and a +//! validity predicates. use namada_tx_prelude::transaction::InitValidator; use namada_tx_prelude::*; @@ -15,12 +15,8 @@ fn apply_tx(ctx: &mut Ctx, tx_data: Vec) -> TxResult { // Register the validator in PoS match ctx.init_validator(init_validator) { - Ok((validator_address, staking_reward_address)) => { - debug_log!( - "Created validator {} and staking reward account {}", - validator_address.encode(), - staking_reward_address.encode() - ) + Ok(validator_address) => { + debug_log!("Created validator {}", validator_address.encode(),) } Err(err) => { debug_log!("Validator creation failed with: {}", err); diff --git a/wasm/wasm_source/src/tx_unbond.rs b/wasm/wasm_source/src/tx_unbond.rs index 6199393fb1..8b978574cd 100644 --- a/wasm/wasm_source/src/tx_unbond.rs +++ b/wasm/wasm_source/src/tx_unbond.rs @@ -30,9 +30,7 @@ mod tests { use namada_tx_prelude::key::RefTo; use namada_tx_prelude::proof_of_stake::parameters::testing::arb_pos_params; use namada_tx_prelude::token; - use namada_vp_prelude::proof_of_stake::types::{ - Bond, Unbond, VotingPower, VotingPowerDelta, - }; + use namada_vp_prelude::proof_of_stake::types::{Bond, Unbond}; use namada_vp_prelude::proof_of_stake::{ staking_token_address, BondId, GenesisValidator, PosVP, }; @@ -67,13 +65,12 @@ mod tests { ) -> TxResult { let is_delegation = matches!( &unbond.source, Some(source) if *source != unbond.validator); - let staking_reward_address = address::testing::established_address_1(); let consensus_key = key::testing::keypair_1().ref_to(); - let staking_reward_key = key::testing::keypair_2().ref_to(); + let commission_rate = rust_decimal::Decimal::new(5, 2); + let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); let genesis_validators = [GenesisValidator { address: unbond.validator.clone(), - staking_reward_address, tokens: if is_delegation { // If we're unbonding a delegation, we'll give the initial stake // to the delegation instead of the validator @@ -82,7 +79,8 @@ mod tests { initial_stake }, consensus_key, - staking_reward_key, + commission_rate, + max_commission_rate_change, }]; init_pos(&genesis_validators[..], &pos_params, Epoch(0)); @@ -104,9 +102,9 @@ mod tests { } }); + // Initialize the delegation if it is the case - unlike genesis + // validator's self-bond, this happens at pipeline offset if is_delegation { - // Initialize the delegation - unlike genesis validator's self-bond, - // this happens at pipeline offset ctx().bond_tokens( unbond.source.as_ref(), &unbond.validator, @@ -138,25 +136,29 @@ mod tests { .read(&pos_balance_key)? .expect("PoS must have balance"); assert_eq!(pos_balance_pre, initial_stake); - let total_voting_powers_pre = ctx().read_total_voting_power()?; + + let _total_deltas_pre = ctx().read_total_deltas()?; let validator_sets_pre = ctx().read_validator_set()?; - let validator_voting_powers_pre = ctx() - .read_validator_voting_power(&unbond.validator)? - .unwrap(); + let _validator_deltas_pre = + ctx().read_validator_deltas(&unbond.validator)?.unwrap(); let bonds_pre = ctx().read_bond(&unbond_id)?.unwrap(); dbg!(&bonds_pre); + // Apply the unbond tx apply_tx(ctx(), tx_data)?; - // Read the data after the tx is executed - + // Read the data after the tx is executed. // The following storage keys should be updated: - // - `#{PoS}/validator/#{validator}/total_deltas` - let total_delta_post = - ctx().read_validator_total_deltas(&unbond.validator)?; + // - `#{PoS}/validator/#{validator}/deltas` + // - `#{PoS}/total_deltas` + // - `#{PoS}/validator_set` + let total_deltas_post = ctx().read_total_deltas()?; + let validator_deltas_post = + ctx().read_validator_deltas(&unbond.validator)?; + let validator_sets_post = ctx().read_validator_set()?; - let expected_deltas_at_pipeline = if is_delegation { + let expected_amount_before_pipeline = if is_delegation { // When this is a delegation, there will be no bond until pipeline 0.into() } else { @@ -167,23 +169,50 @@ mod tests { // Before pipeline offset, there can only be self-bond for genesis // validator. In case of a delegation the state is setup so that there // is no bond until pipeline offset. + // + // TODO: check if this test is correct (0 -> unbonding?) for epoch in 0..pos_params.pipeline_len { assert_eq!( - total_delta_post.as_ref().unwrap().get(epoch), - Some(expected_deltas_at_pipeline.into()), + validator_deltas_post.as_ref().unwrap().get(epoch), + Some(expected_amount_before_pipeline.into()), + "The validator deltas before the pipeline offset must not \ + change - checking in epoch: {epoch}" + ); + assert_eq!( + total_deltas_post.get(epoch), + Some(expected_amount_before_pipeline.into()), "The total deltas before the pipeline offset must not change \ - checking in epoch: {epoch}" ); + assert_eq!( + validator_sets_pre.get(epoch), + validator_sets_post.get(epoch), + "Validator set before pipeline offset must not change - \ + checking epoch {epoch}" + ); } // At and after pipeline offset, there can be either delegation or // self-bond, both of which are initialized to the same `initial_stake` for epoch in pos_params.pipeline_len..pos_params.unbonding_len { assert_eq!( - total_delta_post.as_ref().unwrap().get(epoch), + validator_deltas_post.as_ref().unwrap().get(epoch), Some(initial_stake.into()), - "The total deltas before the unbonding offset must not change \ - - checking in epoch: {epoch}" + "The validator deltas at and after the unbonding offset must \ + have changed - checking in epoch: {epoch}" + ); + assert_eq!( + total_deltas_post.get(epoch), + Some(initial_stake.into()), + "The total deltas at and after the unbonding offset must have \ + changed - checking in epoch: {epoch}" + ); + assert_eq!( + validator_sets_pre.get(epoch), + validator_sets_post.get(epoch), + "Validator set at and after pipeline offset should be the \ + same since we are before the unbonding offset - checking \ + epoch {epoch}" ); } @@ -192,15 +221,23 @@ mod tests { let expected_stake = i128::from(initial_stake) - i128::from(unbond.amount); assert_eq!( - total_delta_post.as_ref().unwrap().get(epoch), + validator_deltas_post.as_ref().unwrap().get(epoch), Some(expected_stake), - "The total deltas after the unbonding offset epoch must be \ + "The total deltas at after the unbonding offset epoch must be \ + decremented by the unbonded amount - checking in epoch: \ + {epoch}" + ); + assert_eq!( + total_deltas_post.get(epoch), + Some(expected_stake), + "The total deltas at after the unbonding offset epoch must be \ decremented by the unbonded amount - checking in epoch: \ {epoch}" ); } // - `#{staking_token}/balance/#{PoS}` + // Check that PoS account balance is unchanged by unbond let pos_balance_post: token::Amount = ctx().read(&pos_balance_key)?.unwrap(); assert_eq!( @@ -209,6 +246,7 @@ mod tests { ); // - `#{PoS}/unbond/#{owner}/#{validator}` + // Check that the unbond doesn't exist until unbonding offset let unbonds_post = ctx().read_unbond(&unbond_id)?.unwrap(); let bonds_post = ctx().read_bond(&unbond_id)?.unwrap(); for epoch in 0..pos_params.unbonding_len { @@ -220,6 +258,7 @@ mod tests { epoch {epoch}" ); } + // Check that the unbond is as expected let start_epoch = match &unbond.source { Some(_) => { // This bond was a delegation @@ -272,102 +311,6 @@ mod tests { deducted, checking epoch {epoch}" ) } - // If the voting power from validator's initial stake is different - // from the voting power after the bond is applied, we expect the - // following 3 fields to be updated: - // - `#{PoS}/total_voting_power` (optional) - // - `#{PoS}/validator_set` (optional) - // - `#{PoS}/validator/#{validator}/voting_power` (optional) - let total_voting_powers_post = ctx().read_total_voting_power()?; - let validator_sets_post = ctx().read_validator_set()?; - let validator_voting_powers_post = ctx() - .read_validator_voting_power(&unbond.validator)? - .unwrap(); - - let voting_power_pre = - VotingPower::from_tokens(initial_stake, &pos_params); - let voting_power_post = VotingPower::from_tokens( - initial_stake - unbond.amount, - &pos_params, - ); - if voting_power_pre == voting_power_post { - // None of the optional storage fields should have been updated - assert_eq!(total_voting_powers_pre, total_voting_powers_post); - assert_eq!(validator_sets_pre, validator_sets_post); - assert_eq!( - validator_voting_powers_pre, - validator_voting_powers_post - ); - } else { - for epoch in 0..pos_params.unbonding_len { - let total_voting_power_pre = total_voting_powers_pre.get(epoch); - let total_voting_power_post = - total_voting_powers_post.get(epoch); - assert_eq!( - total_voting_power_pre, total_voting_power_post, - "Total voting power before pipeline offset must not \ - change - checking epoch {epoch}" - ); - - let validator_set_pre = validator_sets_pre.get(epoch); - let validator_set_post = validator_sets_post.get(epoch); - assert_eq!( - validator_set_pre, validator_set_post, - "Validator set before pipeline offset must not change - \ - checking epoch {epoch}" - ); - - let validator_voting_power_pre = - validator_voting_powers_pre.get(epoch); - let validator_voting_power_post = - validator_voting_powers_post.get(epoch); - assert_eq!( - validator_voting_power_pre, validator_voting_power_post, - "Validator's voting power before pipeline offset must not \ - change - checking epoch {epoch}" - ); - } - { - let epoch = pos_params.unbonding_len; - let total_voting_power_pre = - total_voting_powers_pre.get(epoch).unwrap(); - let total_voting_power_post = - total_voting_powers_post.get(epoch).unwrap(); - assert_ne!( - total_voting_power_pre, total_voting_power_post, - "Total voting power at and after pipeline offset must \ - have changed - checking epoch {epoch}" - ); - - let validator_set_pre = validator_sets_pre.get(epoch).unwrap(); - let validator_set_post = - validator_sets_post.get(epoch).unwrap(); - assert_ne!( - validator_set_pre, validator_set_post, - "Validator set at and after pipeline offset must have \ - changed - checking epoch {epoch}" - ); - - let validator_voting_power_pre = - validator_voting_powers_pre.get(epoch).unwrap(); - let validator_voting_power_post = - validator_voting_powers_post.get(epoch).unwrap(); - assert_ne!( - validator_voting_power_pre, validator_voting_power_post, - "Validator's voting power at and after pipeline offset \ - must have changed - checking epoch {epoch}" - ); - - // Expected voting power from the model ... - let expected_validator_voting_power: VotingPowerDelta = - voting_power_post.try_into().unwrap(); - // ... must match the voting power read from storage - assert_eq!( - validator_voting_power_post, - expected_validator_voting_power - ); - } - } // Use the tx_env to run PoS VP let tx_env = tx_host_env::take(); @@ -385,12 +328,14 @@ mod tests { fn arb_initial_stake_and_unbond() -> impl Strategy { // Generate initial stake - token::testing::arb_amount().prop_flat_map(|initial_stake| { - // Use the initial stake to limit the bond amount - let unbond = arb_unbond(u64::from(initial_stake)); - // Use the generated initial stake too too - (Just(initial_stake), unbond) - }) + token::testing::arb_amount_ceiled((i64::MAX / 8) as u64).prop_flat_map( + |initial_stake| { + // Use the initial stake to limit the bond amount + let unbond = arb_unbond(u64::from(initial_stake)); + // Use the generated initial stake too too + (Just(initial_stake), unbond) + }, + ) } /// Generates an initial validator stake and a unbond, while making sure diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index 8add20a78d..dc054fcd6f 100644 --- a/wasm/wasm_source/src/tx_withdraw.rs +++ b/wasm/wasm_source/src/tx_withdraw.rs @@ -73,13 +73,12 @@ mod tests { ) -> TxResult { let is_delegation = matches!( &withdraw.source, Some(source) if *source != withdraw.validator); - let staking_reward_address = address::testing::established_address_1(); let consensus_key = key::testing::keypair_1().ref_to(); - let staking_reward_key = key::testing::keypair_2().ref_to(); + let commission_rate = rust_decimal::Decimal::new(5, 2); + let max_commission_rate_change = rust_decimal::Decimal::new(1, 2); let genesis_validators = [GenesisValidator { address: withdraw.validator.clone(), - staking_reward_address, tokens: if is_delegation { // If we're withdrawing a delegation, we'll give the initial // stake to the delegation instead of the @@ -89,7 +88,8 @@ mod tests { initial_stake }, consensus_key, - staking_reward_key, + commission_rate, + max_commission_rate_change, }]; init_pos(&genesis_validators[..], &pos_params, Epoch(0)); @@ -201,13 +201,16 @@ mod tests { fn arb_initial_stake_and_unbonded_amount() -> impl Strategy { // Generate initial stake - token::testing::arb_amount().prop_flat_map(|initial_stake| { - // Use the initial stake to limit the unbonded amount from the stake - let unbonded_amount = - token::testing::arb_amount_ceiled(initial_stake.into()); - // Use the generated initial stake too too - (Just(initial_stake), unbonded_amount) - }) + token::testing::arb_amount_ceiled((i64::MAX / 8) as u64).prop_flat_map( + |initial_stake| { + // Use the initial stake to limit the unbonded amount from the + // stake + let unbonded_amount = + token::testing::arb_amount_ceiled(initial_stake.into()); + // Use the generated initial stake too too + (Just(initial_stake), unbonded_amount) + }, + ) } fn arb_withdraw() -> impl Strategy { diff --git a/wasm/wasm_source/src/vp_validator.rs b/wasm/wasm_source/src/vp_validator.rs new file mode 100644 index 0000000000..bdf41d792f --- /dev/null +++ b/wasm/wasm_source/src/vp_validator.rs @@ -0,0 +1,760 @@ +//! A basic validator VP. +//! +//! Like the user VP, this VP currently provides a signature verification +//! against a public key for sending tokens (receiving tokens is permissive). +//! +//! It allows to bond, unbond and withdraw tokens to and from PoS system with a +//! valid signature. +//! +//! Currently, the only difference with respect to the user VP is for a tx to +//! change a validator's commission rate: we require a valid signature only from +//! the validator whose commission rate is being changed. +//! +//! Any other storage key changes are allowed only with a valid signature. + +use namada_vp_prelude::storage::KeySeg; +use namada_vp_prelude::*; +use once_cell::unsync::Lazy; + +enum KeyType<'a> { + Token(&'a Address), + PoS, + Vp(&'a Address), + GovernanceVote(&'a Address), + Unknown, +} + +impl<'a> From<&'a storage::Key> for KeyType<'a> { + fn from(key: &'a storage::Key) -> KeyType<'a> { + if let Some(address) = token::is_any_token_balance_key(key) { + Self::Token(address) + } else if let Some((_, address)) = + token::is_any_multitoken_balance_key(key) + { + Self::Token(address) + } else if proof_of_stake::is_pos_key(key) { + Self::PoS + } else if gov_storage::is_vote_key(key) { + let voter_address = gov_storage::get_voter_address(key); + if let Some(address) = voter_address { + Self::GovernanceVote(address) + } else { + Self::Unknown + } + } else if let Some(address) = key.is_validity_predicate() { + Self::Vp(address) + } else { + Self::Unknown + } + } +} + +#[validity_predicate] +fn validate_tx( + ctx: &Ctx, + tx_data: Vec, + addr: Address, + keys_changed: BTreeSet, + verifiers: BTreeSet
, +) -> VpResult { + debug_log!( + "vp_user called with user addr: {}, key_changed: {:?}, verifiers: {:?}", + addr, + keys_changed, + verifiers + ); + + let signed_tx_data = + Lazy::new(|| SignedTxData::try_from_slice(&tx_data[..])); + + let valid_sig = Lazy::new(|| match &*signed_tx_data { + Ok(signed_tx_data) => { + let pk = key::get(ctx, &addr); + match pk { + Ok(Some(pk)) => { + matches!( + ctx.verify_tx_signature(&pk, &signed_tx_data.sig), + Ok(true) + ) + } + _ => false, + } + } + _ => false, + }); + + if !is_valid_tx(ctx, &tx_data)? { + return reject(); + } + + for key in keys_changed.iter() { + let key_type: KeyType = key.into(); + let is_valid = match key_type { + KeyType::Token(owner) => { + if owner == &addr { + let pre: token::Amount = + ctx.read_pre(key)?.unwrap_or_default(); + let post: token::Amount = + ctx.read_post(key)?.unwrap_or_default(); + let change = post.change() - pre.change(); + // debit has to signed, credit doesn't + let valid = change >= 0 || *valid_sig; + debug_log!( + "token key: {}, change: {}, valid_sig: {}, valid \ + modification: {}", + key, + change, + *valid_sig, + valid + ); + valid + } else { + debug_log!( + "This address ({}) is not of owner ({}) of token key: \ + {}", + addr, + owner, + key + ); + // If this is not the owner, allow any change + true + } + } + KeyType::PoS => { + // Allow the account to be used in PoS + let bond_id = proof_of_stake::is_bond_key(key) + .or_else(|| proof_of_stake::is_unbond_key(key)); + let valid_bond_or_unbond_change = match bond_id { + Some(bond_id) => { + // Bonds and unbonds changes for this address + // must be signed + bond_id.source != addr || *valid_sig + } + None => { + // Any other PoS changes are allowed without signature + true + } + }; + let comm = + proof_of_stake::is_validator_commission_rate_key(key); + // Validator's commission rate change must be signed + let valid_commission_rate_change = match comm { + Some(source) => *source != addr || *valid_sig, + None => true, + }; + let valid = + valid_bond_or_unbond_change && valid_commission_rate_change; + debug_log!( + "PoS key {} {}", + key, + if valid { "accepted" } else { "rejected" } + ); + valid + } + KeyType::GovernanceVote(voter) => { + if voter == &addr { + *valid_sig + } else { + true + } + } + KeyType::Vp(owner) => { + let has_post: bool = ctx.has_key_post(key)?; + if owner == &addr { + if has_post { + let vp: Vec = ctx.read_bytes_post(key)?.unwrap(); + *valid_sig && is_vp_whitelisted(ctx, &vp)? + } else { + false + } + } else { + let vp: Vec = ctx.read_bytes_post(key)?.unwrap(); + is_vp_whitelisted(ctx, &vp)? + } + } + KeyType::Unknown => { + if key.segments.get(0) == Some(&addr.to_db_key()) { + // Unknown changes to this address space require a valid + // signature + *valid_sig + } else { + // Unknown changes anywhere else are permitted + true + } + } + }; + if !is_valid { + debug_log!("key {} modification failed vp", key); + return reject(); + } + } + + accept() +} + +#[cfg(test)] +mod tests { + use address::testing::arb_non_internal_address; + // Use this as `#[test]` annotation to enable logging + use namada_tests::log::test; + use namada_tests::tx::{self, tx_host_env, TestTxEnv}; + use namada_tests::vp::vp_host_env::storage::Key; + use namada_tests::vp::*; + use namada_tx_prelude::{StorageWrite, TxEnv}; + use namada_vp_prelude::key::RefTo; + use proptest::prelude::*; + use storage::testing::arb_account_storage_key_no_vp; + + use super::*; + + const VP_ALWAYS_TRUE_WASM: &str = + "../../wasm_for_tests/vp_always_true.wasm"; + + /// Test that no-op transaction (i.e. no storage modifications) accepted. + #[test] + fn test_no_op_transaction() { + let tx_data: Vec = vec![]; + let addr: Address = address::testing::established_address_1(); + let keys_changed: BTreeSet = BTreeSet::default(); + let verifiers: BTreeSet
= BTreeSet::default(); + + // The VP env must be initialized before calling `validate_tx` + vp_host_env::init(); + + assert!( + validate_tx(&CTX, tx_data, addr, keys_changed, verifiers).unwrap() + ); + } + + /// Test that a credit transfer is accepted. + #[test] + fn test_credit_transfer_accepted() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let vp_owner = address::testing::established_address_1(); + let source = address::testing::established_address_2(); + let token = address::nam(); + let amount = token::Amount::from(10_098_123); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner, &source, &token]); + + // Credit the tokens to the source before running the transaction to be + // able to transfer from it + tx_env.credit_tokens(&source, &token, amount); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Apply transfer in a transaction + tx_host_env::token::transfer( + tx::ctx(), + &source, + address, + &token, + None, + amount, + ) + .unwrap(); + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + /// Test that a debit transfer without a valid signature is rejected. + #[test] + fn test_unsigned_debit_transfer_rejected() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let vp_owner = address::testing::established_address_1(); + let target = address::testing::established_address_2(); + let token = address::nam(); + let amount = token::Amount::from(10_098_123); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner, &target, &token]); + + // Credit the tokens to the VP owner before running the transaction to + // be able to transfer from it + tx_env.credit_tokens(&vp_owner, &token, amount); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Apply transfer in a transaction + tx_host_env::token::transfer( + tx::ctx(), + address, + &target, + &token, + None, + amount, + ) + .unwrap(); + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!( + !validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + /// Test that a debit transfer with a valid signature is accepted. + #[test] + fn test_signed_debit_transfer_accepted() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let vp_owner = address::testing::established_address_1(); + let keypair = key::testing::keypair_1(); + let public_key = keypair.ref_to(); + let target = address::testing::established_address_2(); + let token = address::nam(); + let amount = token::Amount::from(10_098_123); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner, &target, &token]); + + // Credit the tokens to the VP owner before running the transaction to + // be able to transfer from it + tx_env.credit_tokens(&vp_owner, &token, amount); + + tx_env.write_public_key(&vp_owner, &public_key); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Apply transfer in a transaction + tx_host_env::token::transfer( + tx::ctx(), + address, + &target, + &token, + None, + amount, + ) + .unwrap(); + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + let signed_tx = tx.sign(&keypair); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + /// Test that a transfer on with accounts other than self is accepted. + #[test] + fn test_transfer_between_other_parties_accepted() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let vp_owner = address::testing::established_address_1(); + let source = address::testing::established_address_2(); + let target = address::testing::established_address_3(); + let token = address::nam(); + let amount = token::Amount::from(10_098_123); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner, &source, &target, &token]); + + // Credit the tokens to the VP owner before running the transaction to + // be able to transfer from it + tx_env.credit_tokens(&source, &token, amount); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + tx::ctx().insert_verifier(address).unwrap(); + // Apply transfer in a transaction + tx_host_env::token::transfer( + tx::ctx(), + &source, + &target, + &token, + None, + amount, + ) + .unwrap(); + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + prop_compose! { + /// Generates an account address and a storage key inside its storage. + fn arb_account_storage_subspace_key() + // Generate an address + (address in arb_non_internal_address()) + // Generate a storage key other than its VP key (VP cannot be + // modified directly via `write`, it has to be modified via + // `tx::update_validity_predicate`. + (storage_key in arb_account_storage_key_no_vp(address.clone()), + // Use the generated address too + address in Just(address)) + -> (Address, Key) { + (address, storage_key) + } + } + + proptest! { + /// Test that an unsigned tx that performs arbitrary storage writes or + /// deletes to the account is rejected. + #[test] + fn test_unsigned_arb_storage_write_rejected( + (vp_owner, storage_key) in arb_account_storage_subspace_key(), + // Generate bytes to write. If `None`, delete from the key instead + storage_value in any::>>(), + ) { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + // Spawn all the accounts in the storage key to be able to modify + // their storage + let storage_key_addresses = storage_key.find_addresses(); + tx_env.spawn_accounts(storage_key_addresses); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { + // Write or delete some data in the transaction + if let Some(value) = &storage_value { + tx::ctx().write(&storage_key, value).unwrap(); + } else { + tx::ctx().delete(&storage_key).unwrap(); + } + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!(!validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers).unwrap()); + } + } + + proptest! { + /// Test that a signed tx that performs arbitrary storage writes or + /// deletes to the account is accepted. + #[test] + fn test_signed_arb_storage_write( + (vp_owner, storage_key) in arb_account_storage_subspace_key(), + // Generate bytes to write. If `None`, delete from the key instead + storage_value in any::>>(), + ) { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let keypair = key::testing::keypair_1(); + let public_key = keypair.ref_to(); + + // Spawn all the accounts in the storage key to be able to modify + // their storage + let storage_key_addresses = storage_key.find_addresses(); + tx_env.spawn_accounts(storage_key_addresses); + + tx_env.write_public_key(&vp_owner, &public_key); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |_address| { + // Write or delete some data in the transaction + if let Some(value) = &storage_value { + tx::ctx().write(&storage_key, value).unwrap(); + } else { + tx::ctx().delete(&storage_key).unwrap(); + } + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + let signed_tx = tx.sign(&keypair); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!(validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers).unwrap()); + } + } + + /// Test that a validity predicate update without a valid signature is + /// rejected. + #[test] + fn test_unsigned_vp_update_rejected() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let vp_owner = address::testing::established_address_1(); + let vp_code = + std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner]); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Update VP in a transaction + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); + }); + + let vp_env = vp_host_env::take(); + let tx_data: Vec = vec![]; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!( + !validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + /// Test that a validity predicate update with a valid signature is + /// accepted. + #[test] + fn test_signed_vp_update_accepted() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + tx_env.init_parameters(None, None, None); + + let vp_owner = address::testing::established_address_1(); + let keypair = key::testing::keypair_1(); + let public_key = keypair.ref_to(); + let vp_code = + std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner]); + + tx_env.write_public_key(&vp_owner, &public_key); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Update VP in a transaction + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + let signed_tx = tx.sign(&keypair); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + /// Test that a validity predicate update is rejected if not whitelisted + #[test] + fn test_signed_vp_update_not_whitelisted_rejected() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + tx_env.init_parameters(None, Some(vec!["some_hash".to_string()]), None); + + let vp_owner = address::testing::established_address_1(); + let keypair = key::testing::keypair_1(); + let public_key = keypair.ref_to(); + let vp_code = + std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner]); + + tx_env.write_public_key(&vp_owner, &public_key); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Update VP in a transaction + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + let signed_tx = tx.sign(&keypair); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!( + !validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + /// Test that a validity predicate update is accepted if whitelisted + #[test] + fn test_signed_vp_update_whitelisted_accepted() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let vp_owner = address::testing::established_address_1(); + let keypair = key::testing::keypair_1(); + let public_key = keypair.ref_to(); + let vp_code = + std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); + + let vp_hash = sha256(&vp_code); + tx_env.init_parameters(None, Some(vec![vp_hash.to_string()]), None); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner]); + + tx_env.write_public_key(&vp_owner, &public_key); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Update VP in a transaction + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + let signed_tx = tx.sign(&keypair); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + /// Test that a tx is rejected if not whitelisted + #[test] + fn test_tx_not_whitelisted_rejected() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let vp_owner = address::testing::established_address_1(); + let keypair = key::testing::keypair_1(); + let public_key = keypair.ref_to(); + let vp_code = + std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); + + let vp_hash = sha256(&vp_code); + tx_env.init_parameters( + None, + Some(vec![vp_hash.to_string()]), + Some(vec!["some_hash".to_string()]), + ); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner]); + + tx_env.write_public_key(&vp_owner, &public_key); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Update VP in a transaction + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + let signed_tx = tx.sign(&keypair); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!( + !validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } + + #[test] + fn test_tx_whitelisted_accepted() { + // Initialize a tx environment + let mut tx_env = TestTxEnv::default(); + + let vp_owner = address::testing::established_address_1(); + let keypair = key::testing::keypair_1(); + let public_key = keypair.ref_to(); + let vp_code = + std::fs::read(VP_ALWAYS_TRUE_WASM).expect("cannot load wasm"); + + // hardcoded hash of VP_ALWAYS_TRUE_WASM + tx_env.init_parameters(None, None, Some(vec!["E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855".to_string()])); + + // Spawn the accounts to be able to modify their storage + tx_env.spawn_accounts([&vp_owner]); + + tx_env.write_public_key(&vp_owner, &public_key); + + // Initialize VP environment from a transaction + vp_host_env::init_from_tx(vp_owner.clone(), tx_env, |address| { + // Update VP in a transaction + tx::ctx() + .update_validity_predicate(address, &vp_code) + .unwrap(); + }); + + let mut vp_env = vp_host_env::take(); + let tx = vp_env.tx.clone(); + let signed_tx = tx.sign(&keypair); + let tx_data: Vec = signed_tx.data.as_ref().cloned().unwrap(); + vp_env.tx = signed_tx; + let keys_changed: BTreeSet = + vp_env.all_touched_storage_keys(); + let verifiers: BTreeSet
= BTreeSet::default(); + vp_host_env::set(vp_env); + assert!( + validate_tx(&CTX, tx_data, vp_owner, keys_changed, verifiers) + .unwrap() + ); + } +} diff --git a/wasm_for_tests/wasm_source/Cargo.lock b/wasm_for_tests/wasm_source/Cargo.lock index 1cbb2ff069..2ddcecd325 100644 --- a/wasm_for_tests/wasm_source/Cargo.lock +++ b/wasm_for_tests/wasm_source/Cargo.lock @@ -1383,6 +1383,7 @@ dependencies = [ "rand_core 0.6.4", "rayon", "rust_decimal", + "rust_decimal_macros", "serde", "serde_json", "sha2 0.9.9", @@ -1418,6 +1419,8 @@ dependencies = [ "borsh", "derivative", "proptest", + "rust_decimal", + "rust_decimal_macros", "thiserror", ] @@ -1432,6 +1435,8 @@ dependencies = [ "namada_tx_prelude", "namada_vp_prelude", "prost", + "rust_decimal", + "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -1448,6 +1453,7 @@ dependencies = [ "namada", "namada_macros", "namada_vm_env", + "rust_decimal", "sha2 0.10.6", "thiserror", ] @@ -1981,10 +1987,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c" dependencies = [ "arrayvec", + "borsh", "num-traits", "serde", ] +[[package]] +name = "rust_decimal_macros" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4903d8db81d2321699ca8318035d6ff805c548868df435813968795a802171b2" +dependencies = [ + "quote", + "rust_decimal", +] + [[package]] name = "rustc-demangle" version = "0.1.21"