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),
}
}
}