From 02775101d3322e1502907ac55d5144de98e1409b Mon Sep 17 00:00:00 2001 From: SeongChan Lee Date: Tue, 28 May 2019 16:36:19 +0900 Subject: [PATCH 1/6] Cleanup stake tests - Replace assertions with unwrap that are not a concern of a test. - Reorder assertions by states - Add reasons to assertions --- core/src/consensus/stake/mod.rs | 86 ++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/core/src/consensus/stake/mod.rs b/core/src/consensus/stake/mod.rs index 3a8645b3cd..65a31a9094 100644 --- a/core/src/consensus/stake/mod.rs +++ b/core/src/consensus/stake/mod.rs @@ -295,13 +295,16 @@ mod tests { genesis_stakes.insert(address1, 100); Stake::new(genesis_stakes, new_validator_set(Vec::new())) }; - assert_eq!(Ok(()), stake.init(&mut state)); + stake.init(&mut state).unwrap(); let account1 = StakeAccount::load_from_state(&state, &address1).unwrap(); - let account2 = StakeAccount::load_from_state(&state, &address2).unwrap(); assert_eq!(account1.balance, 100); + + let account2 = StakeAccount::load_from_state(&state, &address2).unwrap(); assert_eq!(account2.balance, 0); + let stakeholders = Stakeholders::load_from_state(&state).unwrap(); + assert_eq!(stakeholders.iter().len(), 1); assert!(stakeholders.contains(&address1)); assert!(!stakeholders.contains(&address2)); } @@ -317,16 +320,19 @@ mod tests { genesis_stakes.insert(address1, 100); Stake::new(genesis_stakes, new_validator_set(Vec::new())) }; - assert_eq!(Ok(()), stake.init(&mut state)); + stake.init(&mut state).unwrap(); let result = transfer_ccs(&mut state, &address1, &address2, 10); - assert_eq!(Ok(()), result); + assert_eq!(result, Ok(())); let account1 = StakeAccount::load_from_state(&state, &address1).unwrap(); - let account2 = StakeAccount::load_from_state(&state, &address2).unwrap(); assert_eq!(account1.balance, 90); + + let account2 = StakeAccount::load_from_state(&state, &address2).unwrap(); assert_eq!(account2.balance, 10); + let stakeholders = Stakeholders::load_from_state(&state).unwrap(); + assert_eq!(stakeholders.iter().len(), 2); assert!(stakeholders.contains(&address1)); assert!(stakeholders.contains(&address2)); } @@ -342,17 +348,21 @@ mod tests { genesis_stakes.insert(address1, 100); Stake::new(genesis_stakes, new_validator_set(Vec::new())) }; - assert_eq!(Ok(()), stake.init(&mut state)); + stake.init(&mut state).unwrap(); - transfer_ccs(&mut state, &address1, &address2, 100).unwrap(); + let result = transfer_ccs(&mut state, &address1, &address2, 100); + assert_eq!(result, Ok(())); let account1 = StakeAccount::load_from_state(&state, &address1).unwrap(); - let account2 = StakeAccount::load_from_state(&state, &address2).unwrap(); assert_eq!(account1.balance, 0); - assert_eq!(state.action_data(&get_account_key(&address1)).unwrap(), None); + assert_eq!(state.action_data(&get_account_key(&address1)).unwrap(), None, "Should clear state"); + + let account2 = StakeAccount::load_from_state(&state, &address2).unwrap(); assert_eq!(account2.balance, 100); + let stakeholders = Stakeholders::load_from_state(&state).unwrap(); - assert!(!stakeholders.contains(&address1)); + assert_eq!(stakeholders.iter().len(), 1); + assert!(!stakeholders.contains(&address1), "Not be a stakeholder anymore"); assert!(stakeholders.contains(&address2)); } @@ -369,7 +379,7 @@ mod tests { genesis_stakes.insert(delegator, 100); Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) }; - assert_eq!(Ok(()), stake.init(&mut state)); + stake.init(&mut state).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -379,16 +389,22 @@ mod tests { assert_eq!(result, Ok(())); let delegator_account = StakeAccount::load_from_state(&state, &delegator).unwrap(); - let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); assert_eq!(delegator_account.balance, 60); + + let delegatee_account = StakeAccount::load_from_state(&state, &delegatee).unwrap(); + assert_eq!(delegatee_account.balance, 100, "Shouldn't be touched"); + + let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); assert_eq!(delegation.iter().count(), 1); assert_eq!(delegation.get_quantity(&delegatee), 40); - // Should not be touched - let delegatee_account = StakeAccount::load_from_state(&state, &delegatee).unwrap(); - let delegation_untouched = Delegation::load_from_state(&state, &delegatee).unwrap(); - assert_eq!(delegatee_account.balance, 100); - assert_eq!(delegation_untouched.iter().count(), 0); + let delegation_delegatee = Delegation::load_from_state(&state, &delegatee).unwrap(); + assert_eq!(delegation_delegatee.iter().count(), 0, "Shouldn't be touched"); + + let stakeholders = Stakeholders::load_from_state(&state).unwrap(); + assert_eq!(stakeholders.iter().len(), 2); + assert!(stakeholders.contains(&delegator)); + assert!(stakeholders.contains(&delegatee)); } #[test] @@ -404,7 +420,7 @@ mod tests { genesis_stakes.insert(delegator, 100); Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) }; - assert_eq!(Ok(()), stake.init(&mut state)); + stake.init(&mut state).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -414,17 +430,23 @@ mod tests { assert_eq!(result, Ok(())); let delegator_account = StakeAccount::load_from_state(&state, &delegator).unwrap(); - let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); assert_eq!(delegator_account.balance, 0); - assert_eq!(state.action_data(&get_account_key(&delegator)).unwrap(), None); + assert_eq!(state.action_data(&get_account_key(&delegator)).unwrap(), None, "Should clear state"); + + let delegatee_account = StakeAccount::load_from_state(&state, &delegatee).unwrap(); + assert_eq!(delegatee_account.balance, 100, "Shouldn't be touched"); + + let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); assert_eq!(delegation.iter().count(), 1); assert_eq!(delegation.get_quantity(&delegatee), 100); - // Should not be touched - let delegatee_account = StakeAccount::load_from_state(&state, &delegatee).unwrap(); - let delegation_untouched = Delegation::load_from_state(&state, &delegatee).unwrap(); - assert_eq!(delegatee_account.balance, 100); - assert_eq!(delegation_untouched.iter().count(), 0); + let delegation_delegatee = Delegation::load_from_state(&state, &delegatee).unwrap(); + assert_eq!(delegation_delegatee.iter().count(), 0, "Shouldn't be touched"); + + let stakeholders = Stakeholders::load_from_state(&state).unwrap(); + assert_eq!(stakeholders.iter().len(), 2); + assert!(stakeholders.contains(&delegator), "Should still be a stakeholder after delegated all"); + assert!(stakeholders.contains(&delegatee)); } #[test] @@ -440,7 +462,7 @@ mod tests { genesis_stakes.insert(delegator, 100); Stake::new(genesis_stakes, new_validator_set(Vec::new())) }; - assert_eq!(Ok(()), stake.init(&mut state)); + stake.init(&mut state).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -463,7 +485,7 @@ mod tests { genesis_stakes.insert(delegator, 100); Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) }; - assert_eq!(Ok(()), stake.init(&mut state)); + stake.init(&mut state).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -486,14 +508,13 @@ mod tests { genesis_stakes.insert(delegator, 100); Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) }; - assert_eq!(Ok(()), stake.init(&mut state)); + stake.init(&mut state).unwrap(); let action = Action::DelegateCCS { address: delegatee, quantity: 50, }; - let result = stake.execute(&action.rlp_bytes(), &mut state, &delegator); - assert!(result.is_ok()); + stake.execute(&action.rlp_bytes(), &mut state, &delegator).unwrap(); let action = Action::TransferCCS { address: delegatee, @@ -516,14 +537,13 @@ mod tests { genesis_stakes.insert(delegator, 100); Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) }; - assert_eq!(Ok(()), stake.init(&mut state)); + stake.init(&mut state).unwrap(); let action = Action::DelegateCCS { address: delegatee, quantity: 50, }; - let result = stake.execute(&action.rlp_bytes(), &mut state, &delegator); - assert!(result.is_ok()); + stake.execute(&action.rlp_bytes(), &mut state, &delegator).unwrap(); let action = Action::TransferCCS { address: delegatee, From e6002f30f06d3846862e53d539e715cb9c952d78 Mon Sep 17 00:00:00 2001 From: SeongChan Lee Date: Fri, 17 May 2019 21:46:28 +0900 Subject: [PATCH 2/6] Implement delegation subtract --- core/src/consensus/stake/action_data.rs | 81 +++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/core/src/consensus/stake/action_data.rs b/core/src/consensus/stake/action_data.rs index af0089e1ef..80609ec155 100644 --- a/core/src/consensus/stake/action_data.rs +++ b/core/src/consensus/stake/action_data.rs @@ -16,6 +16,7 @@ #[cfg(test)] use std::collections::btree_map; +use std::collections::btree_map::Entry; use std::collections::{btree_set, BTreeMap, BTreeSet}; use std::mem; @@ -174,6 +175,25 @@ impl<'a> Delegation<'a> { Ok(()) } + pub fn subtract_quantity(&mut self, delegatee: Address, quantity: StakeQuantity) -> StateResult<()> { + if quantity == 0 { + return Ok(()) + } + + if let Entry::Occupied(mut entry) = self.delegatees.entry(delegatee) { + if *entry.get() > quantity { + *entry.get_mut() -= quantity; + return Ok(()) + } else if *entry.get() == quantity { + entry.remove(); + return Ok(()) + } + } + + Err(RuntimeError::FailedToHandleCustomAction("Cannot subtract more than that is delegated to".to_string()) + .into()) + } + #[cfg(test)] pub fn get_quantity(&self, delegatee: &Address) -> StakeQuantity { self.delegatees.get(delegatee).cloned().unwrap_or(0) @@ -581,6 +601,45 @@ mod tests { assert_eq!(&delegated, &[(&delegatee1, &100)]); } + #[test] + fn delegation_can_subtract() { + let mut state = helpers::get_temp_state(); + + // Prepare + let delegator = Address::random(); + let delegatee = Address::random(); + + let mut delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + delegation.add_quantity(delegatee, 100).unwrap(); + delegation.save_to_state(&mut state).unwrap(); + + // Do subtract + let mut delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + delegation.subtract_quantity(delegatee, 30).unwrap(); + delegation.save_to_state(&mut state).unwrap(); + + // Assert + let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + assert_eq!(delegation.get_quantity(&delegatee), 70); + } + + #[test] + fn delegation_cannot_subtract_mor_than_delegated() { + let mut state = helpers::get_temp_state(); + + // Prepare + let delegator = Address::random(); + let delegatee = Address::random(); + + let mut delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + delegation.add_quantity(delegatee, 100).unwrap(); + delegation.save_to_state(&mut state).unwrap(); + + // Do subtract + let mut delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + assert!(delegation.subtract_quantity(delegatee, 130).is_err()); + } + #[test] fn delegation_empty_removed_from_state() { let mut state = helpers::get_temp_state(); @@ -598,6 +657,28 @@ mod tests { assert_eq!(result, None); } + #[test] + fn delegation_became_empty_removed_from_state() { + let mut state = helpers::get_temp_state(); + + // Prepare + let delegator = Address::random(); + let delegatee = Address::random(); + + let mut delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + delegation.add_quantity(delegatee, 100).unwrap(); + delegation.save_to_state(&mut state).unwrap(); + + // Do subtract + let mut delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + delegation.subtract_quantity(delegatee, 100).unwrap(); + delegation.save_to_state(&mut state).unwrap(); + + // Assert + let result = state.action_data(&get_delegation_key(&delegator)).unwrap(); + assert_eq!(result, None); + } + #[test] fn load_and_save_intermediate_rewards() { let mut state = helpers::get_temp_state(); From ae77dcc8e8874b61df2f94f9592abab7106df947 Mon Sep 17 00:00:00 2001 From: SeongChan Lee Date: Fri, 17 May 2019 22:03:16 +0900 Subject: [PATCH 3/6] Implement revoke --- core/src/consensus/stake/actions.rs | 24 ++++ core/src/consensus/stake/mod.rs | 133 ++++++++++++++++++ test/src/e2e.long/staking.test.ts | 209 ++++++++++++++++++++++++++++ test/src/e2e/staking.test.ts | 173 ++++++++++++++++++++++- 4 files changed, 538 insertions(+), 1 deletion(-) diff --git a/core/src/consensus/stake/actions.rs b/core/src/consensus/stake/actions.rs index 973d347b67..99a2b3addc 100644 --- a/core/src/consensus/stake/actions.rs +++ b/core/src/consensus/stake/actions.rs @@ -20,6 +20,7 @@ use rlp::{Decodable, DecoderError, Encodable, RlpStream, UntrustedRlp}; const ACTION_TAG_TRANSFER_CCS: u8 = 1; const ACTION_TAG_DELEGATE_CCS: u8 = 2; +const ACTION_TAG_REVOKE: u8 = 3; const ACTION_TAG_CHANGE_PARAMS: u8 = 0xFF; #[derive(Debug, PartialEq)] @@ -32,6 +33,10 @@ pub enum Action { address: Address, quantity: u64, }, + Revoke { + address: Address, + quantity: u64, + }, ChangeParams { metadata_seq: u64, params: Box, @@ -54,6 +59,12 @@ impl Encodable for Action { } => { s.begin_list(3).append(&ACTION_TAG_DELEGATE_CCS).append(address).append(quantity); } + Action::Revoke { + address, + quantity, + } => { + s.begin_list(3).append(&ACTION_TAG_REVOKE).append(address).append(quantity); + } Action::ChangeParams { metadata_seq, params, @@ -101,6 +112,19 @@ impl Decodable for Action { quantity: rlp.val_at(2)?, }) } + ACTION_TAG_REVOKE => { + let item_count = rlp.item_count()?; + if item_count != 3 { + return Err(DecoderError::RlpInvalidLength { + expected: 3, + got: item_count, + }) + } + Ok(Action::Revoke { + address: rlp.val_at(1)?, + quantity: rlp.val_at(2)?, + }) + } ACTION_TAG_CHANGE_PARAMS => { let item_count = rlp.item_count()?; if item_count < 4 { diff --git a/core/src/consensus/stake/mod.rs b/core/src/consensus/stake/mod.rs index 65a31a9094..6d8a41744a 100644 --- a/core/src/consensus/stake/mod.rs +++ b/core/src/consensus/stake/mod.rs @@ -115,6 +115,16 @@ impl ActionHandler for Stake { Err(RuntimeError::FailedToHandleCustomAction("DelegateCCS is disabled".to_string()).into()) } } + Action::Revoke { + address, + quantity, + } => { + if self.enable_delegations { + revoke(state, sender, &address, quantity) + } else { + Err(RuntimeError::FailedToHandleCustomAction("DelegateCCS is disabled".to_string()).into()) + } + } Action::ChangeParams { metadata_seq, params, @@ -133,6 +143,9 @@ impl ActionHandler for Stake { Action::DelegateCCS { .. } => Ok(()), + Action::Revoke { + .. + } => Ok(()), Action::ChangeParams { metadata_seq, params, @@ -208,6 +221,19 @@ fn delegate_ccs( Ok(()) } +fn revoke(state: &mut TopLevelState, sender: &Address, delegatee: &Address, quantity: u64) -> StateResult<()> { + let mut delegator = StakeAccount::load_from_state(state, sender)?; + let mut delegation = Delegation::load_from_state(state, &sender)?; + + delegator.add_balance(quantity)?; + delegation.subtract_quantity(*delegatee, quantity)?; + // delegation does not touch stakeholders + + delegation.save_to_state(state)?; + delegator.save_to_state(state)?; + Ok(()) +} + pub fn get_stakes(state: &TopLevelState) -> StateResult> { let stakeholders = Stakeholders::load_from_state(state)?; let mut result = HashMap::new(); @@ -279,6 +305,7 @@ mod tests { use super::*; use ckey::{public_to_address, Public}; + use consensus::stake::action_data::get_delegation_key; use consensus::validator_set::new_validator_set; use cstate::tests::helpers; use cstate::TopStateView; @@ -552,4 +579,110 @@ mod tests { let result = stake.execute(&action.rlp_bytes(), &mut state, &delegator); assert!(result.is_err()); } + + #[test] + fn can_revoke_delegated_tokens() { + let delegatee_public = Public::random(); + let delegatee = public_to_address(&delegatee_public); + let delegator = Address::random(); + + let mut state = helpers::get_temp_state(); + let stake = { + let mut genesis_stakes = HashMap::new(); + genesis_stakes.insert(delegatee, 100); + genesis_stakes.insert(delegator, 100); + Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) + }; + stake.init(&mut state).unwrap(); + + let action = Action::DelegateCCS { + address: delegatee, + quantity: 50, + }; + let result = stake.execute(&action.rlp_bytes(), &mut state, &delegator); + assert!(result.is_ok()); + + let action = Action::Revoke { + address: delegatee, + quantity: 20, + }; + let result = stake.execute(&action.rlp_bytes(), &mut state, &delegator); + assert_eq!(Ok(()), result); + + let delegator_account = StakeAccount::load_from_state(&state, &delegator).unwrap(); + let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + assert_eq!(delegator_account.balance, 100 - 50 + 20); + assert_eq!(delegation.iter().count(), 1); + assert_eq!(delegation.get_quantity(&delegatee), 50 - 20); + } + + #[test] + fn cannot_revoke_more_than_delegated_tokens() { + let delegatee_public = Public::random(); + let delegatee = public_to_address(&delegatee_public); + let delegator = Address::random(); + + let mut state = helpers::get_temp_state(); + let stake = { + let mut genesis_stakes = HashMap::new(); + genesis_stakes.insert(delegatee, 100); + genesis_stakes.insert(delegator, 100); + Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) + }; + stake.init(&mut state).unwrap(); + + let action = Action::DelegateCCS { + address: delegatee, + quantity: 50, + }; + let result = stake.execute(&action.rlp_bytes(), &mut state, &delegator); + assert!(result.is_ok()); + + let action = Action::Revoke { + address: delegatee, + quantity: 70, + }; + let result = stake.execute(&action.rlp_bytes(), &mut state, &delegator); + assert!(result.is_err()); + + let delegator_account = StakeAccount::load_from_state(&state, &delegator).unwrap(); + let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + assert_eq!(delegator_account.balance, 100 - 50); + assert_eq!(delegation.iter().count(), 1); + assert_eq!(delegation.get_quantity(&delegatee), 50); + } + + #[test] + fn revoke_all_should_clear_state() { + let delegatee_public = Public::random(); + let delegatee = public_to_address(&delegatee_public); + let delegator = Address::random(); + + let mut state = helpers::get_temp_state(); + let stake = { + let mut genesis_stakes = HashMap::new(); + genesis_stakes.insert(delegatee, 100); + genesis_stakes.insert(delegator, 100); + Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) + }; + stake.init(&mut state).unwrap(); + + let action = Action::DelegateCCS { + address: delegatee, + quantity: 50, + }; + let result = stake.execute(&action.rlp_bytes(), &mut state, &delegator); + assert!(result.is_ok()); + + let action = Action::Revoke { + address: delegatee, + quantity: 50, + }; + let result = stake.execute(&action.rlp_bytes(), &mut state, &delegator); + assert_eq!(Ok(()), result); + + let delegator_account = StakeAccount::load_from_state(&state, &delegator).unwrap(); + assert_eq!(delegator_account.balance, 100); + assert_eq!(state.action_data(&get_delegation_key(&delegator)).unwrap(), None); + } } diff --git a/test/src/e2e.long/staking.test.ts b/test/src/e2e.long/staking.test.ts index 5bc0bb2283..1e07e75540 100644 --- a/test/src/e2e.long/staking.test.ts +++ b/test/src/e2e.long/staking.test.ts @@ -217,6 +217,43 @@ describe("Staking", function() { ); } + async function revokeToken(params: { + senderAddress: PlatformAddress; + senderSecret: string; + delegateeAddress: PlatformAddress; + quantity: number; + fee?: number; + seq?: number; + }): Promise { + const { fee = 10 } = params; + const seq = + params.seq == null + ? await nodes[0].sdk.rpc.chain.getSeq(params.senderAddress) + : params.seq; + + return promiseExpect.shouldFulfill( + "sendSignTransaction", + nodes[0].sdk.rpc.chain.sendSignedTransaction( + nodes[0].sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: Buffer.from( + RLP.encode([ + 3, + params.delegateeAddress.accountId.toEncodeObject(), + params.quantity + ]) + ) + }) + .sign({ + secret: params.senderSecret, + seq, + fee + }) + ) + ); + } + it("should have proper initial stake tokens", async function() { const { amounts, stakeholders } = await getAllStakingInfo(); expect(amounts).to.be.deep.equal([ @@ -449,6 +486,178 @@ describe("Staking", function() { expect(err0 || err1 || err2 || err3).not.null; }); + it("can revoke tokens", async function() { + await connectEachOther(); + + const delegateHash = await delegateToken({ + senderAddress: faucetAddress, + senderSecret: faucetSecret, + receiverAddress: validator0Address, + quantity: 100 + }); + while ( + !(await nodes[0].sdk.rpc.chain.containsTransaction(delegateHash)) + ) { + await wait(500); + } + + const hash = await revokeToken({ + senderAddress: faucetAddress, + senderSecret: faucetSecret, + delegateeAddress: validator0Address, + quantity: 50 + }); + + while (!(await nodes[0].sdk.rpc.chain.containsTransaction(hash))) { + await wait(500); + } + + const { amounts } = await getAllStakingInfo(); + expect(amounts).to.be.deep.equal([ + toHex(RLP.encode(70000 - 100 + 50)), + null, + null, + null, + null, + toHex(RLP.encode(20000)), + toHex(RLP.encode(10000)) + ]); + + const delegations = await getAllDelegation(); + expect(delegations).to.be.deep.equal([ + toHex( + RLP.encode([[validator0Address.accountId.toEncodeObject(), 50]]) + ), + null, + null, + null, + null, + null, + null + ]); + }); + + it("cannot revoke more than delegated", async function() { + await connectEachOther(); + + const delegateHash = await delegateToken({ + senderAddress: faucetAddress, + senderSecret: faucetSecret, + receiverAddress: validator0Address, + quantity: 100 + }); + while ( + !(await nodes[0].sdk.rpc.chain.containsTransaction(delegateHash)) + ) { + await wait(500); + } + + const blockNumber = await nodes[0].getBestBlockNumber(); + const seq = await nodes[0].sdk.rpc.chain.getSeq(faucetAddress); + const pay = await nodes[0].sendPayTx({ + recipient: faucetAddress, + secret: faucetSecret, + quantity: 1, + seq + }); + + // revoke + const hash = await revokeToken({ + senderAddress: faucetAddress, + senderSecret: faucetSecret, + delegateeAddress: validator0Address, + quantity: 200, + seq: seq + 1 + }); + await nodes[0].waitBlockNumber(blockNumber + 1); + + while ( + !(await nodes[0].sdk.rpc.chain.containsTransaction(pay.hash())) + ) { + await wait(500); + } + const err0 = await nodes[0].sdk.rpc.chain.getErrorHint(hash); + const err1 = await nodes[1].sdk.rpc.chain.getErrorHint(hash); + const err2 = await nodes[2].sdk.rpc.chain.getErrorHint(hash); + const err3 = await nodes[3].sdk.rpc.chain.getErrorHint(hash); + expect(err0 || err1 || err2 || err3).not.null; + + const { amounts } = await getAllStakingInfo(); + expect(amounts).to.be.deep.equal([ + toHex(RLP.encode(70000 - 100)), + null, + null, + null, + null, + toHex(RLP.encode(20000)), + toHex(RLP.encode(10000)) + ]); + + const delegations = await getAllDelegation(); + expect(delegations).to.be.deep.equal([ + toHex( + RLP.encode([ + [validator0Address.accountId.toEncodeObject(), 100] + ]) + ), + null, + null, + null, + null, + null, + null + ]); + }); + + it("revoking all should clear delegation", async function() { + await connectEachOther(); + + const delegateHash = await delegateToken({ + senderAddress: faucetAddress, + senderSecret: faucetSecret, + receiverAddress: validator0Address, + quantity: 100 + }); + while ( + !(await nodes[0].sdk.rpc.chain.containsTransaction(delegateHash)) + ) { + await wait(500); + } + + const hash = await revokeToken({ + senderAddress: faucetAddress, + senderSecret: faucetSecret, + delegateeAddress: validator0Address, + quantity: 100 + }); + + while (!(await nodes[0].sdk.rpc.chain.containsTransaction(hash))) { + await wait(500); + } + + const { amounts } = await getAllStakingInfo(); + expect(amounts).to.be.deep.equal([ + toHex(RLP.encode(70000)), + null, + null, + null, + null, + toHex(RLP.encode(20000)), + toHex(RLP.encode(10000)) + ]); + + const delegations = await getAllDelegation(); + expect(delegations).to.be.deep.equal([ + null, + null, + null, + null, + null, + null, + null + ]); + }); + it("get fee in proportion to holding stakes", async function() { await connectEachOther(); diff --git a/test/src/e2e/staking.test.ts b/test/src/e2e/staking.test.ts index 72909b6b53..6d1007567e 100644 --- a/test/src/e2e/staking.test.ts +++ b/test/src/e2e/staking.test.ts @@ -14,7 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { expect } from "chai"; +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; import { H256, PlatformAddress } from "codechain-primitives/lib"; import { toHex } from "codechain-sdk/lib/utils"; import "mocha"; @@ -33,6 +34,9 @@ import { import { PromiseExpect } from "../helper/promise"; import CodeChain from "../helper/spawn"; +chai.use(chaiAsPromised); +const expect = chai.expect; + const RLP = require("rlp"); describe("Staking", function() { @@ -177,6 +181,43 @@ describe("Staking", function() { ); } + async function revokeToken(params: { + senderAddress: PlatformAddress; + senderSecret: string; + delegateeAddress: PlatformAddress; + quantity: number; + fee?: number; + seq?: number; + }): Promise { + const { fee = 10 } = params; + const seq = + params.seq == null + ? await node.sdk.rpc.chain.getSeq(params.senderAddress) + : params.seq; + + return promiseExpect.shouldFulfill( + "sendSignTransaction", + node.sdk.rpc.chain.sendSignedTransaction( + node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: Buffer.from( + RLP.encode([ + 3, + params.delegateeAddress.accountId.toEncodeObject(), + params.quantity + ]) + ) + }) + .sign({ + secret: params.senderSecret, + seq, + fee + }) + ) + ); + } + it("should have proper initial stake tokens", async function() { const { amounts, stakeholders } = await getAllStakingInfo(); expect(amounts).to.be.deep.equal([ @@ -336,6 +377,136 @@ describe("Staking", function() { ]); }); + it("can revoke tokens", async function() { + await delegateToken({ + senderAddress: aliceAddress, + senderSecret: aliceSecret, + receiverAddress: validator0Address, + quantity: 100 + }); + + await revokeToken({ + senderAddress: aliceAddress, + senderSecret: aliceSecret, + delegateeAddress: validator0Address, + quantity: 50 + }); + + const { amounts } = await getAllStakingInfo(); + expect(amounts).to.be.deep.equal([ + null, + null, + null, + toHex(RLP.encode(40000 - 100 + 50)), + toHex(RLP.encode(30000)), + toHex(RLP.encode(20000)), + toHex(RLP.encode(10000)) + ]); + + const delegations = await getAllDelegation(); + expect(delegations).to.be.deep.equal([ + null, + null, + null, + toHex( + RLP.encode([[validator0Address.accountId.toEncodeObject(), 50]]) + ), + null, + null, + null + ]); + }); + + it("cannot revoke more than delegated", async function() { + await delegateToken({ + senderAddress: aliceAddress, + senderSecret: aliceSecret, + receiverAddress: validator0Address, + quantity: 100 + }); + + await node.sdk.rpc.devel.stopSealing(); + await node.sendPayTx({ + recipient: faucetAddress, + secret: validator0Secret, + quantity: 1, + seq: await node.sdk.rpc.chain.getSeq(validator0Address) + }); + const hash = await revokeToken({ + senderAddress: aliceAddress, + senderSecret: aliceSecret, + delegateeAddress: validator0Address, + quantity: 200 + }); + await node.sdk.rpc.devel.startSealing(); + + expect(await node.sdk.rpc.chain.getErrorHint(hash)).not.to.be.null; + + const { amounts } = await getAllStakingInfo(); + expect(amounts).to.be.deep.equal([ + null, + null, + null, + toHex(RLP.encode(40000 - 100)), + toHex(RLP.encode(30000)), + toHex(RLP.encode(20000)), + toHex(RLP.encode(10000)) + ]); + + const delegations = await getAllDelegation(); + expect(delegations).to.be.deep.equal([ + null, + null, + null, + toHex( + RLP.encode([ + [validator0Address.accountId.toEncodeObject(), 100] + ]) + ), + null, + null, + null + ]); + }); + + it("revoking all should clear delegation", async function() { + await delegateToken({ + senderAddress: aliceAddress, + senderSecret: aliceSecret, + receiverAddress: validator0Address, + quantity: 100 + }); + + await revokeToken({ + senderAddress: aliceAddress, + senderSecret: aliceSecret, + delegateeAddress: validator0Address, + quantity: 100 + }); + + const { amounts } = await getAllStakingInfo(); + expect(amounts).to.be.deep.equal([ + null, + null, + null, + toHex(RLP.encode(40000)), + toHex(RLP.encode(30000)), + toHex(RLP.encode(20000)), + toHex(RLP.encode(10000)) + ]); + + const delegations = await getAllDelegation(); + expect(delegations).to.be.deep.equal([ + null, + null, + null, + null, + null, + null, + null + ]); + }); + it("get fee in proportion to holding stakes", async function() { // alice: 40000, bob: 30000, carol: 20000, dave: 10000 const fee = 1000; From e6357bc14289a755e4e41ae22220255f44ac5a7b Mon Sep 17 00:00:00 2001 From: SeongChan Lee Date: Tue, 21 May 2019 14:56:27 +0900 Subject: [PATCH 4/6] Implement self nomination --- core/src/consensus/solo/mod.rs | 6 +- core/src/consensus/stake/action_data.rs | 245 +++++++++++++- core/src/consensus/stake/actions.rs | 25 ++ core/src/consensus/stake/mod.rs | 319 +++++++++++++++--- core/src/consensus/tendermint/mod.rs | 2 +- core/src/consensus/validator_set/mod.rs | 1 - .../consensus/validator_set/null_validator.rs | 55 --- test/src/e2e.long/staking.test.ts | 141 ++++++-- test/src/e2e/staking.test.ts | 87 ++++- 9 files changed, 732 insertions(+), 149 deletions(-) delete mode 100644 core/src/consensus/validator_set/null_validator.rs diff --git a/core/src/consensus/solo/mod.rs b/core/src/consensus/solo/mod.rs index 71be4f9ba2..3e94c3e64b 100644 --- a/core/src/consensus/solo/mod.rs +++ b/core/src/consensus/solo/mod.rs @@ -23,7 +23,6 @@ use ctypes::{CommonParams, Header}; use self::params::SoloParams; use super::stake; -use super::validator_set; use super::{ConsensusEngine, Seal}; use crate::block::{ExecutedBlock, IsBlock}; use crate::codechain_machine::CodeChainMachine; @@ -44,10 +43,7 @@ impl Solo { if params.enable_hit_handler { action_handlers.push(Arc::new(HitHandler::new())); } - action_handlers.push(Arc::new(stake::Stake::new( - params.genesis_stakes.clone(), - Arc::new(validator_set::null_validator::NullValidator {}), - ))); + action_handlers.push(Arc::new(stake::Stake::new(params.genesis_stakes.clone()))); Solo { params, diff --git a/core/src/consensus/stake/action_data.rs b/core/src/consensus/stake/action_data.rs index 80609ec155..bb3e34fc93 100644 --- a/core/src/consensus/stake/action_data.rs +++ b/core/src/consensus/stake/action_data.rs @@ -16,15 +16,15 @@ #[cfg(test)] use std::collections::btree_map; -use std::collections::btree_map::Entry; -use std::collections::{btree_set, BTreeMap, BTreeSet}; +use std::collections::btree_map::{BTreeMap, Entry}; +use std::collections::btree_set::{self, BTreeSet}; use std::mem; use ckey::Address; use cstate::{ActionData, ActionDataKeyBuilder, StateResult, TopLevelState, TopState, TopStateView}; use ctypes::errors::RuntimeError; use primitives::H256; -use rlp::{Decodable, Encodable, Rlp, RlpStream}; +use rlp::{decode_list, Decodable, Encodable, Rlp, RlpStream}; use super::CUSTOM_ACTION_HANDLER_ID; @@ -35,6 +35,8 @@ pub fn get_account_key(address: &Address) -> H256 { lazy_static! { pub static ref STAKEHOLDER_ADDRESSES_KEY: H256 = ActionDataKeyBuilder::new(CUSTOM_ACTION_HANDLER_ID, 1).append(&"StakeholderAddresses").into_key(); + pub static ref CANDIDATES_KEY: H256 = + ActionDataKeyBuilder::new(CUSTOM_ACTION_HANDLER_ID, 1).append(&"Candidates").into_key(); } pub fn get_delegation_key(address: &Address) -> H256 { @@ -46,6 +48,7 @@ pub fn get_intermediate_rewards_key() -> H256 { } pub type StakeQuantity = u64; +pub type Deposit = u64; pub struct StakeAccount<'a> { pub address: &'a Address, @@ -194,7 +197,6 @@ impl<'a> Delegation<'a> { .into()) } - #[cfg(test)] pub fn get_quantity(&self, delegatee: &Address) -> StakeQuantity { self.delegatees.get(delegatee).cloned().unwrap_or(0) } @@ -257,6 +259,62 @@ impl IntermediateRewards { } } +pub struct Candidates(BTreeMap); +#[derive(Clone, Debug, Eq, PartialEq, RlpEncodable, RlpDecodable)] +pub struct Candidate { + pub address: Address, + pub deposit: Deposit, + pub nomination_ends_at: u64, +} + +impl Candidates { + pub fn load_from_state(state: &TopLevelState) -> StateResult { + let key = *CANDIDATES_KEY; + let candidates = state.action_data(&key)?.map(|data| decode_list::(&data)).unwrap_or_default(); + let indexed = candidates.into_iter().map(|c| (c.address, c)).collect(); + Ok(Candidates(indexed)) + } + + pub fn save_to_state(&self, state: &mut TopLevelState) -> StateResult<()> { + let key = *CANDIDATES_KEY; + if !self.0.is_empty() { + let encoded = encode_iter(self.0.values()); + state.update_action_data(&key, encoded)?; + } else { + state.remove_action_data(&key); + } + Ok(()) + } + + pub fn get_candidate(&self, account: &Address) -> Option<&Candidate> { + self.0.get(&account) + } + + #[cfg(test)] + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn add_deposit(&mut self, address: &Address, quantity: Deposit, nomination_ends_at: u64) { + let candidate = self.0.entry(*address).or_insert(Candidate { + address: *address, + deposit: 0, + nomination_ends_at: 0, + }); + candidate.deposit += quantity; + if candidate.nomination_ends_at < nomination_ends_at { + candidate.nomination_ends_at = nomination_ends_at; + } + } + + pub fn drain_expired_candidates(&mut self, term_index: u64) -> Vec { + let (expired, retained): (Vec<_>, Vec<_>) = + self.0.values().cloned().partition(|c| c.nomination_ends_at <= term_index); + self.0 = retained.into_iter().map(|c| (c.address, c)).collect(); + expired + } +} + fn decode_set(data: Option<&ActionData>) -> BTreeSet where V: Ord + Decodable, { @@ -354,6 +412,18 @@ where rlp.drain().into_vec() } +fn encode_iter<'a, V, I>(iter: I) -> Vec +where + V: 'a + Encodable, + I: ExactSizeIterator + Clone, { + let mut rlp = RlpStream::new(); + rlp.begin_list(iter.clone().count()); + for value in iter { + rlp.append(value); + } + rlp.drain().into_vec() +} + #[cfg(test)] mod tests { use super::*; @@ -719,4 +789,171 @@ mod tests { assert_eq!(BTreeMap::new(), final_rewards.current); assert_eq!(current, final_rewards.previous); } + + #[test] + fn candidates_deposit_add() { + let mut state = helpers::get_temp_state(); + + // Prepare + let account = Address::random(); + let deposits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + + for deposit in deposits.iter() { + let mut candidates = Candidates::load_from_state(&state).unwrap(); + candidates.add_deposit(&account, *deposit, 0); + candidates.save_to_state(&mut state).unwrap(); + } + + // Assert + let candidates = Candidates::load_from_state(&state).unwrap(); + let candidate = candidates.get_candidate(&account); + assert_ne!(candidate, None); + assert_eq!(candidate.unwrap().deposit, 55); + } + + #[test] + fn candidates_deposit_can_be_zero() { + let mut state = helpers::get_temp_state(); + + // Prepare + let account = Address::random(); + let mut candidates = Candidates::load_from_state(&state).unwrap(); + candidates.add_deposit(&account, 0, 10); + candidates.save_to_state(&mut state).unwrap(); + + // Assert + let candidates = Candidates::load_from_state(&state).unwrap(); + let candidate = candidates.get_candidate(&account); + assert_ne!(candidate, None); + assert_eq!(candidate.unwrap().deposit, 0); + assert_eq!(candidate.unwrap().nomination_ends_at, 10, "Can be a candidate with 0 deposit"); + } + + #[test] + fn candidates_deposit_should_update_nomination_ends_at() { + let mut state = helpers::get_temp_state(); + + // Prepare + let account = Address::random(); + let deposit_and_nomination_ends_at = [(10, 11), (20, 22), (30, 33), (0, 44)]; + + for (deposit, nomination_ends_at) in &deposit_and_nomination_ends_at { + let mut candidates = Candidates::load_from_state(&state).unwrap(); + candidates.add_deposit(&account, *deposit, *nomination_ends_at); + candidates.save_to_state(&mut state).unwrap(); + } + + // Assert + let candidates = Candidates::load_from_state(&state).unwrap(); + let candidate = candidates.get_candidate(&account); + assert_ne!(candidate, None); + assert_eq!(candidate.unwrap().deposit, 60); + assert_eq!( + candidate.unwrap().nomination_ends_at, + 44, + "nomination_ends_at should be updated incrementally, and including zero deposit" + ); + } + + #[test] + fn candidates_can_remove_expired_deposit() { + let mut state = helpers::get_temp_state(); + + // Prepare + let candidates_prepared = [ + Candidate { + address: Address::from(0), + deposit: 20, + nomination_ends_at: 11, + }, + Candidate { + address: Address::from(1), + deposit: 30, + nomination_ends_at: 22, + }, + Candidate { + address: Address::from(2), + deposit: 40, + nomination_ends_at: 33, + }, + Candidate { + address: Address::from(3), + deposit: 50, + nomination_ends_at: 44, + }, + ]; + + for Candidate { + address, + deposit, + nomination_ends_at, + } in &candidates_prepared + { + let mut candidates = Candidates::load_from_state(&state).unwrap(); + candidates.add_deposit(&address, *deposit, *nomination_ends_at); + candidates.save_to_state(&mut state).unwrap(); + } + + // Remove Expired + let mut candidates = Candidates::load_from_state(&state).unwrap(); + let expired = candidates.drain_expired_candidates(22); + candidates.save_to_state(&mut state).unwrap(); + + // Assert + assert_eq!(expired[..], candidates_prepared[0..=1],); + let candidates = Candidates::load_from_state(&state).unwrap(); + assert_eq!(candidates.len(), 2); + assert_eq!(candidates.get_candidate(&candidates_prepared[2].address), Some(&candidates_prepared[2])); + assert_eq!(candidates.get_candidate(&candidates_prepared[3].address), Some(&candidates_prepared[3])); + } + + #[test] + fn candidates_expire_all_cleanup_state() { + let mut state = helpers::get_temp_state(); + + // Prepare + let candidates_prepared = [ + Candidate { + address: Address::from(0), + deposit: 20, + nomination_ends_at: 11, + }, + Candidate { + address: Address::from(1), + deposit: 30, + nomination_ends_at: 22, + }, + Candidate { + address: Address::from(2), + deposit: 40, + nomination_ends_at: 33, + }, + Candidate { + address: Address::from(3), + deposit: 50, + nomination_ends_at: 44, + }, + ]; + + for Candidate { + address, + deposit, + nomination_ends_at, + } in &candidates_prepared + { + let mut candidates = Candidates::load_from_state(&state).unwrap(); + candidates.add_deposit(&address, *deposit, *nomination_ends_at); + candidates.save_to_state(&mut state).unwrap(); + } + + // Remove Expired + let mut candidates = Candidates::load_from_state(&state).unwrap(); + let expired = candidates.drain_expired_candidates(99); + candidates.save_to_state(&mut state).unwrap(); + + // Assert + assert_eq!(expired[..], candidates_prepared[0..4]); + let result = state.action_data(&*CANDIDATES_KEY).unwrap(); + assert_eq!(result, None); + } } diff --git a/core/src/consensus/stake/actions.rs b/core/src/consensus/stake/actions.rs index 99a2b3addc..df9e2b7504 100644 --- a/core/src/consensus/stake/actions.rs +++ b/core/src/consensus/stake/actions.rs @@ -16,11 +16,13 @@ use ckey::{Address, Signature}; use ctypes::CommonParams; +use primitives::Bytes; use rlp::{Decodable, DecoderError, Encodable, RlpStream, UntrustedRlp}; const ACTION_TAG_TRANSFER_CCS: u8 = 1; const ACTION_TAG_DELEGATE_CCS: u8 = 2; const ACTION_TAG_REVOKE: u8 = 3; +const ACTION_TAG_SELF_NOMINATE: u8 = 4; const ACTION_TAG_CHANGE_PARAMS: u8 = 0xFF; #[derive(Debug, PartialEq)] @@ -37,6 +39,10 @@ pub enum Action { address: Address, quantity: u64, }, + SelfNominate { + deposit: u64, + metadata: Bytes, + }, ChangeParams { metadata_seq: u64, params: Box, @@ -65,6 +71,12 @@ impl Encodable for Action { } => { s.begin_list(3).append(&ACTION_TAG_REVOKE).append(address).append(quantity); } + Action::SelfNominate { + deposit, + metadata, + } => { + s.begin_list(3).append(&ACTION_TAG_SELF_NOMINATE).append(deposit).append(metadata); + } Action::ChangeParams { metadata_seq, params, @@ -125,6 +137,19 @@ impl Decodable for Action { quantity: rlp.val_at(2)?, }) } + ACTION_TAG_SELF_NOMINATE => { + let item_count = rlp.item_count()?; + if item_count != 3 { + return Err(DecoderError::RlpInvalidLength { + expected: 3, + got: item_count, + }) + } + Ok(Action::SelfNominate { + deposit: rlp.val_at(1)?, + metadata: rlp.val_at(2)?, + }) + } ACTION_TAG_CHANGE_PARAMS => { let item_count = rlp.item_count()?; if item_count < 4 { diff --git a/core/src/consensus/stake/mod.rs b/core/src/consensus/stake/mod.rs index 6d8a41744a..e9d4d68618 100644 --- a/core/src/consensus/stake/mod.rs +++ b/core/src/consensus/stake/mod.rs @@ -20,8 +20,6 @@ mod distribute; use std::collections::btree_map::BTreeMap; use std::collections::HashMap; -use std::ops::Deref; -use std::sync::Arc; use ccrypto::Blake; use ckey::{public_to_address, recover, Address, Signature}; @@ -32,34 +30,30 @@ use ctypes::{CommonParams, Header}; use primitives::H256; use rlp::{Decodable, UntrustedRlp}; -use self::action_data::{Delegation, IntermediateRewards, StakeAccount, Stakeholders}; +use self::action_data::{Candidates, Delegation, IntermediateRewards, StakeAccount, Stakeholders}; use self::actions::Action; pub use self::distribute::fee_distribute; -use consensus::ValidatorSet; const CUSTOM_ACTION_HANDLER_ID: u64 = 2; pub struct Stake { genesis_stakes: HashMap, - validators: Arc, enable_delegations: bool, } impl Stake { #[cfg(not(test))] - pub fn new(genesis_stakes: HashMap, validators: Arc) -> Stake { + pub fn new(genesis_stakes: HashMap) -> Stake { Stake { genesis_stakes, - validators, enable_delegations: parse_env_var_enable_delegations(), } } #[cfg(test)] - pub fn new(genesis_stakes: HashMap, validators: Arc) -> Stake { + pub fn new(genesis_stakes: HashMap) -> Stake { Stake { genesis_stakes, - validators, enable_delegations: true, } } @@ -110,7 +104,7 @@ impl ActionHandler for Stake { quantity, } => { if self.enable_delegations { - delegate_ccs(state, sender, &address, quantity, self.validators.deref()) + delegate_ccs(state, sender, &address, quantity) } else { Err(RuntimeError::FailedToHandleCustomAction("DelegateCCS is disabled".to_string()).into()) } @@ -122,7 +116,17 @@ impl ActionHandler for Stake { if self.enable_delegations { revoke(state, sender, &address, quantity) } else { - Err(RuntimeError::FailedToHandleCustomAction("DelegateCCS is disabled".to_string()).into()) + Err(RuntimeError::FailedToHandleCustomAction("Revoke is disabled".to_string()).into()) + } + } + Action::SelfNominate { + deposit, + .. + } => { + if self.enable_delegations { + self_nominate(state, sender, deposit, 0) + } else { + Err(RuntimeError::FailedToHandleCustomAction("SelfNominate is disabled".to_string()).into()) } } Action::ChangeParams { @@ -146,6 +150,12 @@ impl ActionHandler for Stake { Action::Revoke { .. } => Ok(()), + Action::SelfNominate { + .. + } => { + // FIXME: Metadata size limit + Ok(()) + } Action::ChangeParams { metadata_seq, params, @@ -198,16 +208,13 @@ fn transfer_ccs(state: &mut TopLevelState, sender: &Address, receiver: &Address, Ok(()) } -fn delegate_ccs( - state: &mut TopLevelState, - sender: &Address, - delegatee: &Address, - quantity: u64, - validators: &ValidatorSet, -) -> StateResult<()> { +fn delegate_ccs(state: &mut TopLevelState, sender: &Address, delegatee: &Address, quantity: u64) -> StateResult<()> { // TODO: remove parent hash from validator set. - if !validators.contains_address(&Default::default(), delegatee) { - return Err(RuntimeError::FailedToHandleCustomAction("Cannot delegate to non-validator".into()).into()) + // TODO: handle banned account + // TODO: handle jailed account + let candidates = Candidates::load_from_state(state)?; + if candidates.get_candidate(delegatee).is_none() { + return Err(RuntimeError::FailedToHandleCustomAction("Cannot delegate to non-candidate".into()).into()) } let mut delegator = StakeAccount::load_from_state(state, sender)?; let mut delegation = Delegation::load_from_state(state, &sender)?; @@ -234,6 +241,23 @@ fn revoke(state: &mut TopLevelState, sender: &Address, delegatee: &Address, quan Ok(()) } +fn self_nominate( + state: &mut TopLevelState, + sender: &Address, + deposit: u64, + nomination_ends_at: u64, +) -> StateResult<()> { + // TODO: proper handling of get_current_term + // TODO: proper handling of NOMINATE_EXPIRATION + // TODO: check banned accounts + // TODO: check jailed accounts + let mut candidates = Candidates::load_from_state(&state)?; + state.sub_balance(sender, deposit)?; + candidates.add_deposit(sender, deposit, nomination_ends_at); + candidates.save_to_state(state)?; + Ok(()) +} + pub fn get_stakes(state: &TopLevelState) -> StateResult> { let stakeholders = Stakeholders::load_from_state(state)?; let mut result = HashMap::new(); @@ -299,14 +323,54 @@ fn change_params( Ok(()) } +#[allow(dead_code)] +pub fn on_term_close(state: &mut TopLevelState, current_term: u64) -> StateResult<()> { + // TODO: total_slash = slash_unresponsive(headers, pending_rewards) + // TODO: pending_rewards.update(signature_reward(blocks, total_slash)) + + let mut candidates = Candidates::load_from_state(state)?; + let expired = candidates.drain_expired_candidates(current_term); + for candidate in &expired { + state.add_balance(&candidate.address, candidate.deposit)?; + } + candidates.save_to_state(state)?; + + // TODO: auto_withdraw(pending_rewards) + // TODO: kick(jailed) + + // Stakeholders list isn't changed while reverting. + let reverted: Vec<_> = expired.iter().map(|c| c.address).collect(); + revert_delegations(state, &reverted)?; + + // TODO: validators, validator_order = elect() + Ok(()) +} + +fn revert_delegations(state: &mut TopLevelState, reverted_delegatees: &[Address]) -> StateResult<()> { + let stakeholders = Stakeholders::load_from_state(state)?; + for stakeholder in stakeholders.iter() { + let mut balance = StakeAccount::load_from_state(state, stakeholder)?; + let mut delegation = Delegation::load_from_state(state, stakeholder)?; + + for prisoner in reverted_delegatees.iter() { + let quantity = delegation.get_quantity(prisoner); + if quantity > 0 { + delegation.subtract_quantity(*prisoner, quantity)?; + balance.add_balance(quantity)?; + } + } + delegation.save_to_state(state)?; + balance.save_to_state(state)?; + } + Ok(()) +} + #[cfg(test)] mod tests { use super::action_data::get_account_key; use super::*; - use ckey::{public_to_address, Public}; - use consensus::stake::action_data::get_delegation_key; - use consensus::validator_set::new_validator_set; + use consensus::stake::action_data::{get_delegation_key, Candidate}; use cstate::tests::helpers; use cstate::TopStateView; use rlp::Encodable; @@ -320,7 +384,7 @@ mod tests { let stake = { let mut genesis_stakes = HashMap::new(); genesis_stakes.insert(address1, 100); - Stake::new(genesis_stakes, new_validator_set(Vec::new())) + Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); @@ -345,7 +409,7 @@ mod tests { let stake = { let mut genesis_stakes = HashMap::new(); genesis_stakes.insert(address1, 100); - Stake::new(genesis_stakes, new_validator_set(Vec::new())) + Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); @@ -373,7 +437,7 @@ mod tests { let stake = { let mut genesis_stakes = HashMap::new(); genesis_stakes.insert(address1, 100); - Stake::new(genesis_stakes, new_validator_set(Vec::new())) + Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); @@ -395,8 +459,7 @@ mod tests { #[test] fn delegate() { - let delegatee_public = Public::random(); - let delegatee = public_to_address(&delegatee_public); + let delegatee = Address::random(); let delegator = Address::random(); let mut state = helpers::get_temp_state(); @@ -404,9 +467,10 @@ mod tests { let mut genesis_stakes = HashMap::new(); genesis_stakes.insert(delegatee, 100); genesis_stakes.insert(delegator, 100); - Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) + Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); + self_nominate(&mut state, &delegatee, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -436,8 +500,7 @@ mod tests { #[test] fn delegate_all() { - let delegatee_public = Public::random(); - let delegatee = public_to_address(&delegatee_public); + let delegatee = Address::random(); let delegator = Address::random(); let mut state = helpers::get_temp_state(); @@ -445,9 +508,10 @@ mod tests { let mut genesis_stakes = HashMap::new(); genesis_stakes.insert(delegatee, 100); genesis_stakes.insert(delegator, 100); - Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) + Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); + self_nominate(&mut state, &delegatee, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -477,9 +541,8 @@ mod tests { } #[test] - fn delegate_only_to_validator() { - let delegatee_public = Public::random(); - let delegatee = public_to_address(&delegatee_public); + fn delegate_only_to_candidate() { + let delegatee = Address::random(); let delegator = Address::random(); let mut state = helpers::get_temp_state(); @@ -487,7 +550,7 @@ mod tests { let mut genesis_stakes = HashMap::new(); genesis_stakes.insert(delegatee, 100); genesis_stakes.insert(delegator, 100); - Stake::new(genesis_stakes, new_validator_set(Vec::new())) + Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); @@ -501,8 +564,7 @@ mod tests { #[test] fn delegate_too_much() { - let delegatee_public = Public::random(); - let delegatee = public_to_address(&delegatee_public); + let delegatee = Address::random(); let delegator = Address::random(); let mut state = helpers::get_temp_state(); @@ -510,9 +572,10 @@ mod tests { let mut genesis_stakes = HashMap::new(); genesis_stakes.insert(delegatee, 100); genesis_stakes.insert(delegator, 100); - Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) + Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); + self_nominate(&mut state, &delegatee, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -524,8 +587,7 @@ mod tests { #[test] fn can_transfer_within_non_delegated_tokens() { - let delegatee_public = Public::random(); - let delegatee = public_to_address(&delegatee_public); + let delegatee = Address::random(); let delegator = Address::random(); let mut state = helpers::get_temp_state(); @@ -533,9 +595,10 @@ mod tests { let mut genesis_stakes = HashMap::new(); genesis_stakes.insert(delegatee, 100); genesis_stakes.insert(delegator, 100); - Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) + Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); + self_nominate(&mut state, &delegatee, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -553,8 +616,7 @@ mod tests { #[test] fn cannot_transfer_over_non_delegated_tokens() { - let delegatee_public = Public::random(); - let delegatee = public_to_address(&delegatee_public); + let delegatee = Address::random(); let delegator = Address::random(); let mut state = helpers::get_temp_state(); @@ -562,9 +624,10 @@ mod tests { let mut genesis_stakes = HashMap::new(); genesis_stakes.insert(delegatee, 100); genesis_stakes.insert(delegator, 100); - Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) + Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); + self_nominate(&mut state, &delegatee, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -582,8 +645,7 @@ mod tests { #[test] fn can_revoke_delegated_tokens() { - let delegatee_public = Public::random(); - let delegatee = public_to_address(&delegatee_public); + let delegatee = Address::random(); let delegator = Address::random(); let mut state = helpers::get_temp_state(); @@ -591,9 +653,10 @@ mod tests { let mut genesis_stakes = HashMap::new(); genesis_stakes.insert(delegatee, 100); genesis_stakes.insert(delegator, 100); - Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) + Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); + self_nominate(&mut state, &delegatee, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -618,8 +681,7 @@ mod tests { #[test] fn cannot_revoke_more_than_delegated_tokens() { - let delegatee_public = Public::random(); - let delegatee = public_to_address(&delegatee_public); + let delegatee = Address::random(); let delegator = Address::random(); let mut state = helpers::get_temp_state(); @@ -627,9 +689,10 @@ mod tests { let mut genesis_stakes = HashMap::new(); genesis_stakes.insert(delegatee, 100); genesis_stakes.insert(delegator, 100); - Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) + Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); + self_nominate(&mut state, &delegatee, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -654,8 +717,7 @@ mod tests { #[test] fn revoke_all_should_clear_state() { - let delegatee_public = Public::random(); - let delegatee = public_to_address(&delegatee_public); + let delegatee = Address::random(); let delegator = Address::random(); let mut state = helpers::get_temp_state(); @@ -663,9 +725,10 @@ mod tests { let mut genesis_stakes = HashMap::new(); genesis_stakes.insert(delegatee, 100); genesis_stakes.insert(delegator, 100); - Stake::new(genesis_stakes, new_validator_set(vec![delegatee_public])) + Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); + self_nominate(&mut state, &delegatee, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -685,4 +748,152 @@ mod tests { assert_eq!(delegator_account.balance, 100); assert_eq!(state.action_data(&get_delegation_key(&delegator)).unwrap(), None); } + + #[test] + fn self_nominate_deposit_test() { + let address = Address::random(); + + let mut state = helpers::get_temp_state(); + state.add_balance(&address, 1000).unwrap(); + + let stake = Stake::new(HashMap::new()); + stake.init(&mut state).unwrap(); + + // TODO: change with stake.execute() + let result = self_nominate(&mut state, &address, 0, 5); + assert_eq!(result, Ok(())); + + assert_eq!(state.balance(&address).unwrap(), 1000); + let candidates = Candidates::load_from_state(&state).unwrap(); + assert_eq!( + candidates.get_candidate(&address), + Some(&Candidate { + address, + deposit: 0, + nomination_ends_at: 5, + }), + "nomination_ends_at should be updated even if candidate deposits 0" + ); + + let result = self_nominate(&mut state, &address, 200, 10); + assert_eq!(result, Ok(())); + + assert_eq!(state.balance(&address).unwrap(), 800); + let candidates = Candidates::load_from_state(&state).unwrap(); + assert_eq!( + candidates.get_candidate(&address), + Some(&Candidate { + address, + deposit: 200, + nomination_ends_at: 10, + }) + ); + + let result = self_nominate(&mut state, &address, 0, 15); + assert_eq!(result, Ok(())); + + assert_eq!(state.balance(&address).unwrap(), 800); + let candidates = Candidates::load_from_state(&state).unwrap(); + assert_eq!( + candidates.get_candidate(&address), + Some(&Candidate { + address, + deposit: 200, + nomination_ends_at: 15, + }), + "nomination_ends_at should be updated even if candidate deposits 0" + ); + } + + #[test] + fn self_nominate_fail_with_insufficient_balance() { + let address = Address::random(); + + let mut state = helpers::get_temp_state(); + state.add_balance(&address, 1000).unwrap(); + + let stake = Stake::new(HashMap::new()); + stake.init(&mut state).unwrap(); + + // TODO: change with stake.execute() + let result = self_nominate(&mut state, &address, 2000, 5); + assert!(result.is_err(), "Cannot self-nominate without a sufficient balance"); + } + + #[test] + fn self_nominate_returns_deposits_after_expiration() { + let address = Address::random(); + + let mut state = helpers::get_temp_state(); + state.add_balance(&address, 1000).unwrap(); + + let stake = Stake::new(HashMap::new()); + stake.init(&mut state).unwrap(); + + // TODO: change with stake.execute() + self_nominate(&mut state, &address, 200, 30).unwrap(); + + let result = on_term_close(&mut state, 29); + assert_eq!(result, Ok(())); + + assert_eq!(state.balance(&address).unwrap(), 800, "Should keep nomination before expiration"); + let candidates = Candidates::load_from_state(&state).unwrap(); + assert_eq!( + candidates.get_candidate(&address), + Some(&Candidate { + address, + deposit: 200, + nomination_ends_at: 30, + }), + "Keep deposit before expiration", + ); + + let result = on_term_close(&mut state, 30); + assert_eq!(result, Ok(())); + + assert_eq!(state.balance(&address).unwrap(), 1000, "Return deposit after expiration"); + let candidates = Candidates::load_from_state(&state).unwrap(); + assert_eq!(candidates.get_candidate(&address), None, "Removed from candidates after expiration"); + } + + #[test] + fn self_nominate_reverts_delegations_after_expiration() { + let address = Address::random(); + let delegator = Address::random(); + + let mut state = helpers::get_temp_state(); + state.add_balance(&address, 1000).unwrap(); + + let stake = { + let mut genesis_stakes = HashMap::new(); + genesis_stakes.insert(delegator, 100); + Stake::new(genesis_stakes) + }; + stake.init(&mut state).unwrap(); + + // TODO: change with stake.execute() + self_nominate(&mut state, &address, 0, 30).unwrap(); + + let action = Action::DelegateCCS { + address, + quantity: 40, + }; + stake.execute(&action.rlp_bytes(), &mut state, &delegator).unwrap(); + + let result = on_term_close(&mut state, 29); + assert_eq!(result, Ok(())); + + let account = StakeAccount::load_from_state(&state, &delegator).unwrap(); + assert_eq!(account.balance, 100 - 40); + let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + assert_eq!(delegation.get_quantity(&address), 40, "Should keep delegation before expiration"); + + let result = on_term_close(&mut state, 30); + assert_eq!(result, Ok(())); + + let account = StakeAccount::load_from_state(&state, &delegator).unwrap(); + assert_eq!(account.balance, 100); + let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + assert_eq!(delegation.get_quantity(&address), 0, "Should revert before expiration"); + } } diff --git a/core/src/consensus/tendermint/mod.rs b/core/src/consensus/tendermint/mod.rs index 1fbdea070c..d072ea7f6f 100644 --- a/core/src/consensus/tendermint/mod.rs +++ b/core/src/consensus/tendermint/mod.rs @@ -87,7 +87,7 @@ impl Tendermint { /// Create a new instance of Tendermint engine pub fn new(our_params: TendermintParams, machine: CodeChainMachine) -> Arc { let validators = Arc::clone(&our_params.validators); - let stake = stake::Stake::new(our_params.genesis_stakes, Arc::clone(&validators)); + let stake = stake::Stake::new(our_params.genesis_stakes); let timeouts = our_params.timeouts; let machine = Arc::new(machine); diff --git a/core/src/consensus/validator_set/mod.rs b/core/src/consensus/validator_set/mod.rs index e57c2fe610..8d6ce2fb16 100644 --- a/core/src/consensus/validator_set/mod.rs +++ b/core/src/consensus/validator_set/mod.rs @@ -23,7 +23,6 @@ use primitives::{Bytes, H256}; use self::validator_list::ValidatorList; use crate::client::ConsensusClient; -pub mod null_validator; pub mod validator_list; /// Creates a validator set from validator public keys. diff --git a/core/src/consensus/validator_set/null_validator.rs b/core/src/consensus/validator_set/null_validator.rs deleted file mode 100644 index cd5aeedfb4..0000000000 --- a/core/src/consensus/validator_set/null_validator.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2019. Kodebox, Inc. -// This file is part of CodeChain. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -use ckey::{Address, Public}; -use primitives::H256; - -use super::ValidatorSet; - -/// Validator set containing a known set of public keys. -#[derive(Clone, Debug, PartialEq, Eq, Default)] -pub struct NullValidator {} - -impl ValidatorSet for NullValidator { - fn contains(&self, _bh: &H256, _public: &Public) -> bool { - true - } - - fn contains_address(&self, _bh: &H256, _address: &Address) -> bool { - true - } - - fn get(&self, _parent: &H256, _nonce: usize) -> Public { - unimplemented!() - } - - fn get_index(&self, _parent: &H256, _public: &Public) -> Option { - unimplemented!() - } - - fn get_index_by_address(&self, _parent: &H256, _address: &Address) -> Option { - unimplemented!() - } - - fn count(&self, _parent: &H256) -> usize { - unimplemented!() - } - - fn addresses(&self, _parent: &H256) -> Vec
{ - vec![] - } -} diff --git a/test/src/e2e.long/staking.test.ts b/test/src/e2e.long/staking.test.ts index 1e07e75540..fa50608fd2 100644 --- a/test/src/e2e.long/staking.test.ts +++ b/test/src/e2e.long/staking.test.ts @@ -15,7 +15,11 @@ // along with this program. If not, see . import { expect } from "chai"; -import { H256, PlatformAddress } from "codechain-primitives/lib"; +import { + H256, + PlatformAddress, + PlatformAddressValue +} from "codechain-primitives/lib"; import { toHex } from "codechain-sdk/lib/utils"; import "mocha"; import { @@ -27,6 +31,7 @@ import { validator0Address, validator0Secret, validator1Address, + validator1Secret, validator2Address, validator3Address } from "../helper/constants"; @@ -64,9 +69,10 @@ describe("Staking", function() { }); }); await Promise.all(nodes.map(node => node.start())); + await prepare(); }); - async function connectEachOther() { + async function prepare() { await promiseExpect.shouldFulfill( "connect", Promise.all([ @@ -87,6 +93,26 @@ describe("Staking", function() { nodes[3].waitPeers(4 - 1) ]) ); + + // give some ccc to pay fee + const pay1 = await nodes[0].sendPayTx({ + recipient: validator0Address, + quantity: 100000, + fee: 12, + seq: 0 + }); + const pay2 = await nodes[0].sendPayTx({ + recipient: validator1Address, + quantity: 100000, + fee: 12, + seq: 1 + }); + while ( + !(await nodes[0].sdk.rpc.chain.containsTransaction(pay1.hash())) || + !(await nodes[0].sdk.rpc.chain.containsTransaction(pay2.hash())) + ) { + await wait(500); + } } async function getAllStakingInfo() { @@ -254,6 +280,45 @@ describe("Staking", function() { ); } + async function selfNominate(params: { + senderAddress: PlatformAddress; + senderSecret: string; + deposit: number; + metadata: Buffer | null; + fee?: number; + seq?: number; + waitForEnd?: boolean; + }): Promise { + const { fee = 10, deposit, metadata, waitForEnd = true } = params; + const seq = + params.seq == null + ? await nodes[0].sdk.rpc.chain.getSeq(params.senderAddress) + : params.seq; + + const promise = promiseExpect.shouldFulfill( + "sendSignTransaction", + nodes[0].sdk.rpc.chain.sendSignedTransaction( + nodes[0].sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: Buffer.from(RLP.encode([4, deposit, metadata])) + }) + .sign({ + secret: params.senderSecret, + seq, + fee + }) + ) + ); + if (waitForEnd) { + const hash = await promise; + while (!(await nodes[0].sdk.rpc.chain.containsTransaction(hash))) { + await wait(500); + } + } + return promise; + } + it("should have proper initial stake tokens", async function() { const { amounts, stakeholders } = await getAllStakingInfo(); expect(amounts).to.be.deep.equal([ @@ -278,8 +343,6 @@ describe("Staking", function() { }); it("should send stake tokens", async function() { - await connectEachOther(); - const hash = await sendStakeToken({ senderAddress: faucetAddress, senderSecret: faucetSecret, @@ -315,8 +378,6 @@ describe("Staking", function() { }).timeout(60_000); it("doesn't leave zero balance account after transfer", async function() { - await connectEachOther(); - const hash = await sendStakeToken({ senderAddress: faucetAddress, senderSecret: faucetSecret, @@ -351,7 +412,12 @@ describe("Staking", function() { }).timeout(60_000); it("can delegate tokens", async function() { - await connectEachOther(); + await selfNominate({ + senderAddress: validator0Address, + senderSecret: validator0Secret, + deposit: 0, + metadata: null + }); const hash = await delegateToken({ senderAddress: faucetAddress, @@ -391,7 +457,12 @@ describe("Staking", function() { }); it("doesn't leave zero balanced account after delegate", async function() { - await connectEachOther(); + await selfNominate({ + senderAddress: validator0Address, + senderSecret: validator0Secret, + deposit: 0, + metadata: null + }); const hash = await delegateToken({ senderAddress: faucetAddress, @@ -430,19 +501,7 @@ describe("Staking", function() { ]); }); - it("cannot delegate to non-validator", async function() { - await connectEachOther(); - // give some ccc to pay fee - const pay1 = await nodes[0].sendPayTx({ - recipient: validator0Address, - quantity: 100000 - }); - - while ( - !(await nodes[0].sdk.rpc.chain.containsTransaction(pay1.hash())) - ) { - await wait(500); - } + it("cannot delegate to non-candidate", async function() { // give some ccs to delegate. const hash1 = await sendStakeToken({ @@ -487,7 +546,12 @@ describe("Staking", function() { }); it("can revoke tokens", async function() { - await connectEachOther(); + await selfNominate({ + senderAddress: validator0Address, + senderSecret: validator0Secret, + deposit: 0, + metadata: null + }); const delegateHash = await delegateToken({ senderAddress: faucetAddress, @@ -538,7 +602,12 @@ describe("Staking", function() { }); it("cannot revoke more than delegated", async function() { - await connectEachOther(); + await selfNominate({ + senderAddress: validator0Address, + senderSecret: validator0Secret, + deposit: 0, + metadata: null + }); const delegateHash = await delegateToken({ senderAddress: faucetAddress, @@ -610,7 +679,12 @@ describe("Staking", function() { }); it("revoking all should clear delegation", async function() { - await connectEachOther(); + await selfNominate({ + senderAddress: validator0Address, + senderSecret: validator0Secret, + deposit: 0, + metadata: null + }); const delegateHash = await delegateToken({ senderAddress: faucetAddress, @@ -659,8 +733,6 @@ describe("Staking", function() { }); it("get fee in proportion to holding stakes", async function() { - await connectEachOther(); - // faucet: 70000, alice: 20000, bob: 10000 const fee = 1000; const hash = await sendStakeToken({ @@ -757,7 +829,13 @@ describe("Staking", function() { }); it("get fee even if it delegated stakes to other", async function() { - await connectEachOther(); + await selfNominate({ + senderAddress: validator1Address, + senderSecret: validator1Secret, + deposit: 0, + metadata: null + }); + // faucet: 70000, alice: 20000, bob: 10000 const hash1 = await sendStakeToken({ senderAddress: faucetAddress, @@ -897,7 +975,13 @@ describe("Staking", function() { }); it("get fee even if it delegated stakes to other stakeholder", async function() { - await connectEachOther(); + await selfNominate({ + senderAddress: validator1Address, + senderSecret: validator1Secret, + deposit: 0, + metadata: null + }); + // faucet: 70000, alice: 20000, bob: 10000 const hash1 = await sendStakeToken({ senderAddress: faucetAddress, @@ -1268,6 +1352,7 @@ describe("Staking-disable-delegation", function() { it("cannot delegate tokens", async function() { await connectEachOther(); + const hash = await delegateToken({ senderAddress: faucetAddress, senderSecret: faucetSecret, diff --git a/test/src/e2e/staking.test.ts b/test/src/e2e/staking.test.ts index 6d1007567e..36d39412ef 100644 --- a/test/src/e2e/staking.test.ts +++ b/test/src/e2e/staking.test.ts @@ -29,7 +29,8 @@ import { stakeActionHandlerId, validator0Address, validator0Secret, - validator1Address + validator1Address, + validator1Secret } from "../helper/constants"; import { PromiseExpect } from "../helper/promise"; import CodeChain from "../helper/spawn"; @@ -51,6 +52,10 @@ describe("Staking", function() { }); await node.start(); await node.sendPayTx({ recipient: aliceAddress, quantity: 100_000 }); + await node.sendPayTx({ + recipient: validator1Address, + quantity: 100_000 + }); }); async function getAllStakingInfo() { @@ -218,6 +223,37 @@ describe("Staking", function() { ); } + async function selfNominate(params: { + senderAddress: PlatformAddress; + senderSecret: string; + deposit: number; + metadata: Buffer | null; + fee?: number; + seq?: number; + }): Promise { + const { fee = 10, deposit, metadata } = params; + const seq = + params.seq == null + ? await node.sdk.rpc.chain.getSeq(params.senderAddress) + : params.seq; + + return promiseExpect.shouldFulfill( + "sendSignTransaction", + node.sdk.rpc.chain.sendSignedTransaction( + node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: Buffer.from(RLP.encode([4, deposit, metadata])) + }) + .sign({ + secret: params.senderSecret, + seq, + fee + }) + ) + ); + } + it("should have proper initial stake tokens", async function() { const { amounts, stakeholders } = await getAllStakingInfo(); expect(amounts).to.be.deep.equal([ @@ -308,6 +344,13 @@ describe("Staking", function() { }).timeout(60_000); it("can delegate tokens", async function() { + await selfNominate({ + senderAddress: validator0Address, + senderSecret: validator0Secret, + deposit: 0, + metadata: null + }); + await delegateToken({ senderAddress: aliceAddress, senderSecret: aliceSecret, @@ -343,6 +386,13 @@ describe("Staking", function() { }); it("doesn't leave zero balanced account after delegate", async function() { + await selfNominate({ + senderAddress: validator0Address, + senderSecret: validator0Secret, + deposit: 0, + metadata: null + }); + await delegateToken({ senderAddress: aliceAddress, senderSecret: aliceSecret, @@ -378,6 +428,13 @@ describe("Staking", function() { }); it("can revoke tokens", async function() { + await selfNominate({ + senderAddress: validator0Address, + senderSecret: validator0Secret, + deposit: 0, + metadata: null + }); + await delegateToken({ senderAddress: aliceAddress, senderSecret: aliceSecret, @@ -418,6 +475,13 @@ describe("Staking", function() { }); it("cannot revoke more than delegated", async function() { + await selfNominate({ + senderAddress: validator0Address, + senderSecret: validator0Secret, + deposit: 0, + metadata: null + }); + await delegateToken({ senderAddress: aliceAddress, senderSecret: aliceSecret, @@ -470,6 +534,13 @@ describe("Staking", function() { }); it("revoking all should clear delegation", async function() { + await selfNominate({ + senderAddress: validator0Address, + senderSecret: validator0Secret, + deposit: 0, + metadata: null + }); + await delegateToken({ senderAddress: aliceAddress, senderSecret: aliceSecret, @@ -581,6 +652,13 @@ describe("Staking", function() { }); it("get fee even if it delegated stakes to other", async function() { + await selfNominate({ + senderAddress: validator1Address, + senderSecret: validator1Secret, + deposit: 0, + metadata: null + }); + // alice: 40000, bob: 30000, carol 20000, dave: 10000 await sendStakeToken({ senderAddress: aliceAddress, @@ -691,6 +769,13 @@ describe("Staking", function() { }); it("get fee even if it delegated stakes to other stakeholder", async function() { + await selfNominate({ + senderAddress: validator1Address, + senderSecret: validator1Secret, + deposit: 0, + metadata: null + }); + // alice: 40000, bob: 30000, carol 20000, dave: 10000 await sendStakeToken({ senderAddress: aliceAddress, From 390bdc15c758d766ff6bb2e8287637867d82350e Mon Sep 17 00:00:00 2001 From: SeongChan Lee Date: Mon, 27 May 2019 14:33:31 +0900 Subject: [PATCH 5/6] Implement jail --- core/src/consensus/stake/action_data.rs | 303 ++++++++++++++++++++ core/src/consensus/stake/mod.rs | 357 ++++++++++++++++++++++-- 2 files changed, 637 insertions(+), 23 deletions(-) diff --git a/core/src/consensus/stake/action_data.rs b/core/src/consensus/stake/action_data.rs index bb3e34fc93..175c343058 100644 --- a/core/src/consensus/stake/action_data.rs +++ b/core/src/consensus/stake/action_data.rs @@ -37,6 +37,7 @@ lazy_static! { ActionDataKeyBuilder::new(CUSTOM_ACTION_HANDLER_ID, 1).append(&"StakeholderAddresses").into_key(); pub static ref CANDIDATES_KEY: H256 = ActionDataKeyBuilder::new(CUSTOM_ACTION_HANDLER_ID, 1).append(&"Candidates").into_key(); + pub static ref JAIL_KEY: H256 = ActionDataKeyBuilder::new(CUSTOM_ACTION_HANDLER_ID, 1).append(&"Jail").into_key(); } pub fn get_delegation_key(address: &Address) -> H256 { @@ -313,6 +314,84 @@ impl Candidates { self.0 = retained.into_iter().map(|c| (c.address, c)).collect(); expired } + + pub fn remove(&mut self, address: &Address) -> Option { + self.0.remove(address) + } +} + +pub struct Jail(BTreeMap); +#[derive(Clone, Debug, Eq, PartialEq, RlpEncodable, RlpDecodable)] +pub struct Prisoner { + pub address: Address, + pub deposit: Deposit, + pub custody_until: u64, + pub kicked_at: u64, +} + +#[derive(Debug, Eq, PartialEq)] +pub enum ReleaseResult { + NotExists, + InCustody, + Released(Prisoner), +} + +impl Jail { + pub fn load_from_state(state: &TopLevelState) -> StateResult { + let key = *JAIL_KEY; + let prisoner = state.action_data(&key)?.map(|data| decode_list::(&data)).unwrap_or_default(); + let indexed = prisoner.into_iter().map(|c| (c.address, c)).collect(); + Ok(Jail(indexed)) + } + + pub fn save_to_state(&self, state: &mut TopLevelState) -> StateResult<()> { + let key = *JAIL_KEY; + if !self.0.is_empty() { + let encoded = encode_iter(self.0.values()); + state.update_action_data(&key, encoded)?; + } else { + state.remove_action_data(&key); + } + Ok(()) + } + + pub fn get_prisoner(&self, address: &Address) -> Option<&Prisoner> { + self.0.get(address) + } + + #[cfg(test)] + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn add(&mut self, candidate: Candidate, custody_until: u64, kicked_at: u64) { + assert!(custody_until <= kicked_at); + self.0.insert(candidate.address, Prisoner { + address: candidate.address, + deposit: candidate.deposit, + custody_until, + kicked_at, + }); + } + + pub fn try_release(&mut self, address: &Address, term_index: u64) -> ReleaseResult { + match self.0.entry(*address) { + Entry::Occupied(entry) => { + if entry.get().custody_until < term_index { + ReleaseResult::Released(entry.remove()) + } else { + ReleaseResult::InCustody + } + } + _ => ReleaseResult::NotExists, + } + } + + pub fn kick_prisoners(&mut self, term_index: u64) -> Vec { + let (kicked, retained): (Vec<_>, Vec<_>) = self.0.values().cloned().partition(|c| c.kicked_at <= term_index); + self.0 = retained.into_iter().map(|c| (c.address, c)).collect(); + kicked + } } fn decode_set(data: Option<&ActionData>) -> BTreeSet @@ -956,4 +1035,228 @@ mod tests { let result = state.action_data(&*CANDIDATES_KEY).unwrap(); assert_eq!(result, None); } + + #[test] + fn jail_try_free_not_existing() { + let mut state = helpers::get_temp_state(); + + // Prepare + let address = Address::from(1); + let mut jail = Jail::load_from_state(&state).unwrap(); + jail.add( + Candidate { + address, + deposit: 100, + nomination_ends_at: 0, + }, + 10, + 20, + ); + jail.save_to_state(&mut state).unwrap(); + + let mut jail = Jail::load_from_state(&state).unwrap(); + let freed = jail.try_release(&Address::from(1000), 5); + assert_eq!(freed, ReleaseResult::NotExists); + assert_eq!(jail.len(), 1); + assert_ne!(jail.get_prisoner(&address), None); + } + + #[test] + fn jail_try_release_none_until_custody() { + let mut state = helpers::get_temp_state(); + + // Prepare + let address = Address::from(1); + let mut jail = Jail::load_from_state(&state).unwrap(); + jail.add( + Candidate { + address, + deposit: 100, + nomination_ends_at: 0, + }, + 10, + 20, + ); + jail.save_to_state(&mut state).unwrap(); + + let mut jail = Jail::load_from_state(&state).unwrap(); + let released = jail.try_release(&address, 10); + assert_eq!(released, ReleaseResult::InCustody); + assert_eq!(jail.len(), 1); + assert_ne!(jail.get_prisoner(&address), None); + } + + #[test] + fn jail_try_release_prisoner_after_custody() { + let mut state = helpers::get_temp_state(); + + // Prepare + let address = Address::from(1); + let mut jail = Jail::load_from_state(&state).unwrap(); + jail.add( + Candidate { + address, + deposit: 100, + nomination_ends_at: 0, + }, + 10, + 20, + ); + jail.save_to_state(&mut state).unwrap(); + + let mut jail = Jail::load_from_state(&state).unwrap(); + let released = jail.try_release(&address, 11); + jail.save_to_state(&mut state).unwrap(); + + // Assert + assert_eq!( + released, + ReleaseResult::Released(Prisoner { + address, + deposit: 100, + custody_until: 10, + kicked_at: 20, + }) + ); + assert_eq!(jail.len(), 0); + assert_eq!(jail.get_prisoner(&address), None); + + let result = state.action_data(&*JAIL_KEY).unwrap(); + assert_eq!(result, None, "Should clean the state if all prisoners are released"); + } + + #[test] + fn jail_keep_prisoners_until_kick_at() { + let mut state = helpers::get_temp_state(); + + // Prepare + let mut jail = Jail::load_from_state(&state).unwrap(); + jail.add( + Candidate { + address: Address::from(1), + deposit: 100, + nomination_ends_at: 0, + }, + 10, + 20, + ); + jail.add( + Candidate { + address: Address::from(2), + deposit: 200, + nomination_ends_at: 0, + }, + 15, + 25, + ); + jail.save_to_state(&mut state).unwrap(); + + // Kick + let mut jail = Jail::load_from_state(&state).unwrap(); + let kicked = jail.kick_prisoners(19); + jail.save_to_state(&mut state).unwrap(); + + // Assert + assert_eq!(kicked, Vec::new()); + assert_eq!(jail.len(), 2); + assert_ne!(jail.get_prisoner(&Address::from(1)), None); + assert_ne!(jail.get_prisoner(&Address::from(2)), None); + } + + #[test] + fn jail_partially_kick_prisoners() { + let mut state = helpers::get_temp_state(); + + // Prepare + let mut jail = Jail::load_from_state(&state).unwrap(); + jail.add( + Candidate { + address: Address::from(1), + deposit: 100, + nomination_ends_at: 0, + }, + 10, + 20, + ); + jail.add( + Candidate { + address: Address::from(2), + deposit: 200, + nomination_ends_at: 0, + }, + 15, + 25, + ); + jail.save_to_state(&mut state).unwrap(); + + // Kick + let mut jail = Jail::load_from_state(&state).unwrap(); + let kicked = jail.kick_prisoners(20); + jail.save_to_state(&mut state).unwrap(); + + // Assert + assert_eq!(kicked, vec![Prisoner { + address: Address::from(1), + deposit: 100, + custody_until: 10, + kicked_at: 20, + }]); + assert_eq!(jail.len(), 1); + assert_eq!(jail.get_prisoner(&Address::from(1)), None); + assert_ne!(jail.get_prisoner(&Address::from(2)), None); + } + + #[test] + fn jail_kick_all_prisoners() { + let mut state = helpers::get_temp_state(); + + // Prepare + let mut jail = Jail::load_from_state(&state).unwrap(); + jail.add( + Candidate { + address: Address::from(1), + deposit: 100, + nomination_ends_at: 0, + }, + 10, + 20, + ); + jail.add( + Candidate { + address: Address::from(2), + deposit: 200, + nomination_ends_at: 0, + }, + 15, + 25, + ); + jail.save_to_state(&mut state).unwrap(); + + // Kick + let mut jail = Jail::load_from_state(&state).unwrap(); + let kicked = jail.kick_prisoners(25); + jail.save_to_state(&mut state).unwrap(); + + // Assert + assert_eq!(kicked, vec![ + Prisoner { + address: Address::from(1), + deposit: 100, + custody_until: 10, + kicked_at: 20, + }, + Prisoner { + address: Address::from(2), + deposit: 200, + custody_until: 15, + kicked_at: 25, + } + ]); + assert_eq!(jail.len(), 0); + assert_eq!(jail.get_prisoner(&Address::from(1)), None); + assert_eq!(jail.get_prisoner(&Address::from(2)), None); + + let result = state.action_data(&*JAIL_KEY).unwrap(); + assert_eq!(result, None, "Should clean the state if all prisoners are kicked"); + } } diff --git a/core/src/consensus/stake/mod.rs b/core/src/consensus/stake/mod.rs index e9d4d68618..09cdd33eb8 100644 --- a/core/src/consensus/stake/mod.rs +++ b/core/src/consensus/stake/mod.rs @@ -30,7 +30,7 @@ use ctypes::{CommonParams, Header}; use primitives::H256; use rlp::{Decodable, UntrustedRlp}; -use self::action_data::{Candidates, Delegation, IntermediateRewards, StakeAccount, Stakeholders}; +use self::action_data::{Candidates, Delegation, IntermediateRewards, Jail, ReleaseResult, StakeAccount, Stakeholders}; use self::actions::Action; pub use self::distribute::fee_distribute; @@ -124,7 +124,7 @@ impl ActionHandler for Stake { .. } => { if self.enable_delegations { - self_nominate(state, sender, deposit, 0) + self_nominate(state, sender, deposit, 0, 0) } else { Err(RuntimeError::FailedToHandleCustomAction("SelfNominate is disabled".to_string()).into()) } @@ -213,8 +213,11 @@ fn delegate_ccs(state: &mut TopLevelState, sender: &Address, delegatee: &Address // TODO: handle banned account // TODO: handle jailed account let candidates = Candidates::load_from_state(state)?; - if candidates.get_candidate(delegatee).is_none() { - return Err(RuntimeError::FailedToHandleCustomAction("Cannot delegate to non-candidate".into()).into()) + let jail = Jail::load_from_state(state)?; + if candidates.get_candidate(delegatee).is_none() && jail.get_prisoner(delegatee).is_none() { + return Err( + RuntimeError::FailedToHandleCustomAction("Can delegate to who is a candidate or a prisoner".into()).into() + ) } let mut delegator = StakeAccount::load_from_state(state, sender)?; let mut delegation = Delegation::load_from_state(state, &sender)?; @@ -245,15 +248,30 @@ fn self_nominate( state: &mut TopLevelState, sender: &Address, deposit: u64, + current_term: u64, nomination_ends_at: u64, ) -> StateResult<()> { // TODO: proper handling of get_current_term // TODO: proper handling of NOMINATE_EXPIRATION // TODO: check banned accounts - // TODO: check jailed accounts + + let mut jail = Jail::load_from_state(&state)?; + let total_deposit = match jail.try_release(sender, current_term) { + ReleaseResult::InCustody => { + return Err(RuntimeError::FailedToHandleCustomAction("Account is still in custody".to_string()).into()) + } + ReleaseResult::NotExists => deposit, + ReleaseResult::Released(prisoner) => { + assert_eq!(&prisoner.address, sender); + prisoner.deposit + deposit + } + }; + let mut candidates = Candidates::load_from_state(&state)?; state.sub_balance(sender, deposit)?; - candidates.add_deposit(sender, deposit, nomination_ends_at); + candidates.add_deposit(sender, total_deposit, nomination_ends_at); + + jail.save_to_state(state)?; candidates.save_to_state(state)?; Ok(()) } @@ -336,16 +354,35 @@ pub fn on_term_close(state: &mut TopLevelState, current_term: u64) -> StateResul candidates.save_to_state(state)?; // TODO: auto_withdraw(pending_rewards) - // TODO: kick(jailed) + + let mut jailed = Jail::load_from_state(&state)?; + let kicked = jailed.kick_prisoners(current_term); + for prisoner in &kicked { + state.add_balance(&prisoner.address, prisoner.deposit)?; + } + jailed.save_to_state(state)?; // Stakeholders list isn't changed while reverting. - let reverted: Vec<_> = expired.iter().map(|c| c.address).collect(); + let reverted: Vec<_> = expired.iter().map(|c| c.address).chain(kicked.iter().map(|p| p.address)).collect(); revert_delegations(state, &reverted)?; // TODO: validators, validator_order = elect() Ok(()) } +#[allow(dead_code)] +pub fn jail(state: &mut TopLevelState, address: &Address, custody_until: u64, kick_at: u64) -> StateResult<()> { + let mut candidates = Candidates::load_from_state(state)?; + let mut jail = Jail::load_from_state(state)?; + + let candidate = candidates.remove(address).expect("There should be a candidate to jail"); + jail.add(candidate, custody_until, kick_at); + + jail.save_to_state(state)?; + candidates.save_to_state(state)?; + Ok(()) +} + fn revert_delegations(state: &mut TopLevelState, reverted_delegatees: &[Address]) -> StateResult<()> { let stakeholders = Stakeholders::load_from_state(state)?; for stakeholder in stakeholders.iter() { @@ -370,7 +407,7 @@ mod tests { use super::action_data::get_account_key; use super::*; - use consensus::stake::action_data::{get_delegation_key, Candidate}; + use consensus::stake::action_data::{get_delegation_key, Candidate, Prisoner}; use cstate::tests::helpers; use cstate::TopStateView; use rlp::Encodable; @@ -470,7 +507,7 @@ mod tests { Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); - self_nominate(&mut state, &delegatee, 0, 10).unwrap(); + self_nominate(&mut state, &delegatee, 0, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -511,7 +548,7 @@ mod tests { Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); - self_nominate(&mut state, &delegatee, 0, 10).unwrap(); + self_nominate(&mut state, &delegatee, 0, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -575,7 +612,7 @@ mod tests { Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); - self_nominate(&mut state, &delegatee, 0, 10).unwrap(); + self_nominate(&mut state, &delegatee, 0, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -598,7 +635,7 @@ mod tests { Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); - self_nominate(&mut state, &delegatee, 0, 10).unwrap(); + self_nominate(&mut state, &delegatee, 0, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -627,7 +664,7 @@ mod tests { Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); - self_nominate(&mut state, &delegatee, 0, 10).unwrap(); + self_nominate(&mut state, &delegatee, 0, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -656,7 +693,7 @@ mod tests { Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); - self_nominate(&mut state, &delegatee, 0, 10).unwrap(); + self_nominate(&mut state, &delegatee, 0, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -692,7 +729,7 @@ mod tests { Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); - self_nominate(&mut state, &delegatee, 0, 10).unwrap(); + self_nominate(&mut state, &delegatee, 0, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -728,7 +765,7 @@ mod tests { Stake::new(genesis_stakes) }; stake.init(&mut state).unwrap(); - self_nominate(&mut state, &delegatee, 0, 10).unwrap(); + self_nominate(&mut state, &delegatee, 0, 0, 10).unwrap(); let action = Action::DelegateCCS { address: delegatee, @@ -760,7 +797,7 @@ mod tests { stake.init(&mut state).unwrap(); // TODO: change with stake.execute() - let result = self_nominate(&mut state, &address, 0, 5); + let result = self_nominate(&mut state, &address, 0, 0, 5); assert_eq!(result, Ok(())); assert_eq!(state.balance(&address).unwrap(), 1000); @@ -775,7 +812,7 @@ mod tests { "nomination_ends_at should be updated even if candidate deposits 0" ); - let result = self_nominate(&mut state, &address, 200, 10); + let result = self_nominate(&mut state, &address, 200, 0, 10); assert_eq!(result, Ok(())); assert_eq!(state.balance(&address).unwrap(), 800); @@ -789,7 +826,7 @@ mod tests { }) ); - let result = self_nominate(&mut state, &address, 0, 15); + let result = self_nominate(&mut state, &address, 0, 0, 15); assert_eq!(result, Ok(())); assert_eq!(state.balance(&address).unwrap(), 800); @@ -816,7 +853,7 @@ mod tests { stake.init(&mut state).unwrap(); // TODO: change with stake.execute() - let result = self_nominate(&mut state, &address, 2000, 5); + let result = self_nominate(&mut state, &address, 2000, 0, 5); assert!(result.is_err(), "Cannot self-nominate without a sufficient balance"); } @@ -831,7 +868,7 @@ mod tests { stake.init(&mut state).unwrap(); // TODO: change with stake.execute() - self_nominate(&mut state, &address, 200, 30).unwrap(); + self_nominate(&mut state, &address, 200, 0, 30).unwrap(); let result = on_term_close(&mut state, 29); assert_eq!(result, Ok(())); @@ -872,7 +909,7 @@ mod tests { stake.init(&mut state).unwrap(); // TODO: change with stake.execute() - self_nominate(&mut state, &address, 0, 30).unwrap(); + self_nominate(&mut state, &address, 0, 0, 30).unwrap(); let action = Action::DelegateCCS { address, @@ -896,4 +933,278 @@ mod tests { let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); assert_eq!(delegation.get_quantity(&address), 0, "Should revert before expiration"); } + + #[test] + fn jail_candidate() { + let address = Address::random(); + + let mut state = helpers::get_temp_state(); + state.add_balance(&address, 1000).unwrap(); + + let stake = Stake::new(HashMap::new()); + stake.init(&mut state).unwrap(); + + // TODO: change with stake.execute() + let deposit = 200; + self_nominate(&mut state, &address, deposit, 0, 5).unwrap(); + + let custody_until = 10; + let kicked_at = 20; + let result = jail(&mut state, &address, custody_until, kicked_at); + assert!(result.is_ok()); + + let candidates = Candidates::load_from_state(&state).unwrap(); + assert_eq!(candidates.get_candidate(&address), None, "The candidate is removed"); + + let jail = Jail::load_from_state(&state).unwrap(); + assert_eq!( + jail.get_prisoner(&address), + Some(&Prisoner { + address, + deposit, + custody_until, + kicked_at, + }), + "The candidate become a prisoner" + ); + + assert_eq!(state.balance(&address).unwrap(), 1000 - deposit, "Deposited ccs is temporarily unavailable"); + } + + #[test] + fn cannot_self_nominate_while_custody() { + let address = Address::random(); + + let mut state = helpers::get_temp_state(); + state.add_balance(&address, 1000).unwrap(); + + let stake = Stake::new(HashMap::new()); + stake.init(&mut state).unwrap(); + + // TODO: change with stake.execute() + let deposit = 200; + let nominate_expire = 5; + let custody_until = 10; + let kicked_at = 20; + self_nominate(&mut state, &address, deposit, 0, nominate_expire).unwrap(); + jail(&mut state, &address, custody_until, kicked_at).unwrap(); + + for current_term in 0..=custody_until { + let result = self_nominate(&mut state, &address, 0, current_term, current_term + nominate_expire); + assert!( + result.is_err(), + "Shouldn't nominate while current_term({}) <= custody_until({})", + current_term, + custody_until + ); + on_term_close(&mut state, current_term).unwrap(); + } + } + + #[test] + fn can_self_nominate_after_custody() { + let address = Address::random(); + + let mut state = helpers::get_temp_state(); + state.add_balance(&address, 1000).unwrap(); + + let stake = Stake::new(HashMap::new()); + stake.init(&mut state).unwrap(); + + // TODO: change with stake.execute() + let deposit = 200; + let nominate_expire = 5; + let custody_until = 10; + let kicked_at = 20; + self_nominate(&mut state, &address, deposit, 0, nominate_expire).unwrap(); + jail(&mut state, &address, custody_until, kicked_at).unwrap(); + for current_term in 0..=custody_until { + on_term_close(&mut state, current_term).unwrap(); + } + + let current_term = custody_until + 1; + let additional_deposit = 123; + let result = + self_nominate(&mut state, &address, additional_deposit, current_term, current_term + nominate_expire); + assert!(result.is_ok()); + + let candidates = Candidates::load_from_state(&state).unwrap(); + assert_eq!( + candidates.get_candidate(&address), + Some(&Candidate { + deposit: deposit + additional_deposit, + nomination_ends_at: current_term + nominate_expire, + address, + }), + "The prisoner is become a candidate", + ); + + let jail = Jail::load_from_state(&state).unwrap(); + assert_eq!(jail.get_prisoner(&address), None, "The prisoner is removed"); + + assert_eq!(state.balance(&address).unwrap(), 1000 - deposit - additional_deposit, "Deposit is accumulated"); + } + + #[test] + fn jail_kicked_after() { + let address = Address::random(); + + let mut state = helpers::get_temp_state(); + state.add_balance(&address, 1000).unwrap(); + + let stake = Stake::new(HashMap::new()); + stake.init(&mut state).unwrap(); + + // TODO: change with stake.execute() + let deposit = 200; + let nominate_expire = 5; + let custody_until = 10; + let kicked_at = 20; + self_nominate(&mut state, &address, deposit, 0, nominate_expire).unwrap(); + jail(&mut state, &address, custody_until, kicked_at).unwrap(); + + for current_term in 0..kicked_at { + on_term_close(&mut state, current_term).unwrap(); + + let candidates = Candidates::load_from_state(&state).unwrap(); + assert_eq!(candidates.get_candidate(&address), None); + + let jail = Jail::load_from_state(&state).unwrap(); + assert!(jail.get_prisoner(&address).is_some()); + } + + on_term_close(&mut state, kicked_at).unwrap(); + + let candidates = Candidates::load_from_state(&state).unwrap(); + assert_eq!(candidates.get_candidate(&address), None, "A prisoner should not become a candidate"); + + let jail = Jail::load_from_state(&state).unwrap(); + assert_eq!(jail.get_prisoner(&address), None, "A prisoner should be kicked"); + + assert_eq!(state.balance(&address).unwrap(), 1000, "Balance should be restored after being kicked"); + } + + #[test] + fn can_delegate_until_kicked() { + let address = Address::random(); + let delegator = Address::random(); + + let mut state = helpers::get_temp_state(); + state.add_balance(&address, 1000).unwrap(); + + let stake = { + let mut genesis_stakes = HashMap::new(); + genesis_stakes.insert(delegator, 100); + Stake::new(genesis_stakes) + }; + stake.init(&mut state).unwrap(); + + // TODO: change with stake.execute() + let deposit = 200; + let nominate_expire = 5; + let custody_until = 10; + let kicked_at = 20; + self_nominate(&mut state, &address, deposit, 0, nominate_expire).unwrap(); + jail(&mut state, &address, custody_until, kicked_at).unwrap(); + + for current_term in 0..=kicked_at { + let action = Action::DelegateCCS { + address, + quantity: 1, + }; + let result = stake.execute(&action.rlp_bytes(), &mut state, &delegator); + assert!(result.is_ok()); + + on_term_close(&mut state, current_term).unwrap(); + } + + let action = Action::DelegateCCS { + address, + quantity: 1, + }; + let result = stake.execute(&action.rlp_bytes(), &mut state, &delegator); + assert!(result.is_err()); + } + + #[test] + fn kick_reverts_delegations() { + let address = Address::random(); + let delegator = Address::random(); + + let mut state = helpers::get_temp_state(); + state.add_balance(&address, 1000).unwrap(); + + let stake = { + let mut genesis_stakes = HashMap::new(); + genesis_stakes.insert(delegator, 100); + Stake::new(genesis_stakes) + }; + stake.init(&mut state).unwrap(); + + // TODO: change with stake.execute() + let deposit = 200; + let nominate_expire = 5; + let custody_until = 10; + let kicked_at = 20; + self_nominate(&mut state, &address, deposit, 0, nominate_expire).unwrap(); + jail(&mut state, &address, custody_until, kicked_at).unwrap(); + + let action = Action::DelegateCCS { + address, + quantity: 40, + }; + stake.execute(&action.rlp_bytes(), &mut state, &delegator).unwrap(); + + for current_term in 0..=kicked_at { + on_term_close(&mut state, current_term).unwrap(); + } + + let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + assert_eq!(delegation.get_quantity(&address), 0, "Delegation should be reverted"); + + let account = StakeAccount::load_from_state(&state, &delegator).unwrap(); + assert_eq!(account.balance, 100, "Delegation should be reverted"); + } + + #[test] + fn self_nomination_before_kick_preserves_delegations() { + let address = Address::random(); + let delegator = Address::random(); + + let mut state = helpers::get_temp_state(); + state.add_balance(&address, 1000).unwrap(); + + let stake = { + let mut genesis_stakes = HashMap::new(); + genesis_stakes.insert(delegator, 100); + Stake::new(genesis_stakes) + }; + stake.init(&mut state).unwrap(); + + // TODO: change with stake.execute() + let nominate_expire = 5; + let custody_until = 10; + let kicked_at = 20; + self_nominate(&mut state, &address, 0, 0, nominate_expire).unwrap(); + jail(&mut state, &address, custody_until, kicked_at).unwrap(); + + let action = Action::DelegateCCS { + address, + quantity: 40, + }; + stake.execute(&action.rlp_bytes(), &mut state, &delegator).unwrap(); + for current_term in 0..custody_until { + on_term_close(&mut state, current_term).unwrap(); + } + + let current_term = custody_until + 1; + let result = self_nominate(&mut state, &address, 0, current_term, current_term + nominate_expire); + assert!(result.is_ok()); + + let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + assert_eq!(delegation.get_quantity(&address), 40, "Delegation should be preserved"); + + let account = StakeAccount::load_from_state(&state, &delegator).unwrap(); + assert_eq!(account.balance, 100 - 40, "Delegation should be preserved"); + } } From dfffeb4284638866d9d82884f44740bfcb4d7729 Mon Sep 17 00:00:00 2001 From: SeongChan Lee Date: Tue, 28 May 2019 11:55:04 +0900 Subject: [PATCH 6/6] Implement ban --- core/src/consensus/stake/action_data.rs | 60 +++++++++++++++ core/src/consensus/stake/mod.rs | 98 ++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 4 deletions(-) diff --git a/core/src/consensus/stake/action_data.rs b/core/src/consensus/stake/action_data.rs index 175c343058..5382ac8c8e 100644 --- a/core/src/consensus/stake/action_data.rs +++ b/core/src/consensus/stake/action_data.rs @@ -38,6 +38,8 @@ lazy_static! { pub static ref CANDIDATES_KEY: H256 = ActionDataKeyBuilder::new(CUSTOM_ACTION_HANDLER_ID, 1).append(&"Candidates").into_key(); pub static ref JAIL_KEY: H256 = ActionDataKeyBuilder::new(CUSTOM_ACTION_HANDLER_ID, 1).append(&"Jail").into_key(); + pub static ref BANNED_KEY: H256 = + ActionDataKeyBuilder::new(CUSTOM_ACTION_HANDLER_ID, 1).append(&"Banned").into_key(); } pub fn get_delegation_key(address: &Address) -> H256 { @@ -374,6 +376,10 @@ impl Jail { }); } + pub fn remove(&mut self, address: &Address) { + self.0.remove(address); + } + pub fn try_release(&mut self, address: &Address, term_index: u64) -> ReleaseResult { match self.0.entry(*address) { Entry::Occupied(entry) => { @@ -394,6 +400,34 @@ impl Jail { } } +pub struct Banned(BTreeSet
); +impl Banned { + pub fn load_from_state(state: &TopLevelState) -> StateResult { + let key = *BANNED_KEY; + let action_data = state.action_data(&key)?; + Ok(Banned(decode_set(action_data.as_ref()))) + } + + pub fn save_to_state(&self, state: &mut TopLevelState) -> StateResult<()> { + let key = *BANNED_KEY; + if !self.0.is_empty() { + let encoded = encode_set(&self.0); + state.update_action_data(&key, encoded)?; + } else { + state.remove_action_data(&key); + } + Ok(()) + } + + pub fn add(&mut self, address: Address) { + self.0.insert(address); + } + + pub fn is_banned(&self, address: &Address) -> bool { + self.0.contains(address) + } +} + fn decode_set(data: Option<&ActionData>) -> BTreeSet where V: Ord + Decodable, { @@ -1259,4 +1293,30 @@ mod tests { let result = state.action_data(&*JAIL_KEY).unwrap(); assert_eq!(result, None, "Should clean the state if all prisoners are kicked"); } + + #[test] + fn empty_ban_save_clean_state() { + let mut state = helpers::get_temp_state(); + let banned = Banned::load_from_state(&state).unwrap(); + banned.save_to_state(&mut state).unwrap(); + + let result = state.action_data(&*BANNED_KEY).unwrap(); + assert_eq!(result, None, "Should clean the state if there are no banned accounts"); + } + + #[test] + fn added_to_ban_is_banned() { + let mut state = helpers::get_temp_state(); + + let address = Address::from(1); + let innocent = Address::from(2); + + let mut banned = Banned::load_from_state(&state).unwrap(); + banned.add(address); + banned.save_to_state(&mut state).unwrap(); + + let banned = Banned::load_from_state(&state).unwrap(); + assert!(banned.is_banned(&address)); + assert!(!banned.is_banned(&innocent)); + } } diff --git a/core/src/consensus/stake/mod.rs b/core/src/consensus/stake/mod.rs index 09cdd33eb8..8fc3a9bcea 100644 --- a/core/src/consensus/stake/mod.rs +++ b/core/src/consensus/stake/mod.rs @@ -33,6 +33,7 @@ use rlp::{Decodable, UntrustedRlp}; use self::action_data::{Candidates, Delegation, IntermediateRewards, Jail, ReleaseResult, StakeAccount, Stakeholders}; use self::actions::Action; pub use self::distribute::fee_distribute; +use consensus::stake::action_data::Banned; const CUSTOM_ACTION_HANDLER_ID: u64 = 2; @@ -209,9 +210,10 @@ fn transfer_ccs(state: &mut TopLevelState, sender: &Address, receiver: &Address, } fn delegate_ccs(state: &mut TopLevelState, sender: &Address, delegatee: &Address, quantity: u64) -> StateResult<()> { - // TODO: remove parent hash from validator set. - // TODO: handle banned account - // TODO: handle jailed account + let banned = Banned::load_from_state(state)?; + if banned.is_banned(&delegatee) { + return Err(RuntimeError::FailedToHandleCustomAction("Delegatee is banned".to_string()).into()) + } let candidates = Candidates::load_from_state(state)?; let jail = Jail::load_from_state(state)?; if candidates.get_candidate(delegatee).is_none() && jail.get_prisoner(delegatee).is_none() { @@ -219,6 +221,7 @@ fn delegate_ccs(state: &mut TopLevelState, sender: &Address, delegatee: &Address RuntimeError::FailedToHandleCustomAction("Can delegate to who is a candidate or a prisoner".into()).into() ) } + let mut delegator = StakeAccount::load_from_state(state, sender)?; let mut delegation = Delegation::load_from_state(state, &sender)?; @@ -253,7 +256,10 @@ fn self_nominate( ) -> StateResult<()> { // TODO: proper handling of get_current_term // TODO: proper handling of NOMINATE_EXPIRATION - // TODO: check banned accounts + let blacklist = Banned::load_from_state(state)?; + if blacklist.is_banned(&sender) { + return Err(RuntimeError::FailedToHandleCustomAction("Account is blacklisted".to_string()).into()) + } let mut jail = Jail::load_from_state(&state)?; let total_deposit = match jail.try_release(sender, current_term) { @@ -383,6 +389,30 @@ pub fn jail(state: &mut TopLevelState, address: &Address, custody_until: u64, ki Ok(()) } +#[allow(dead_code)] +pub fn ban(state: &mut TopLevelState, criminal: Address) -> StateResult<()> { + // TODO: remove pending rewards. + // TODO: remove from validators. + // TODO: give criminal's deposits to the informant + // TODO: give criminal's rewards to diligent validators + let mut candidates = Candidates::load_from_state(state)?; + let mut banned = Banned::load_from_state(state)?; + let mut jailed = Jail::load_from_state(state)?; + + candidates.remove(&criminal); + jailed.remove(&criminal); + banned.add(criminal); + + jailed.save_to_state(state)?; + banned.save_to_state(state)?; + candidates.save_to_state(state)?; + + // Revert delegations + revert_delegations(state, &[criminal])?; + + Ok(()) +} + fn revert_delegations(state: &mut TopLevelState, reverted_delegatees: &[Address]) -> StateResult<()> { let stakeholders = Stakeholders::load_from_state(state)?; for stakeholder in stakeholders.iter() { @@ -1207,4 +1237,64 @@ mod tests { let account = StakeAccount::load_from_state(&state, &delegator).unwrap(); assert_eq!(account.balance, 100 - 40, "Delegation should be preserved"); } + + #[test] + fn test_ban() { + let criminal = Address::random(); + let delegator = Address::random(); + + let mut state = helpers::get_temp_state(); + state.add_balance(&criminal, 1000).unwrap(); + + let stake = { + let mut genesis_stakes = HashMap::new(); + genesis_stakes.insert(delegator, 100); + Stake::new(genesis_stakes) + }; + stake.init(&mut state).unwrap(); + + self_nominate(&mut state, &criminal, 100, 0, 10).unwrap(); + let action = Action::DelegateCCS { + address: criminal, + quantity: 40, + }; + stake.execute(&action.rlp_bytes(), &mut state, &delegator).unwrap(); + + let result = ban(&mut state, criminal); + assert!(result.is_ok()); + + let banned = Banned::load_from_state(&state).unwrap(); + assert!(banned.is_banned(&criminal)); + + let candidates = Candidates::load_from_state(&state).unwrap(); + assert_eq!(candidates.len(), 0); + + assert_eq!(state.balance(&criminal).unwrap(), 900, "Should lose deposit"); + + let delegation = Delegation::load_from_state(&state, &delegator).unwrap(); + assert_eq!(delegation.get_quantity(&criminal), 0, "Delegation should be reverted"); + + let account_delegator = StakeAccount::load_from_state(&state, &delegator).unwrap(); + assert_eq!(account_delegator.balance, 100, "Delegation should be reverted"); + } + + #[test] + fn ban_should_remove_prisoner_from_jail() { + let criminal = Address::random(); + + let mut state = helpers::get_temp_state(); + let stake = Stake::new(HashMap::new()); + stake.init(&mut state).unwrap(); + + self_nominate(&mut state, &criminal, 0, 0, 10).unwrap(); + let custody_until = 10; + let kicked_at = 20; + jail(&mut state, &criminal, custody_until, kicked_at).unwrap(); + + let result = ban(&mut state, criminal); + assert!(result.is_ok()); + + let jail = Jail::load_from_state(&state).unwrap(); + assert_eq!(jail.get_prisoner(&criminal), None, "Should be removed from the jail"); + } }