diff --git a/.changelog/unreleased/features/658-add-client-block-query.md b/.changelog/unreleased/features/658-add-client-block-query.md new file mode 100644 index 00000000000..29584d3a0fc --- /dev/null +++ b/.changelog/unreleased/features/658-add-client-block-query.md @@ -0,0 +1,2 @@ +- Client: Add a command to query the last committed block's hash, height and + timestamp. ([#658](https://github.com/anoma/namada/issues/658)) diff --git a/Cargo.lock b/Cargo.lock index 34ab8ab550d..95f44c0056c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2946,6 +2946,7 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.4", "rust_decimal", + "rust_decimal_macros", "serde 1.0.145", "serde_json", "sha2 0.9.9", @@ -3020,6 +3021,8 @@ dependencies = [ "rlimit", "rocksdb", "rpassword", + "rust_decimal", + "rust_decimal_macros", "serde 1.0.145", "serde_bytes", "serde_json", @@ -3078,7 +3081,11 @@ version = "0.8.1" dependencies = [ "borsh", "derivative", + "hex", "proptest", + "rust_decimal", + "rust_decimal_macros", + "tendermint-proto 0.23.5", "thiserror", ] @@ -3107,6 +3114,8 @@ dependencies = [ "proptest", "prost", "rand 0.8.5", + "rust_decimal", + "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -3124,6 +3133,7 @@ dependencies = [ "namada", "namada_macros", "namada_vm_env", + "rust_decimal", "sha2 0.10.6", "thiserror", ] @@ -4326,10 +4336,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 45cdb9aa5eb..af18e076c03 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 b87cdb5c661..9d731991507 100644 --- a/apps/src/bin/anoma-client/cli.rs +++ b/apps/src/bin/anoma-client/cli.rs @@ -52,6 +52,9 @@ pub async fn main() -> Result<()> { Sub::QueryEpoch(QueryEpoch(args)) => { rpc::query_epoch(args).await; } + Sub::QueryBlock(QueryBlock(args)) => { + rpc::query_block(args).await; + } Sub::QueryBalance(QueryBalance(args)) => { rpc::query_balance(ctx, args).await; } @@ -61,6 +64,9 @@ pub async fn main() -> Result<()> { Sub::QueryVotingPower(QueryVotingPower(args)) => { rpc::query_voting_power(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 8e4c7f78c94..2659cd2f224 100644 --- a/apps/src/lib/cli.rs +++ b/apps/src/lib/cli.rs @@ -167,6 +167,7 @@ pub mod cmds { .subcommand(Withdraw::def().display_order(2)) // Queries .subcommand(QueryEpoch::def().display_order(3)) + .subcommand(QueryBlock::def().display_order(3)) .subcommand(QueryBalance::def().display_order(3)) .subcommand(QueryBonds::def().display_order(3)) .subcommand(QueryVotingPower::def().display_order(3)) @@ -198,6 +199,7 @@ pub mod cmds { let unbond = Self::parse_with_ctx(matches, Unbond); let withdraw = Self::parse_with_ctx(matches, Withdraw); let query_epoch = Self::parse_with_ctx(matches, QueryEpoch); + 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 = @@ -224,6 +226,7 @@ pub mod cmds { .or(unbond) .or(withdraw) .or(query_epoch) + .or(query_block) .or(query_balance) .or(query_bonds) .or(query_voting_power) @@ -283,9 +286,11 @@ pub mod cmds { Unbond(Unbond), Withdraw(Withdraw), QueryEpoch(QueryEpoch), + QueryBlock(QueryBlock), QueryBalance(QueryBalance), QueryBonds(QueryBonds), QueryVotingPower(QueryVotingPower), + QueryCommissionRate(QueryCommissionRate), QuerySlashes(QuerySlashes), QueryRawBytes(QueryRawBytes), QueryProposal(QueryProposal), @@ -936,6 +941,25 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct QueryBlock(pub args::Query); + + impl SubCmd for QueryBlock { + const CMD: &'static str = "block"; + + fn parse(matches: &ArgMatches) -> Option { + matches + .subcommand_matches(Self::CMD) + .map(|matches| QueryBlock(args::Query::parse(matches))) + } + + fn def() -> App { + App::new(Self::CMD) + .about("Query the last committed block.") + .add_args::() + } + } + #[derive(Clone, Debug)] pub struct QueryBalance(pub args::QueryBalance); @@ -993,6 +1017,25 @@ pub mod cmds { } } + #[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::() + } + } + #[derive(Clone, Debug)] pub struct QuerySlashes(pub args::QuerySlashes); @@ -1255,6 +1298,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::*; @@ -1283,6 +1327,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()), @@ -1314,6 +1359,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 NFT_ADDRESS: Arg
= arg("nft-address"); @@ -1329,7 +1376,6 @@ pub mod args { 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"); @@ -1567,8 +1613,9 @@ 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, @@ -1581,8 +1628,10 @@ 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); @@ -1592,8 +1641,9 @@ 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, @@ -1617,14 +1667,19 @@ 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. 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(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 \ @@ -2169,6 +2224,41 @@ pub mod args { } } + /// Query PoS commission rate + #[derive(Clone, Debug)] + pub struct QueryCommissionRate { + /// Common query args + pub query: Query, + /// Address of a validator + pub validator: Option, + /// 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_OPT.parse(matches); + let epoch = EPOCH.parse(matches); + Self { + query, + validator, + epoch, + } + } + + fn def(app: App) -> App { + app.add_args::() + .arg(VALIDATOR_OPT.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 \ + specified).", + )) + } + } + /// Query PoS slashes #[derive(Clone, Debug)] pub struct QuerySlashes { @@ -2668,6 +2758,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, @@ -2676,6 +2768,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); @@ -2684,6 +2779,8 @@ pub mod args { net_address, unsafe_dont_encrypt, key_scheme, + commission_rate, + max_commission_rate_change, } } @@ -2694,6 +2791,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 6c1e3fb5f31..e6638f89ff7 100644 --- a/apps/src/lib/client/rpc.rs +++ b/apps/src/lib/client/rpc.rs @@ -18,10 +18,11 @@ 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, + self, into_tm_voting_power, is_validator_slashes_key, BondId, Bonds, + PosParams, Slash, Unbonds, }; use namada::types::address::Address; use namada::types::governance::{ @@ -72,6 +73,21 @@ pub async fn query_epoch(args: args::Query) -> Epoch { cli::safe_exit(1) } +/// Query the last committed block +pub async fn query_block( + args: args::Query, +) -> tendermint_rpc::endpoint::block::Response { + let client = HttpClient::new(args.ledger_address).unwrap(); + let response = client.latest_block().await.unwrap(); + println!( + "Last committed block ID: {}, height: {}, time: {}", + response.block_id, + response.block.header.height, + response.block.header.time + ); + response +} + /// Query the raw bytes of given storage key pub async fn query_raw_bytes(_ctx: Context, args: args::QueryRawBytes) { let client = HttpClient::new(args.query.ledger_address).unwrap(); @@ -486,7 +502,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."); @@ -544,7 +560,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) @@ -923,22 +939,21 @@ pub async fn query_voting_power(ctx: Context, args: args::QueryVotingPower) { 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", - ); + 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 { @@ -947,14 +962,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()) } } } @@ -969,7 +984,7 @@ pub async fn query_voting_power(ctx: Context, args: args::QueryVotingPower) { w, " {}: {}", active.address.encode(), - active.voting_power + active.bonded_stake ) .unwrap(); } @@ -980,26 +995,81 @@ 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"); + .expect("Total bonded stake should be always set in the current epoch"); + let pos_params_key = pos::params_key(); + let pos_params = + query_storage_value::(&client, &pos_params_key) + .await + .expect("PoS parameters should always exist in storage"); + let total_bonded_stake: u64 = total_bonded_stake + .try_into() + .expect("total_bonded_stake should be a positive value"); + let total_voting_power = + into_tm_voting_power(pos_params.tm_votes_per_token, total_bonded_stake); + println!("Total voting power: {}", total_voting_power); } +/// Query PoS commssion 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).unwrap(); + + match args.validator { + Some(validator) => { + let validator = ctx.get(&validator); + let validator_commission_key = + pos::validator_commission_rate_key(&validator); + let commission_rates = query_storage_value::( + &client, + &validator_commission_key, + ) + .await; + let commission_rates = + commission_rates.expect("No commission rate found "); + match commission_rates.get(epoch) { + Some(rate) => { + println!( + "Validator {} commission rate: {}", + validator.encode(), + *rate + ) + } + None => { + println!( + "No commission rate found for {} in epoch {}", + validator.encode(), + epoch + ) + } + } + } + None => { + println!("No validator found from the args") + } + } +} + /// Query PoS slashes pub async fn query_slashes(ctx: Context, args: args::QuerySlashes) { let client = HttpClient::new(args.query.ledger_address).unwrap(); @@ -1173,7 +1243,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; } @@ -1888,17 +1959,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 e749a681c6f..e59d686b73a 100644 --- a/apps/src/lib/client/tx.rs +++ b/apps/src/lib/client/tx.rs @@ -25,6 +25,7 @@ use namada::types::transaction::nft::{CreateNft, MintNft}; 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; @@ -159,8 +160,9 @@ 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, @@ -208,18 +210,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() { @@ -240,6 +230,22 @@ 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 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" + ); + } + 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%" + ); + } // Validate the validator VP code if let Err(err) = vm::validate_untrusted_wasm(&validator_vp_code) { eprintln!( @@ -269,9 +275,10 @@ 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, }; diff --git a/apps/src/lib/client/utils.rs b/apps/src/lib/client/utils.rs index 88487267924..6b68b72a89c 100644 --- a/apps/src/lib/client/utils.rs +++ b/apps/src/lib/client/utils.rs @@ -478,7 +478,6 @@ pub fn init_network( 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 +517,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 +564,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())); @@ -905,6 +884,8 @@ 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, @@ -940,9 +921,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 +939,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 9425e3b0194..8781ef649cb 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 @@ -233,6 +232,16 @@ 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, + /// PoS staked ratio + pub staked_ratio: Decimal, + /// PoS inflation amount last epoch + pub pos_inflation_amount: u64, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -248,21 +257,29 @@ pub mod genesis_config { pub unbonding_len: u64, // Votes per token (in basis points). // 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). // XXX: u64 doesn't work with toml-rs! - pub duplicate_vote_slash_rate: u64, + pub duplicate_vote_slash_rate: Decimal, // Portion of a validator's stake that should be slashed on a // light client attack (in basis points). // XXX: u64 doesn't work with toml-rs! - pub light_client_attack_slash_rate: u64, + pub light_client_attack_slash_rate: Decimal, + /// The minimum amount of bonded tokens that a validator needs to be in + /// either the `consensus` or `below_capacity` validator sets + pub min_validator_stake: u64, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -277,17 +294,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 +306,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 +358,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(), } } @@ -527,6 +545,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: config.parameters.staked_ratio, + pos_inflation_amount: config.parameters.pos_inflation_amount, }; let gov_params = GovParams { @@ -546,17 +569,18 @@ 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_slash_rate: config + .pos_params + .duplicate_vote_slash_rate, + light_client_attack_slash_rate: config + .pos_params + .light_client_attack_slash_rate, + min_validator_stake: config.pos_params.min_validator_stake, }; let mut genesis = Genesis { @@ -651,17 +675,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 +745,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 +757,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 +774,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 +783,11 @@ pub fn genesis() -> Genesis { max_expected_time_per_block: namada::types::time::DurationSecs(30), vp_whitelist: vec![], tx_whitelist: vec![], + epochs_per_year: 365, + 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(), @@ -857,20 +873,14 @@ pub mod tests { #[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 2080b8d23de..0df05abc2e2 100644 --- a/apps/src/lib/node/ledger/shell/finalize_block.rs +++ b/apps/src/lib/node/ledger/shell/finalize_block.rs @@ -1,7 +1,17 @@ //! Implementation of the `FinalizeBlock` ABCI++ method for the Shell -use namada::types::storage::{BlockHash, Header}; - +use namada::ledger::inflation::{self, RewardsController}; +use namada::ledger::parameters::storage as params_storage; +use namada::ledger::pos::types::{self, decimal_mult_u64, VoteInfo}; +use namada::ledger::pos::{ + consensus_validator_set_accumulator_key, staking_token_address, +}; +use namada::types::address::Address; +#[cfg(feature = "abcipp")] +use namada::types::key::tm_raw_hash_to_string; +use namada::types::storage::{BlockHash, Epoch, Header}; +use namada::types::token::{total_supply_key, Amount}; +use rust_decimal::prelude::Decimal; use super::governance::execute_governance_proposals; use super::*; use crate::facade::tendermint_proto::abci::Misbehavior as Evidence; @@ -36,13 +46,16 @@ where &mut self, req: shim::request::FinalizeBlock, ) -> Result { - // reset gas meter before we start + // Reset the gas meter before we start self.gas_meter.reset(); let mut response = shim::response::FinalizeBlock::default(); - // begin the next block and check if a new epoch began + + // Begin the new block and check if a new epoch has begun let (height, new_epoch) = self.update_state(req.header, req.hash, req.byzantine_validators); + let (current_epoch, _gas) = self.storage.get_current_epoch(); + if new_epoch { let _proposals_result = @@ -233,6 +246,77 @@ where self.update_epoch(&mut response); } + // Read the block proposer of the previously committed block in storage + // (n-1 if we are in the process of finalizing n right now). + match self.storage.read_last_block_proposer_address() { + Some(proposer_address) => { + if new_epoch { + println!("APPLYING INFLATION"); + self.apply_inflation( + current_epoch, + &proposer_address, + &req.votes, + ); + } else { + // TODO: watch out because this is likely not using the + // proper block proposer address + self.storage + .log_block_rewards( + current_epoch, + &proposer_address, + &req.votes, + ) + .unwrap(); + } + #[cfg(feature = "abcipp")] + { + let tm_raw_hash_string = + tm_raw_hash_to_string(req.proposer_address); + let native_proposer_address = self + .storage + .read_validator_address_raw_hash(tm_raw_hash_string) + .expect( + "Unable to find native validator address of block \ + proposer from tendermint raw hash", + ); + self.storage.write_last_block_proposer_address( + &native_proposer_address, + ); + } + + #[cfg(not(feature = "abcipp"))] + { + let cur_proposer = self + .storage + .read_current_block_proposer_address() + .unwrap(); + self.storage + .write_last_block_proposer_address(&cur_proposer); + } + } + None => { + #[cfg(feature = "abcipp")] + if req.votes.len() == 0 && req.proposer_address.len() > 0 { + // Get proposer address from storage based on the consensus + // key hash + let tm_raw_hash_string = + tm_raw_hash_to_string(req.proposer_address); + let native_proposer_address = self + .storage + .read_validator_address_raw_hash(tm_raw_hash_string) + .expect( + "Unable to find native validator address of block \ + proposer from tendermint raw hash", + ); + self.storage.write_last_block_proposer_address( + &native_proposer_address, + ); + } else { + eprintln!("WE SHOULD NEVER BE REACHING HERE"); + } + } + } + let _ = self .gas_meter .finalize_transaction() @@ -291,10 +375,9 @@ where 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 + let power: i64 = bonded_stake .try_into() .expect("unexpected validator's voting power"); (consensus_key, power) @@ -315,6 +398,234 @@ where response.validator_updates.push(update); }); } + + /// Calculate the new inflation rate, mint the new tokens to the PoS + /// account, then update the reward products of the validators. This is + /// executed while finalizing the first block of a new epoch and is applied + /// with respect to the previous epoch. + fn apply_inflation( + &mut self, + current_epoch: Epoch, + proposer_address: &Address, + votes: &Vec, + ) { + let last_epoch = current_epoch - 1; + // Get input values needed for the PD controller for PoS and MASP. + // Run the PD controllers to calculate new rates. + // + // MASP is included below just for some completeness. + + // Calculate the fractional block rewards for the previous block (final + // block of the previous epoch), which also gives the final + // accumulator value updates + self.storage + .log_block_rewards(last_epoch, &proposer_address, votes) + .unwrap(); + + // TODO: review if the appropriate epoch is being used (last vs now) + + // Read from Parameters storage + let epochs_per_year: u64 = self + .read_storage_key(¶ms_storage::get_epochs_per_year_key()) + .expect("Epochs per year should exist in storage"); + let pos_p_gain_nom: Decimal = self + .read_storage_key(¶ms_storage::get_pos_gain_p_key()) + .expect("PoS P-gain factor should exist in storage"); + let pos_d_gain_nom: Decimal = self + .read_storage_key(¶ms_storage::get_pos_gain_d_key()) + .expect("PoS D-gain factor should exist in storage"); + + let pos_last_staked_ratio: Decimal = self + .read_storage_key(¶ms_storage::get_staked_ratio_key()) + .expect("PoS staked ratio should exist in storage"); + let pos_last_inflation_amount: u64 = self + .read_storage_key(¶ms_storage::get_pos_inflation_amount_key()) + .expect("PoS inflation rate should exist in storage"); + // Read from PoS storage + let total_tokens = self + .read_storage_key(&total_supply_key(&staking_token_address())) + .expect("Total NAM balance should exist in storage"); + let total_deltas = self.storage.read_total_deltas(); + let pos_locked_supply = total_deltas + .get(last_epoch) + .expect("maximum possible sum should fit within an i128"); + let pos_locked_supply: Amount = u64::try_from(pos_locked_supply) + .expect("pos_locked_supply should be positive") + .into(); + let pos_params = self.storage.read_pos_params(); + let pos_locked_ratio_target = pos_params.target_staked_ratio; + let pos_max_inflation_rate = pos_params.max_inflation_rate; + + // TODO: properly fetch these values (arbitrary for now) + let masp_locked_supply: Amount = Amount::default(); + let masp_locked_ratio_target = Decimal::new(5, 1); + let masp_locked_ratio_last = Decimal::new(5, 1); + let masp_max_inflation_rate = Decimal::new(2, 1); + let masp_last_inflation_rate = Decimal::new(12, 2); + let masp_p_gain = Decimal::new(1, 1); + let masp_d_gain = Decimal::new(1, 1); + + // Run rewards PD controller + let pos_controller = inflation::RewardsController::new( + pos_locked_supply, + total_tokens, + pos_locked_ratio_target, + pos_last_staked_ratio, + pos_max_inflation_rate, + token::Amount::from(pos_last_inflation_amount), + pos_p_gain_nom, + pos_d_gain_nom, + epochs_per_year, + ); + let _masp_controller = inflation::RewardsController::new( + masp_locked_supply, + total_tokens, + masp_locked_ratio_target, + masp_locked_ratio_last, + masp_max_inflation_rate, + token::Amount::from(masp_last_inflation_rate), + masp_p_gain, + masp_d_gain, + epochs_per_year, + ); + + // Run the rewards controllers + let new_pos_vals = RewardsController::run(&pos_controller); + // let new_masp_vals = RewardsController::run(&_masp_controller); + + // Mint tokens to the PoS account for the last epoch's inflation + let pos_minted_tokens = new_pos_vals.inflation; + let pos_address = self.storage.read_pos_address(); + inflation::mint_tokens( + &mut self.storage, + &pos_address, + &staking_token_address(), + Amount::from(pos_minted_tokens), + ) + .unwrap(); + + // For each consensus validator, update the rewards products + // + // TODO: update implementation using lazy DS and be more + // memory-efficient + + // Get the number of blocks in the last epoch + let first_block_of_last_epoch = + self.storage.block.pred_epochs.first_block_heights + [last_epoch.0 as usize] + .0; + let num_blocks_in_last_epoch = if first_block_of_last_epoch == 0 { + self.storage.block.height.0 - first_block_of_last_epoch - 1 + } else { + self.storage.block.height.0 - first_block_of_last_epoch + }; + + // Read the rewards accumulator, which was last updated when finalizing + // the previous block + // TODO: may need to change logic of how this gets initialized + // TODO: can/should this be optimized? Since we are reading and writing + // to the accumulator storage earlier in apply_inflation + let accumulators = self + .storage + .read_consensus_validator_rewards_accumulator() + .expect("Accumulators should exist"); + + let current_epoch = types::Epoch::from(current_epoch.0); + let last_epoch = types::Epoch::from(last_epoch.0); + + // TODO: think about changing the reward to Decimal + let mut reward_tokens_remaining = pos_minted_tokens.clone(); + for (address, value) in accumulators.iter() { + // Get reward token amount for this validator + let fractional_claim = + value / Decimal::from(num_blocks_in_last_epoch); + let reward = decimal_mult_u64( + fractional_claim, + u64::from(pos_minted_tokens), + ); + + // Read epoched validator data and rewards products + let validator_deltas = + self.storage.read_validator_deltas(address).unwrap(); + let commission_rates = + self.storage.read_validator_commission_rate(address); + let mut rewards_products = self + .storage + .read_validator_rewards_products(address) + .unwrap_or(std::collections::HashMap::new()); + let mut delegation_rewards_products = self + .storage + .read_validator_delegation_rewards_products(address) + .unwrap_or(std::collections::HashMap::new()); + + // Get validator data at the last epoch + let stake = validator_deltas + .get(last_epoch) + .map(|sum| Decimal::from(sum)) + .unwrap(); + let last_product = + *rewards_products.get(&last_epoch).unwrap_or(&Decimal::ONE); + let last_delegation_product = *delegation_rewards_products + .get(&last_epoch) + .unwrap_or(&Decimal::ONE); + let commission_rate = *commission_rates.get(last_epoch).unwrap(); + // Calculate new rewards products and write them to storage (for the + // current epoch) + let new_product = + last_product * (Decimal::ONE + Decimal::from(reward) / stake); + let new_delegation_product = last_delegation_product + * (Decimal::ONE + + (Decimal::ONE - commission_rate) * Decimal::from(reward) + / stake); + rewards_products.insert(current_epoch, new_product); + delegation_rewards_products + .insert(current_epoch, new_delegation_product); + self.storage + .write_validator_rewards_products(address, &rewards_products); + self.storage.write_validator_delegation_rewards_products( + address, + &delegation_rewards_products, + ); + + reward_tokens_remaining -= reward; + + // TODO: Figure out how to deal with round-off to a whole number of tokens. May be tricky. + // TODO: Storing reward products as a Decimal suggests that no round-off should be done here, + // TODO: perhaps only upon withdrawal. But by truncating at withdrawal, may leave tokens in + // TDOD: the PoS account that are not accounted for. Is this an issue? + } + + if reward_tokens_remaining > 0 { + // TODO: do something here? + dbg!(reward_tokens_remaining.clone()); + } + + // Write new rewards parameters that will be used for the inflation of + // the current new epoch + self.storage + .write( + ¶ms_storage::get_pos_inflation_amount_key(), + new_pos_vals + .inflation + .try_to_vec() + .expect("encode new reward rate"), + ) + .expect("unable to encode new reward rate (Decimal)"); + self.storage + .write( + ¶ms_storage::get_staked_ratio_key(), + new_pos_vals + .locked_ratio + .try_to_vec() + .expect("encode new locked ratio"), + ) + .expect("unable to encode new locked ratio (Decimal)"); + + // Delete the accumulators from storage + self.storage + .delete(&consensus_validator_set_accumulator_key()) + .unwrap(); + } } /// We test the failure cases of [`finalize_block`]. The happy flows diff --git a/apps/src/lib/node/ledger/shell/init_chain.rs b/apps/src/lib/node/ledger/shell/init_chain.rs index 8cb6842c50f..81cf8812140 100644 --- a/apps/src/lib/node/ledger/shell/init_chain.rs +++ b/apps/src/lib/node/ledger/shell/init_chain.rs @@ -2,7 +2,11 @@ use std::collections::HashMap; use std::hash::Hash; +use namada::ledger::parameters::storage::get_staked_ratio_key; +use namada::ledger::pos::{into_tm_voting_power, staking_token_address}; use namada::types::key::*; +use namada::types::token::total_supply_key; +use rust_decimal::Decimal; #[cfg(not(feature = "dev"))] use sha2::{Digest, Sha256}; @@ -125,6 +129,8 @@ where } // Initialize genesis implicit + // TODO: verify if we can get the total initial token supply from simply + // looping over the set of implicit accounts for genesis::ImplicitAccount { public_key } in genesis.implicit_accounts { let address: address::Address = (&public_key).into(); @@ -135,6 +141,7 @@ where } // Initialize genesis token accounts + let mut total_nam_balance = token::Amount::default(); for genesis::TokenAccount { address, vp_code_path, @@ -169,6 +176,9 @@ where .unwrap(); for (owner, amount) in balances { + if address == staking_token_address() { + total_nam_balance += amount; + } self.storage .write( &token::balance_key(&address, &owner), @@ -179,6 +189,7 @@ where } // Initialize genesis validator accounts + let mut total_staked_nam_tokens = token::Amount::default(); for validator in &genesis.validators { let vp_code = vp_code_cache.get_or_insert_with( validator.validator_vp_code_path.clone(), @@ -219,7 +230,12 @@ where .expect("encode public key"), ) .expect("Unable to set genesis user public key"); - // Account balance (tokens no staked in PoS) + + // Balances + total_staked_nam_tokens += validator.pos_data.tokens; + total_nam_balance += + validator.pos_data.tokens + validator.non_staked_balance; + self.storage .write( &token::balance_key(&address::xan(), addr), @@ -250,7 +266,9 @@ where .expect("Unable to set genesis user public DKG session key"); } - // PoS system depends on epoch being initialized + // PoS system depends on epoch being initialized. Write the total + // genesis staking token balance to storage after + // initialization. let (current_epoch, _gas) = self.storage.get_current_epoch(); pos::init_genesis_storage( &mut self.storage, @@ -261,6 +279,27 @@ where .map(|validator| &validator.pos_data), current_epoch, ); + // Set total supply of staking token in storage + self.storage + .write( + &total_supply_key(&staking_token_address()), + total_nam_balance + .try_to_vec() + .expect("encode initial total NAM balance"), + ) + .expect("unable to set total NAM balance in storage"); + + // Set the ratio of staked to total NAM tokens in the parameters storage + self.storage + .write( + &get_staked_ratio_key(), + (Decimal::from(total_staked_nam_tokens) + / Decimal::from(total_nam_balance)) + .try_to_vec() + .expect("encode initial NAM staked ratio"), + ) + .expect("unable to set staked ratio of NAM in storage"); + ibc::init_genesis_storage(&mut self.storage); // Set the initial validator set @@ -272,11 +311,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/mod.rs b/apps/src/lib/node/ledger/shell/mod.rs index 28167d4ebac..6b6dc48c0eb 100644 --- a/apps/src/lib/node/ledger/shell/mod.rs +++ b/apps/src/lib/node/ledger/shell/mod.rs @@ -827,6 +827,9 @@ mod test_utils { }, byzantine_validators: vec![], txs: vec![], + #[cfg(feature = "abcipp")] + proposer_address: vec![], + votes: vec![], } } } diff --git a/apps/src/lib/node/ledger/shell/queries.rs b/apps/src/lib/node/ledger/shell/queries.rs index 53587d5ebfc..59d55a7315d 100644 --- a/apps/src/lib/node/ledger/shell/queries.rs +++ b/apps/src/lib/node/ledger/shell/queries.rs @@ -313,7 +313,7 @@ where "DKG public key in storage should be deserializable", ); TendermintValidator { - power: validator.voting_power.into(), + power: validator.bonded_stake, address: validator.address.to_string(), public_key: dkg_publickey.into(), } diff --git a/apps/src/lib/node/ledger/shims/abcipp_shim.rs b/apps/src/lib/node/ledger/shims/abcipp_shim.rs index e919610f8e0..17188eaddda 100644 --- a/apps/src/lib/node/ledger/shims/abcipp_shim.rs +++ b/apps/src/lib/node/ledger/shims/abcipp_shim.rs @@ -4,10 +4,14 @@ use std::path::PathBuf; use std::pin::Pin; use std::task::{Context, Poll}; +#[cfg(not(feature = "abcipp"))] +use namada::ledger::pos::namada_proof_of_stake::PosBase; use futures::future::FutureExt; #[cfg(not(feature = "abcipp"))] use namada::types::hash::Hash; #[cfg(not(feature = "abcipp"))] +use namada::types::key::tm_raw_hash_to_string; +#[cfg(not(feature = "abcipp"))] use namada::types::storage::BlockHash; #[cfg(not(feature = "abcipp"))] use namada::types::transaction::hash_tx; @@ -85,18 +89,41 @@ impl AbcippShim { /// Run the shell's blocking loop that receives messages from the /// [`AbciService`]. pub fn run(mut self) { + println!("\nStarting AbcippShim::run\n"); while let Ok((req, resp_sender)) = self.shell_recv.recv() { + println!("REQUEST RECEIVED"); let resp = match req { - Req::ProcessProposal(proposal) => self - .service - .call(Request::ProcessProposal(proposal)) - .map_err(Error::from) - .and_then(|res| match res { - Response::ProcessProposal(resp) => { - Ok(Resp::ProcessProposal((&resp).into())) - } - _ => unreachable!(), - }), + Req::ProcessProposal(proposal) => { + #[cfg(not(feature = "abcipp"))] + if proposal.proposer_address.len() > 0 { + let tm_raw_hash_string = tm_raw_hash_to_string( + proposal.proposer_address.clone(), + ); + let native_proposer_address = self + .service + .storage + .read_validator_address_raw_hash(tm_raw_hash_string) + .expect( + "Unable to find native validator address of \ + block proposer from tendermint raw hash", + ); + println!("BLOCK PROPOSER: {}", native_proposer_address); + self.service + .storage + .write_current_block_proposer_address( + &native_proposer_address, + ); + } + self.service + .call(Request::ProcessProposal(proposal)) + .map_err(Error::from) + .and_then(|res| match res { + Response::ProcessProposal(resp) => { + Ok(Resp::ProcessProposal((&resp).into())) + } + _ => unreachable!(), + }) + } #[cfg(feature = "abcipp")] Req::FinalizeBlock(block) => { let unprocessed_txs = block.txs.clone(); @@ -161,15 +188,17 @@ impl AbcippShim { _ => Err(Error::ConvertResp(res)), }) } - _ => match Request::try_from(req.clone()) { - Ok(request) => self - .service - .call(request) - .map(Resp::try_from) - .map_err(Error::Shell) - .and_then(|inner| inner), - Err(err) => Err(err), - }, + _ => { + match Request::try_from(req.clone()) { + Ok(request) => self + .service + .call(request) + .map(Resp::try_from) + .map_err(Error::Shell) + .and_then(|inner| inner), + Err(err) => Err(err), + } + } }; let resp = resp.map_err(|e| e.into()); if resp_sender.send(resp).is_err() { diff --git a/apps/src/lib/node/ledger/shims/abcipp_shim_types.rs b/apps/src/lib/node/ledger/shims/abcipp_shim_types.rs index 5d9f2c420c7..3a5afe3de8c 100644 --- a/apps/src/lib/node/ledger/shims/abcipp_shim_types.rs +++ b/apps/src/lib/node/ledger/shims/abcipp_shim_types.rs @@ -15,7 +15,7 @@ pub mod shim { ResponseCheckTx, ResponseCommit, ResponseEcho, ResponseEndBlock, ResponseFlush, ResponseInfo, ResponseInitChain, ResponseListSnapshots, ResponseLoadSnapshotChunk, ResponseOfferSnapshot, - ResponsePrepareProposal, ResponseQuery, + ResponsePrepareProposal, ResponseQuery, VoteInfo as TendermintVoteInfo, }; #[cfg(feature = "abcipp")] use tendermint_proto_abcipp::abci::{ @@ -28,6 +28,7 @@ pub mod shim { ResponseFlush, ResponseInfo, ResponseInitChain, ResponseListSnapshots, ResponseLoadSnapshotChunk, ResponseOfferSnapshot, ResponsePrepareProposal, ResponseQuery, ResponseVerifyVoteExtension, + VoteInfo as TendermintVoteInfo, }; use thiserror::Error; @@ -195,6 +196,7 @@ pub mod shim { #[cfg(not(feature = "abcipp"))] use namada::tendermint_proto::abci::RequestBeginBlock; + use namada::ledger::pos::types::VoteInfo; use namada::types::hash::Hash; use namada::types::storage::{BlockHash, Header}; use namada::types::time::DateTimeUtc; @@ -205,6 +207,8 @@ pub mod shim { Misbehavior as Evidence, RequestFinalizeBlock, }; + use super::TendermintVoteInfo; + pub struct VerifyHeader; pub struct RevertProposal; @@ -221,6 +225,9 @@ pub mod shim { pub header: Header, pub byzantine_validators: Vec, pub txs: Vec, + #[cfg(feature = "abcipp")] + pub proposer_address: Vec, + pub votes: Vec, } #[cfg(feature = "abcipp")] @@ -238,10 +245,30 @@ pub mod shim { }, byzantine_validators: req.byzantine_validators, txs: vec![], + #[cfg(feature = "abcipp")] + proposer_address: req.proposer_address, + votes: req + .decided_last_commit + .unwrap() + .votes + .iter() + .map(|tm_vote_info| { + vote_info_to_tendermint(tm_vote_info.clone()) + }) + .collect(), } } } + fn vote_info_to_tendermint(info: TendermintVoteInfo) -> VoteInfo { + let val_info = info.validator.clone().unwrap(); + VoteInfo { + validator_address: info.validator.unwrap().address, + validator_vp: val_info.power as u64, + signed_last_block: info.signed_last_block, + } + } + #[cfg(not(feature = "abcipp"))] impl From for FinalizeBlock { fn from(req: RequestBeginBlock) -> FinalizeBlock { @@ -259,6 +286,15 @@ pub mod shim { }, byzantine_validators: req.byzantine_validators, txs: vec![], + votes: req + .last_commit_info + .unwrap() + .votes + .iter() + .map(|tm_vote_info| { + vote_info_to_tendermint(tm_vote_info.clone()) + }) + .collect(), } } } diff --git a/genesis/dev.toml b/genesis/dev.toml index 29a4b4d7917..e66fb9e38fb 100644 --- a/genesis/dev.toml +++ b/genesis/dev.toml @@ -21,6 +21,10 @@ non_staked_balance = 100000 validator_vp = "vp_user" # VP for the staking reward account staking_reward_vp = "vp_user" +# 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" @@ -152,19 +156,26 @@ 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_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_slash_rate = 0.001 +# The minimum amount of bonded tokens that a validator needs to be in either the `consensus` +# or `below_capacity` validator sets +min_validator_stake = 1_000_000 # Governance parameters. [gov_params] diff --git a/genesis/e2e-tests-single-node.toml b/genesis/e2e-tests-single-node.toml index 0e3a6d3fc86..21fa954d7a8 100644 --- a/genesis/e2e-tests-single-node.toml +++ b/genesis/e2e-tests-single-node.toml @@ -13,6 +13,10 @@ non_staked_balance = 1000000000000 validator_vp = "vp_user" # VP for the staking reward account staking_reward_vp = "vp_user" +# 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. @@ -142,6 +146,16 @@ max_expected_time_per_block = 30 vp_whitelist = [] # tx whitelist tx_whitelist = [] +# Expected number of epochs per year +epochs_per_year = 365 +# 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 +# The ratio of tokens locked in Proof of Stake to the total token supply +staked_ratio = 0 +# The inflation rate for Proof of Stake +pos_inflation_amount = 0 # Proof of stake parameters. [pos_params] @@ -153,18 +167,25 @@ 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_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_slash_rate = 0.001 +# The minimum amount of bonded tokens that a validator needs to be in either the `consensus` +# or `below_capacity` validator sets +min_validator_stake = 1_000_000 # Governance parameters. [gov_params] diff --git a/proof_of_stake/Cargo.toml b/proof_of_stake/Cargo.toml index 82522bc3d63..67b3f3b1dc7 100644 --- a/proof_of_stake/Cargo.toml +++ b/proof_of_stake/Cargo.toml @@ -16,8 +16,13 @@ testing = ["proptest"] [dependencies] borsh = "0.9.1" thiserror = "1.0.30" +hex = "0.4.3" # 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" +tendermint-proto = {git = "https://github.com/heliaxdev/tendermint-rs", rev = "95c52476bc37927218374f94ac8e2a19bd35bec9"} + [dev-dependencies] diff --git a/proof_of_stake/src/epoched.rs b/proof_of_stake/src/epoched.rs index f13bec3ee08..d45afef18d8 100644 --- a/proof_of_stake/src/epoched.rs +++ b/proof_of_stake/src/epoched.rs @@ -194,7 +194,7 @@ where self.get_at_index(index) } - /// Find the delta value at or before the given index. + /// Find the value at or before the given index. fn get_at_index(&self, offset: usize) -> Option<&Data> { let mut index = cmp::min(offset, self.data.len()); loop { @@ -453,7 +453,7 @@ where current_epoch: impl Into, params: &PosParams, ) { - let epoch = current_epoch.into(); + let epoch: Epoch = current_epoch.into(); let offset = Offset::value(params) as usize; let last_update = self.last_update; let shift: usize = diff --git a/proof_of_stake/src/lib.rs b/proof_of_stake/src/lib.rs index 7676ee7d2e4..ad4bab4204c 100644 --- a/proof_of_stake/src/lib.rs +++ b/proof_of_stake/src/lib.rs @@ -15,11 +15,12 @@ pub mod btree_set; pub mod epoched; pub mod parameters; +pub mod rewards; pub mod types; pub mod validation; use core::fmt::Debug; -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::convert::TryFrom; use std::fmt::Display; use std::hash::Hash; @@ -31,16 +32,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, + decimal_mult_i128, decimal_mult_u64, ActiveValidator, Bonds, + CommissionRates, Epoch, GenesisValidator, RewardsProducts, 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::rewards::PosRewardsCalculator; +use crate::types::{Bond, BondId, VoteInfo, WeightedValidator}; /// Read-only part of the PoS system pub trait PosReadOnly { @@ -107,11 +111,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, @@ -122,22 +121,29 @@ pub trait PosReadOnly { &self, key: &Self::Address, ) -> Result, Self::Error>; - /// Read PoS validator's total deltas of their bonds (validator self-bonds + /// Read PoS validator's deltas of their bonds (validator self-bonds /// and delegations). - fn read_validator_total_deltas( - &self, - key: &Self::Address, - ) -> Result>, Self::Error>; - /// Read PoS validator's voting power. - fn read_validator_voting_power( + fn read_validator_deltas( &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 +159,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 @@ -186,13 +193,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 +205,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 +243,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 +273,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 +284,38 @@ 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 +353,28 @@ 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()?; + // Update/initialize and then write the bond data to storage 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 @@ -405,18 +409,12 @@ pub trait PosActions: PosReadOnly { None => Err(UnbondError::NoBondFound)?, }; 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 +424,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 +445,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 +509,75 @@ pub trait PosActions: PosReadOnly { Ok(slashed) } + + /// Change the commission rate of a validator + fn change_validator_commission_rate( + &mut self, + params: &PosParams, + validator: &Self::Address, + new_rate: Decimal, + current_epoch: impl Into, + ) -> Result<(), CommissionRateChangeError> { + if new_rate < Decimal::ZERO { + return Err(CommissionRateChangeError::NegativeRate( + new_rate, + validator.clone(), + )); + } + let current_epoch = current_epoch.into(); + let max_change = self + .read_validator_max_commission_rate_change(validator) + .map_err(|_| { + CommissionRateChangeError::NoMaxSetInStorage(validator) + }) + .unwrap() + .unwrap(); + let mut commission_rates = + match self.read_validator_commission_rate(validator) { + Ok(Some(rates)) => rates, + _ => { + return Err(CommissionRateChangeError::CannotRead( + validator.clone(), + )); + } + }; + let rate_at_pipeline = *commission_rates + .get_at_offset(current_epoch, DynEpochOffset::PipelineLen, params) + .expect("Could not find a rate in given epoch"); + if new_rate == rate_at_pipeline { + return Err(CommissionRateChangeError::ChangeIsZero( + validator.clone(), + )); + } + + let rate_before_pipeline = *commission_rates + .get_at_offset( + current_epoch - 1, + DynEpochOffset::PipelineLen, + params, + ) + .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(), + )); + } + commission_rates.update_from_offset( + |val, _epoch| { + *val = new_rate; + }, + current_epoch, + DynEpochOffset::PipelineLen, + params, + ); + self.write_validator_commission_rate(validator, commission_rates) + .map_err(|_| CommissionRateChangeError::CannotWrite(validator)) + .unwrap(); + + Ok(()) + } } /// PoS system base trait for system initialization on genesis block, updating @@ -580,6 +645,8 @@ pub trait PosBase { /// TODO: this should be `const`, but in the ledger `address::xan` is not a /// `const fn` fn staking_token_address() -> Self::Address; + /// Get address of the PoS account + fn read_pos_address(&self) -> Self::Address; /// Address of the slash pool, into which slashed tokens are transferred. const POS_SLASH_POOL_ADDRESS: Self::Address; @@ -600,24 +667,51 @@ pub trait PosBase { &self, key: &Self::Address, ) -> Option; - /// Read PoS validator's total deltas of their bonds (validator self-bonds + /// Read PoS validator's bond deltas (validator self-bonds /// and delegations). - fn read_validator_total_deltas( - &self, - key: &Self::Address, - ) -> Option>; - /// Read PoS validator's voting power. - fn read_validator_voting_power( + fn read_validator_deltas( &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's reward products + fn read_validator_rewards_products( + &self, + key: &Self::Address, + ) -> Option; + /// Read PoS validator's delegation reward products + fn read_validator_delegation_rewards_products( + &self, + key: &Self::Address, + ) -> Option; + /// Read PoS validator's last known epoch with rewards products + fn read_validator_last_known_product_epoch( + &self, + key: &Self::Address, + ) -> Epoch; + /// Read PoS consensus validator's rewards accumulator + fn read_consensus_validator_rewards_accumulator( + &self, + ) -> Option>; /// 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; + /// Read the last block proposer's namada address + fn read_last_block_proposer_address(&self) -> Option; + /// Read the current block proposer's namada address + fn read_current_block_proposer_address(&self) -> Option; /// Write PoS parameters. fn write_pos_params(&mut self, params: &PosParams); /// Write PoS validator's raw hash of its consensus key. @@ -626,13 +720,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 +734,47 @@ 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: &ValidatorDeltas, + ); + /// Write PoS validator's commission rate. + fn write_validator_commission_rate( &mut self, key: &Self::Address, - value: &ValidatorTotalDeltas, + value: &CommissionRates, ); - /// Write PoS validator's voting power. - fn write_validator_voting_power( + /// Write PoS validator's commission rate. + fn write_validator_max_commission_rate_change( &mut self, key: &Self::Address, - value: &ValidatorVotingPowers, + value: &Decimal, + ); + // TODO: should the rewards products be written entirely or appended? + + /// Write PoS validator's rewards products. + fn write_validator_rewards_products( + &mut self, + key: &Self::Address, + value: &RewardsProducts, + ); + /// Write PoS validator's delegation rewards products. + fn write_validator_delegation_rewards_products( + &mut self, + key: &Self::Address, + value: &RewardsProducts, + ); + /// Write PoS validator's last known epoch with rewards products + fn write_validator_last_known_product_epoch( + &mut self, + key: &Self::Address, + value: &Epoch, + ); + /// Write PoS validator's delegation rewards products. + fn write_consensus_validator_rewards_accumulator( + &mut self, + value: &std::collections::HashMap, ); /// Write (append) PoS slash applied to a validator. fn write_validator_slash( @@ -672,14 +790,12 @@ 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); + /// Write the last block proposer's namada address + fn write_last_block_proposer_address(&mut self, value: &Self::Address); + /// Write the current block proposer's namada address + fn write_current_block_proposer_address(&mut self, value: &Self::Address); /// Credit tokens to the `target` account. This should only be used at /// genesis. fn credit_tokens( @@ -720,19 +836,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 +856,19 @@ 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); + // Credit the bonded tokens to the PoS account self.credit_tokens( &Self::staking_token_address(), @@ -812,7 +924,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 +951,7 @@ pub trait PosBase { .clone(); Some(ValidatorSetUpdate::Active(ActiveValidator { consensus_key, - voting_power: validator.voting_power, + bonded_stake: validator.bonded_stake, })) }, ); @@ -854,7 +966,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. @@ -881,6 +993,118 @@ pub trait PosBase { active_validators.chain(inactive_validators).for_each(f) } + /// Tally a running sum of the fracton of rewards owed to each validator in + /// the consensus set. This is used to keep track of the rewards due to each + /// consensus validator over the lifetime of an epoch. + fn log_block_rewards( + &mut self, + epoch: impl Into, + proposer_address: &Self::Address, + votes: &[VoteInfo], + ) -> Result<(), InflationError> { + // TODO: all values collected here need to be consistent with the same + // block that the voting info corresponds to, which is the + // previous block from the current one we are in. + + // The votes correspond to the last committed block (n-1 if we are + // finalizing block n) + + let epoch: Epoch = epoch.into(); + let validator_set = self.read_validator_set(); + let validators = validator_set.get(epoch).unwrap(); + let pos_params = self.read_pos_params(); + + // Get total stake of the consensus validator set + // TODO: does this need to account for rewards prodcuts? + let total_active_stake = validators.active.iter().fold( + 0_u64, + |sum, + WeightedValidator { + bonded_stake, + address: _, + }| { sum + *bonded_stake }, + ); + + // Get set of signing validator addresses and the combined stake of + // these signers + let mut signer_set: HashSet = HashSet::new(); + let mut total_signing_stake: u64 = 0; + for vote in votes.iter() { + if !vote.signed_last_block { + continue; + } + let tm_raw_hash_string = + hex::encode_upper(vote.validator_address.clone()); + let native_address = self + .read_validator_address_raw_hash(tm_raw_hash_string) + .expect( + "Unable to read native address of validator from \ + tendermint raw hash", + ); + signer_set.insert(native_address.clone()); + + // vote.validator_vp is updating at a constant delay relative to the + // validator deltas. + // Use validator deltas in namada protocol to get voting power instead + let deltas = self.read_validator_deltas(&native_address).unwrap(); + let stake: Self::TokenChange = deltas.get(epoch).unwrap(); + let stake: u64 = Into::::into(stake).try_into().unwrap(); + total_signing_stake += stake; + } + + // Get the block rewards coefficients (proposing, signing/voting, + // consensus set status) + let active_val_stake: Decimal = total_active_stake.into(); + let signing_stake: Decimal = total_signing_stake.into(); + let rewards_calculator = PosRewardsCalculator::new( + pos_params.block_proposer_reward, + pos_params.block_vote_reward, + total_signing_stake, + total_active_stake, + ); + let coeffs = match rewards_calculator.get_reward_coeffs() { + Ok(coeffs) => coeffs, + Err(_) => return Err(InflationError::Error), + }; + + // Calculate the fraction block rewards for each consensus validator and + // update the reward accumulators + let mut validator_accumulators = self + .read_consensus_validator_rewards_accumulator() + .unwrap_or_default(); + for validator in validators.active.iter() { + let mut rewards_frac = Decimal::default(); + let stake: Decimal = validator.bonded_stake.into(); + + // Proposer reward + if validator.address == *proposer_address { + rewards_frac += coeffs.proposer_coeff; + } + + // Signer reward + if signer_set.contains(&validator.address) { + let signing_frac = stake / signing_stake; + rewards_frac += coeffs.signer_coeff * signing_frac; + } + + // Active validator reward + let active_val_frac = stake / active_val_stake; + rewards_frac += coeffs.active_val_coeff * active_val_frac; + + let prev_val = *validator_accumulators + .get(&validator.address) + .unwrap_or(&Decimal::ZERO); + validator_accumulators + .insert(validator.address.clone(), prev_val + rewards_frac); + } + + // Write the updated map of reward accumulators back to storage + self.write_consensus_validator_rewards_accumulator( + &validator_accumulators, + ); + Ok(()) + } + /// Apply a slash to a byzantine validator for the given evidence. fn slash( &mut self, @@ -901,37 +1125,32 @@ pub trait PosBase { block_height: evidence_block_height.into(), }; - let mut total_deltas = - self.read_validator_total_deltas(validator).ok_or_else(|| { + let mut validator_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 validator_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, &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); + // Transfer the slashed tokens to the PoS slash pool self.transfer( &Self::staking_token_address(), @@ -950,16 +1169,18 @@ pub enum GenesisError { VotingPowerOverflow(TryFromIntError), } +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum InflationError { + #[error("Error")] + Error, +} + #[allow(missing_docs)] #[derive(Error, Debug)] 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 +1258,28 @@ where NegativeStake(i128, Address), } +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum CommissionRateChangeError
+where + Address: Display + Debug + Clone + PartialOrd + Ord + Hash, +{ + #[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 +1315,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 +1347,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 +1400,7 @@ where + BorshSchema, TokenChange: 'a + Debug + + Default + Copy + Add + From @@ -1166,24 +1409,26 @@ 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(); + let mut total_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(), }); } + total_balance += total_bonded_balance; // Pop the smallest validators from the active set until its size is under // the limit and insert them into the inactive set let mut inactive: BTreeSet> = @@ -1198,33 +1443,31 @@ 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 +1483,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 +1509,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 +1537,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 +1545,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 +1561,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 +1598,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 +1610,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 +1638,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 +1648,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 +1666,9 @@ where BecomeValidatorData { consensus_key, state, - total_deltas, - voting_power, + deltas, + commission_rate, + max_commission_rate_change, } } @@ -1454,8 +1692,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 +1703,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
> @@ -1519,8 +1755,8 @@ where return Err(BondError::NotAValidator(bond_id.validator.clone())); } Some(validator_state) => { - // Check that it's not inactive anywhere from the current epoch - // to the pipeline offset + // Check that the validator is not inactive anywhere from the + // current epoch to the pipeline offset for epoch in current_epoch.iter_range(OffsetPipelineLen::value(params)) { @@ -1535,16 +1771,19 @@ 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 new bond at the pipeline offset + let update_offset = DynEpochOffset::PipelineLen; value .pos_deltas .insert(current_epoch + update_offset.value(params), amount); + // If bond deltas do not exist, initialize them; otherwise, add to existing + // ones let bond = match current_bond { None => EpochedDelta::init_at_offset( value, @@ -1558,9 +1797,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 - // its voting power before the change. + // Update validator set. let token_change = TokenChange::from(amount); update_validator_set( params, @@ -1568,21 +1805,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 deltas and total 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, @@ -1591,31 +1828,11 @@ where params, ), }; - - // 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 +1860,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> @@ -1716,7 +1932,7 @@ where let to_unbond = &mut to_unbond; let mut slashed_amount = TokenAmount::default(); // Decrement the bond deltas starting from the rightmost value (a bond in a - // future-most epoch) until whole amount is decremented + // future-most epoch) until the entire amount is decremented bond.rev_while( |bonds, _epoch| { for (epoch_start, bond_delta) in bonds.pos_deltas.iter() { @@ -1745,7 +1961,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 +1991,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 +2000,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 +2022,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 @@ -1842,7 +2052,7 @@ fn update_validator_set( |validator_set, epoch| { // Find the validator's voting power 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 +2060,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 +2097,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 +2123,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 +2189,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 7ee0abdf986..a5c6142046a 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,20 +20,28 @@ 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, - /// 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, - /// 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, + 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, + /// Fraction of validator's stake that should be slashed on a duplicate + /// vote. + pub duplicate_vote_slash_rate: Decimal, + /// Fraction of validator's stake that should be slashed on a light client + /// attack. + pub light_client_attack_slash_rate: Decimal, + /// The minimum amount of bonded tokens that a validator needs to be in + /// either the `consensus` or `below_capacity` validator sets + pub min_validator_stake: u64, } impl Default for PosParams { @@ -40,14 +50,20 @@ impl Default for PosParams { max_validator_slots: 128, 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, + // 1 tendermint 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 5% - duplicate_vote_slash_rate: BasisPoints::new(500), + duplicate_vote_slash_rate: dec!(0.05), // slash 5% - light_client_attack_slash_rate: BasisPoints::new(500), + light_client_attack_slash_rate: dec!(0.05), + min_validator_stake: 1_000_000, } } } @@ -61,7 +77,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 +87,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 +117,30 @@ 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); + // + // TODO: decide if this is still a check we want to do (in its current + // state with our latest voting power conventions, it will fail + // always) + 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, )) } @@ -161,13 +185,13 @@ 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() diff --git a/proof_of_stake/src/rewards.rs b/proof_of_stake/src/rewards.rs new file mode 100644 index 00000000000..9b021af6130 --- /dev/null +++ b/proof_of_stake/src/rewards.rs @@ -0,0 +1,85 @@ +//! PoS rewards + +use rust_decimal::Decimal; +use rust_decimal_macros::dec; +use thiserror::Error; + +/// Errors during rewards calculation +#[derive(Debug, Error)] +pub enum RewardsError { + /// number of votes is less than the threshold of 2/3 + #[error( + "Insufficient votes, needed at least 2/3 of the total bonded stake" + )] + InsufficentVotes, + /// rewards coefficients are not set + #[error("Rewards coefficients are not properly set.")] + CoeffsNotSet, +} + +/// Holds coefficients for the three different ways to get PoS rewards +#[derive(Debug, Copy, Clone)] +#[allow(missing_docs)] +pub struct PosRewards { + pub proposer_coeff: Decimal, + pub signer_coeff: Decimal, + pub active_val_coeff: Decimal, +} + +/// Holds relevant PoS parameters and is used to calculate the coefficients for +/// the rewards +#[derive(Debug, Copy, Clone)] +pub struct PosRewardsCalculator { + proposer_param: Decimal, + signer_param: Decimal, + signing_stake: u64, + total_stake: u64, +} + +impl PosRewardsCalculator { + /// Instantiate a new PosRewardsCalculator + pub fn new( + proposer_param: Decimal, + signer_param: Decimal, + signing_stake: u64, + total_stake: u64, + ) -> Self { + Self { + proposer_param, + signer_param, + signing_stake, + total_stake, + } + } + + /// Calculate the reward coefficients + pub fn get_reward_coeffs(&self) -> Result { + // TODO: think about possibility of u64 overflow + let votes_needed = self.get_min_required_votes(); + if self.signing_stake < votes_needed { + return Err(RewardsError::InsufficentVotes); + } + + // Logic for determining the coefficients + // TODO: error handling to ensure proposer_coeff is > 0? + let proposer_coeff = self.proposer_param + * Decimal::from(self.signing_stake - votes_needed) + / Decimal::from(self.total_stake) + + dec!(0.01); + let signer_coeff = self.signer_param; + let active_val_coeff = dec!(1.0) - proposer_coeff - signer_coeff; + + let coeffs = PosRewards { + proposer_coeff, + signer_coeff, + active_val_coeff, + }; + + Ok(coeffs) + } + + /// Implement as ceiling (2/3) * validator set stake + fn get_min_required_votes(&self) -> u64 { + ((2 * self.total_stake) + 3 - 1) / 3 + } +} diff --git a/proof_of_stake/src/types.rs b/proof_of_stake/src/types.rs index 5f5ac6846d5..ae35eae2d8d 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,13 @@ 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 rewards products +pub type RewardsProducts = std::collections::HashMap; +/// Epoched validator commission rate +pub type CommissionRates = Epoched; /// Epoch identifier. Epochs are identified by consecutive natural numbers. /// @@ -59,40 +62,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 +77,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. @@ -133,8 +101,8 @@ pub enum ValidatorSetUpdate { 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 +163,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, } @@ -222,7 +190,7 @@ where write!( f, "{} with voting power {}", - self.address, self.voting_power + self.address, self.bonded_stake ) } } @@ -321,17 +289,17 @@ pub struct Slash { pub epoch: Epoch, /// Block height at which the slashable event occurred. pub block_height: u64, - /// A type of slashsable event. + /// A type of slashable 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 /// their staked tokens at and before the epoch of the slash. pub type Slashes = Vec; -/// A type of slashsable event. +/// A type of slashable event. #[derive(Debug, Clone, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum SlashType { /// Duplicate block vote. @@ -340,22 +308,17 @@ 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); +/// VoteInfo inspired from tendermint +#[derive(Debug, Clone, BorshDeserialize, BorshSerialize)] +pub struct VoteInfo { + /// the first 20 bytes of the validator public key hash (SHA-256) taken + /// from tendermint + pub validator_address: Vec, + /// validator voting power + pub validator_vp: u64, + /// was the validator signature was included in the last block? + pub signed_last_block: bool, +} /// Derive Tendermint raw hash from the public key pub trait PublicKeyTmRawHash { @@ -363,87 +326,6 @@ pub trait PublicKeyTmRawHash { 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,86 +521,10 @@ 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::LightClientAttack => { @@ -737,35 +543,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 faef13f4570..0b60632d136 100644 --- a/proof_of_stake/src/validation.rs +++ b/proof_of_stake/src/validation.rs @@ -16,10 +16,10 @@ 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, Epoch, + PublicKeyTmRawHash, Slash, Slashes, TotalDeltas, Unbonds, + ValidatorConsensusKeys, ValidatorDeltas, ValidatorSets, ValidatorState, + ValidatorStates, WeightedValidator, }; #[allow(missing_docs)] @@ -149,24 +149,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")] @@ -245,12 +231,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 +248,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 +268,8 @@ 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>), } /// Data update with prior and posterior state. @@ -312,10 +293,9 @@ pub struct NewValidator { has_state: bool, has_consensus_key: Option, has_total_deltas: bool, - has_voting_power: bool, - has_staking_reward_address: bool, + has_bonded_stake: bool, has_address_raw_hash: Option, - voting_power: VotingPower, + bonded_stake: u64, } /// Validation constants @@ -407,20 +387,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() { @@ -466,8 +445,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 +466,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 +511,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 +538,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 +580,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 +614,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 +644,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,17 +659,12 @@ where has_state, has_consensus_key, has_total_deltas, - has_voting_power, - has_staking_reward_address, + has_bonded_stake, has_address_raw_hash, - voting_power, + bonded_stake, } = &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) - { + if !(*has_state && *has_total_deltas && *has_bonded_stake) { errors.push(Error::InvalidNewValidator( address.clone(), new_validator.clone(), @@ -837,7 +686,7 @@ where )), } let weighted_validator = WeightedValidator { - voting_power: *voting_power, + bonded_stake: *bonded_stake, address: address.clone(), }; match validator_sets { @@ -944,13 +793,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 +859,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(), } } @@ -1104,11 +949,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 +973,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_total_deltas( constants, errors, total_deltas, @@ -1147,16 +982,6 @@ 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, - ), }, Balance(data) => Self::balance(errors, balance_delta, data), Bond { id, data, slashes } => { @@ -1177,10 +1002,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 } => { @@ -1327,32 +1152,6 @@ where } } - 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( constants: &Constants, errors: &mut Vec>, @@ -1363,7 +1162,7 @@ where >, new_validators: &mut HashMap>, address: Address, - data: Data>, + data: Data>, ) { match (data.pre, data.post) { (Some(pre), Some(post)) => { @@ -1520,104 +1319,6 @@ where } } - #[allow(clippy::too_many_arguments)] - fn validator_voting_power( - params: &PosParams, - 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, - ) { - match (&data.pre, data.post) { - (Some(_), Some(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` - for epoch in Epoch::iter_range( - constants.current_epoch, - constants.unbonding_offset + 1, - ) { - 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, - )) - } - } - } - } - 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() - } - } - (Some(_), None) => { - errors.push(Error::MissingValidatorVotingPower(address)) - } - (None, None) => {} - } - } - fn balance( errors: &mut Vec>, balance_delta: &mut TokenChange, @@ -1805,8 +1506,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 +1573,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 +1605,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 +1699,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 +1737,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 +1770,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 +1807,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 +1840,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 6469694ea9b..480a148bed8 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -101,7 +101,8 @@ pwasm-utils = {version = "0.18.0", optional = true} 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} -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 152a6295750..be66e098de7 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 00000000000..2500c48f22f --- /dev/null +++ b/shared/src/ledger/inflation.rs @@ -0,0 +1,142 @@ +//! General inflation system that will be used to process rewards for +//! proof-of-stake, providing liquity to shielded asset pools, and public goods +//! funding. + +use rust_decimal::prelude::ToPrimitive; +use rust_decimal::Decimal; +use rust_decimal_macros::dec; + +use crate::ledger::storage_api::{self, StorageRead, StorageWrite}; +use crate::types::address::Address; +use crate::types::token; + +/// The domains of inflation +pub enum RewardsType { + /// Proof-of-stake rewards + Staking, + /// Rewards for locking tokens in the multi-asset shielded pool + Masp, + /// Rewards for public goods funding (PGF) + PubGoodsFunding, +} + +/// Holds the PD controller values that should be updated in storage +#[allow(missing_docs)] +pub struct ValsToUpdate { + pub locked_ratio: Decimal, + pub inflation: u64, +} + +/// PD controller used to dynamically adjust the rewards rates +#[derive(Debug, Clone)] +pub struct RewardsController { + locked_tokens: token::Amount, + total_tokens: token::Amount, + locked_ratio_target: Decimal, + locked_ratio_last: Decimal, + max_reward_rate: Decimal, + last_inflation_amount: token::Amount, + p_gain_nom: Decimal, + d_gain_nom: Decimal, + epochs_per_year: u64, +} + +impl RewardsController { + /// Initialize a new PD controller + pub fn new( + locked_tokens: token::Amount, + total_tokens: token::Amount, + locked_ratio_target: Decimal, + locked_ratio_last: Decimal, + max_reward_rate: Decimal, + last_inflation_amount: token::Amount, + p_gain_nom: Decimal, + d_gain_nom: Decimal, + epochs_per_year: u64, + ) -> Self { + Self { + locked_tokens, + total_tokens, + locked_ratio_target, + locked_ratio_last, + max_reward_rate, + last_inflation_amount, + p_gain_nom, + d_gain_nom, + epochs_per_year, + } + } + + /// Calculate a new rewards rate + pub fn run( + Self { + locked_tokens, + total_tokens, + locked_ratio_target, + locked_ratio_last, + max_reward_rate, + last_inflation_amount, + p_gain_nom, + d_gain_nom, + epochs_per_year, + }: &Self, + ) -> ValsToUpdate { + let locked: Decimal = u64::from(*locked_tokens).into(); + let total: Decimal = u64::from(*total_tokens).into(); + let epochs_py: Decimal = (*epochs_per_year).into(); + + let locked_ratio = locked / total; + let max_inflation = total * max_reward_rate / epochs_py; + let p_gain = p_gain_nom * max_inflation; + let d_gain = d_gain_nom * max_inflation; + + let error = locked_ratio_target - locked_ratio; + let delta_error = locked_ratio_last - locked_ratio; + let control_val = p_gain * error - d_gain * delta_error; + + let last_inflation_amount = Decimal::from(*last_inflation_amount); + let inflation = if last_inflation_amount + control_val > max_inflation { + max_inflation + } else { + if last_inflation_amount + control_val > dec!(0.0) { + last_inflation_amount + control_val + } else { + dec!(0.0) + } + }; + let inflation: u64 = inflation.to_u64().unwrap(); + + ValsToUpdate { + locked_ratio, + inflation, + } + } +} + +/// Function that allows the protocol to mint some number of tokens of a desired +/// type to a destination address. +/// TODO: think of error cases that must be handled. +pub fn mint_tokens( + storage: &mut S, + target: &Address, + token: &Address, + amount: token::Amount, +) -> storage_api::Result<()> +where + S: StorageWrite + for<'iter> StorageRead<'iter>, +{ + let dest_key = token::balance_key(token, target); + let mut dest_bal: token::Amount = + storage.read(&dest_key)?.unwrap_or_default(); + dest_bal.receive(&amount); + storage.write(&dest_key, dest_bal)?; + + // Update the total supply of the tokens in storage + let mut total_tokens: token::Amount = storage + .read(&token::total_supply_key(token))? + .unwrap_or_default(); + total_tokens.receive(&amount); + storage.write(&token::total_supply_key(token), total_tokens)?; + + Ok(()) +} diff --git a/shared/src/ledger/mod.rs b/shared/src/ledger/mod.rs index ef92b1e2d9c..3ea70cd0bcd 100644 --- a/shared/src/ledger/mod.rs +++ b/shared/src/ledger/mod.rs @@ -4,6 +4,7 @@ pub mod eth_bridge; pub mod gas; pub mod governance; pub mod ibc; +pub mod inflation; pub mod native_vp; pub mod parameters; pub mod pos; diff --git a/shared/src/ledger/parameters/mod.rs b/shared/src/ledger/parameters/mod.rs index fdc2a110d0d..32e4ff9d964 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 @@ -317,6 +433,7 @@ where decode(value.ok_or(ReadError::ParametersMissing)?) .map_err(ReadError::StorageTypeError)?; + // read max expected block time let max_expected_time_per_block_key = storage::get_max_expected_time_per_block_key(); let (value, gas_time) = storage @@ -326,14 +443,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 4041b82f2f0..e2e25b92f53 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 3b498727df3..fc8eafbf207 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::xan() } +/// 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,12 @@ 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 TotalDeltas = namada_proof_of_stake::types::TotalDeltas; + +/// Alias for a PoS type with the same name with concrete type parameters +pub type CommissionRates = namada_proof_of_stake::types::CommissionRates; + impl From for namada_proof_of_stake::types::Epoch { fn from(epoch: Epoch) -> Self { let epoch: u64 = epoch.into(); @@ -166,49 +182,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 +260,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 366ce489b50..9a512b40c06 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::{Epoch, RewardsProducts, 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,31 @@ 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 = "deltas"; +const VALIDATOR_COMMISSION_RATE_STORAGE_KEY: &str = "commission_rate"; +const VALIDATOR_MAX_COMMISSION_CHANGE_STORAGE_KEY: &str = + "max_commission_rate_change"; +const VALIDATOR_SELF_REWARDS_PRODUCT_KEY: &str = "validator_rewards_product"; +const VALIDATOR_DELEGATION_REWARDS_PRODUCT_KEY: &str = + "delegation_rewards_product"; +const VALIDATOR_LAST_KNOWN_PRODUCT_EPOCH_KEY: &str = + "last_known_rewards_product_epoch"; 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 VALIDATOR_SET_STORAGE_PREFIX: &str = "validator_set"; +const CONSENSUS_VALIDATOR_SET_STORAGE_PREFIX: &str = "consensus"; +const CONSENSUS_VALIDATOR_SET_STORAGE_KEY: &str = "set"; +const CONSENSUS_VALIDATOR_SET_ACCUMULATOR_STORAGE_KEY: &str = + "reward_accumulator"; +const BELOW_CAPACITY_VALIDATOR_SET_STORAGE_KEY: &str = "below_capacity"; +const BELOW_THRESHOLD_VALIDATOR_SET_STORAGE_KEY: &str = "below_threshold"; +const TOTAL_DELTAS_STORAGE_KEY: &str = "total_deltas"; +const LAST_BLOCK_PROPOSER_STORAGE_KEY: &str = "last_block_proposer"; +const CURRENT_BLOCK_PROPOSER_STORAGE_KEY: &str = "current_block_proposer"; /// Is the given key a PoS storage key? pub fn is_pos_key(key: &Key) -> bool { @@ -92,15 +104,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 commission rate. +pub fn validator_commission_rate_key(validator: &Address) -> Key { validator_prefix(validator) - .push(&VALIDATOR_STAKING_REWARD_ADDRESS_STORAGE_KEY.to_owned()) + .push(&VALIDATOR_COMMISSION_RATE_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 commissionr ate? +pub fn is_validator_commission_rate_key(key: &Key) -> Option<&Address> { match &key.segments[..] { [ DbKeySeg::AddressSeg(addr), @@ -109,7 +121,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_COMMISSION_RATE_STORAGE_KEY => { Some(validator) } @@ -117,15 +129,17 @@ 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 maximum commission rate change per epoch. +pub fn validator_max_commission_rate_change_key(validator: &Address) -> Key { validator_prefix(validator) - .push(&VALIDATOR_CONSENSUS_KEY_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 consensus key? -pub fn is_validator_consensus_key_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), @@ -134,7 +148,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_MAX_COMMISSION_CHANGE_STORAGE_KEY => { Some(validator) } @@ -142,15 +156,15 @@ 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 self rewards products. +pub fn validator_self_rewards_product_key(validator: &Address) -> Key { validator_prefix(validator) - .push(&VALIDATOR_STATE_STORAGE_KEY.to_owned()) + .push(&VALIDATOR_SELF_REWARDS_PRODUCT_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 self rewards products? +pub fn is_validator_self_rewards_product_key(key: &Key) -> Option<&Address> { match &key.segments[..] { [ DbKeySeg::AddressSeg(addr), @@ -159,7 +173,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_SELF_REWARDS_PRODUCT_KEY => { Some(validator) } @@ -167,15 +181,94 @@ 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 delegation rewards products. +pub fn validator_delegation_rewards_product_key(validator: &Address) -> Key { validator_prefix(validator) - .push(&VALIDATOR_TOTAL_DELTAS_STORAGE_KEY.to_owned()) + .push(&VALIDATOR_DELEGATION_REWARDS_PRODUCT_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 delegation rewards products? +pub fn is_validator_delegation_rewards_product_key( + key: &Key, +) -> Option<&Address> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(key), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && key == VALIDATOR_DELEGATION_REWARDS_PRODUCT_KEY => + { + Some(validator) + } + _ => None, + } +} + +/// Storage key for validator's last known rewards product epoch. +pub fn validator_last_known_product_epoch_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_LAST_KNOWN_PRODUCT_EPOCH_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for validator's last known rewards product epoch? +pub fn is_validator_last_known_product_epoch_key( + key: &Key, +) -> Option<&Address> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(key), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && key == VALIDATOR_LAST_KNOWN_PRODUCT_EPOCH_KEY => + { + Some(validator) + } + _ => None, + } +} + +/// Storage key for validator's consensus key. +pub fn validator_consensus_key_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_CONSENSUS_KEY_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> { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(prefix), + DbKeySeg::AddressSeg(validator), + DbKeySeg::StringSeg(key), + ] if addr == &ADDRESS + && prefix == VALIDATOR_STORAGE_PREFIX + && key == VALIDATOR_CONSENSUS_KEY_STORAGE_KEY => + { + Some(validator) + } + _ => None, + } +} + +/// Storage key for validator's state. +pub fn validator_state_key(validator: &Address) -> Key { + validator_prefix(validator) + .push(&VALIDATOR_STATE_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> { match &key.segments[..] { [ DbKeySeg::AddressSeg(addr), @@ -184,7 +277,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 +285,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 +302,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 +429,171 @@ pub fn is_validator_set_key(key: &Key) -> bool { } } -/// Storage key for total voting power. -pub fn total_voting_power_key() -> Key { +/// Storage key prefix for validator sets. +pub fn validator_set_prefix() -> Key { Key::from(ADDRESS.to_db_key()) - .push(&TOTAL_VOTING_POWER_STORAGE_KEY.to_owned()) + .push(&VALIDATOR_SET_STORAGE_PREFIX.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key prefix for the consensus validator set. +pub fn consensus_validator_set_prefix() -> Key { + validator_set_prefix() + .push(&CONSENSUS_VALIDATOR_SET_STORAGE_PREFIX.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Storage key for consensus validator set. +pub fn consensus_validator_set_key() -> Key { + consensus_validator_set_prefix() + .push(&CONSENSUS_VALIDATOR_SET_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for the consensus validator set? +pub fn is_consensus_validator_set_key(key: &Key) -> bool { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(key), + DbKeySeg::StringSeg(set), + DbKeySeg::StringSeg(field), + ] if addr == &ADDRESS + && key == VALIDATOR_SET_STORAGE_KEY + && set == CONSENSUS_VALIDATOR_SET_STORAGE_PREFIX + && field == CONSENSUS_VALIDATOR_SET_STORAGE_KEY => + { + true + } + _ => false, + } +} + +/// Storage key for the consensus validator set rewards accumulator. +pub fn consensus_validator_set_accumulator_key() -> Key { + consensus_validator_set_prefix() + .push(&CONSENSUS_VALIDATOR_SET_ACCUMULATOR_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 the consensus validator set? +pub fn is_consensus_validator_set_accumulator_key(key: &Key) -> bool { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(key), + DbKeySeg::StringSeg(set), + DbKeySeg::StringSeg(field), + ] if addr == &ADDRESS + && key == VALIDATOR_SET_STORAGE_PREFIX + && set == CONSENSUS_VALIDATOR_SET_STORAGE_PREFIX + && field == CONSENSUS_VALIDATOR_SET_ACCUMULATOR_STORAGE_KEY => + { + true + } + _ => false, + } +} + +/// Storage key for the below capacity validator set +pub fn below_capacity_validator_set_key() -> Key { + validator_set_prefix() + .push(&BELOW_CAPACITY_VALIDATOR_SET_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for the below capacity validator set? +pub fn is_below_capacity_validator_set_key(key: &Key) -> bool { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(key), + DbKeySeg::StringSeg(set), + ] if addr == &ADDRESS + && key == VALIDATOR_SET_STORAGE_PREFIX + && set == BELOW_CAPACITY_VALIDATOR_SET_STORAGE_KEY => + { + true + } + _ => false, + } +} + +/// Storage key for the below threshold validator set +pub fn below_threshold_validator_set_key() -> Key { + validator_set_prefix() + .push(&BELOW_THRESHOLD_VALIDATOR_SET_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for the below threshold validator set? +pub fn is_below_threshold_validator_set_key(key: &Key) -> bool { + match &key.segments[..] { + [ + DbKeySeg::AddressSeg(addr), + DbKeySeg::StringSeg(key), + DbKeySeg::StringSeg(set), + ] if addr == &ADDRESS + && key == VALIDATOR_SET_STORAGE_PREFIX + && set == BELOW_THRESHOLD_VALIDATOR_SET_STORAGE_KEY => + { + true + } + _ => false, + } +} + +/// Storage key for total deltas of all validators. +pub fn total_deltas_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&TOTAL_DELTAS_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// 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_DELTAS_STORAGE_KEY => + { + true + } + _ => false, + } +} + +/// Storage key for block proposer address of the previous block. +pub fn last_block_proposer_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&LAST_BLOCK_PROPOSER_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for block proposer address of the previous block? +pub fn is_last_block_proposer_key(key: &Key) -> bool { + match &key.segments[..] { + [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key)] + if addr == &ADDRESS && key == LAST_BLOCK_PROPOSER_STORAGE_KEY => + { + true + } + _ => false, + } +} + +/// Storage key for block proposer address of the current block. +pub fn current_block_proposer_key() -> Key { + Key::from(ADDRESS.to_db_key()) + .push(&CURRENT_BLOCK_PROPOSER_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for block proposer address of the current block? +pub fn is_current_block_proposer_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 == CURRENT_BLOCK_PROPOSER_STORAGE_KEY => { true } @@ -383,6 +629,10 @@ where super::staking_token_address() } + fn read_pos_address(&self) -> Self::Address { + Self::POS_ADDRESS + } + fn read_pos_params(&self) -> PosParams { let (value, _gas) = self.read(¶ms_key()).unwrap(); decode(value.unwrap()).unwrap() @@ -415,21 +665,21 @@ 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(); + ) -> Option> { + let (value, _gas) = self.read(&validator_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(); + fn read_last_block_proposer_address(&self) -> Option { + let (value, _gas) = self.read(&last_block_proposer_key()).unwrap(); + value.map(|value| decode(value).unwrap()) + } + + fn read_current_block_proposer_address(&self) -> Option { + let (value, _gas) = self.read(¤t_block_proposer_key()).unwrap(); value.map(|value| decode(value).unwrap()) } @@ -440,13 +690,70 @@ 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_commission_rate_key(key)).unwrap(); + decode(value.unwrap()).unwrap() + } + + fn read_validator_rewards_products( + &self, + key: &Self::Address, + ) -> Option { + let (value, _gas) = + self.read(&validator_self_rewards_product_key(key)).unwrap(); + value.map(|value| decode(value).unwrap()) + } + + fn read_validator_delegation_rewards_products( + &self, + key: &Self::Address, + ) -> Option { + let (value, _gas) = self + .read(&validator_delegation_rewards_product_key(key)) + .unwrap(); + value.map(|value| decode(value).unwrap()) + } + + fn read_validator_last_known_product_epoch( + &self, + key: &Self::Address, + ) -> Epoch { + let (value, _gas) = self + .read(&validator_delegation_rewards_product_key(key)) + .unwrap(); + decode(value.unwrap()).unwrap() + } + + fn read_consensus_validator_rewards_accumulator( + &self, + ) -> Option> + { + let (value, _gas) = self + .read(&consensus_validator_set_accumulator_key()) + .unwrap(); + value.map(|value| decode(value).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,48 +771,89 @@ 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_consensus_key( + fn write_validator_max_commission_rate_change( &mut self, key: &Self::Address, - value: &ValidatorConsensusKeys, + value: &rust_decimal::Decimal, ) { - self.write(&validator_consensus_key_key(key), encode(value)) + self.write( + &validator_max_commission_rate_change_key(key), + encode(value), + ) + .unwrap(); + } + + fn write_validator_rewards_products( + &mut self, + key: &Self::Address, + value: &RewardsProducts, + ) { + self.write(&validator_self_rewards_product_key(key), encode(value)) .unwrap(); } - fn write_validator_state( + fn write_validator_delegation_rewards_products( &mut self, key: &Self::Address, - value: &ValidatorStates, + value: &RewardsProducts, ) { - self.write(&validator_state_key(key), encode(value)) + self.write( + &validator_delegation_rewards_product_key(key), + encode(value), + ) + .unwrap(); + } + + fn write_validator_last_known_product_epoch( + &mut self, + key: &Self::Address, + value: &Epoch, + ) { + self.write(&validator_last_known_product_epoch_key(key), encode(value)) + .unwrap(); + } + + fn write_consensus_validator_rewards_accumulator( + &mut self, + value: &std::collections::HashMap, + ) { + self.write(&consensus_validator_set_accumulator_key(), encode(value)) + .unwrap(); + } + + fn write_validator_consensus_key( + &mut self, + key: &Self::Address, + value: &ValidatorConsensusKeys, + ) { + self.write(&validator_consensus_key_key(key), encode(value)) .unwrap(); } - fn write_validator_total_deltas( + fn write_validator_state( &mut self, key: &Self::Address, - value: &ValidatorTotalDeltas, + value: &ValidatorStates, ) { - self.write(&validator_total_deltas_key(key), encode(value)) + self.write(&validator_state_key(key), encode(value)) .unwrap(); } - fn write_validator_voting_power( + fn write_validator_deltas( &mut self, key: &Self::Address, - value: &ValidatorVotingPowers, + value: &ValidatorDeltas, ) { - self.write(&validator_voting_power_key(key), encode(value)) + self.write(&validator_deltas_key(key), encode(value)) .unwrap(); } @@ -528,25 +876,18 @@ 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)) + fn write_total_deltas(&mut self, value: &TotalDeltas) { + self.write(&total_deltas_key(), encode(value)).unwrap(); + } + + fn write_last_block_proposer_address(&mut self, value: &Self::Address) { + self.write(&last_block_proposer_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_current_block_proposer_address(&mut self, value: &Self::Address) { + self.write(¤t_block_proposer_key(), encode(value)) + .unwrap(); } fn credit_tokens( diff --git a/shared/src/ledger/pos/vp.rs b/shared/src/ledger/pos/vp.rs index 60264e49260..89ed161d4f5 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; @@ -149,21 +146,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 +158,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,14 +239,14 @@ 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| { - TotalVotingPowers::try_from_slice(&bytes[..]).ok() + super::TotalDeltas::try_from_slice(&bytes[..]).ok() }); let post = self.ctx.post().read_bytes(key)?.and_then(|bytes| { - TotalVotingPowers::try_from_slice(&bytes[..]).ok() + super::TotalDeltas::try_from_slice(&bytes[..]).ok() }); - changes.push(TotalVotingPower(Data { pre, post })); + changes.push(TotalDeltas(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); diff --git a/shared/src/ledger/storage/mod.rs b/shared/src/ledger/storage/mod.rs index 16c3ecf1803..bde43d58264 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/storage.rs b/shared/src/types/storage.rs index 57d0f66c5d1..746f2d9b2d3 100644 --- a/shared/src/types/storage.rs +++ b/shared/src/types/storage.rs @@ -871,7 +871,7 @@ pub struct Epochs { first_known_epoch: Epoch, /// The block heights of the first block of each known epoch. /// Invariant: the values must be sorted in ascending order. - first_block_heights: Vec, + pub first_block_heights: Vec, } impl Default for Epochs { diff --git a/shared/src/types/token.rs b/shared/src/types/token.rs index 787a8855dc5..45e7c5529cf 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 = @@ -239,6 +234,7 @@ impl From for Change { /// Key segment for a balance key pub const BALANCE_STORAGE_KEY: &str = "balance"; +const TOTAL_SUPPLY_STORAGE_KEY: &str = "total_supply"; /// Obtain a storage key for user's balance. pub fn balance_key(token_addr: &Address, owner: &Address) -> Key { @@ -302,6 +298,25 @@ pub fn is_any_token_balance_key(key: &Key) -> Option<&Address> { } } +/// Storage key for total supply of a token +pub fn total_supply_key(token_address: &Address) -> Key { + Key::from(token_address.to_db_key()) + .push(&TOTAL_SUPPLY_STORAGE_KEY.to_owned()) + .expect("Cannot obtain a storage key") +} + +/// Is storage key for total supply of a specific token? +pub fn is_total_supply_key(key: &Key, token_address: &Address) -> bool { + match &key.segments[..] { + [DbKeySeg::AddressSeg(addr), DbKeySeg::StringSeg(key)] + if addr == token_address && key == TOTAL_SUPPLY_STORAGE_KEY => + { + true + } + _ => false, + } +} + /// Check if the given storage key is non-owner's balance key. If it is, returns /// the address. pub fn is_non_owner_balance_key(key: &Key) -> Option<&Address> { @@ -440,11 +455,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 a7d5ee864bf..6b138ed64b5 100644 --- a/shared/src/types/transaction/mod.rs +++ b/shared/src/types/transaction/mod.rs @@ -23,6 +23,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::*; @@ -187,14 +188,15 @@ 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 diff --git a/tests/Cargo.toml b/tests/Cargo.toml index dc3bf7b8aaf..13f1ae62590 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 705c8227609..f6797015fa6 100644 --- a/tests/src/e2e/helpers.rs +++ b/tests/src/e2e/helpers.rs @@ -3,6 +3,7 @@ use std::path::Path; use std::process::Command; use std::str::FromStr; +use std::time::{Duration, Instant}; use std::{env, time}; use color_eyre::eyre::Result; @@ -14,7 +15,7 @@ use namada::types::key::*; use namada::types::storage::Epoch; use namada_apps::config::{Config, TendermintMode}; -use super::setup::{Test, ENV_VAR_DEBUG, ENV_VAR_USE_PREBUILT_BINARIES}; +use super::setup::{sleep, Test, ENV_VAR_DEBUG, ENV_VAR_USE_PREBUILT_BINARIES}; use crate::e2e::setup::{Bin, Who, APPS_PACKAGE}; use crate::run; @@ -93,7 +94,7 @@ pub fn find_keypair( }) } -/// Find the address of an account by its alias from the wallet +/// Find the voting power of an account by its alias from the wallet pub fn find_voting_power( test: &Test, alias: impl AsRef, @@ -148,6 +149,63 @@ pub fn get_epoch(test: &Test, ledger_address: &str) -> Result { Ok(Epoch(epoch)) } +/// Get the last committed block height. +pub fn get_height(test: &Test, ledger_address: &str) -> Result { + let mut find = run!( + test, + Bin::Client, + &["block", "--ledger-address", ledger_address], + Some(10) + )?; + let (unread, matched) = find.exp_regex("Last committed block ID: .*")?; + // Expected `matched` string is e.g.: + // + // ``` + // Last committed block F10B5E77F972F68CA051D289474B6E75574B446BF713A7B7B71D7ECFC61A3B21, height: 4, time: 2022-10-20T10:52:28.828745Z + // ``` + let height_str = strip_trailing_newline(&matched) + .trim() + // Find the height part ... + .split_once("height: ") + .unwrap() + // ... take what's after it ... + .1 + // ... find the next comma ... + .rsplit_once(',') + .unwrap() + // ... and take what's before it. + .0; + u64::from_str(height_str).map_err(|e| { + eyre!(format!( + "Height parsing failed from {} trimmed from {}, Error: \ + {}\n\nUnread output: {}", + height_str, matched, e, unread + )) + }) +} + +/// Sleep until the given height is reached or panic when time out is reached +/// before the height +pub fn wait_for_block_height( + test: &Test, + ledger_address: &str, + height: u64, + timeout_secs: u64, +) -> Result<()> { + let start = Instant::now(); + let loop_timeout = Duration::new(timeout_secs, 0); + loop { + let current = get_height(test, ledger_address)?; + if current >= height { + break Ok(()); + } + if Instant::now().duration_since(start) > loop_timeout { + panic!("Timed out waiting for height {height}, current {current}"); + } + sleep(1); + } +} + pub fn generate_bin_command(bin_name: &str, manifest_path: &Path) -> Command { let use_prebuilt_binaries = match env::var(ENV_VAR_USE_PREBUILT_BINARIES) { Ok(var) => var.to_ascii_lowercase() != "false", diff --git a/tests/src/e2e/ledger_tests.rs b/tests/src/e2e/ledger_tests.rs index a998844764e..29b814f486b 100644 --- a/tests/src/e2e/ledger_tests.rs +++ b/tests/src/e2e/ledger_tests.rs @@ -20,9 +20,11 @@ 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, @@ -123,6 +125,16 @@ fn test_node_connectivity() -> Result<()> { let _bg_validator_0 = validator_0.background(); let _bg_validator_1 = validator_1.background(); + let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); + let validator_1_rpc = get_actor_rpc(&test, &Who::Validator(1)); + let non_validator_rpc = get_actor_rpc(&test, &Who::NonValidator); + + // Find the block height on the validator + let after_tx_height = get_height(&test, &validator_0_rpc)?; + + // Wait for the non-validator to be synced to at least the same height + wait_for_block_height(&test, &non_validator_rpc, after_tx_height, 10)?; + let query_balance_args = |ledger_rpc| { vec![ "balance", @@ -134,10 +146,6 @@ fn test_node_connectivity() -> Result<()> { ledger_rpc, ] }; - - let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); - let validator_1_rpc = get_actor_rpc(&test, &Who::Validator(1)); - let non_validator_rpc = get_actor_rpc(&test, &Who::NonValidator); for ledger_rpc in &[validator_0_rpc, validator_1_rpc, non_validator_rpc] { let mut client = run!(test, Bin::Client, query_balance_args(ledger_rpc), Some(40))?; @@ -592,7 +600,7 @@ fn pos_bonds() -> Result<()> { let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); - // 2. Submit a self-bond for the gepnesis validator + // 2. Submit a self-bond for the genesis validator let tx_args = vec![ "bond", "--validator", @@ -738,7 +746,6 @@ fn pos_bonds() -> Result<()> { let mut client = run!(test, Bin::Client, tx_args, Some(40))?; client.exp_string("Transaction is valid.")?; client.assert_success(); - Ok(()) } @@ -1036,6 +1043,11 @@ fn proposal_submission() -> Result<()> { &working_dir, Some("tx_"), )), + epochs_per_year: 31_526_000, + pos_gain_p: dec!(0.1), + pos_gain_d: dec!(0.1), + staked_ratio: dec!(0), + pos_inflation_amount: 0, }; GenesisConfig { @@ -1667,7 +1679,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; @@ -1848,7 +1859,7 @@ fn test_genesis_validators() -> Result<()> { let bg_validator_0 = validator_0.background(); let bg_validator_1 = validator_1.background(); - let bg_non_validator = non_validator.background(); + let _bg_non_validator = non_validator.background(); // 4. Submit a valid token transfer tx let validator_one_rpc = get_actor_rpc(&test, &Who::Validator(0)); @@ -1879,12 +1890,42 @@ fn test_genesis_validators() -> Result<()> { // 3. Check that all the nodes processed the tx with the same result let mut validator_0 = bg_validator_0.foreground(); let mut validator_1 = bg_validator_1.foreground(); - let mut non_validator = bg_non_validator.foreground(); let expected_result = "all VPs accepted transaction"; + // We cannot check this on non-validator node as it might sync without + // applying the tx itself, but its state should be the same, checked below. validator_0.exp_string(expected_result)?; validator_1.exp_string(expected_result)?; - non_validator.exp_string(expected_result)?; + let _bg_validator_0 = validator_0.background(); + let _bg_validator_1 = validator_1.background(); + + let validator_0_rpc = get_actor_rpc(&test, &Who::Validator(0)); + let validator_1_rpc = get_actor_rpc(&test, &Who::Validator(1)); + let non_validator_rpc = get_actor_rpc(&test, &Who::NonValidator); + + // Find the block height on the validator + let after_tx_height = get_height(&test, &validator_0_rpc)?; + + // Wait for the non-validator to be synced to at least the same height + wait_for_block_height(&test, &non_validator_rpc, after_tx_height, 10)?; + + let query_balance_args = |ledger_rpc| { + vec![ + "balance", + "--owner", + validator_1_alias, + "--token", + XAN, + "--ledger-address", + ledger_rpc, + ] + }; + for ledger_rpc in &[validator_0_rpc, validator_1_rpc, non_validator_rpc] { + let mut client = + run!(test, Bin::Client, query_balance_args(ledger_rpc), Some(40))?; + client.exp_string("XAN: 1000000000010.1")?; + client.assert_success(); + } Ok(()) } diff --git a/tests/src/e2e/setup.rs b/tests/src/e2e/setup.rs index b20f14d54f0..daa48f29bd2 100644 --- a/tests/src/e2e/setup.rs +++ b/tests/src/e2e/setup.rs @@ -147,7 +147,7 @@ pub fn network( format!("{}:{}", std::file!(), std::line!()), )?; - // Get the generated chain_id` from result of the last command + // Get the generated chain_id from result of the last command let (unread, matched) = init_network.exp_regex(r"Derived chain ID: .*\n")?; let chain_id_raw = diff --git a/tests/src/native_vp/pos.rs b/tests/src/native_vp/pos.rs index ec6f75a15f0..d3f3c7fcb69 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 @@ -583,8 +579,7 @@ pub mod testing { DynEpochOffset, Epoched, EpochedDelta, }; 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, @@ -597,7 +592,6 @@ pub mod testing { #[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)>, @@ -665,8 +659,8 @@ pub mod testing { owner: Address, validator: Address, }, - TotalVotingPower { - vp_delta: i128, + TotalDeltas { + delta: i128, offset: Either, }, ValidatorSet { @@ -679,20 +673,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, @@ -851,7 +836,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 +850,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; @@ -893,10 +877,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,16 +885,11 @@ 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), - }, ] } ValidPosAction::Bond { @@ -923,120 +898,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()), - }, - ]); - } - } + // IMPORTANT: we have to update `ValidatorSet` and + // `TotalDeltas` before we update + // `ValidatorDeltas` because they need to + // read the total deltas before they change. + 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([ PosStorageChange::Bond { @@ -1045,7 +934,7 @@ pub mod testing { delta: token_delta, offset, }, - PosStorageChange::ValidatorTotalDeltas { + PosStorageChange::ValidatorDeltas { validator, delta: token_delta, offset, @@ -1063,56 +952,32 @@ 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), - }, - ]); - } + // 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::TotalDeltas { + delta: token_delta, + offset: Either::Left(offset), + }, + PosStorageChange::ValidatorDeltas { + validator: validator.clone(), + delta: token_delta, + offset: offset, + }, + ]); + // do I need ValidatorDeltas in here again?? changes.extend([ // IMPORTANT: we have to update `Unbond` before we // update `Bond`, because it needs to read the bonds to @@ -1128,7 +993,7 @@ pub mod testing { delta: token_delta, offset, }, - PosStorageChange::ValidatorTotalDeltas { + PosStorageChange::ValidatorDeltas { validator, delta: token_delta, offset, @@ -1323,31 +1188,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 +1246,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 +1272,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 } => { @@ -1516,41 +1328,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 +1381,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 +1412,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 992a2146e17..8629c10d180 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 c11b035495c..21a8b8bbb6b 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::*; @@ -80,13 +81,14 @@ impl Ctx { 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 +99,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) } } @@ -140,14 +138,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 +154,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 +201,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/vp_prelude/src/token.rs b/vp_prelude/src/token.rs index 0dcb0b10e99..05297b2b82b 100644 --- a/vp_prelude/src/token.rs +++ b/vp_prelude/src/token.rs @@ -15,21 +15,24 @@ use super::*; pub fn vp( ctx: &Ctx, token: &Address, - keys_changed: &BTreeSet, + keys_touched: &BTreeSet, verifiers: &BTreeSet
, ) -> VpResult { let mut change: Change = 0; - for key in keys_changed.iter() { - let owner: Option<&Address> = - match token::is_multitoken_balance_key(token, key) { - Some((_, o)) => Some(o), - None => token::is_balance_key(token, key), - }; - match owner { + for key in keys_touched.iter() { + match token::is_balance_key(token, key) { None => { - // Unknown changes to this address space are disallowed, but - // unknown changes anywhere else are permitted - if key.segments.get(0) == Some(&token.to_db_key()) { + if token::is_total_supply_key(key, token) { + // check if total supply is changed, which it should never + // be from a tx + let total_pre: Amount = ctx.read_pre(key)?.unwrap(); + let total_post: Amount = ctx.read_post(key)?.unwrap(); + if total_pre != total_post { + return reject(); + } + } else if key.segments.get(0) == Some(&token.to_db_key()) { + // Unknown changes to this address space are disallowed, but + // unknown changes anywhere else are permitted return reject(); } } diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index f9030a471a9..d0fe406abaf 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -1076,7 +1076,7 @@ dependencies = [ "subtle-encoding", "tendermint", "tendermint-light-client-verifier", - "tendermint-proto", + "tendermint-proto 0.23.6", "tendermint-testgen", "time", "tracing", @@ -1092,7 +1092,7 @@ dependencies = [ "prost", "prost-types", "serde", - "tendermint-proto", + "tendermint-proto 0.23.6", ] [[package]] @@ -1380,13 +1380,14 @@ dependencies = [ "rand", "rand_core 0.6.4", "rust_decimal", + "rust_decimal_macros", "serde", "serde_json", "sha2 0.9.9", "sparse-merkle-tree", "tempfile", "tendermint", - "tendermint-proto", + "tendermint-proto 0.23.6", "thiserror", "tonic-build", "tracing", @@ -1414,7 +1415,11 @@ version = "0.8.1" dependencies = [ "borsh", "derivative", + "hex", "proptest", + "rust_decimal", + "rust_decimal_macros", + "tendermint-proto 0.23.5", "thiserror", ] @@ -1429,6 +1434,8 @@ dependencies = [ "namada_tx_prelude", "namada_vp_prelude", "prost", + "rust_decimal", + "rust_decimal_macros", "serde_json", "sha2 0.9.9", "tempfile", @@ -1445,6 +1452,7 @@ dependencies = [ "namada", "namada_macros", "namada_vm_env", + "rust_decimal", "sha2 0.10.6", "thiserror", ] @@ -1984,10 +1992,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" @@ -2340,7 +2359,7 @@ dependencies = [ "signature", "subtle", "subtle-encoding", - "tendermint-proto", + "tendermint-proto 0.23.6", "time", "zeroize", ] @@ -2357,6 +2376,23 @@ dependencies = [ "time", ] +[[package]] +name = "tendermint-proto" +version = "0.23.5" +source = "git+https://github.com/heliaxdev/tendermint-rs?rev=95c52476bc37927218374f94ac8e2a19bd35bec9#95c52476bc37927218374f94ac8e2a19bd35bec9" +dependencies = [ + "bytes", + "flex-error", + "num-derive", + "num-traits", + "prost", + "prost-types", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + [[package]] name = "tendermint-proto" version = "0.23.6" diff --git a/wasm/checksums.json b/wasm/checksums.json index 01140354ba3..969d20b3dca 100644 --- a/wasm/checksums.json +++ b/wasm/checksums.json @@ -1,18 +1,19 @@ { - "tx_bond.wasm": "tx_bond.38c037a51f9215c2be9c1b01f647251ffdc96a02a0c958c5d3db4ee36ccde43b.wasm", - "tx_ibc.wasm": "tx_ibc.5f86477029d987073ebfec66019dc991b0bb8b80717d4885b860f910916cbcdd.wasm", - "tx_init_account.wasm": "tx_init_account.8d901bce15d1ab63a591def00421183a651d4d5e09ace4291bf0a9044692741d.wasm", - "tx_init_nft.wasm": "tx_init_nft.1991808f44c1c24d4376a3d46b602bed27575f6c0359095c53f37b9225050ffc.wasm", - "tx_init_proposal.wasm": "tx_init_proposal.716cd08d59b26bd75815511f03e141e6ac27bc0b7d7be10a71b04559244722c2.wasm", - "tx_init_validator.wasm": "tx_init_validator.611edff2746f71cdaa7547a84a96676b555821f00af8375a28f8dab7ae9fc9fa.wasm", - "tx_mint_nft.wasm": "tx_mint_nft.3f20f1a86da43cc475ccc127428944bd177d40fbe2d2d1588c6fadd069cbe4b2.wasm", - "tx_transfer.wasm": "tx_transfer.5653340103a32e6685f9668ec24855f65ae17bcc43035c2559a13f5c47bb67af.wasm", - "tx_unbond.wasm": "tx_unbond.71e66ac6f792123a2aaafd60b3892d74a7d0e7a03c3ea34f15fea9089010b810.wasm", - "tx_update_vp.wasm": "tx_update_vp.6d291dadb43545a809ba33fe26582b7984c67c65f05e363a93dbc62e06a33484.wasm", - "tx_vote_proposal.wasm": "tx_vote_proposal.ff3def7b4bb0c46635bd6d544ac1745362757ce063feb8142d2ed9ab207f2a12.wasm", - "tx_withdraw.wasm": "tx_withdraw.ba1a743cf8914a353d7706777e0b1a37e20cd271b16e022fd3b50ad28971291f.wasm", - "vp_nft.wasm": "vp_nft.4471284b5c5f3e28c973f0a2ad2dde52ebe4a1dcd5dc15e93b380706fd0e35ea.wasm", - "vp_testnet_faucet.wasm": "vp_testnet_faucet.7d7eb09cddc7ae348417da623e21ec4a4f8c78f15ae12de5abe7087eeab1e0db.wasm", - "vp_token.wasm": "vp_token.4a5436f7519de15c80103557add57e8d06e766e1ec1f7a642ffca252be01c5d0.wasm", - "vp_user.wasm": "vp_user.729b18aab60e8ae09b75b5f067658f30459a5ccfcd34f909b88da96523681019.wasm" + "tx_bond.wasm": "tx_bond.cb8f047f5a0e6ac521c1d2fd651170c102cdf7ff80f2e2c5c7fabe1b88e820d7.wasm", + "tx_from_intent.wasm": "tx_from_intent.d7977ef830aa14c8edb8882d5734939f9f2a0aea72c912badee3448b54cb21bc.wasm", + "tx_ibc.wasm": "tx_ibc.c8604d629e70348dbb3733b957c11aa7adbfb7d9dd6a07736010374cf9195d60.wasm", + "tx_init_account.wasm": "tx_init_account.4c9538065c75e0a80225aecede32d740f603cf266596fd708eefbb82add658ad.wasm", + "tx_init_nft.wasm": "tx_init_nft.4dea35d5f06c33f5eb0939fb65e5171d85ad6e5d45e9a1cee09d8d7f6799a768.wasm", + "tx_init_proposal.wasm": "tx_init_proposal.1b47b223f3b2854d83ea12aa873a42a121e860abc9f478c8f5c73dcaf3934228.wasm", + "tx_init_validator.wasm": "tx_init_validator.ed63edcbbc716f8be2827076734dade0496cc81812366c272a225b6a58cc1a34.wasm", + "tx_mint_nft.wasm": "tx_mint_nft.9fc30725c9ae3908f43ca3d5319fa1411e748ea4211957bc247656b4bdbd7822.wasm", + "tx_transfer.wasm": "tx_transfer.68aaab04b6dd5df68f6091002829668cdeb2e9c409a197e9f5a3d5b936cbedd5.wasm", + "tx_unbond.wasm": "tx_unbond.37ae72a8f9665ef0aa51b1bafddc7694dce2ac349d94c5072f7e370793fb4a4c.wasm", + "tx_update_vp.wasm": "tx_update_vp.195f3b8e0d983844ccf8f77e108ec53373ae58517ef59b31f6654720d0b92dd1.wasm", + "tx_vote_proposal.wasm": "tx_vote_proposal.f348a57705af3a73aad2b87df114a61cd44a54c756e58977a5bc304160d9273c.wasm", + "tx_withdraw.wasm": "tx_withdraw.f7517ca7b1b78128403526139bfa14cdab537f6e51c1be57a3d63fd4ff8ed6f5.wasm", + "vp_nft.wasm": "vp_nft.5d297f64811416f36fa53c2c90f0bfba0b15f42d4f7ed35dc4707a12442c2221.wasm", + "vp_testnet_faucet.wasm": "vp_testnet_faucet.7199981a2ff40a493c340870145590821c5f47efdf2a5b4e07c482298b09ce59.wasm", + "vp_token.wasm": "vp_token.211a3440a31c964117d58f4322864ea4053703c7ddb935a77b6e071794f6154b.wasm", + "vp_user.wasm": "vp_user.08e17bcc59f5a99bd25bffe8d2e421cbe60f5541ad9d10600bd092dc2f780e06.wasm" } \ No newline at end of file diff --git a/wasm/wasm_source/Cargo.toml b/wasm/wasm_source/Cargo.toml index 7f48f2aea68..e438f45106a 100644 --- a/wasm/wasm_source/Cargo.toml +++ b/wasm/wasm_source/Cargo.toml @@ -35,7 +35,7 @@ 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"] } @@ -48,3 +48,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/src/tx_bond.rs b/wasm/wasm_source/src/tx_bond.rs index 6718988657a..b590aba2d7c 100644 --- a/wasm/wasm_source/src/tx_bond.rs +++ b/wasm/wasm_source/src/tx_bond.rs @@ -32,9 +32,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, VotingPower, VotingPowerDelta, - }; + use namada_vp_prelude::proof_of_stake::types::Bond; use namada_vp_prelude::proof_of_stake::{ staking_token_address, BondId, GenesisValidator, PosVP, }; @@ -43,12 +41,12 @@ mod tests { 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 +66,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 +96,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 +106,85 @@ 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 TODO: should end be pipeline + unbonding + // now? + 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); diff --git a/wasm/wasm_source/src/tx_init_validator.rs b/wasm/wasm_source/src/tx_init_validator.rs index 2d5f1a62564..a99bb8cde94 100644 --- a/wasm/wasm_source/src/tx_init_validator.rs +++ b/wasm/wasm_source/src/tx_init_validator.rs @@ -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 6199393fb1f..e3e48e56cb5 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,40 +169,76 @@ 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 must have changed \ + - checking epoch {epoch}" ); } { + // TODO: should this loop over epochs after this one as well? Are + // there any? let epoch = pos_params.unbonding_len + 1; 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 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 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}" ); } // - `#{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 +247,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 +259,7 @@ mod tests { epoch {epoch}" ); } + // Check that the unbond is as expected let start_epoch = match &unbond.source { Some(_) => { // This bond was a delegation @@ -257,6 +297,7 @@ mod tests { ); } { + // TODO: checl logic here let epoch = pos_params.unbonding_len + 1; let bond: Bond = bonds_post.get(epoch).unwrap(); let expected_bond = @@ -272,102 +313,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(); diff --git a/wasm/wasm_source/src/tx_withdraw.rs b/wasm/wasm_source/src/tx_withdraw.rs index 8add20a78d8..3525b7b7cc4 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));