From 391e90d15c72d6fba7874fef2378e1cc52398df8 Mon Sep 17 00:00:00 2001 From: Calvin Lau Date: Sat, 18 Apr 2020 01:41:13 +0800 Subject: [PATCH 1/2] Problem: (Fix #1435) No standardized staking event Solution: Refactor staking related events to standardized events --- CHANGELOG.md | 1 + chain-abci/src/app/mod.rs | 177 ++-- chain-abci/src/app/staking_event.rs | 893 ++++++++++++++++++ chain-abci/src/app/validate_tx.rs | 36 +- chain-abci/src/enclave_bridge/real/mod.rs | 2 +- chain-abci/src/staking/mod.rs | 181 ++-- chain-abci/src/staking/table.rs | 60 +- chain-abci/src/storage/mod.rs | 82 +- chain-abci/tests/abci_app.rs | 31 +- chain-abci/tests/tx_validation.rs | 5 +- chain-core/src/common/mod.rs | 37 +- chain-core/src/init/coin.rs | 3 +- chain-core/src/state/account.rs | 8 +- .../src/tendermint/types/block_results.rs | 74 +- 14 files changed, 1355 insertions(+), 235 deletions(-) create mode 100644 chain-abci/src/app/staking_event.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 82674acc0..765d89b6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## v0.4.0 ### Breaking changes +* *chain-abci* [1449](https://github.com/crypto-com/chain/pull/1449): introduce staking_change event, remove RewardsDistribution and SlashValidators event * *client-rpc* [1443](https://github.com/crypto-com/chain/pull/1443): add progress, and multi-thread to sync api * *chain-abci* [1239](https://github.com/crypto-com/chain/pull/1239): basic versioning * *chain-abci* [1090](https://github.com/crypto-com/chain/pull/1090): upper bounds of reward parameters changed diff --git a/chain-abci/src/app/mod.rs b/chain-abci/src/app/mod.rs index 781ba6bf3..77421a9f9 100644 --- a/chain-abci/src/app/mod.rs +++ b/chain-abci/src/app/mod.rs @@ -6,6 +6,7 @@ mod commit; mod end_block; mod query; mod rewards; +mod staking_event; pub mod validate_tx; use abci::Pair as KVPair; @@ -17,14 +18,15 @@ pub use self::app_init::check_validators; pub use self::app_init::{ get_validator_key, init_app_hash, BufferType, ChainNodeApp, ChainNodeState, }; +use crate::app::staking_event::StakingEvent; use crate::app::validate_tx::ResponseWithCodeAndLog; use crate::enclave_bridge::EnclaveProxy; use crate::staking::RewardsDistribution; +use crate::storage::{TxAction, TxEnclaveAction, TxPublicAction}; use chain_core::common::{TendermintEventKey, TendermintEventType, Timespec}; use chain_core::init::coin::Coin; -use chain_core::state::account::{PunishmentKind, StakedStateAddress}; +use chain_core::state::account::PunishmentKind; use chain_core::state::tendermint::{BlockHeight, TendermintValidatorAddress, TendermintVotePower}; -use chain_core::tx::fee::Fee; use chain_core::tx::TxAux; use std::convert::{TryFrom, TryInto}; @@ -142,7 +144,7 @@ impl abci::Application for ChainNodeApp { last_state.block_time = block_time; last_state.block_height = block_height; - let slashes = last_state.staking_table.begin_block( + let punishment_outcomes = last_state.staking_table.begin_block( &mut staking_store!(self, last_state.staking_version), &last_state.top_level.network_params, block_time, @@ -151,40 +153,39 @@ impl abci::Application for ChainNodeApp { &evidences, ); - let mut jailing_event = Event::new(); - jailing_event.field_type = TendermintEventType::JailValidators.to_string(); - let mut slashing_event = Event::new(); - slashing_event.field_type = TendermintEventType::SlashValidators.to_string(); + let mut response = ResponseBeginBlock::new(); let rewards_pool = &mut last_state.top_level.rewards_pool; - for (addr, amount, punishment_kind) in slashes.iter() { - rewards_pool.period_bonus = (rewards_pool.period_bonus + amount) + for punishment_outcome in punishment_outcomes.iter() { + // slashed_amount <= bonded + unbonded <= max supply + let slashed_amount = punishment_outcome + .slashed_coin + .sum() + .expect("sum of bonded and unbonded slash amount exceed maximum coin"); + rewards_pool.period_bonus = (rewards_pool.period_bonus + slashed_amount) .expect("rewards pool + fee greater than max coin?"); self.rewards_pool_updated = true; - let mut kvpair = KVPair::new(); - kvpair.key = TendermintEventKey::Account.into(); - kvpair.value = addr.to_string().into_bytes(); - if *punishment_kind == PunishmentKind::ByzantineFault { - jailing_event.attributes.push(kvpair.clone()); + let event = StakingEvent::Slash( + &punishment_outcome.staking_address, + punishment_outcome.slashed_coin.bonded, + punishment_outcome.slashed_coin.unbonded, + punishment_outcome.punishment_kind, + ); + response.events.push(event.into()); + + if punishment_outcome.punishment_kind == PunishmentKind::ByzantineFault { + let jailed_until = punishment_outcome + .jailed_until + .expect("jailed until should exist when being jailed"); + let event = StakingEvent::Jail( + &punishment_outcome.staking_address, + jailed_until, + punishment_outcome.punishment_kind, + ); + response.events.push(event.into()); } - slashing_event.attributes.push(kvpair); - - let mut kvpair = KVPair::new(); - kvpair.key = TendermintEventKey::Slash.into(); - kvpair.value = amount.to_string().into_bytes(); - slashing_event.attributes.push(kvpair); - } - - let mut response = ResponseBeginBlock::new(); - - if !jailing_event.attributes.is_empty() { - response.events.push(jailing_event); - } - - if !slashing_event.attributes.is_empty() { - response.events.push(slashing_event); } if let Some(last_commit_info) = req.last_commit_info.as_ref() { @@ -204,9 +205,10 @@ impl abci::Application for ChainNodeApp { } if let Some((distributed, minted)) = self.rewards_try_distribute() { - response - .events - .push(write_reward_event(distributed, minted)); + let events = generate_reward_events(distributed, minted); + for event in events.iter() { + response.events.push(event.to_owned()); + } } response @@ -219,13 +221,18 @@ impl abci::Application for ChainNodeApp { let mut resp = ResponseDeliverTx::new(); let result = self.process_tx(req, BufferType::Consensus); match result { - Ok((txaux, fee, maccount)) => { + Ok((txaux, tx_action)) => { + let fee_amount = tx_action.fee().to_coin(); + let tx_events = generate_tx_events(&txaux, tx_action); + resp.set_code(0); - resp.events.push(write_tx_event(&txaux, fee, maccount)); + + for event in tx_events.iter() { + resp.events.push(event.to_owned()); + } self.delivered_txs.push(txaux); - let fee_amount = fee.to_coin(); if fee_amount > Coin::zero() { let rewards_pool = &mut self.last_state.as_mut().unwrap().top_level.rewards_pool; @@ -292,47 +299,79 @@ fn abci_block_height(i: i64) -> Option { result } -fn write_reward_event(distributed: RewardsDistribution, minted: Coin) -> Event { - let mut event = Event::new(); - event.field_type = TendermintEventType::RewardsDistribution.to_string(); +fn generate_reward_events(distribution: RewardsDistribution, minted: Coin) -> Vec { + let mut events: Vec = Vec::new(); + + for reward in distribution.iter() { + let event = StakingEvent::Reward(&reward.0, reward.1).into(); - let mut kvpair = KVPair::new(); - kvpair.key = TendermintEventKey::RewardsDistribution.into(); - kvpair.value = serde_json::to_string(&distributed) - .expect("encode rewards result failed") + events.push(event); + } + + let mut reward_event = Event::new(); + reward_event.field_type = TendermintEventType::Reward.to_string(); + + let mut minted_kvpair = KVPair::new(); + minted_kvpair.key = TendermintEventKey::CoinMinted.into(); + minted_kvpair.value = serde_json::to_string(&minted) + .expect("encode coin minted failed") .as_bytes() .to_owned(); - event.attributes.push(kvpair); + reward_event.attributes.push(minted_kvpair); - let mut kvpair = KVPair::new(); - kvpair.key = TendermintEventKey::CoinMinted.into(); - kvpair.value = minted.to_string().as_bytes().to_owned(); - event.attributes.push(kvpair); + events.push(reward_event); - event + events } -fn write_tx_event(txaux: &TxAux, fee: Fee, maccount: Option) -> abci::Event { - let mut event = Event::new(); - event.field_type = TendermintEventType::ValidTransactions.to_string(); - - // write fee into event - let mut kvpair_fee = KVPair::new(); - kvpair_fee.key = TendermintEventKey::Fee.into(); - kvpair_fee.value = Vec::from(format!("{}", fee.to_coin())); - event.attributes.push(kvpair_fee); - - if let Some(address) = maccount { - let mut kvpair = KVPair::new(); - kvpair.key = TendermintEventKey::Account.into(); - kvpair.value = Vec::from(format!("{}", address)); - event.attributes.push(kvpair); +fn generate_tx_events(txaux: &TxAux, tx_action: TxAction) -> Vec { + let mut events = Vec::new(); + + let mut valid_txs_event = Event::new(); + valid_txs_event.field_type = TendermintEventType::ValidTransactions.to_string(); + + let mut fee_kvpair = KVPair::new(); + let fee = tx_action.fee(); + fee_kvpair.key = TendermintEventKey::Fee.into(); + fee_kvpair.value = Vec::from(format!("{}", fee.to_coin())); + valid_txs_event.attributes.push(fee_kvpair); + + let mut txid_kvpair = KVPair::new(); + txid_kvpair.key = TendermintEventKey::TxId.into(); + txid_kvpair.value = Vec::from(hex::encode(txaux.tx_id()).as_bytes()); + valid_txs_event.attributes.push(txid_kvpair); + + events.push(valid_txs_event); + + let maybe_tx_staking_event = generate_tx_staking_change_event(tx_action); + if let Some(tx_staking_event) = maybe_tx_staking_event { + events.push(tx_staking_event); } - let mut kvpair = KVPair::new(); - kvpair.key = TendermintEventKey::TxId.into(); - kvpair.value = Vec::from(hex::encode(txaux.tx_id()).as_bytes()); - event.attributes.push(kvpair); + events +} - event +fn generate_tx_staking_change_event(tx_action: TxAction) -> Option { + match tx_action { + TxAction::Enclave(tx_enclave_action) => match tx_enclave_action { + TxEnclaveAction::Transfer { .. } => None, + TxEnclaveAction::Deposit { deposit, .. } => { + Some(StakingEvent::Deposit(&deposit.0, deposit.1).into()) + } + TxEnclaveAction::Withdraw { withdraw, .. } => { + Some(StakingEvent::Withdraw(&withdraw.0, withdraw.1).into()) + } + }, + TxAction::Public(tx_public_action) => match tx_public_action { + TxPublicAction::Unbond { unbond, .. } => { + Some(StakingEvent::Unbond(&unbond.0, unbond.1).into()) + } + TxPublicAction::NodeJoin(staking_address, council_node) => { + Some(StakingEvent::NodeJoin(&staking_address, council_node).into()) + } + TxPublicAction::Unjail(staking_address) => { + Some(StakingEvent::Unjail(&staking_address).into()) + } + }, + } } diff --git a/chain-abci/src/app/staking_event.rs b/chain-abci/src/app/staking_event.rs new file mode 100644 index 000000000..69f313486 --- /dev/null +++ b/chain-abci/src/app/staking_event.rs @@ -0,0 +1,893 @@ +use std::fmt; + +use serde::ser::{Serialize, SerializeStruct, Serializer}; + +use abci::Pair as KVPair; +use abci::*; +use chain_core::common::{TendermintEventKey, TendermintEventType, Timespec}; +use chain_core::init::coin::Coin; +use chain_core::state::account::{CouncilNode, PunishmentKind, StakedStateAddress}; + +pub(crate) enum StakingEvent<'a> { + Deposit(&'a StakedStateAddress, Coin), + Unbond(&'a StakedStateAddress, Coin), + Withdraw(&'a StakedStateAddress, Coin), + NodeJoin(&'a StakedStateAddress, CouncilNode), + Reward(&'a StakedStateAddress, Coin), + Jail(&'a StakedStateAddress, Timespec, PunishmentKind), + Slash(&'a StakedStateAddress, Coin, Coin, PunishmentKind), + Unjail(&'a StakedStateAddress), +} + +impl<'a> From> for Event { + fn from(event: StakingEvent) -> Self { + let mut builder = StakingEventBuilder::default(); + + match event { + StakingEvent::Deposit(staking_address, deposit_amount) => { + builder.deposit(staking_address, deposit_amount) + } + StakingEvent::Unbond(staking_address, unbond_amount) => { + builder.unbond(staking_address, unbond_amount) + } + StakingEvent::Withdraw(staking_address, withdraw_amount) => { + builder.withdraw(staking_address, withdraw_amount) + } + StakingEvent::NodeJoin(staking_address, council_node) => { + builder.node_join(staking_address, council_node) + } + StakingEvent::Reward(staking_address, reward_amount) => { + builder.reward(staking_address, reward_amount) + } + StakingEvent::Jail(staking_address, timespec, punishment_kind) => { + builder.jail(staking_address, timespec, punishment_kind) + } + StakingEvent::Slash( + staking_address, + bonded_slash_amount, + unbonded_slash_amount, + punishment_kind, + ) => builder.slash( + staking_address, + bonded_slash_amount, + unbonded_slash_amount, + punishment_kind, + ), + StakingEvent::Unjail(staking_address) => builder.unjail(staking_address), + } + + builder.to_event() + } +} + +#[derive(Default)] +struct StakingEventBuilder { + attributes: Vec, +} + +impl StakingEventBuilder { + fn deposit(&mut self, staking_address: &StakedStateAddress, deposit_amount: Coin) { + self.attributes + .push(staking_address_attribute(staking_address)); + self.attributes.push(StakingEventOpType::Deposit.into()); + + self.attributes.push( + StakingDiffField(vec![StakingDiff::Bonded( + StakingCoinChange::Increase, + deposit_amount, + )]) + .into(), + ); + } + + fn unbond(&mut self, staking_address: &StakedStateAddress, unbond_amount: Coin) { + self.attributes + .push(staking_address_attribute(staking_address)); + self.attributes.push(StakingEventOpType::Unbond.into()); + + self.attributes.push( + StakingDiffField(vec![ + StakingDiff::Bonded(StakingCoinChange::Decrease, unbond_amount), + StakingDiff::Unbonded(StakingCoinChange::Increase, unbond_amount), + ]) + .into(), + ); + } + + fn withdraw(&mut self, staking_address: &StakedStateAddress, withdraw_amount: Coin) { + self.attributes + .push(staking_address_attribute(staking_address)); + self.attributes.push(StakingEventOpType::Withdraw.into()); + + self.attributes.push( + StakingDiffField(vec![StakingDiff::Unbonded( + StakingCoinChange::Decrease, + withdraw_amount, + )]) + .into(), + ); + } + + fn node_join(&mut self, staking_address: &StakedStateAddress, node: CouncilNode) { + self.attributes + .push(staking_address_attribute(staking_address)); + self.attributes.push(StakingEventOpType::NodeJoin.into()); + self.attributes + .push(StakingDiffField(vec![StakingDiff::NodeJoin(node)]).into()); + } + + fn reward(&mut self, staking_address: &StakedStateAddress, reward_amount: Coin) { + self.attributes + .push(staking_address_attribute(staking_address)); + self.attributes.push(StakingEventOpType::Reward.into()); + self.attributes.push( + StakingDiffField(vec![StakingDiff::Bonded( + StakingCoinChange::Increase, + reward_amount, + )]) + .into(), + ); + } + + fn jail( + &mut self, + staking_address: &StakedStateAddress, + jailed_until: Timespec, + punishment_kind: PunishmentKind, + ) { + self.attributes + .push(staking_address_attribute(staking_address)); + self.attributes.push(StakingEventOpType::Jail.into()); + self.attributes + .push(StakingDiffField(vec![StakingDiff::JailedUntil(jailed_until)]).into()); + + let mut reason_kv_pair = KVPair::new(); + reason_kv_pair.key = TendermintEventKey::StakingOpReason.into(); + reason_kv_pair.value = punishment_reason(punishment_kind).into_bytes(); + self.attributes.push(reason_kv_pair) + } + + fn slash( + &mut self, + staking_address: &StakedStateAddress, + bonded_slash_amount: Coin, + unbonded_slash_amount: Coin, + punishment_kind: PunishmentKind, + ) { + self.attributes + .push(staking_address_attribute(staking_address)); + self.attributes.push(StakingEventOpType::Slash.into()); + self.attributes.push( + StakingDiffField(vec![ + StakingDiff::Bonded(StakingCoinChange::Decrease, bonded_slash_amount), + StakingDiff::Unbonded(StakingCoinChange::Decrease, unbonded_slash_amount), + ]) + .into(), + ); + + let mut reason_kv_pair = KVPair::new(); + reason_kv_pair.key = TendermintEventKey::StakingOpReason.into(); + reason_kv_pair.value = punishment_reason(punishment_kind).into_bytes(); + self.attributes.push(reason_kv_pair) + } + + fn unjail(&mut self, staking_address: &StakedStateAddress) { + self.attributes + .push(staking_address_attribute(staking_address)); + self.attributes.push(StakingEventOpType::Unjail.into()); + } + + fn to_event(&self) -> Event { + let mut event = Event::new(); + event.field_type = TendermintEventType::StakingChange.to_string(); + for attribute in self.attributes.iter() { + event.attributes.push(attribute.clone()) + } + + event + } +} + +#[inline] +fn staking_address_attribute(staking_address: &StakedStateAddress) -> KVPair { + let mut kv_pair = KVPair::new(); + kv_pair.key = TendermintEventKey::StakingAddress.into(); + kv_pair.value = staking_address.to_string().into_bytes(); + + kv_pair +} + +#[inline] +fn punishment_reason(punishment_kind: PunishmentKind) -> String { + match punishment_kind { + PunishmentKind::ByzantineFault => String::from("ByzantineFault"), + PunishmentKind::NonLive => String::from("NonLive"), + } +} + +enum StakingEventOpType { + Deposit, + Unbond, + Withdraw, + NodeJoin, + Reward, + Jail, + Slash, + Unjail, +} + +impl fmt::Display for StakingEventOpType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StakingEventOpType::Deposit => write!(f, "deposit"), + StakingEventOpType::Unbond => write!(f, "unbond"), + StakingEventOpType::Withdraw => write!(f, "withdraw"), + StakingEventOpType::NodeJoin => write!(f, "nodejoin"), + StakingEventOpType::Reward => write!(f, "reward"), + StakingEventOpType::Jail => write!(f, "jail"), + StakingEventOpType::Slash => write!(f, "slash"), + StakingEventOpType::Unjail => write!(f, "unjail"), + } + } +} + +impl From for KVPair { + fn from(op_type: StakingEventOpType) -> Self { + let mut kv_pair = KVPair::new(); + + kv_pair.key = TendermintEventKey::StakingOpType.into(); + kv_pair.value = op_type.to_string().into_bytes(); + + kv_pair + } +} + +struct StakingDiffField(Vec); + +impl From for KVPair { + fn from(builder: StakingDiffField) -> Self { + let mut kv_pair = KVPair::new(); + + kv_pair.key = TendermintEventKey::StakingDiff.into(); + kv_pair.value = builder.to_string().into_bytes(); + + kv_pair + } +} + +impl fmt::Display for StakingDiffField { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + serde_json::to_string(&self.0).expect("StakingDiffBuilder serialization error") + ) + } +} + +enum StakingDiff { + Bonded(StakingCoinChange, Coin), + Unbonded(StakingCoinChange, Coin), + NodeJoin(CouncilNode), + JailedUntil(Timespec), +} + +impl Serialize for StakingDiff { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + StakingDiff::Bonded(change, coin) => { + let mut state = serializer.serialize_struct("Bonded", 2)?; + state.serialize_field("key", "Bonded")?; + state.serialize_field( + "value", + format!("{}{}", change, u64::from(coin.to_owned())).as_str(), + )?; + state.end() + } + StakingDiff::Unbonded(change, coin) => { + let mut state = serializer.serialize_struct("Unbonded", 2)?; + state.serialize_field("key", "Unbonded")?; + state.serialize_field( + "value", + format!("{}{}", change, u64::from(coin.to_owned())).as_str(), + )?; + state.end() + } + StakingDiff::NodeJoin(node) => { + let mut state = serializer.serialize_struct("NodeJoin", 2)?; + state.serialize_field("key", "CouncilNode")?; + state.serialize_field("value", node)?; + state.end() + } + StakingDiff::JailedUntil(jailed_until) => { + let mut state = serializer.serialize_struct("JailedUntil", 2)?; + state.serialize_field("key", "JailedUntil")?; + state.serialize_field("value", &jailed_until.to_string())?; + state.end() + } + } + } +} + +impl fmt::Display for StakingDiff { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + serde_json::to_string(self).expect("StakingDiff serialization error") + ) + } +} + +enum StakingCoinChange { + Increase, + Decrease, +} + +impl fmt::Display for StakingCoinChange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StakingCoinChange::Increase => write!(f, ""), + StakingCoinChange::Decrease => write!(f, "-"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chain_core::state::account::ConfidentialInit; + use chain_core::state::tendermint::TendermintValidatorPubKey; + use std::str::FromStr; + + mod staking_diff_field { + use super::*; + + #[test] + fn to_string_should_serialize_to_json() { + let any_amount = Coin::unit(); + let any_staking_diff_1 = StakingDiff::Bonded(StakingCoinChange::Decrease, any_amount); + let any_staking_diff_2 = StakingDiff::Unbonded(StakingCoinChange::Increase, any_amount); + let field = StakingDiffField(vec![any_staking_diff_1, any_staking_diff_2]); + + assert_eq!( + field.to_string(), + "[{\"key\":\"Bonded\",\"value\":\"-1\"},{\"key\":\"Unbonded\",\"value\":\"1\"}]", + ); + } + } + + mod staking_diff { + use super::*; + + mod bonded { + use super::*; + + #[test] + fn to_string_should_serialize_to_json() { + let any_amount = Coin::unit(); + let staking_diff = StakingDiff::Bonded(StakingCoinChange::Increase, any_amount); + + assert_eq!( + staking_diff.to_string(), + "{\"key\":\"Bonded\",\"value\":\"1\"}", + ); + } + } + + mod unbonded { + use super::*; + + #[test] + fn to_string_should_serialize_to_json() { + let any_amount = Coin::unit(); + let staking_diff = StakingDiff::Unbonded(StakingCoinChange::Increase, any_amount); + + assert_eq!( + staking_diff.to_string(), + "{\"key\":\"Unbonded\",\"value\":\"1\"}", + ); + } + } + + mod node_join { + use super::*; + + #[test] + fn to_string_should_serialize_to_json() { + let any_council_node = any_council_node(); + let staking_diff = StakingDiff::NodeJoin(any_council_node); + + assert_eq!( + staking_diff.to_string(), + "{\"key\":\"CouncilNode\",\"value\":{\"name\":\"Council Node\",\"security_contact\":\"security@crypto.com\",\"consensus_pubkey\":{\"type\":\"tendermint/PubKeyEd25519\",\"value\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"},\"confidential_init\":{\"cert\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}}}", + ); + } + + fn any_council_node() -> CouncilNode { + let any_name = String::from("Council Node"); + let any_security_contact = Some(String::from("security@crypto.com")); + let any_pub_key = TendermintValidatorPubKey::Ed25519([0u8; 32]); + let any_cert = ConfidentialInit { + cert: [0u8; 32].to_vec(), + }; + + CouncilNode::new_with_details(any_name, any_security_contact, any_pub_key, any_cert) + } + } + + mod jailed_until { + use super::*; + + #[test] + fn to_string_should_serialize_to_json() { + let any_jailed_until: Timespec = 1587071014; + let staking_diff = StakingDiff::JailedUntil(any_jailed_until); + + assert_eq!( + staking_diff.to_string(), + "{\"key\":\"JailedUntil\",\"value\":\"1587071014\"}", + ); + } + } + } + + mod staking_event { + use super::*; + + mod deposit { + use super::*; + + #[test] + fn should_create_deposit_event() { + let any_staking_address = any_staking_address(); + let any_amount = Coin::unit(); + + let event: Event = StakingEvent::Deposit(&any_staking_address, any_amount).into(); + + assert_deposit_event(event, any_staking_address, any_amount); + } + } + + mod unbond { + use super::*; + + #[test] + fn should_create_unbond_event() { + let any_staking_address = any_staking_address(); + let any_amount = Coin::unit(); + + let event: Event = StakingEvent::Unbond(&any_staking_address, any_amount).into(); + + assert_unbonded_event(event, &any_staking_address, any_amount); + } + } + + mod withdraw { + use super::*; + + #[test] + fn should_create_withdraw_event() { + let any_staking_address = any_staking_address(); + let any_amount = Coin::unit(); + + let event: Event = StakingEvent::Withdraw(&any_staking_address, any_amount).into(); + + assert_withdraw_event(event, &any_staking_address, any_amount) + } + } + + mod node_join { + use super::*; + use chain_core::state::tendermint::TendermintValidatorPubKey; + + #[test] + fn should_create_node_join_event() { + let any_staking_address = any_staking_address(); + let any_council_node = any_council_node(); + + let event: Event = + StakingEvent::NodeJoin(&any_staking_address, any_council_node.clone()).into(); + + assert_node_join_event(event, &any_staking_address, any_council_node) + } + + fn any_council_node() -> CouncilNode { + let any_name = String::from("Council Node"); + let any_security_contact = Some(String::from("security@crypto.com")); + let any_pub_key = TendermintValidatorPubKey::Ed25519([0u8; 32]); + let any_cert = ConfidentialInit { + cert: [0u8; 32].to_vec(), + }; + + CouncilNode::new_with_details(any_name, any_security_contact, any_pub_key, any_cert) + } + } + + mod reward { + use super::*; + + #[test] + fn should_create_reward_event() { + let any_staking_address = any_staking_address(); + let any_amount = Coin::unit(); + + let event: Event = StakingEvent::Reward(&any_staking_address, any_amount).into(); + + assert_reward_event(event, any_staking_address, any_amount); + } + } + + mod jail { + use super::*; + + #[test] + fn should_create_jail_event() { + let any_staking_address = any_staking_address(); + let any_time: Timespec = 1587071014; + let any_jail_reason = PunishmentKind::ByzantineFault; + + let event: Event = + StakingEvent::Jail(&any_staking_address, any_time, any_jail_reason).into(); + + assert_jail_event(event, any_staking_address, any_time, any_jail_reason); + } + } + + mod slash { + use super::*; + + #[test] + fn should_create_slash_event() { + let any_staking_address = any_staking_address(); + let any_bonded_slash_amount = Coin::unit(); + let any_unbonded_slash_amount = Coin::unit(); + let any_jail_reason = PunishmentKind::ByzantineFault; + + let event: Event = StakingEvent::Slash( + &any_staking_address, + any_bonded_slash_amount, + any_unbonded_slash_amount, + any_jail_reason, + ) + .into(); + + assert_slash_event( + event, + any_staking_address, + any_bonded_slash_amount, + any_unbonded_slash_amount, + any_jail_reason, + ); + } + } + + mod unjail { + use super::*; + + #[test] + fn should_create_unjail_event() { + let any_staking_address = any_staking_address(); + + let event: Event = StakingEvent::Unjail(&any_staking_address).into(); + + assert_unjail_event(event, any_staking_address); + } + } + + fn assert_deposit_event( + event: Event, + staking_address: StakedStateAddress, + deposit_amount: Coin, + ) { + assert_eq!( + event.field_type, + TendermintEventType::StakingChange.to_string() + ); + assert_eq!(event.attributes.len(), 3); + + let staking_address_attribute = event.attributes.first().unwrap(); + assert_kv_pair( + staking_address_attribute, + TendermintEventKey::StakingAddress.to_string(), + staking_address.to_string(), + ); + + let staking_optype_attribute = event.attributes.get(1).unwrap(); + assert_kv_pair( + staking_optype_attribute, + TendermintEventKey::StakingOpType.to_string(), + StakingEventOpType::Deposit.to_string(), + ); + + let staking_diff_attribute = event.attributes.get(2).unwrap(); + let expected_value = format!( + "[{{\"key\":\"Bonded\",\"value\":\"{}\"}}]", + u64::from(deposit_amount) + ); + assert_kv_pair( + staking_diff_attribute, + TendermintEventKey::StakingDiff.to_string(), + expected_value, + ); + } + + fn assert_unbonded_event( + event: Event, + staking_address: &StakedStateAddress, + unbond_amount: Coin, + ) { + assert_eq!( + event.field_type, + TendermintEventType::StakingChange.to_string() + ); + assert_eq!(event.attributes.len(), 3); + + let staking_address_attribute = event.attributes.first().unwrap(); + assert_kv_pair( + staking_address_attribute, + TendermintEventKey::StakingAddress.to_string(), + staking_address.to_string(), + ); + + let staking_optype_attribute = event.attributes.get(1).unwrap(); + assert_kv_pair( + staking_optype_attribute, + TendermintEventKey::StakingOpType.to_string(), + StakingEventOpType::Unbond.to_string(), + ); + + let staking_diff_bonded_attribute = event.attributes.get(2).unwrap(); + let expected_value = format!( + "[{{\"key\":\"Bonded\",\"value\":\"-{}\"}},{{\"key\":\"Unbonded\",\"value\":\"{}\"}}]", + u64::from(unbond_amount), + u64::from(unbond_amount), + ); + assert_kv_pair( + staking_diff_bonded_attribute, + TendermintEventKey::StakingDiff.to_string(), + expected_value, + ); + } + + fn assert_withdraw_event( + event: Event, + staking_address: &StakedStateAddress, + withdraw_amount: Coin, + ) { + assert_eq!( + event.field_type, + TendermintEventType::StakingChange.to_string() + ); + assert_eq!(event.attributes.len(), 3); + + let staking_address_attribute = event.attributes.first().unwrap(); + assert_kv_pair( + staking_address_attribute, + TendermintEventKey::StakingAddress.to_string(), + staking_address.to_string(), + ); + + let staking_optype_attribute = event.attributes.get(1).unwrap(); + assert_kv_pair( + staking_optype_attribute, + TendermintEventKey::StakingOpType.to_string(), + StakingEventOpType::Withdraw.to_string(), + ); + + let staking_diff_attribute = event.attributes.get(2).unwrap(); + let expected_value = format!( + "[{{\"key\":\"Unbonded\",\"value\":\"-{}\"}}]", + u64::from(withdraw_amount) + ); + assert_kv_pair( + staking_diff_attribute, + TendermintEventKey::StakingDiff.to_string(), + expected_value, + ); + } + + fn assert_node_join_event( + event: Event, + staking_address: &StakedStateAddress, + council_node: CouncilNode, + ) { + assert_eq!( + event.field_type, + TendermintEventType::StakingChange.to_string() + ); + assert_eq!(event.attributes.len(), 3); + + let staking_address_attribute = event.attributes.first().unwrap(); + assert_kv_pair( + staking_address_attribute, + TendermintEventKey::StakingAddress.to_string(), + staking_address.to_string(), + ); + + let staking_optype_attribute = event.attributes.get(1).unwrap(); + assert_kv_pair( + staking_optype_attribute, + TendermintEventKey::StakingOpType.to_string(), + StakingEventOpType::NodeJoin.to_string(), + ); + + let staking_diff_attribute = event.attributes.get(2).unwrap(); + let expected_council_node = serde_json::to_string(&council_node) + .expect("Error when serializing council node info"); + let expected_value = format!( + "[{{\"key\":\"CouncilNode\",\"value\":{}}}]", + expected_council_node + ); + assert_kv_pair( + staking_diff_attribute, + TendermintEventKey::StakingDiff.to_string(), + expected_value, + ); + } + + fn assert_reward_event( + event: Event, + staking_address: StakedStateAddress, + deposit_amount: Coin, + ) { + assert_eq!( + event.field_type, + TendermintEventType::StakingChange.to_string() + ); + assert_eq!(event.attributes.len(), 3); + + let staking_address_attribute = event.attributes.first().unwrap(); + assert_kv_pair( + staking_address_attribute, + TendermintEventKey::StakingAddress.to_string(), + staking_address.to_string(), + ); + + let staking_optype_attribute = event.attributes.get(1).unwrap(); + assert_kv_pair( + staking_optype_attribute, + TendermintEventKey::StakingOpType.to_string(), + StakingEventOpType::Reward.to_string(), + ); + + let staking_diff_attribute = event.attributes.get(2).unwrap(); + let expected_value = format!( + "[{{\"key\":\"Bonded\",\"value\":\"{}\"}}]", + u64::from(deposit_amount) + ); + assert_kv_pair( + staking_diff_attribute, + TendermintEventKey::StakingDiff.to_string(), + expected_value, + ); + } + + fn assert_jail_event( + event: Event, + staking_address: StakedStateAddress, + timespec: Timespec, + punishment_kind: PunishmentKind, + ) { + assert_eq!( + event.field_type, + TendermintEventType::StakingChange.to_string() + ); + assert_eq!(event.attributes.len(), 4); + + let staking_address_attribute = event.attributes.first().unwrap(); + assert_kv_pair( + staking_address_attribute, + TendermintEventKey::StakingAddress.to_string(), + staking_address.to_string(), + ); + + let staking_optype_attribute = event.attributes.get(1).unwrap(); + assert_kv_pair( + staking_optype_attribute, + TendermintEventKey::StakingOpType.to_string(), + StakingEventOpType::Jail.to_string(), + ); + + let staking_diff_attribute = event.attributes.get(2).unwrap(); + let expected_timespec = timespec.to_string(); + let expected_value = format!( + "[{{\"key\":\"JailedUntil\",\"value\":\"{}\"}}]", + expected_timespec + ); + assert_kv_pair( + staking_diff_attribute, + TendermintEventKey::StakingDiff.to_string(), + expected_value, + ); + + let staking_opreason_attribute = event.attributes.get(3).unwrap(); + assert_kv_pair( + staking_opreason_attribute, + TendermintEventKey::StakingOpReason.to_string(), + punishment_reason(punishment_kind), + ); + } + + fn assert_slash_event( + event: Event, + staking_address: StakedStateAddress, + bonded_slash_amount: Coin, + unbonded_slash_amount: Coin, + punishment_kind: PunishmentKind, + ) { + assert_eq!( + event.field_type, + TendermintEventType::StakingChange.to_string() + ); + assert_eq!(event.attributes.len(), 4); + + let staking_address_attribute = event.attributes.first().unwrap(); + assert_kv_pair( + staking_address_attribute, + TendermintEventKey::StakingAddress.to_string(), + staking_address.to_string(), + ); + + let staking_optype_attribute = event.attributes.get(1).unwrap(); + assert_kv_pair( + staking_optype_attribute, + TendermintEventKey::StakingOpType.to_string(), + StakingEventOpType::Slash.to_string(), + ); + + let staking_diff_bonded_attribute = event.attributes.get(2).unwrap(); + let expected_value = format!( + "[{{\"key\":\"Bonded\",\"value\":\"-{}\"}},{{\"key\":\"Unbonded\",\"value\":\"-{}\"}}]", + u64::from(bonded_slash_amount), + u64::from(unbonded_slash_amount) + ); + assert_kv_pair( + staking_diff_bonded_attribute, + TendermintEventKey::StakingDiff.to_string(), + expected_value, + ); + + let staking_opreason_attribute = event.attributes.get(3).unwrap(); + assert_kv_pair( + staking_opreason_attribute, + TendermintEventKey::StakingOpReason.to_string(), + punishment_reason(punishment_kind), + ); + } + + fn assert_unjail_event(event: Event, staking_address: StakedStateAddress) { + assert_eq!( + event.field_type, + TendermintEventType::StakingChange.to_string() + ); + assert_eq!(event.attributes.len(), 2); + + let staking_address_attribute = event.attributes.first().unwrap(); + assert_kv_pair( + staking_address_attribute, + TendermintEventKey::StakingAddress.to_string(), + staking_address.to_string(), + ); + + let staking_optype_attribute = event.attributes.get(1).unwrap(); + assert_kv_pair( + staking_optype_attribute, + TendermintEventKey::StakingOpType.to_string(), + StakingEventOpType::Unjail.to_string(), + ); + } + + fn assert_kv_pair(kv_pair: &KVPair, expected_key: String, expected_value: String) { + assert_eq!(String::from_utf8_lossy(&kv_pair.key), expected_key); + assert_eq!(String::from_utf8_lossy(&kv_pair.value), expected_value); + } + } + + fn any_staking_address() -> StakedStateAddress { + StakedStateAddress::from_str("0x83fe11feb0887183eb62c30994bdd9e303497e3d").unwrap() + } +} diff --git a/chain-abci/src/app/validate_tx.rs b/chain-abci/src/app/validate_tx.rs index 7e33ee916..e654b88b5 100644 --- a/chain-abci/src/app/validate_tx.rs +++ b/chain-abci/src/app/validate_tx.rs @@ -1,11 +1,9 @@ use super::{BufferType, ChainNodeApp, ChainNodeState}; use crate::enclave_bridge::EnclaveProxy; -use crate::storage::{process_public_tx, verify_enclave_tx, TxEnclaveAction}; +use crate::storage::{process_public_tx, verify_enclave_tx, TxAction, TxEnclaveAction}; use crate::tx_error::TxError; use abci::*; -use chain_core::state::account::StakedStateAddress; use chain_core::tx::data::TxId; -use chain_core::tx::fee::Fee; use chain_core::tx::TxAux; use chain_storage::buffer::{StoreKV, StoreStaking}; use parity_scale_codec::Decode; @@ -65,7 +63,7 @@ impl ChainNodeApp { &mut self, req: &impl RequestWithTx, buffer_type: BufferType, - ) -> Result<(TxAux, Fee, Option), TxError> { + ) -> Result<(TxAux, TxAction), TxError> { let extra_info = self.tx_extra_info(req.tx().len()); let state = match buffer_type { BufferType::Consensus => self.last_state.as_mut().expect("expect last_state"), @@ -73,7 +71,7 @@ impl ChainNodeApp { }; let txaux = TxAux::decode(&mut req.tx())?; let txid = txaux.tx_id(); - let (fee, maccount) = match &txaux { + let tx_action = match &txaux { TxAux::EnclaveTx(tx) => { let action = verify_enclave_tx( &mut self.tx_validator, @@ -83,23 +81,28 @@ impl ChainNodeApp { &kv_store!(self, buffer_type), )?; // execute the action - let maccount = execute_enclave_tx( + execute_enclave_tx( &mut staking_store!(self, state.staking_version, buffer_type), &mut kv_store!(self, buffer_type), state, &txid, &action, ); - (action.fee(), maccount) + + TxAction::Enclave(action) + } + TxAux::PublicTx(tx) => { + let action = process_public_tx( + &mut staking_store!(self, state.staking_version, buffer_type), + &mut state.staking_table, + &extra_info, + &tx, + )?; + + TxAction::Public(action) } - TxAux::PublicTx(tx) => process_public_tx( - &mut staking_store!(self, state.staking_version, buffer_type), - &mut state.staking_table, - &extra_info, - &tx, - )?, }; - Ok((txaux, fee, maccount)) + Ok((txaux, tx_action)) } } @@ -109,7 +112,7 @@ fn execute_enclave_tx( state: &mut ChainNodeState, txid: &TxId, action: &TxEnclaveAction, -) -> Option { +) { match action { TxEnclaveAction::Transfer { spend_utxo, @@ -120,7 +123,6 @@ fn execute_enclave_tx( // Done in commit event // storage.create_utxo(no_of_outputs, txid); chain_storage::store_sealed_log(kvdb, &txid, sealed_log); - None } TxEnclaveAction::Deposit { spend_utxo, @@ -132,7 +134,6 @@ fn execute_enclave_tx( .staking_table .deposit(trie, address, *amount) .expect("deposit sanity check"); - Some(*address) } TxEnclaveAction::Withdraw { withdraw: (address, amount), @@ -149,7 +150,6 @@ fn execute_enclave_tx( .staking_table .withdraw(trie, state.block_time, address, *amount) .expect("withdraw sanity check"); - Some(*address) } } } diff --git a/chain-abci/src/enclave_bridge/real/mod.rs b/chain-abci/src/enclave_bridge/real/mod.rs index 95fc05fbe..25b722e28 100644 --- a/chain-abci/src/enclave_bridge/real/mod.rs +++ b/chain-abci/src/enclave_bridge/real/mod.rs @@ -33,7 +33,7 @@ impl Default for TxValidationApp { } Err(x) => { panic!( - "[-] Init TX Validation Sercer Enclave Failed {}!", + "[-] Init TX Validation Server Enclave Failed {}!", x.as_str() ); } diff --git a/chain-abci/src/staking/mod.rs b/chain-abci/src/staking/mod.rs index a6d8bae39..9499a6ad0 100644 --- a/chain-abci/src/staking/mod.rs +++ b/chain-abci/src/staking/mod.rs @@ -11,6 +11,7 @@ mod tests { }; use std::str::FromStr; + use chain_core::common::Timespec; use chain_core::init::address::RedeemAddress; use chain_core::init::coin::Coin; use chain_core::init::config::SlashRatio; @@ -25,6 +26,7 @@ mod tests { use test_common::chain_env::get_init_network_params; use super::*; + use crate::staking::table::{PunishmentOutcome, SlashedCoin}; use crate::tx_error::{ DepositError, NodeJoinError, PublicTxError, UnbondError, UnjailError, WithdrawError, }; @@ -177,20 +179,36 @@ mod tests { let slash_ratio: SlashRatio = "0.01".parse().unwrap(); init_params.slashing_config.liveness_slash_percent = slash_ratio; init_params.slashing_config.byzantine_slash_percent = slash_ratio; - init_params.unbonding_period = 10; + let unbonding_period = 10; + init_params.unbonding_period = unbonding_period; let params = NetworkParameters::Genesis(init_params); let (mut table, mut store) = init_staking_table(); let addr1 = staking_address(&[0xcc; 32]); let val_pk1 = validator_pubkey(&[0xcc; 32]); let evidence = (val_pk1.clone().into(), 1.into(), 0); - let slashes = table.begin_block(&mut store, ¶ms, 0, 1.into(), &[], &[evidence.clone()]); - let slash = ( - addr1, - Coin::new(11_0000_0000).unwrap() * slash_ratio, - PunishmentKind::ByzantineFault, + let block_time: u64 = 0; + let punishment_outcomes = table.begin_block( + &mut store, + ¶ms, + block_time, + 1.into(), + &[], + &[evidence.clone()], ); - assert_eq!(slashes, vec![slash]); + + let bonded_slashed = Coin::new(11_0000_0000).unwrap() * slash_ratio; + let unbonded_slashed = Coin::zero(); + let punishment_outcome = PunishmentOutcome { + staking_address: addr1, + slashed_coin: SlashedCoin { + bonded: bonded_slashed, + unbonded: unbonded_slashed, + }, + punishment_kind: PunishmentKind::ByzantineFault, + jailed_until: Some(block_time.saturating_add(unbonding_period as Timespec)), + }; + assert_eq!(punishment_outcomes, vec![punishment_outcome]); let staking = store.get(&addr1).unwrap(); assert!(staking.is_jailed()); assert_eq!( @@ -203,8 +221,9 @@ mod tests { assert_eq!(nonce, 0); // byzantine faults won't slashed again. - let slashes = table.begin_block(&mut store, ¶ms, 1, 2.into(), &[], &[evidence]); - assert_eq!(slashes, vec![]); + let punishment_outcomes = + table.begin_block(&mut store, ¶ms, 1, 2.into(), &[], &[evidence]); + assert_eq!(punishment_outcomes, vec![]); // transaction denied after jailed let unbond = UnbondTx { @@ -380,7 +399,7 @@ mod tests { let params = NetworkParameters::Genesis(init_params); for i in 1..=3 { - let slashes = table.begin_block( + let punishment_outcomes = table.begin_block( &mut store, ¶ms, 1 + i, @@ -388,10 +407,10 @@ mod tests { &[(val_pk1.clone().into(), false)], &[], ); - assert_eq!(slashes, vec![]); + assert_eq!(punishment_outcomes, vec![]); } // non-live fault - let slashes = table.begin_block( + let punishment_outcomes = table.begin_block( &mut store, ¶ms, 1, @@ -399,13 +418,20 @@ mod tests { &[(val_pk1.clone().into(), false)], &[], ); - assert_eq!(slashes[0].0, addr1); - assert_eq!(slashes[0].2, PunishmentKind::NonLive); + assert_eq!(punishment_outcomes[0].staking_address, addr1); + assert_eq!( + punishment_outcomes[0].punishment_kind, + PunishmentKind::NonLive + ); + assert_eq!( + punishment_outcomes[0].jailed_until, None, + "NonLive should not jail" + ); } /// Tests: - /// - liveness tracking not interuppted when temporarily not selected - /// - liveness tracking not interuppted when temporarily unbonded and re-joined again + /// - liveness tracking not interrupted when temporarily not selected + /// - liveness tracking not interrupted when temporarily unbonded and re-joined again #[test] fn check_liveness_tracking() { // check liveness tracking not interuppted by temporarily inactive. @@ -432,7 +458,7 @@ mod tests { // miss two blocks for i in 1..=2 { - let slashes = table.begin_block( + let punishment_outcomes = table.begin_block( &mut store, ¶ms, 1 + i, @@ -440,7 +466,7 @@ mod tests { &[(val_pk1.clone().into(), false)], &[], ); - assert_eq!(slashes, vec![]); + assert_eq!(punishment_outcomes, vec![]); } // validator1 not selected @@ -450,8 +476,9 @@ mod tests { ); for i in 3..=4 { - let slashes = table.begin_block(&mut store, ¶ms, 1 + i, i.into(), &[], &[]); - assert_eq!(slashes, vec![]); + let punishment_outcomes = + table.begin_block(&mut store, ¶ms, 1 + i, i.into(), &[], &[]); + assert_eq!(punishment_outcomes, vec![]); } // validator1 selected again @@ -461,7 +488,7 @@ mod tests { ); for i in 5..=6 { - let slashes = table.begin_block( + let punishment_outcomes = table.begin_block( &mut store, ¶ms, 1 + i, @@ -469,11 +496,11 @@ mod tests { &[(val_pk1.clone().into(), false)], &[], ); - assert_eq!(slashes, vec![]); + assert_eq!(punishment_outcomes, vec![]); } // non-live fault - let slashes = table.begin_block( + let punishment_outcomes = table.begin_block( &mut store, ¶ms, 8, @@ -481,19 +508,29 @@ mod tests { &[(val_pk1.clone().into(), false)], &[], ); - assert_eq!(slashes[0].0, addr1); - assert_eq!(slashes[0].2, PunishmentKind::NonLive); - let slashed = slashes[0].1; + assert_eq!(punishment_outcomes[0].staking_address, addr1); + assert_eq!( + punishment_outcomes[0].punishment_kind, + PunishmentKind::NonLive + ); assert_eq!( - slashed, + punishment_outcomes[0].jailed_until, None, + "NonLive should not jail" + ); + let bonded_slashed = punishment_outcomes[0].slashed_coin.bonded; + let unbonded_slashed = punishment_outcomes[0].slashed_coin.unbonded; + assert_eq!( + bonded_slashed, Coin::new(11_0000_0000).unwrap() * SlashRatio::from_str("0.1").unwrap() ); + assert_eq!(unbonded_slashed, Coin::zero(),); assert_eq!( table.end_block(&mut store, 3), vec![(val_pk1.clone(), Coin::zero().into())] ); // re-join + let slashed = (bonded_slashed + unbonded_slashed).unwrap(); table.deposit(&mut store, &addr1, slashed).unwrap(); table.node_join(&mut store, 8, &node_join_tx(0)).unwrap(); assert_eq!( @@ -503,7 +540,7 @@ mod tests { // miss two blocks for i in 8..=9 { - let slashes = table.begin_block( + let punishment_outcomes = table.begin_block( &mut store, ¶ms, 1 + i, @@ -511,7 +548,7 @@ mod tests { &[(val_pk1.clone().into(), false)], &[], ); - assert_eq!(slashes, vec![]); + assert_eq!(punishment_outcomes, vec![]); } let unbond = UnbondTx { @@ -528,8 +565,9 @@ mod tests { ); for i in 10..=11 { - let slashes = table.begin_block(&mut store, ¶ms, 1 + i, i.into(), &[], &[]); - assert_eq!(slashes, vec![]); + let punishment_outcomes = + table.begin_block(&mut store, ¶ms, 1 + i, i.into(), &[], &[]); + assert_eq!(punishment_outcomes, vec![]); } table @@ -542,7 +580,7 @@ mod tests { ); for i in 12..=13 { - let slashes = table.begin_block( + let punishment_outcomes = table.begin_block( &mut store, ¶ms, 1 + i, @@ -550,11 +588,11 @@ mod tests { &[(val_pk1.clone().into(), false)], &[], ); - assert_eq!(slashes, vec![]); + assert_eq!(punishment_outcomes, vec![]); } // non-live fault again - let slashes = table.begin_block( + let punishment_outcomes = table.begin_block( &mut store, ¶ms, 15, @@ -562,8 +600,15 @@ mod tests { &[(val_pk1.clone().into(), false)], &[], ); - assert_eq!(slashes[0].0, addr1); - assert_eq!(slashes[0].2, PunishmentKind::NonLive); + assert_eq!(punishment_outcomes[0].staking_address, addr1); + assert_eq!( + punishment_outcomes[0].punishment_kind, + PunishmentKind::NonLive + ); + assert_eq!( + punishment_outcomes[0].jailed_until, None, + "NonLive should not jail" + ); assert_eq!( table.end_block(&mut store, 3), @@ -577,48 +622,59 @@ mod tests { #[test] fn check_byzantine() { let (mut table, mut store) = init_staking_table(); + let bonded = Coin::new(11_0000_0000).unwrap(); let mut init_params = get_init_network_params(Coin::zero()); init_params.slashing_config.liveness_slash_percent = "0.1".parse().unwrap(); - init_params.slashing_config.byzantine_slash_percent = "0.1".parse().unwrap(); - init_params.unbonding_period = 10; + let slash_percent = "0.1"; + init_params.slashing_config.byzantine_slash_percent = slash_percent.parse().unwrap(); + let unbonding_period = 10; + init_params.unbonding_period = unbonding_period; let params = NetworkParameters::Genesis(init_params); let addr1 = staking_address(&[0xcc; 32]); let val_pk1 = validator_pubkey(&[0xcc; 32]); + let unbond_amount = Coin::new(11_0000_0000).unwrap(); let unbond = UnbondTx { from_staked_account: addr1, nonce: 0, - value: Coin::new(11_0000_0000).unwrap(), + value: unbond_amount, attributes: Default::default(), }; table.unbond(&mut store, 10, 1, 1.into(), &unbond).unwrap(); - assert_eq!( - store.get(&addr1).unwrap().unbonded, - Coin::new(11_0000_0000).unwrap() - ); + assert_eq!(store.get(&addr1).unwrap().unbonded, unbond_amount); + let bonded = (bonded - unbond_amount).unwrap(); assert_eq!( table.end_block(&mut store, 3), vec![(val_pk1.clone(), Coin::zero().into())] ); - let slashes = table.begin_block( + let block_time = 2; + let punishment_outcomes = table.begin_block( &mut store, ¶ms, - 2, + block_time, 2.into(), &[], &[(val_pk1.clone().into(), 1.into(), 1)], ); + let slash_ratio = SlashRatio::from_str(slash_percent).unwrap(); + let bonded_slashed = bonded * slash_ratio; + let unbonded_slashed = unbond_amount * slash_ratio; + let expected_jailed_until = block_time.saturating_add(unbonding_period as Timespec); assert_eq!( - slashes, - vec![( - addr1, - Coin::new(1_1000_0000).unwrap(), - PunishmentKind::ByzantineFault - )] + punishment_outcomes, + vec![PunishmentOutcome { + staking_address: addr1, + slashed_coin: SlashedCoin { + bonded: bonded_slashed, + unbonded: unbonded_slashed, + }, + punishment_kind: PunishmentKind::ByzantineFault, + jailed_until: Some(expected_jailed_until), + }] ); let staking = store.get(&addr1).unwrap(); assert_eq!(staking.unbonded, Coin::new(9_9000_0000).unwrap()); @@ -662,21 +718,32 @@ mod tests { ); let staking = store.get(&addr2).unwrap(); - let to_slashed = - (staking.bonded + staking.unbonded).unwrap() * SlashRatio::from_str("0.1").unwrap(); + let slash_ratio = SlashRatio::from_str("0.1").unwrap(); + let bonded_slashed = staking.bonded * slash_ratio; + let unbonded_slashed = staking.unbonded * slash_ratio; // byzantine evidence of old key - let slashes = table.begin_block( + let block_time = 3; + let punishment_outcomes = table.begin_block( &mut store, ¶ms, - 3, + block_time, 3.into(), &[], &[(val_pk2.clone().into(), 2.into(), 2)], ); + let expected_jailed_until = block_time + unbonding_period as Timespec; assert_eq!( - slashes, - vec![(addr2, to_slashed, PunishmentKind::ByzantineFault)] + punishment_outcomes, + vec![PunishmentOutcome { + staking_address: addr2, + slashed_coin: SlashedCoin { + bonded: bonded_slashed, + unbonded: unbonded_slashed, + }, + punishment_kind: PunishmentKind::ByzantineFault, + jailed_until: Some(expected_jailed_until), + }] ); let staking = store.get(&addr2).unwrap(); assert_eq!( diff --git a/chain-abci/src/staking/table.rs b/chain-abci/src/staking/table.rs index df17eb136..c88460a2e 100644 --- a/chain-abci/src/staking/table.rs +++ b/chain-abci/src/staking/table.rs @@ -9,7 +9,7 @@ use parity_scale_codec::{Decode, Encode}; use serde::{Deserialize, Serialize}; use chain_core::common::Timespec; -use chain_core::init::coin::{sum_coins, Coin, CoinError}; +use chain_core::init::coin::{sum_coins, Coin, CoinError, CoinResult}; use chain_core::init::config::SlashRatio; use chain_core::init::params::NetworkParameters; use chain_core::state::account::{ @@ -78,6 +78,26 @@ impl Into for &mut StakedState { } } +#[derive(Debug, PartialEq, Eq)] +pub struct PunishmentOutcome { + pub staking_address: StakedStateAddress, + pub slashed_coin: SlashedCoin, + pub punishment_kind: PunishmentKind, + pub jailed_until: Option, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct SlashedCoin { + pub bonded: Coin, + pub unbonded: Coin, +} + +impl SlashedCoin { + pub fn sum(&self) -> CoinResult { + self.bonded + self.unbonded + } +} + /// StakedState indexes, and other tracking data structures. /// The heap of records are stored outside. /// Primary key is `StakedStateAddress`, secondary index reference the primary key. @@ -170,7 +190,7 @@ impl StakingTable { block_height: BlockHeight, voters: &[(TendermintValidatorAddress, bool)], evidences: &[(TendermintValidatorAddress, BlockHeight, Timespec)], - ) -> Vec<(StakedStateAddress, Coin, PunishmentKind)> { + ) -> Vec { self.cleanup(heap, params.get_unbonding_period() as Timespec, block_time); self.punish(heap, params, block_time, block_height, voters, evidences) } @@ -369,7 +389,7 @@ impl StakingTable { block_height: BlockHeight, staking: &mut StakedState, ratio: SlashRatio, - ) -> Coin { + ) -> SlashedCoin { let bonded_slashed = staking.bonded * ratio; let unbonded_slashed = staking.unbonded * ratio; // no panic: SlashRatio invariant(<= 1.0) @@ -378,8 +398,10 @@ impl StakingTable { // no panic: SlashRatio invariant(<= 1.0) staking.unbonded = (staking.unbonded - unbonded_slashed).unwrap(); // no panic: Invariant: 4.1 + SlashRatio invariant - // slashed_amount <= bonded + unbonded <= max supply - (bonded_slashed + unbonded_slashed).unwrap() + SlashedCoin { + bonded: bonded_slashed, + unbonded: unbonded_slashed, + } } fn choose_validators( @@ -471,7 +493,7 @@ impl StakingTable { block_height: BlockHeight, voters: &[(TendermintValidatorAddress, bool)], evidences: &[(TendermintValidatorAddress, BlockHeight, Timespec)], - ) -> Vec<(StakedStateAddress, Coin, PunishmentKind)> { + ) -> Vec { let mut slashes = Vec::new(); // handle non-live @@ -506,7 +528,7 @@ impl StakingTable { // panic: Invariant 2.3 + 2.2 if val.is_active() { val.inactivate(block_time, block_height); - slashes.push((*addr, PunishmentKind::NonLive)); + slashes.push((*addr, PunishmentKind::NonLive, None)); } tracker.reset(); @@ -531,13 +553,14 @@ impl StakingTable { // panic: Invariant 2.2 let val = staking.validator.as_mut().unwrap(); if !val.is_jailed() { - val.jail( + let jailed_until = val.jail( block_time, block_height, params.get_unbonding_period() as Timespec, ); + let maybe_jailed_until = Some(jailed_until); self.participator_stats.remove(addr); - slashes.push((*addr, PunishmentKind::ByzantineFault)); + slashes.push((*addr, PunishmentKind::ByzantineFault, maybe_jailed_until)); set_staking(heap, staking, self.minimal_required_staking); } } @@ -550,9 +573,9 @@ impl StakingTable { // execute slashes let slashes = slashes .into_iter() - .map(|(addr, kind)| { + .map(|(addr, kind, maybe_jailed_until)| { let mut staking = heap.get(&addr).unwrap(); - let amount = self.slash( + let slashed_coin = self.slash( block_time, block_height, &mut staking, @@ -561,14 +584,25 @@ impl StakingTable { PunishmentKind::ByzantineFault => params.get_byzantine_slash_percent(), }, ); + + let total_slashed_amount = slashed_coin + .sum() + .expect("sum of bonded and unboned slash amount exceed maximum coin"); + // Update the last slash record for query staking.last_slash = Some(SlashRecord { kind, time: block_time, - amount, + amount: total_slashed_amount, }); set_staking(heap, staking, self.minimal_required_staking); - (addr, amount, kind) + + PunishmentOutcome { + staking_address: addr, + slashed_coin, + punishment_kind: kind, + jailed_until: maybe_jailed_until, + } }) .collect::>(); diff --git a/chain-abci/src/storage/mod.rs b/chain-abci/src/storage/mod.rs index 5d1b71862..6f30dbab9 100644 --- a/chain-abci/src/storage/mod.rs +++ b/chain-abci/src/storage/mod.rs @@ -3,7 +3,7 @@ use crate::staking::StakingTable; use crate::tx_error::PublicTxError; use chain_core::common::Timespec; use chain_core::init::coin::Coin; -use chain_core::state::account::{StakedStateAddress, StakedStateOpAttributes}; +use chain_core::state::account::{CouncilNode, StakedStateAddress, StakedStateOpAttributes}; use chain_core::tx::data::input::{TxoPointer, TxoSize}; use chain_core::tx::fee::Fee; use chain_core::tx::{TransactionId, TxEnclaveAux, TxObfuscated, TxPublicAux}; @@ -11,6 +11,27 @@ use chain_storage::buffer::{GetKV, GetStaking, StoreStaking}; use chain_tx_validation::{verify_unjailed, witness::verify_tx_recover_address, ChainInfo, Error}; use enclave_protocol::{IntraEnclaveRequest, IntraEnclaveResponseOk, SealedLog}; +pub enum TxAction { + Enclave(TxEnclaveAction), + Public(TxPublicAction), +} + +impl TxAction { + pub fn fee(&self) -> Fee { + match self { + Self::Enclave(action) => action.fee(), + Self::Public(action) => action.fee(), + } + } + + pub fn staking_address(&self) -> Option { + match self { + Self::Enclave(action) => action.staking_address(), + Self::Public(action) => action.staking_address(), + } + } +} + /// fee: Written into block result events /// spend_utxo: Modify UTxO storage /// create_utxo: Write into UTxO storage @@ -79,6 +100,51 @@ impl TxEnclaveAction { Self::Withdraw { fee, .. } => *fee, } } + + pub fn staking_address(&self) -> Option { + match self { + Self::Transfer { .. } => None, + Self::Deposit { deposit, .. } => Some(deposit.0), + Self::Withdraw { withdraw, .. } => Some(withdraw.0), + } + } +} + +pub enum TxPublicAction { + Unbond { + fee: Fee, + unbond: (StakedStateAddress, Coin), + }, + NodeJoin(StakedStateAddress, CouncilNode), + Unjail(StakedStateAddress), +} + +impl TxPublicAction { + fn unbond(fee: Fee, unbond: (StakedStateAddress, Coin)) -> Self { + Self::Unbond { fee, unbond } + } + fn node_join(staking_address: StakedStateAddress, council_node: CouncilNode) -> Self { + Self::NodeJoin(staking_address, council_node) + } + fn unjail(staking_address: StakedStateAddress) -> Self { + Self::Unjail(staking_address) + } + + pub fn fee(&self) -> Fee { + match self { + Self::Unbond { fee, .. } => *fee, + Self::NodeJoin(_, _) => Fee::new(Coin::zero()), + Self::Unjail(_) => Fee::new(Coin::zero()), + } + } + + pub fn staking_address(&self) -> Option { + match self { + Self::Unbond { unbond, .. } => Some(unbond.0), + Self::NodeJoin(staking_address, _) => Some(*staking_address), + Self::Unjail(staking_address) => Some(*staking_address), + } + } } fn check_spent_input_lookup( @@ -217,7 +283,7 @@ pub fn process_public_tx( staking_table: &mut StakingTable, chain_info: &ChainInfo, txaux: &TxPublicAux, -) -> Result<(Fee, Option), PublicTxError> { +) -> Result { check_staking_attributes(txaux.attributes(), chain_info.chain_hex_id)?; match txaux { // TODO: delay checking witness, as address is contained in Tx? @@ -233,7 +299,11 @@ pub fn process_public_tx( chain_info.block_height, &maintx, )?; - Ok((chain_info.min_fee_computed, Some(address))) + + Ok(TxPublicAction::unbond( + chain_info.min_fee_computed, + (address, maintx.value), + )) } // TODO: delay checking witness, as address is contained in Tx? TxPublicAux::UnjailTx(maintx, witness) => { @@ -243,7 +313,8 @@ pub fn process_public_tx( } staking_table.unjail(staking_store, chain_info.block_time, maintx)?; - Ok((Fee::new(Coin::zero()), Some(address))) + + Ok(TxPublicAction::unjail(address)) } // TODO: delay checking witness, as address is contained in Tx? TxPublicAux::NodeJoinTx(maintx, witness) => { @@ -252,7 +323,8 @@ pub fn process_public_tx( return Err(PublicTxError::StakingWitnessNotMatch); } staking_table.node_join(staking_store, chain_info.block_time, maintx)?; - Ok((Fee::new(Coin::zero()), Some(address))) + + Ok(TxPublicAction::node_join(address, maintx.node_meta.clone())) } } } diff --git a/chain-abci/tests/abci_app.rs b/chain-abci/tests/abci_app.rs index 6a4ce37d2..57a1baee9 100644 --- a/chain-abci/tests/abci_app.rs +++ b/chain-abci/tests/abci_app.rs @@ -558,24 +558,37 @@ fn deliver_valid_tx() -> ( } #[test] -fn deliver_tx_should_add_valid_tx() { +fn deliver_tx_should_add_tx_events() { let (app, tx, _, cresp) = deliver_valid_tx(); assert_eq!(0, cresp.code); assert_eq!(1, app.delivered_txs.len()); - assert_eq!(1, cresp.events.len()); - assert_eq!(3, cresp.events[0].attributes.len()); + assert_eq!(2, cresp.events.len()); + + let valid_tx_event = &cresp.events[0]; + assert_eq!(2, valid_tx_event.attributes.len()); // the unit test transaction just three outputs: 1 CRO + 1 carson / base unit + the rest assert_eq!( - &b"0.00000331".to_vec(), - &cresp.events[0].attributes[0].value + "0.00000331", + String::from_utf8(valid_tx_event.attributes[0].value.clone()).unwrap() ); assert_eq!( - &b"0x89aef553a06ab0c3173e79de1ce241a9ed3b992c".to_vec(), - &cresp.events[0].attributes[1].value + &hex::encode(&tx.id()).as_bytes().to_vec(), + &valid_tx_event.attributes[1].value ); + + let staking_event = &cresp.events[1]; + assert_eq!(2, valid_tx_event.attributes.len()); assert_eq!( - &hex::encode(&tx.id()).as_bytes().to_vec(), - &cresp.events[0].attributes[2].value + "0x89aef553a06ab0c3173e79de1ce241a9ed3b992c", + String::from_utf8(staking_event.attributes[0].value.clone()).unwrap() + ); + assert_eq!( + "withdraw", + String::from_utf8(staking_event.attributes[1].value.clone()).unwrap() + ); + assert_eq!( + "[{\"key\":\"Unbonded\",\"value\":\"-9999999999999999999\"}]", + String::from_utf8(staking_event.attributes[2].value.clone()).unwrap() ); } diff --git a/chain-abci/tests/tx_validation.rs b/chain-abci/tests/tx_validation.rs index 8778d639d..cee660a8b 100644 --- a/chain-abci/tests/tx_validation.rs +++ b/chain-abci/tests/tx_validation.rs @@ -82,7 +82,10 @@ fn verify_public_tx( let mut buffer = HashMap::new(); let mut store = StakingBufferStore::new(StakingGetter::new(storage, version), &mut buffer); - let (fee, maddress) = process_public_tx(&mut store, &mut tbl, extra_info, txaux)?; + let tx_action = process_public_tx(&mut store, &mut tbl, extra_info, txaux)?; + + let fee = tx_action.fee(); + let maddress = tx_action.staking_address(); Ok((fee, maddress.map(|addr| store.get(&addr).unwrap()))) } diff --git a/chain-core/src/common/mod.rs b/chain-core/src/common/mod.rs index 3072e6164..dcedfd2a3 100644 --- a/chain-core/src/common/mod.rs +++ b/chain-core/src/common/mod.rs @@ -41,12 +41,10 @@ pub enum TendermintEventType { ValidTransactions, /// filter for view pub keys BlockFilter, - /// validators that were jailed - JailValidators, - /// validators that were slashed -- TODO: it'll be the same? needed? - SlashValidators, + /// staking account related changes + StakingChange, /// when reward was distributed - RewardsDistribution, + Reward, } impl fmt::Display for TendermintEventType { @@ -54,9 +52,8 @@ impl fmt::Display for TendermintEventType { match self { TendermintEventType::ValidTransactions => write!(f, "valid_txs"), TendermintEventType::BlockFilter => write!(f, "block_filter"), - TendermintEventType::JailValidators => write!(f, "jail_validators"), - TendermintEventType::SlashValidators => write!(f, "slash_validators"), - TendermintEventType::RewardsDistribution => write!(f, "rewards_distribution"), + TendermintEventType::StakingChange => write!(f, "staking_change"), + TendermintEventType::Reward => write!(f, "reward"), } } } @@ -64,16 +61,20 @@ impl fmt::Display for TendermintEventType { #[derive(Debug, Clone, Copy)] /// Attribute key of tendermint events pub enum TendermintEventKey { - /// affected state - Account, /// paid fee Fee, /// transaction identifier (in valid transactions) TxId, /// bloom filter of view keys EthBloom, - /// when reward was distributed - RewardsDistribution, + /// affected staking address + StakingAddress, + /// staking operation type + StakingOpType, + /// staking state difference + StakingDiff, + /// staking operation reason + StakingOpReason, /// new coins minted from rewards pool CoinMinted, /// when state was slashed @@ -101,11 +102,13 @@ impl PartialEq> for TendermintEventKey { impl fmt::Display for TendermintEventKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - TendermintEventKey::Account => write!(f, "account"), TendermintEventKey::Fee => write!(f, "fee"), TendermintEventKey::TxId => write!(f, "txid"), TendermintEventKey::EthBloom => write!(f, "ethbloom"), - TendermintEventKey::RewardsDistribution => write!(f, "dist"), + TendermintEventKey::StakingAddress => write!(f, "staking_address"), + TendermintEventKey::StakingOpType => write!(f, "staking_optype"), + TendermintEventKey::StakingDiff => write!(f, "staking_diff"), + TendermintEventKey::StakingOpReason => write!(f, "staking_opreason"), TendermintEventKey::CoinMinted => write!(f, "minted"), TendermintEventKey::Slash => write!(f, "slash"), } @@ -123,11 +126,13 @@ impl TendermintEventKey { #[inline] pub fn to_base64_string(self) -> String { match self { - TendermintEventKey::Account => String::from("YWNjb3VudA=="), TendermintEventKey::Fee => String::from("ZmVl"), TendermintEventKey::TxId => String::from("dHhpZA=="), TendermintEventKey::EthBloom => String::from("ZXRoYmxvb20="), - TendermintEventKey::RewardsDistribution => String::from("ZGlzdA=="), + TendermintEventKey::StakingAddress => String::from("c3Rha2luZ19hZGRyZXNz"), + TendermintEventKey::StakingOpType => String::from("c3Rha2luZ19vcHR5cGU="), + TendermintEventKey::StakingDiff => String::from("c3Rha2luZ19kaWZm"), + TendermintEventKey::StakingOpReason => String::from("c3Rha2luZ19vcHJlYXNvbg=="), TendermintEventKey::CoinMinted => String::from("bWludGVk"), TendermintEventKey::Slash => String::from("c2xhc2g="), } diff --git a/chain-core/src/init/coin.rs b/chain-core/src/init/coin.rs index c6a4eb657..f1b167563 100644 --- a/chain-core/src/init/coin.rs +++ b/chain-core/src/init/coin.rs @@ -105,7 +105,8 @@ impl fmt::Display for CoinError { impl ::std::error::Error for CoinError {} -type CoinResult = Result; +/// result type relating to `Coin` operations +pub type CoinResult = Result; impl Coin { /// create a coin of value `0`. diff --git a/chain-core/src/state/account.rs b/chain-core/src/state/account.rs index 70b3b1177..3311d4e89 100644 --- a/chain-core/src/state/account.rs +++ b/chain-core/src/state/account.rs @@ -278,12 +278,16 @@ impl Validator { block_time: Timespec, block_height: BlockHeight, jail_duration: Timespec, - ) { + ) -> Timespec { assert!(!self.is_jailed()); - self.jailed_until = Some(block_time.saturating_add(jail_duration)); + let jailed_until = block_time.saturating_add(jail_duration); + + self.jailed_until = Some(jailed_until); if self.is_active() { self.inactivate(block_time, block_height); } + + jailed_until } /// updates this state to be "inactive" diff --git a/client-common/src/tendermint/types/block_results.rs b/client-common/src/tendermint/types/block_results.rs index f6bc19088..59cb316c9 100644 --- a/client-common/src/tendermint/types/block_results.rs +++ b/client-common/src/tendermint/types/block_results.rs @@ -4,7 +4,6 @@ use std::convert::TryFrom; use std::str::{from_utf8, FromStr}; use chain_core::common::{TendermintEventKey, TendermintEventType}; -use chain_core::init::address::RedeemAddress; use chain_core::init::{coin::Coin, MAX_COIN_DECIMALS}; use chain_core::state::account::StakedStateAddress; use chain_core::tx::data::TxId; @@ -53,16 +52,17 @@ impl BlockResults for BlockResultsResponse { fn contains_account(&self, target_account: &StakedStateAddress) -> Result { match &self.txs_results { None => Ok(false), - Some(deliver_txs) => { - for deliver_txs in deliver_txs.iter() { - for event in deliver_txs.events.iter() { - if event.type_str == TendermintEventType::ValidTransactions.to_string() { - match find_account_from_event_attributes(&event.attributes)? { - None => continue, - Some(address) => { - if address == *target_account { - return Ok(true); - } + Some(deliver_tx) => { + for deliver_tx in deliver_tx.iter() { + for event in deliver_tx.events.iter() { + if event.type_str != TendermintEventType::StakingChange.to_string() { + continue; + } + match find_staking_address_from_event_attributes(&event.attributes)? { + None => continue, + Some(address) => { + if address == *target_account { + return Ok(true); } } } @@ -179,34 +179,34 @@ fn find_tx_id_from_event_attributes(attributes: &[Attribute]) -> Result Result> { - let maybe_attribute = find_event_attribute_by_key(attributes, TendermintEventKey::Account)?; + let maybe_attribute = + find_event_attribute_by_key(attributes, TendermintEventKey::StakingAddress)?; match maybe_attribute { None => Ok(None), Some(attribute) => { - let account = base64::decode(&attribute.value.as_ref()).chain(|| { + let staking_address = base64::decode(&attribute.value.as_ref()).chain(|| { ( ErrorKind::DeserializationError, "Unable to decode base64 bytes of account in block results", ) })?; - let address = String::from_utf8(account).chain(|| { + let staking_address = String::from_utf8(staking_address).chain(|| { ( ErrorKind::DeserializationError, "Unable to decode string of account in block results", ) })?; - let redeem_address = RedeemAddress::from_str(&address).chain(|| { + let staking_address = StakedStateAddress::from_str(&staking_address).chain(|| { ( ErrorKind::DeserializationError, "Unable to decode account address in block results", ) })?; - let address = StakedStateAddress::from(redeem_address); - Ok(Some(address)) + Ok(Some(staking_address)) } } } @@ -214,28 +214,15 @@ fn find_account_from_event_attributes( #[cfg(test)] mod tests { use super::*; + use chain_core::init::address::RedeemAddress; use tendermint::abci::tag::{Key, Value}; - mod find_account_from_event_attributes { + mod block_results_contains_account { use super::*; #[test] - fn should_return_err_when_event_value_is_invalid_base64_encoded() { - let response_str = r#"{"height": "37", "txs_results": [{"code": 0, "data": null, "log": "", "info": "", "gasWanted": "0", "gasUsed": "0", "events": [{"type": "valid_txs", "attributes": [{"key": "ZmVl", "value": "MC4wMDAwMDMwNw=="}, {"key": "YWNjb3VudA==", "value": "invalidbase64string"}, {"key": "dHhpZA==", "value": "ZjFmNzNkNmFjZWMyMTExOGRkMWUzNmY2ODRhYWUyMmM2Y2IxN2ZjNTFhZGEzNGEzNDIzMDlkNTMxY2I5YmU4ZA=="}]}], "codespace": ""}], "begin_block_events": null, "end_block_events": [{"type": "block_filter", "attributes": [{"key": "ZXRoYmxvb20=", "value": "AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="}]}], "validator_updates": null, "consensus_param_updates": null}"#; - - let block_results: BlockResultsResponse = - serde_json::from_str(response_str).expect("invalid response str"); - let target_account = StakedStateAddress::from( - RedeemAddress::from_str("0x0e7c045110b8dbf29765047380898919c5cb56f4").unwrap(), - ); - let result = block_results.contains_account(&target_account); - assert!(result.is_err()); - assert_eq!(ErrorKind::DeserializationError, result.unwrap_err().kind()); - } - - #[test] - fn should_return_err_when_account_value_is_invalid_utf8_string() { - let response_str = r#"{"height": "37", "txs_results": [{"code": 0, "data": null, "log": "", "info": "", "gasWanted": "0", "gasUsed": "0", "events": [{"type": "valid_txs", "attributes": [{"key": "ZmVl", "value": "MC4wMDAwMDMwNw=="}, {"key": "YWNjb3VudA==", "value": "AJ+Slg=="}, {"key": "dHhpZA==", "value": "ZjFmNzNkNmFjZWMyMTExOGRkMWUzNmY2ODRhYWUyMmM2Y2IxN2ZjNTFhZGEzNGEzNDIzMDlkNTMxY2I5YmU4ZA=="}]}], "codespace": ""}], "begin_block_events": null, "end_block_events": [{"type": "block_filter", "attributes": [{"key": "ZXRoYmxvb20=", "value": "AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="}]}], "validator_updates": null, "consensus_param_updates": null}"#; + fn should_return_err_when_staking_address_value_is_invalid_utf8_string() { + let response_str = r#"{"height": "37", "txs_results": [{"code": 0, "data": null, "log": "", "info": "", "gasWanted": "0", "gasUsed": "0", "events": [{"type": "staking_change", "attributes": [{"key": "c3Rha2luZ19hZGRyZXNz", "value": "AJ+Slg=="}]}], "codespace": ""}], "begin_block_events": null, "end_block_events": [{"type": "block_filter", "attributes": [{"key": "ZXRoYmxvb20=", "value": "AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="}]}], "validator_updates": null, "consensus_param_updates": null}"#; let block_results: BlockResultsResponse = serde_json::from_str(&response_str).expect("invalid response str"); let target_account = StakedStateAddress::from( @@ -247,8 +234,8 @@ mod tests { } #[test] - fn should_return_err_when_account_address_is_invalid() { - let response_str = r#"{"height": "37", "txs_results": [{"code": 0, "data": null, "log": "", "info": "", "gasWanted": "0", "gasUsed": "0", "events": [{"type": "valid_txs", "attributes": [{"key": "ZmVl", "value": "MC4wMDAwMDMwNw=="}, {"key": "YWNjb3VudA==", "value": "invalidbase64string"}, {"key": "dHhpZA==", "value": "ZjFmNzNkNmFjZWMyMTExOGRkMWUzNmY2ODRhYWUyMmM2Y2IxN2ZjNTFhZGEzNGEzNDIzMDlkNTMxY2I5YmU4ZA=="}]}], "codespace": ""}], "begin_block_events": null, "end_block_events": [{"type": "block_filter", "attributes": [{"key": "ZXRoYmxvb20=", "value": "AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="}]}], "validator_updates": null, "consensus_param_updates": null}"#; + fn should_return_err_when_staking_address_is_invalid() { + let response_str = r#"{"height": "37", "txs_results": [{"code": 0, "data": null, "log": "", "info": "", "gasWanted": "0", "gasUsed": "0", "events": [{"type": "staking_change", "attributes": [{"key": "c3Rha2luZ19hZGRyZXNz", "value": "invalidbase64string"}]}], "codespace": ""}], "begin_block_events": null, "end_block_events": [{"type": "block_filter", "attributes": [{"key": "ZXRoYmxvb20=", "value": "AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="}]}], "validator_updates": null, "consensus_param_updates": null}"#; let block_results: BlockResultsResponse = serde_json::from_str(response_str).unwrap(); let target_account = StakedStateAddress::from( RedeemAddress::from_str("0x0e7c045110b8dbf29765047380898919c5cb56f4").unwrap(), @@ -259,7 +246,7 @@ mod tests { } #[test] - fn should_return_ok_of_none_when_block_results_has_no_account_event() { + fn should_return_ok_of_none_when_block_results_has_no_staking_change_event() { let response_str = r#"{"height": "3", "txs_results": null, "begin_block_events": null, "end_block_events": null, "validator_updates": null, "consensus_param_updates": null}"#; let block_results: BlockResultsResponse = serde_json::from_str(response_str).expect("invalid response str"); @@ -273,7 +260,7 @@ mod tests { #[test] fn should_return_ok_of_true_when_block_results_has_the_target_account_event() { - let response_str = r#"{"height": "37", "txs_results": [{"code": 0, "data": null, "log": "", "info": "", "gasWanted": "0", "gasUsed": "0", "events": [{"type": "valid_txs", "attributes": [{"key": "ZmVl", "value": "MC4wMDAwMDMwNw=="}, {"key": "YWNjb3VudA==", "value": "MHgzMzUwMmVkMzlkMGM0ZTIwNDRmYjM3ZmRjZDUxNjE0OTNmNTkwMGMz"}, {"key": "dHhpZA==", "value": "ZjFmNzNkNmFjZWMyMTExOGRkMWUzNmY2ODRhYWUyMmM2Y2IxN2ZjNTFhZGEzNGEzNDIzMDlkNTMxY2I5YmU4ZA=="}]}], "codespace": ""}], "begin_block_events": null, "end_block_events": [{"type": "block_filter", "attributes": [{"key": "ZXRoYmxvb20=", "value": "AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="}]}], "validator_updates": null, "consensus_param_updates": null}"#; + let response_str = r#"{"height": "37", "txs_results": [{"code": 0, "data": null, "log": "", "info": "", "gasWanted": "0", "gasUsed": "0", "events": [{"type": "staking_change", "attributes": [{"key": "c3Rha2luZ19hZGRyZXNz", "value": "MHgzMzUwMmVkMzlkMGM0ZTIwNDRmYjM3ZmRjZDUxNjE0OTNmNTkwMGMz"}]}], "codespace": ""}], "begin_block_events": null, "end_block_events": [{"type": "block_filter", "attributes": [{"key": "ZXRoYmxvb20=", "value": "AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="}]}], "validator_updates": null, "consensus_param_updates": null}"#; let block_results: BlockResultsResponse = serde_json::from_str(response_str).expect("invalid response str"); let target_account = StakedStateAddress::from( @@ -327,7 +314,8 @@ mod tests { }; let attributes = vec![account_attribute.clone()]; - let result = find_event_attribute_by_key(&attributes, TendermintEventKey::Account); + let result = + find_event_attribute_by_key(&attributes, TendermintEventKey::StakingAddress); assert!(result.is_err()); assert_eq!(ErrorKind::DeserializationError, result.unwrap_err().kind()); } @@ -342,7 +330,7 @@ mod tests { let attributes = vec![account_attribute.clone()]; assert!( - find_event_attribute_by_key(&attributes, TendermintEventKey::Account) + find_event_attribute_by_key(&attributes, TendermintEventKey::StakingAddress) .unwrap() .is_none() ); @@ -351,13 +339,13 @@ mod tests { #[test] fn should_return_result_of_the_attribute_when_key_exist() { let account_attribute = Attribute { - key: Key::from_str(&TendermintEventKey::Account.to_base64_string()).unwrap(), + key: Key::from_str(&TendermintEventKey::StakingAddress.to_base64_string()).unwrap(), value: Value::from_str("MHhlNGEyYTcxOWNhOTMzZDNmNzlhODUwNmFhOTZjZWZkZTM0MDViMGE3") .unwrap(), }; let attributes = vec![account_attribute.clone()]; let attribute_finded = - find_event_attribute_by_key(&attributes, TendermintEventKey::Account) + find_event_attribute_by_key(&attributes, TendermintEventKey::StakingAddress) .unwrap() .unwrap() .to_owned(); From 8d0cd086c76029b6b256878c3cdcf9f157a3b07f Mon Sep 17 00:00:00 2001 From: Calvin Lau Date: Wed, 22 Apr 2020 10:39:45 +0800 Subject: [PATCH 2/2] Problem: (Fix #1459) Missing bonded from in staking change Solution: Add bonded_from back to staking change --- chain-abci/src/app/mod.rs | 4 +- chain-abci/src/app/staking_event.rs | 57 +++++++++++++++++++++++------ chain-abci/src/staking/tx.rs | 8 ++-- chain-abci/src/storage/mod.rs | 8 ++-- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/chain-abci/src/app/mod.rs b/chain-abci/src/app/mod.rs index 77421a9f9..4f31ae42b 100644 --- a/chain-abci/src/app/mod.rs +++ b/chain-abci/src/app/mod.rs @@ -363,8 +363,8 @@ fn generate_tx_staking_change_event(tx_action: TxAction) -> Option } }, TxAction::Public(tx_public_action) => match tx_public_action { - TxPublicAction::Unbond { unbond, .. } => { - Some(StakingEvent::Unbond(&unbond.0, unbond.1).into()) + TxPublicAction::Unbond { unbond, unbonded_from, .. } => { + Some(StakingEvent::Unbond(&unbond.0, unbond.1, unbonded_from).into()) } TxPublicAction::NodeJoin(staking_address, council_node) => { Some(StakingEvent::NodeJoin(&staking_address, council_node).into()) diff --git a/chain-abci/src/app/staking_event.rs b/chain-abci/src/app/staking_event.rs index 69f313486..c60e8e4ed 100644 --- a/chain-abci/src/app/staking_event.rs +++ b/chain-abci/src/app/staking_event.rs @@ -10,7 +10,7 @@ use chain_core::state::account::{CouncilNode, PunishmentKind, StakedStateAddress pub(crate) enum StakingEvent<'a> { Deposit(&'a StakedStateAddress, Coin), - Unbond(&'a StakedStateAddress, Coin), + Unbond(&'a StakedStateAddress, Coin, Timespec), Withdraw(&'a StakedStateAddress, Coin), NodeJoin(&'a StakedStateAddress, CouncilNode), Reward(&'a StakedStateAddress, Coin), @@ -27,8 +27,8 @@ impl<'a> From> for Event { StakingEvent::Deposit(staking_address, deposit_amount) => { builder.deposit(staking_address, deposit_amount) } - StakingEvent::Unbond(staking_address, unbond_amount) => { - builder.unbond(staking_address, unbond_amount) + StakingEvent::Unbond(staking_address, unbond_amount, unbonded_from) => { + builder.unbond(staking_address, unbond_amount, unbonded_from) } StakingEvent::Withdraw(staking_address, withdraw_amount) => { builder.withdraw(staking_address, withdraw_amount) @@ -80,7 +80,12 @@ impl StakingEventBuilder { ); } - fn unbond(&mut self, staking_address: &StakedStateAddress, unbond_amount: Coin) { + fn unbond( + &mut self, + staking_address: &StakedStateAddress, + unbond_amount: Coin, + unbonded_from: Timespec, + ) { self.attributes .push(staking_address_attribute(staking_address)); self.attributes.push(StakingEventOpType::Unbond.into()); @@ -89,6 +94,7 @@ impl StakingEventBuilder { StakingDiffField(vec![ StakingDiff::Bonded(StakingCoinChange::Decrease, unbond_amount), StakingDiff::Unbonded(StakingCoinChange::Increase, unbond_amount), + StakingDiff::UnbondedFrom(unbonded_from), ]) .into(), ); @@ -268,6 +274,7 @@ impl fmt::Display for StakingDiffField { enum StakingDiff { Bonded(StakingCoinChange, Coin), Unbonded(StakingCoinChange, Coin), + UnbondedFrom(Timespec), NodeJoin(CouncilNode), JailedUntil(Timespec), } @@ -296,6 +303,12 @@ impl Serialize for StakingDiff { )?; state.end() } + StakingDiff::UnbondedFrom(unbonded_from) => { + let mut state = serializer.serialize_struct("UnbondedFrom", 2)?; + state.serialize_field("key", "UnbondedFrom")?; + state.serialize_field("value", &unbonded_from)?; + state.end() + } StakingDiff::NodeJoin(node) => { let mut state = serializer.serialize_struct("NodeJoin", 2)?; state.serialize_field("key", "CouncilNode")?; @@ -305,7 +318,7 @@ impl Serialize for StakingDiff { StakingDiff::JailedUntil(jailed_until) => { let mut state = serializer.serialize_struct("JailedUntil", 2)?; state.serialize_field("key", "JailedUntil")?; - state.serialize_field("value", &jailed_until.to_string())?; + state.serialize_field("value", &jailed_until)?; state.end() } } @@ -393,6 +406,21 @@ mod tests { } } + mod unbonded_from { + use super::*; + + #[test] + fn to_string_should_serialize_to_json() { + let any_unbonded_from: Timespec = 1587071014; + let staking_diff = StakingDiff::UnbondedFrom(any_unbonded_from); + + assert_eq!( + staking_diff.to_string(), + "{\"key\":\"UnbondedFrom\",\"value\":1587071014}", + ); + } + } + mod node_join { use super::*; @@ -429,7 +457,7 @@ mod tests { assert_eq!( staking_diff.to_string(), - "{\"key\":\"JailedUntil\",\"value\":\"1587071014\"}", + "{\"key\":\"JailedUntil\",\"value\":1587071014}", ); } } @@ -459,10 +487,13 @@ mod tests { fn should_create_unbond_event() { let any_staking_address = any_staking_address(); let any_amount = Coin::unit(); + let any_unbonded_from: Timespec = 1587071014; - let event: Event = StakingEvent::Unbond(&any_staking_address, any_amount).into(); + let event: Event = + StakingEvent::Unbond(&any_staking_address, any_amount, any_unbonded_from) + .into(); - assert_unbonded_event(event, &any_staking_address, any_amount); + assert_unbonded_event(event, &any_staking_address, any_amount, any_unbonded_from); } } @@ -619,6 +650,7 @@ mod tests { event: Event, staking_address: &StakedStateAddress, unbond_amount: Coin, + unbonded_from: Timespec, ) { assert_eq!( event.field_type, @@ -640,14 +672,15 @@ mod tests { StakingEventOpType::Unbond.to_string(), ); - let staking_diff_bonded_attribute = event.attributes.get(2).unwrap(); + let staking_diff_attribute = event.attributes.get(2).unwrap(); let expected_value = format!( - "[{{\"key\":\"Bonded\",\"value\":\"-{}\"}},{{\"key\":\"Unbonded\",\"value\":\"{}\"}}]", + "[{{\"key\":\"Bonded\",\"value\":\"-{}\"}},{{\"key\":\"Unbonded\",\"value\":\"{}\"}},{{\"key\":\"UnbondedFrom\",\"value\":{}}}]", u64::from(unbond_amount), u64::from(unbond_amount), + unbonded_from, ); assert_kv_pair( - staking_diff_bonded_attribute, + staking_diff_attribute, TendermintEventKey::StakingDiff.to_string(), expected_value, ); @@ -795,7 +828,7 @@ mod tests { let staking_diff_attribute = event.attributes.get(2).unwrap(); let expected_timespec = timespec.to_string(); let expected_value = format!( - "[{{\"key\":\"JailedUntil\",\"value\":\"{}\"}}]", + "[{{\"key\":\"JailedUntil\",\"value\":{}}}]", expected_timespec ); assert_kv_pair( diff --git a/chain-abci/src/staking/tx.rs b/chain-abci/src/staking/tx.rs index 82461de5d..ba44082fb 100644 --- a/chain-abci/src/staking/tx.rs +++ b/chain-abci/src/staking/tx.rs @@ -145,7 +145,7 @@ impl StakingTable { block_time: Timespec, block_height: BlockHeight, tx: &UnbondTx, - ) -> Result<(), PublicTxError> { + ) -> Result { let mut staking = self.get_or_default(heap, &tx.from_staked_account); if tx.nonce != staking.nonce { return Err(PublicTxError::IncorrectNonce); @@ -160,12 +160,14 @@ impl StakingTable { self.sub_bonded(block_time, block_height, tx.value, &mut staking) .map_err(UnbondError::CoinError)?; staking.unbonded = unbonded; - staking.unbonded_from = block_time.saturating_add(unbonding_period); + + let unbonded_from = block_time.saturating_add(unbonding_period); + staking.unbonded_from = unbonded_from; staking.inc_nonce(); set_staking(heap, staking, self.minimal_required_staking); #[cfg(debug_assertions)] self.check_invariants(heap); - Ok(()) + Ok(unbonded_from) } /// Handle withdraw tx diff --git a/chain-abci/src/storage/mod.rs b/chain-abci/src/storage/mod.rs index 6f30dbab9..0a4e534d2 100644 --- a/chain-abci/src/storage/mod.rs +++ b/chain-abci/src/storage/mod.rs @@ -114,14 +114,15 @@ pub enum TxPublicAction { Unbond { fee: Fee, unbond: (StakedStateAddress, Coin), + unbonded_from: Timespec, }, NodeJoin(StakedStateAddress, CouncilNode), Unjail(StakedStateAddress), } impl TxPublicAction { - fn unbond(fee: Fee, unbond: (StakedStateAddress, Coin)) -> Self { - Self::Unbond { fee, unbond } + fn unbond(fee: Fee, unbond: (StakedStateAddress, Coin), unbonded_from: Timespec) -> Self { + Self::Unbond { fee, unbond, unbonded_from } } fn node_join(staking_address: StakedStateAddress, council_node: CouncilNode) -> Self { Self::NodeJoin(staking_address, council_node) @@ -292,7 +293,7 @@ pub fn process_public_tx( if address != maintx.from_staked_account { return Err(PublicTxError::StakingWitnessNotMatch); } - staking_table.unbond( + let unbonded_from = staking_table.unbond( staking_store, chain_info.unbonding_period as Timespec, chain_info.block_time, @@ -303,6 +304,7 @@ pub fn process_public_tx( Ok(TxPublicAction::unbond( chain_info.min_fee_computed, (address, maintx.value), + unbonded_from, )) } // TODO: delay checking witness, as address is contained in Tx?