diff --git a/core/src/consensus/stake/actions.rs b/core/src/consensus/stake/actions.rs index 7d727b5385..973d347b67 100644 --- a/core/src/consensus/stake/actions.rs +++ b/core/src/consensus/stake/actions.rs @@ -14,13 +14,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use ckey::Address; +use ckey::{Address, Signature}; +use ctypes::CommonParams; use rlp::{Decodable, DecoderError, Encodable, RlpStream, UntrustedRlp}; const ACTION_TAG_TRANSFER_CCS: u8 = 1; const ACTION_TAG_DELEGATE_CCS: u8 = 2; +const ACTION_TAG_CHANGE_PARAMS: u8 = 0xFF; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Action { TransferCCS { address: Address, @@ -30,6 +32,11 @@ pub enum Action { address: Address, quantity: u64, }, + ChangeParams { + metadata_seq: u64, + params: Box, + signatures: Vec, + }, } impl Encodable for Action { @@ -38,11 +45,28 @@ impl Encodable for Action { Action::TransferCCS { address, quantity, - } => s.begin_list(3).append(&ACTION_TAG_TRANSFER_CCS).append(address).append(quantity), + } => { + s.begin_list(3).append(&ACTION_TAG_TRANSFER_CCS).append(address).append(quantity); + } Action::DelegateCCS { address, quantity, - } => s.begin_list(3).append(&ACTION_TAG_DELEGATE_CCS).append(address).append(quantity), + } => { + s.begin_list(3).append(&ACTION_TAG_DELEGATE_CCS).append(address).append(quantity); + } + Action::ChangeParams { + metadata_seq, + params, + signatures, + } => { + s.begin_list(3 + signatures.len()) + .append(&ACTION_TAG_CHANGE_PARAMS) + .append(metadata_seq) + .append(&**params); + for signature in signatures { + s.append(signature); + } + } }; } } @@ -77,7 +101,56 @@ impl Decodable for Action { quantity: rlp.val_at(2)?, }) } + ACTION_TAG_CHANGE_PARAMS => { + let item_count = rlp.item_count()?; + if item_count < 4 { + return Err(DecoderError::RlpIncorrectListLen { + expected: 4, + got: item_count, + }) + } + let metadata_seq = rlp.val_at(1)?; + let params = Box::new(rlp.val_at(2)?); + let signatures = (3..item_count).map(|i| rlp.val_at(i)).collect::>()?; + Ok(Action::ChangeParams { + metadata_seq, + params, + signatures, + }) + } _ => Err(DecoderError::Custom("Unexpected Tendermint Stake Action Type")), } } } + +#[cfg(test)] +mod tests { + use rlp::rlp_encode_and_decode_test; + + use super::*; + + #[test] + fn decode_fail_if_change_params_have_no_signatures() { + let action = Action::ChangeParams { + metadata_seq: 3, + params: CommonParams::default_for_test().into(), + signatures: vec![], + }; + assert_eq!( + Err(DecoderError::RlpIncorrectListLen { + expected: 4, + got: 3, + }), + UntrustedRlp::new(&rlp::encode(&action)).as_val::() + ); + } + + #[test] + fn rlp_of_change_params() { + rlp_encode_and_decode_test!(Action::ChangeParams { + metadata_seq: 3, + params: CommonParams::default_for_test().into(), + signatures: vec![Signature::random(), Signature::random()], + }); + } +} diff --git a/core/src/consensus/stake/mod.rs b/core/src/consensus/stake/mod.rs index b51fbb6908..6017f50889 100644 --- a/core/src/consensus/stake/mod.rs +++ b/core/src/consensus/stake/mod.rs @@ -22,10 +22,13 @@ use std::collections::HashMap; use std::ops::Deref; use std::sync::Arc; -use ckey::Address; -use cstate::{ActionHandler, StateResult, TopLevelState}; +use ccrypto::Blake; +use ckey::{public_to_address, recover, Address, Signature}; +use cstate::{ActionHandler, StateResult, TopLevelState, TopState}; use ctypes::errors::{RuntimeError, SyntaxError}; +use ctypes::util::unexpected::Mismatch; use ctypes::{CommonParams, Header}; +use primitives::H256; use rlp::{Decodable, UntrustedRlp}; use self::action_data::{Delegation, StakeAccount, Stakeholders}; @@ -111,12 +114,44 @@ impl ActionHandler for Stake { Err(RuntimeError::FailedToHandleCustomAction("DelegateCCS is disabled".to_string()).into()) } } + Action::ChangeParams { + metadata_seq, + params, + signatures, + } => change_params(state, metadata_seq, *params, &signatures), } } fn verify(&self, bytes: &[u8]) -> Result<(), SyntaxError> { - Action::decode(&UntrustedRlp::new(bytes)).map_err(|err| SyntaxError::InvalidCustomAction(err.to_string()))?; - Ok(()) + let action = Action::decode(&UntrustedRlp::new(bytes)) + .map_err(|err| SyntaxError::InvalidCustomAction(err.to_string()))?; + match action { + Action::TransferCCS { + .. + } => Ok(()), + Action::DelegateCCS { + .. + } => Ok(()), + Action::ChangeParams { + metadata_seq, + params, + signatures, + } => { + let action = Action::ChangeParams { + metadata_seq, + params, + signatures: vec![], + }; + let encoded_action = H256::blake(rlp::encode(&action)); + for signature in signatures { + // XXX: Signature recovery is an expensive job. Should we do it twice? + recover(&signature, &encoded_action).map_err(|err| { + SyntaxError::InvalidCustomAction(format!("Cannot decode the signature: {}", err)) + })?; + } + Ok(()) + } + } } fn on_close_block( @@ -183,6 +218,40 @@ pub fn get_stakes(state: &TopLevelState) -> StateResult> { Ok(result) } +fn change_params( + state: &mut TopLevelState, + metadata_seq: u64, + params: CommonParams, + signatures: &[Signature], +) -> StateResult<()> { + // Update state first because the signature validation is more expensive. + state.update_params(metadata_seq, params)?; + + let action = Action::ChangeParams { + metadata_seq, + params: params.into(), + signatures: vec![], + }; + let encoded_action = H256::blake(rlp::encode(&action)); + let stakes = get_stakes(state)?; + let signed_stakes = signatures.iter().try_fold(0, |sum, signature| { + let public = recover(signature, &encoded_action).unwrap_or_else(|err| { + unreachable!("The transaction with an invalid signature cannot pass the verification: {}", err); + }); + let address = public_to_address(&public); + stakes.get(&address).map(|stake| sum + stake).ok_or_else(|| RuntimeError::SignatureOfInvalidAccount(address)) + })?; + let total_stakes: u64 = stakes.values().sum(); + if total_stakes / 2 >= signed_stakes { + return Err(RuntimeError::InsufficientStakes(Mismatch { + expected: total_stakes, + found: signed_stakes, + }) + .into()) + } + Ok(()) +} + #[cfg(test)] mod tests { use super::action_data::get_account_key; diff --git a/spec/Staking.md b/spec/Staking.md index f3cd291b1f..f8b2a1426a 100644 --- a/spec/Staking.md +++ b/spec/Staking.md @@ -359,6 +359,7 @@ It also does not provide a voting feature. The vote initiator should collect the signatures through the off-chain. This transaction increases the `seq` of `Metadata` and changes the `params` of `Metadata`. +The changed parameters are applied from the next block that the changing transaction is included. ### Action `[ 0xFF, metadata_seq, new_parameters, ...signatures ]` diff --git a/state/src/impls/top_level.rs b/state/src/impls/top_level.rs index 594cee51d8..694df40113 100644 --- a/state/src/impls/top_level.rs +++ b/state/src/impls/top_level.rs @@ -46,7 +46,7 @@ use ctypes::transaction::{ Action, AssetOutPoint, AssetTransferInput, AssetWrapCCCOutput, ShardTransaction, Transaction, }; use ctypes::util::unexpected::Mismatch; -use ctypes::{BlockNumber, ShardId}; +use ctypes::{BlockNumber, CommonParams, ShardId}; use cvm::ChainTimeInfo; use hashdb::AsHashDB; use kvdb::DBTransaction; @@ -989,6 +989,21 @@ impl TopState for TopLevelState { fn remove_action_data(&mut self, key: &H256) { self.top_cache.remove_action_data(key) } + + fn update_params(&mut self, metadata_seq: u64, params: CommonParams) -> StateResult<()> { + let mut metadata = self.get_metadata_mut()?; + if metadata.seq() != metadata_seq { + return Err(RuntimeError::InvalidSeq(Mismatch { + found: metadata_seq, + expected: metadata.seq(), + }) + .into()) + } + + metadata.set_params(params); + metadata.increase_seq(); + Ok(()) + } } fn is_active_account(state: &TopStateView, address: &Address) -> TrieResult { diff --git a/state/src/item/metadata.rs b/state/src/item/metadata.rs index c21d59537b..cad3ea466a 100644 --- a/state/src/item/metadata.rs +++ b/state/src/item/metadata.rs @@ -14,11 +14,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +use ctypes::{CommonParams, ShardId}; use primitives::H256; use rlp::{Decodable, DecoderError, Encodable, RlpStream, UntrustedRlp}; -use ctypes::{CommonParams, ShardId}; - use crate::CacheableItem; #[derive(Clone, Debug, Default, PartialEq)] @@ -33,7 +32,7 @@ pub struct Metadata { number_of_initial_shards: ShardId, hashes: Vec, term: TermMetadata, - seq: usize, + seq: u64, params: Option, } @@ -78,10 +77,22 @@ impl Metadata { }) } + pub fn seq(&self) -> u64 { + self.seq + } + + pub fn increase_seq(&mut self) { + self.seq += 1; + } + pub fn params(&self) -> Option<&CommonParams> { self.params.as_ref() } + pub fn set_params(&mut self, params: CommonParams) { + self.params = Some(params); + } + pub fn change_term(&mut self, last_term_finished_block_num: u64, current_term_id: u64) { assert!(self.term.last_term_finished_block_num < last_term_finished_block_num); assert!(self.term.current_term_id < current_term_id); diff --git a/state/src/traits.rs b/state/src/traits.rs index b288fb5c60..757815b051 100644 --- a/state/src/traits.rs +++ b/state/src/traits.rs @@ -17,7 +17,7 @@ use ckey::{public_to_address, Address, Public, Signature}; use cmerkle::Result as TrieResult; use ctypes::transaction::ShardTransaction; -use ctypes::{BlockNumber, ShardId}; +use ctypes::{BlockNumber, CommonParams, ShardId}; use cvm::ChainTimeInfo; use primitives::{Bytes, H160, H256}; @@ -181,6 +181,8 @@ pub trait TopState { fn update_action_data(&mut self, key: &H256, data: Bytes) -> StateResult<()>; fn remove_action_data(&mut self, key: &H256); + + fn update_params(&mut self, metadata_seq: u64, params: CommonParams) -> StateResult<()>; } pub trait StateWithCache { diff --git a/test/src/e2e/changeParams.test.ts b/test/src/e2e/changeParams.test.ts new file mode 100644 index 0000000000..43a7c415ad --- /dev/null +++ b/test/src/e2e/changeParams.test.ts @@ -0,0 +1,688 @@ +// 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 . + +import { expect } from "chai"; +import { H256, PlatformAddress } from "codechain-primitives/lib"; +import { blake256 } from "codechain-sdk/lib/utils"; +import "mocha"; +import { + aliceAddress, + aliceSecret, + bobSecret, + carolAddress, + carolSecret, + faucetAddress, + faucetSecret, + stakeActionHandlerId, + validator0Address +} from "../helper/constants"; +import CodeChain from "../helper/spawn"; + +const RLP = require("rlp"); + +describe("ChangeParams", function() { + const chain = `${__dirname}/../scheme/solo-block-reward-50.json`; + let node: CodeChain; + + beforeEach(async function() { + node = new CodeChain({ + chain, + argv: ["--author", validator0Address.toString(), "--force-sealing"] + }); + await node.start(); + + const tx = await node.sendPayTx({ + fee: 10, + quantity: 100_000, + recipient: aliceAddress + }); + expect(await node.sdk.rpc.chain.containsTransaction(tx.hash())).be.true; + }); + + it("change", async function() { + const newParams = [ + 0x20, // maxExtraDataSize + 0x0400, // maxAssetSchemeMetadataSize + 0x0100, // maxTransferMetadataSize + 0x0200, // maxTextContentSize + "tc", // networkID + 11, // minPayCost + 10, // minSetRegularKeyCost + 10, // minCreateShardCost + 10, // minSetShardOwnersCost + 10, // minSetShardUsersCost + 10, // minWrapCccCost + 10, // minCustomCost + 10, // minStoreCost + 10, // minRemoveCost + 10, // minMintAssetCost + 10, // minTransferAssetCost + 10, // minChangeAssetSchemeCost + 10, // minIncreaseAssetSupplyCost + 10, // minComposeAssetCost + 10, // minDecomposeAssetCost + 10, // minUnwrapCccCost + 4194304, // maxBodySize + 16384 // snapshotPeriod + ]; + const changeParams: (number | string | (number | string)[])[] = [ + 0xff, + 0, + newParams + ]; + const message = blake256(RLP.encode(changeParams).toString("hex")); + changeParams.push(`0x${node.sdk.util.signEcdsa(message, aliceSecret)}`); + changeParams.push(`0x${node.sdk.util.signEcdsa(message, carolSecret)}`); + + { + const hash = await node.sdk.rpc.chain.sendSignedTransaction( + node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: RLP.encode(changeParams) + }) + .sign({ + secret: faucetSecret, + seq: await node.sdk.rpc.chain.getSeq(faucetAddress), + fee: 10 + }) + ); + expect(await node.sdk.rpc.chain.containsTransaction(hash)).be.true; + } + + try { + await node.sendPayTx({ fee: 10 }); + } catch (err) { + expect(err.message).contains("Too Low Fee"); + } + }); + + it("the parameter is applied from the next block", async function() { + const newParams = [ + 0x20, // maxExtraDataSize + 0x0400, // maxAssetSchemeMetadataSize + 0x0100, // maxTransferMetadataSize + 0x0200, // maxTextContentSize + "tc", // networkID + 11, // minPayCost + 10, // minSetRegularKeyCost + 10, // minCreateShardCost + 10, // minSetShardOwnersCost + 10, // minSetShardUsersCost + 10, // minWrapCccCost + 10, // minCustomCost + 10, // minStoreCost + 10, // minRemoveCost + 10, // minMintAssetCost + 10, // minTransferAssetCost + 10, // minChangeAssetSchemeCost + 10, // minIncreaseAssetSupplyCost + 10, // minComposeAssetCost + 10, // minDecomposeAssetCost + 10, // minUnwrapCccCost + 4194304, // maxBodySize + 16384 // snapshotPeriod + ]; + const changeParams: (number | string | (number | string)[])[] = [ + 0xff, + 0, + newParams + ]; + const message = blake256(RLP.encode(changeParams).toString("hex")); + changeParams.push(`0x${node.sdk.util.signEcdsa(message, aliceSecret)}`); + changeParams.push(`0x${node.sdk.util.signEcdsa(message, bobSecret)}`); + changeParams.push(`0x${node.sdk.util.signEcdsa(message, carolSecret)}`); + + { + await node.sdk.rpc.devel.stopSealing(); + const blockNumber = await node.sdk.rpc.chain.getBestBlockNumber(); + const seq = await node.sdk.rpc.chain.getSeq(faucetAddress); + const changeHash = await node.sdk.rpc.chain.sendSignedTransaction( + node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: RLP.encode(changeParams) + }) + .sign({ + secret: faucetSecret, + seq, + fee: 10 + }) + ); + const pay = await node.sendPayTx({ seq: seq + 1, fee: 10 }); + await node.sdk.rpc.devel.startSealing(); + expect(await node.sdk.rpc.chain.containsTransaction(changeHash)).be + .true; + expect(await node.sdk.rpc.chain.containsTransaction(pay.hash())).be + .true; + expect(await node.sdk.rpc.chain.getBestBlockNumber()).equal( + blockNumber + 1 + ); + } + + try { + await node.sendPayTx({ fee: 10 }); + } catch (err) { + expect(err.message).contains("Too Low Fee"); + } + }); + + it("the parameter changed twice in the same block", async function() { + const newParams1 = [ + 0x20, // maxExtraDataSize + 0x0400, // maxAssetSchemeMetadataSize + 0x0100, // maxTransferMetadataSize + 0x0200, // maxTextContentSize + "tc", // networkID + 11, // minPayCost + 10, // minSetRegularKeyCost + 10, // minCreateShardCost + 10, // minSetShardOwnersCost + 10, // minSetShardUsersCost + 10, // minWrapCccCost + 10, // minCustomCost + 10, // minStoreCost + 10, // minRemoveCost + 10, // minMintAssetCost + 10, // minTransferAssetCost + 10, // minChangeAssetSchemeCost + 10, // minIncreaseAssetSupplyCost + 10, // minComposeAssetCost + 10, // minDecomposeAssetCost + 10, // minUnwrapCccCost + 4194304, // maxBodySize + 16384 // snapshotPeriod + ]; + const newParams2 = [ + 0x20, // maxExtraDataSize + 0x0400, // maxAssetSchemeMetadataSize + 0x0100, // maxTransferMetadataSize + 0x0200, // maxTextContentSize + "tc", // networkID + 5, // minPayCost + 10, // minSetRegularKeyCost + 10, // minCreateShardCost + 10, // minSetShardOwnersCost + 10, // minSetShardUsersCost + 10, // minWrapCccCost + 10, // minCustomCost + 10, // minStoreCost + 10, // minRemoveCost + 10, // minMintAssetCost + 10, // minTransferAssetCost + 10, // minChangeAssetSchemeCost + 10, // minIncreaseAssetSupplyCost + 10, // minComposeAssetCost + 10, // minDecomposeAssetCost + 10, // minUnwrapCccCost + 4194304, // maxBodySize + 16384 // snapshotPeriod + ]; + const changeParams1: (number | string | (number | string)[])[] = [ + 0xff, + 0, + newParams1 + ]; + const changeParams2: (number | string | (number | string)[])[] = [ + 0xff, + 1, + newParams2 + ]; + const message1 = blake256(RLP.encode(changeParams1).toString("hex")); + changeParams1.push( + `0x${node.sdk.util.signEcdsa(message1, aliceSecret)}` + ); + changeParams1.push(`0x${node.sdk.util.signEcdsa(message1, bobSecret)}`); + changeParams1.push( + `0x${node.sdk.util.signEcdsa(message1, carolSecret)}` + ); + const message2 = blake256(RLP.encode(changeParams2).toString("hex")); + changeParams2.push( + `0x${node.sdk.util.signEcdsa(message2, aliceSecret)}` + ); + changeParams2.push(`0x${node.sdk.util.signEcdsa(message2, bobSecret)}`); + changeParams2.push( + `0x${node.sdk.util.signEcdsa(message2, carolSecret)}` + ); + + { + await node.sdk.rpc.devel.stopSealing(); + const blockNumber = await node.sdk.rpc.chain.getBestBlockNumber(); + const seq = await node.sdk.rpc.chain.getSeq(faucetAddress); + const changeHash1 = await node.sdk.rpc.chain.sendSignedTransaction( + node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: RLP.encode(changeParams1) + }) + .sign({ + secret: faucetSecret, + seq, + fee: 10 + }) + ); + const changeHash2 = await node.sdk.rpc.chain.sendSignedTransaction( + node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: RLP.encode(changeParams2) + }) + .sign({ + secret: faucetSecret, + seq: seq + 1, + fee: 10 + }) + ); + await node.sdk.rpc.devel.startSealing(); + expect(await node.sdk.rpc.chain.containsTransaction(changeHash1)).be + .true; + expect(await node.sdk.rpc.chain.containsTransaction(changeHash2)).be + .true; + expect(await node.sdk.rpc.chain.getBestBlockNumber()).equal( + blockNumber + 1 + ); + } + + const pay = await node.sendPayTx({ fee: 5 }); + expect(await node.sdk.rpc.chain.containsTransaction(pay.hash())).be + .true; + try { + await node.sendPayTx({ fee: 4 }); + } catch (err) { + expect(err.message).contains("Too Low Fee"); + } + }); + + it("cannot reuse the same signature", async function() { + const newParams1 = [ + 0x20, // maxExtraDataSize + 0x0400, // maxAssetSchemeMetadataSize + 0x0100, // maxTransferMetadataSize + 0x0200, // maxTextContentSize + "tc", // networkID + 11, // minPayCost + 10, // minSetRegularKeyCost + 10, // minCreateShardCost + 10, // minSetShardOwnersCost + 10, // minSetShardUsersCost + 10, // minWrapCccCost + 10, // minCustomCost + 10, // minStoreCost + 10, // minRemoveCost + 10, // minMintAssetCost + 10, // minTransferAssetCost + 10, // minChangeAssetSchemeCost + 10, // minIncreaseAssetSupplyCost + 10, // minComposeAssetCost + 10, // minDecomposeAssetCost + 10, // minUnwrapCccCost + 4194304, // maxBodySize + 16384 // snapshotPeriod + ]; + const newParams2 = [ + 0x20, // maxExtraDataSize + 0x0400, // maxAssetSchemeMetadataSize + 0x0100, // maxTransferMetadataSize + 0x0200, // maxTextContentSize + "tc", // networkID + 5, // minPayCost + 10, // minSetRegularKeyCost + 10, // minCreateShardCost + 10, // minSetShardOwnersCost + 10, // minSetShardUsersCost + 10, // minWrapCccCost + 10, // minCustomCost + 10, // minStoreCost + 10, // minRemoveCost + 10, // minMintAssetCost + 10, // minTransferAssetCost + 10, // minChangeAssetSchemeCost + 10, // minIncreaseAssetSupplyCost + 10, // minComposeAssetCost + 10, // minDecomposeAssetCost + 10, // minUnwrapCccCost + 4194304, // maxBodySize + 16384 // snapshotPeriod + ]; + const changeParams1: (number | string | (number | string)[])[] = [ + 0xff, + 0, + newParams1 + ]; + const changeParams2: (number | string | (number | string)[])[] = [ + 0xff, + 1, + newParams2 + ]; + const message1 = blake256(RLP.encode(changeParams1).toString("hex")); + changeParams1.push( + `0x${node.sdk.util.signEcdsa(message1, aliceSecret)}` + ); + changeParams1.push(`0x${node.sdk.util.signEcdsa(message1, bobSecret)}`); + changeParams1.push( + `0x${node.sdk.util.signEcdsa(message1, carolSecret)}` + ); + const message2 = blake256(RLP.encode(changeParams2).toString("hex")); + changeParams2.push( + `0x${node.sdk.util.signEcdsa(message2, aliceSecret)}` + ); + changeParams2.push(`0x${node.sdk.util.signEcdsa(message2, bobSecret)}`); + changeParams2.push( + `0x${node.sdk.util.signEcdsa(message2, carolSecret)}` + ); + + { + await node.sdk.rpc.devel.stopSealing(); + const blockNumber = await node.sdk.rpc.chain.getBestBlockNumber(); + const seq = await node.sdk.rpc.chain.getSeq(faucetAddress); + const changeHash1 = await node.sdk.rpc.chain.sendSignedTransaction( + node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: RLP.encode(changeParams1) + }) + .sign({ + secret: faucetSecret, + seq, + fee: 10 + }) + ); + const changeHash2 = await node.sdk.rpc.chain.sendSignedTransaction( + node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: RLP.encode(changeParams2) + }) + .sign({ + secret: faucetSecret, + seq: seq + 1, + fee: 10 + }) + ); + await node.sdk.rpc.devel.startSealing(); + expect(await node.sdk.rpc.chain.containsTransaction(changeHash1)).be + .true; + expect(await node.sdk.rpc.chain.containsTransaction(changeHash2)).be + .true; + expect(await node.sdk.rpc.chain.getBestBlockNumber()).equal( + blockNumber + 1 + ); + } + + const pay = await node.sendPayTx({ fee: 5 }); + expect(await node.sdk.rpc.chain.containsTransaction(pay.hash())).be + .true; + try { + await node.sendPayTx({ fee: 4 }); + } catch (err) { + expect(err.message).contains("Too Low Fee"); + } + }); + + it("cannot change params with insufficient stakes", async function() { + const newParams = [ + 0x20, // maxExtraDataSize + 0x0400, // maxAssetSchemeMetadataSize + 0x0100, // maxTransferMetadataSize + 0x0200, // maxTextContentSize + "tc", // networkID + 11, // minPayCost + 10, // minSetRegularKeyCost + 10, // minCreateShardCost + 10, // minSetShardOwnersCost + 10, // minSetShardUsersCost + 10, // minWrapCccCost + 10, // minCustomCost + 10, // minStoreCost + 10, // minRemoveCost + 10, // minMintAssetCost + 10, // minTransferAssetCost + 10, // minChangeAssetSchemeCost + 10, // minIncreaseAssetSupplyCost + 10, // minComposeAssetCost + 10, // minDecomposeAssetCost + 10, // minUnwrapCccCost + 4194304, // maxBodySize + 16384 // snapshotPeriod + ]; + const changeParams: (number | string | (number | string)[])[] = [ + 0xff, + 0, + newParams + ]; + const message = blake256(RLP.encode(changeParams).toString("hex")); + changeParams.push(`0x${node.sdk.util.signEcdsa(message, aliceSecret)}`); + changeParams.push(`0x${node.sdk.util.signEcdsa(message, carolSecret)}`); + + { + const hash = await node.sdk.rpc.chain.sendSignedTransaction( + node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: RLP.encode(changeParams) + }) + .sign({ + secret: faucetSecret, + seq: await node.sdk.rpc.chain.getSeq(faucetAddress), + fee: 10 + }) + ); + expect(await node.sdk.rpc.chain.containsTransaction(hash)).be.true; + } + + { + await node.sendSignedTransactionExpectedToFail( + node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: RLP.encode(changeParams) + }) + .sign({ + secret: faucetSecret, + seq: + (await node.sdk.rpc.chain.getSeq(faucetAddress)) + + 1, + fee: 10 + }), + { error: "Invalid transaction seq Expected 1, found 0" } + ); + } + }); + + it("the amount of stakes not the number of stakeholders", async function() { + const newParams = [ + 0x20, // maxExtraDataSize + 0x0400, // maxAssetSchemeMetadataSize + 0x0100, // maxTransferMetadataSize + 0x0200, // maxTextContentSize + "tc", // networkID + 11, // minPayCost + 10, // minSetRegularKeyCost + 10, // minCreateShardCost + 10, // minSetShardOwnersCost + 10, // minSetShardUsersCost + 10, // minWrapCccCost + 10, // minCustomCost + 10, // minStoreCost + 10, // minRemoveCost + 10, // minMintAssetCost + 10, // minTransferAssetCost + 10, // minChangeAssetSchemeCost + 10, // minIncreaseAssetSupplyCost + 10, // minComposeAssetCost + 10, // minDecomposeAssetCost + 10, // minUnwrapCccCost + 4194304, // maxBodySize + 16384 // snapshotPeriod + ]; + const changeParams: (number | string | (number | string)[])[] = [ + 0xff, + 0, + newParams + ]; + const message = blake256(RLP.encode(changeParams).toString("hex")); + changeParams.push(`0x${node.sdk.util.signEcdsa(message, bobSecret)}`); + changeParams.push(`0x${node.sdk.util.signEcdsa(message, carolSecret)}`); + + const tx = node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: RLP.encode(changeParams) + }) + .sign({ + secret: faucetSecret, + seq: (await node.sdk.rpc.chain.getSeq(faucetAddress)) + 1, + fee: 10 + }); + await node.sendSignedTransactionExpectedToFail(tx, { + error: "Insufficient stakes:" + }); + }); + + it("needs more than half to change params", async function() { + const newParams = [ + 0x20, // maxExtraDataSize + 0x0400, // maxAssetSchemeMetadataSize + 0x0100, // maxTransferMetadataSize + 0x0200, // maxTextContentSize + "tc", // networkID + 11, // minPayCost + 10, // minSetRegularKeyCost + 10, // minCreateShardCost + 10, // minSetShardOwnersCost + 10, // minSetShardUsersCost + 10, // minWrapCccCost + 10, // minCustomCost + 10, // minStoreCost + 10, // minRemoveCost + 10, // minMintAssetCost + 10, // minTransferAssetCost + 10, // minChangeAssetSchemeCost + 10, // minIncreaseAssetSupplyCost + 10, // minComposeAssetCost + 10, // minDecomposeAssetCost + 10, // minUnwrapCccCost + 4194304, // maxBodySize + 16384 // snapshotPeriod + ]; + + const changeParams: (number | string | (number | string)[])[] = [ + 0xff, + 0, + newParams + ]; + { + const message = blake256(RLP.encode(changeParams).toString("hex")); + changeParams.push( + `0x${node.sdk.util.signEcdsa(message, bobSecret)}` + ); + changeParams.push( + `0x${node.sdk.util.signEcdsa(message, carolSecret)}` + ); + + const tx = node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: RLP.encode(changeParams) + }) + .sign({ + secret: faucetSecret, + seq: (await node.sdk.rpc.chain.getSeq(faucetAddress)) + 1, + fee: 10 + }); + await node.sendSignedTransactionExpectedToFail(tx, { + error: "Insufficient" + }); + } + + await sendStakeToken({ + node, + senderAddress: aliceAddress, + senderSecret: aliceSecret, + receiverAddress: carolAddress, + quantity: 1, + fee: 1000 + }); + + { + const tx = node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: RLP.encode(changeParams) + }) + .sign({ + secret: faucetSecret, + seq: await node.sdk.rpc.chain.getSeq(faucetAddress), + fee: 10 + }); + const hash = await node.sdk.rpc.chain.sendSignedTransaction(tx); + expect(await node.sdk.rpc.chain.containsTransaction(hash)).be.true; + expect(await node.sdk.rpc.chain.getTransaction(hash)).not.be.null; + } + + try { + await node.sendPayTx({ fee: 10 }); + } catch (err) { + expect(err.message).contains("Too Low Fee"); + } + }); + + afterEach(async function() { + if (this.currentTest!.state === "failed") { + node.testFailed(this.currentTest!.fullTitle()); + } + await node.clean(); + }); +}); + +async function sendStakeToken(params: { + node: CodeChain; + senderAddress: PlatformAddress; + senderSecret: string; + receiverAddress: PlatformAddress; + quantity: number; + fee?: number; + seq?: number; +}): Promise { + const { + fee = 10, + node, + senderAddress, + receiverAddress, + senderSecret, + quantity + } = params; + const { seq = await node.sdk.rpc.chain.getSeq(senderAddress) } = params; + + return node.sdk.rpc.chain.sendSignedTransaction( + node.sdk.core + .createCustomTransaction({ + handlerId: stakeActionHandlerId, + bytes: Buffer.from( + RLP.encode([ + 1, + receiverAddress.accountId.toEncodeObject(), + quantity + ]) + ) + }) + .sign({ + secret: senderSecret, + seq, + fee + }) + ); +} diff --git a/test/src/helper/spawn.ts b/test/src/helper/spawn.ts index be82cd29f5..943e71095c 100644 --- a/test/src/helper/spawn.ts +++ b/test/src/helper/spawn.ts @@ -617,6 +617,40 @@ export default class CodeChain { return targetTxHash; } + public async sendSignedTransactionExpectedToFail( + tx: SignedTransaction, + options: { error?: string } = {} + ): Promise { + await this.sdk.rpc.devel.stopSealing(); + + const blockNumber = await this.getBestBlockNumber(); + const signedDummyTxHash = (await this.sendPayTx({ + fee: 1000, + quantity: 1 + })).hash(); + const targetTxHash = await this.sdk.rpc.chain.sendSignedTransaction(tx); + + await this.sdk.rpc.devel.startSealing(); + await this.waitBlockNumber(blockNumber + 1); + + expect(await this.sdk.rpc.chain.containsTransaction(targetTxHash)).be + .false; + const hint = await this.sdk.rpc.chain.getErrorHint(targetTxHash); + expect(hint).not.null; + if (options.error != null) { + expect(hint).contains(options.error); + } + expect(await this.sdk.rpc.chain.getTransaction(targetTxHash)).be.null; + + expect(await this.sdk.rpc.chain.containsTransaction(signedDummyTxHash)) + .be.true; + expect(await this.sdk.rpc.chain.getErrorHint(signedDummyTxHash)).null; + expect(await this.sdk.rpc.chain.getTransaction(signedDummyTxHash)).not + .be.null; + + return targetTxHash; + } + public sendSignedTransactionWithRlpBytes(rlpBytes: Buffer): Promise { return new Promise((resolve, reject) => { const bytes = Array.from(rlpBytes) diff --git a/types/src/errors/runtime_error.rs b/types/src/errors/runtime_error.rs index c4c1423f10..d32b065bd5 100644 --- a/types/src/errors/runtime_error.rs +++ b/types/src/errors/runtime_error.rs @@ -115,6 +115,8 @@ pub enum Error { address: Address, name: String, }, + SignatureOfInvalidAccount(Address), + InsufficientStakes(Mismatch), } const ERROR_ID_ASSET_NOT_FOUND: u8 = 1; @@ -147,6 +149,8 @@ const ERROR_ID_INVALID_SEQ: u8 = 28; const ERROR_ID_ASSET_SUPPLY_OVERFLOW: u8 = 29; const ERROR_ID_NON_ACTIVE_ACCOUNT: u8 = 30; const ERROR_ID_FAILED_TO_HANDLE_CUSTOM_ACTION: u8 = 31; +const ERROR_ID_SIGNATURE_OF_INVALID_ACCOUNT: u8 = 32; +const ERROR_ID_INSUFFICIENT_STAKES: u8 = 33; struct RlpHelper; impl TaggedRlp for RlpHelper { @@ -184,6 +188,8 @@ impl TaggedRlp for RlpHelper { ERROR_ID_TEXT_VERIFICATION_FAIL => 2, ERROR_ID_CANNOT_USE_MASTER_KEY => 1, ERROR_ID_NON_ACTIVE_ACCOUNT => 3, + ERROR_ID_SIGNATURE_OF_INVALID_ACCOUNT => 2, + ERROR_ID_INSUFFICIENT_STAKES => 3, _ => return Err(DecoderError::Custom("Invalid RuntimeError")), }) } @@ -304,6 +310,13 @@ impl Encodable for Error { address, name, } => RlpHelper::new_tagged_list(s, ERROR_ID_NON_ACTIVE_ACCOUNT).append(address).append(name), + Error::SignatureOfInvalidAccount(address) => { + RlpHelper::new_tagged_list(s, ERROR_ID_SIGNATURE_OF_INVALID_ACCOUNT).append(address) + } + Error::InsufficientStakes(Mismatch { + expected, + found, + }) => RlpHelper::new_tagged_list(s, ERROR_ID_INSUFFICIENT_STAKES).append(expected).append(found), }; } } @@ -387,6 +400,11 @@ impl Decodable for Error { address: rlp.val_at(1)?, name: rlp.val_at(2)?, }, + ERROR_ID_SIGNATURE_OF_INVALID_ACCOUNT => Error::SignatureOfInvalidAccount(rlp.val_at(1)?), + ERROR_ID_INSUFFICIENT_STAKES => Error::InsufficientStakes(Mismatch { + expected: rlp.val_at(1)?, + found: rlp.val_at(2)?, + }), _ => return Err(DecoderError::Custom("Invalid RuntimeError")), }; RlpHelper::check_size(rlp, tag)?; @@ -481,6 +499,10 @@ impl Display for Error { } => { write!(f, "Non active account({}) cannot be {}", address, name) } + Error::SignatureOfInvalidAccount(address) => + write!(f, "Signature of invalid account({}) received", address), + Error::InsufficientStakes(mismatch) => + write!(f, "Insufficient stakes: {}", mismatch), } } }