diff --git a/core/src/consensus/stake/actions.rs b/core/src/consensus/stake/actions.rs index a0f63c4dbf..912461813b 100644 --- a/core/src/consensus/stake/actions.rs +++ b/core/src/consensus/stake/actions.rs @@ -105,15 +105,7 @@ impl Action { params, signatures, } => { - let current_network_id = current_params.network_id(); - let transaction_network_id = params.network_id(); - if current_network_id != transaction_network_id { - return Err(SyntaxError::InvalidCustomAction(format!( - "The current network id is {} but the transaction tries to change the network id to {}", - current_network_id, transaction_network_id - ))) - } - params.verify().map_err(SyntaxError::InvalidCustomAction)?; + params.verify_change(current_params).map_err(SyntaxError::InvalidCustomAction)?; let action = Action::ChangeParams { metadata_seq: *metadata_seq, params: params.clone(), diff --git a/json/src/scheme/params.rs b/json/src/scheme/params.rs index 2244961556..500b2eb26a 100644 --- a/json/src/scheme/params.rs +++ b/json/src/scheme/params.rs @@ -66,6 +66,10 @@ pub struct Params { pub delegation_threshold: Option, pub min_deposit: Option, pub max_candidate_metadata_size: Option, + + /// A monotonically increasing number to denote the consensus version. + /// It is increased when we fork. + pub era: Option, } #[cfg(test)] @@ -277,4 +281,79 @@ mod tests { assert_eq!(deserialized.min_deposit, Some(32.into())); assert_eq!(deserialized.max_candidate_metadata_size, Some(33.into())); } + + #[test] + #[allow(clippy::cognitive_complexity)] + fn params_deserialization_with_era() { + let s = r#"{ + "maxExtraDataSize": "0x20", + "maxAssetSchemeMetadataSize": "0x0400", + "maxTransferMetadataSize": "0x0100", + "maxTextContentSize": "0x0200", + "networkID" : "tc", + "minPayCost" : 10, + "minSetRegularKeyCost" : 11, + "minCreateShardCost" : 12, + "minSetShardOwnersCost" : 13, + "minSetShardUsersCost" : 14, + "minWrapCccCost" : 15, + "minCustomCost" : 16, + "minStoreCost" : 17, + "minRemoveCost" : 18, + "minMintAssetCost" : 19, + "minTransferAssetCost" : 20, + "minChangeAssetSchemeCost" : 21, + "minComposeAssetCost" : 22, + "minDecomposeAssetCost" : 23, + "minUnwrapCccCost" : 24, + "minIncreaseAssetSupplyCost": 25, + "maxBodySize" : 4194304, + "snapshotPeriod": 16384, + "termSeconds": 3600, + "nominationExpiration": 26, + "custodyPeriod": 27, + "releasePeriod": 28, + "maxNumOfValidators": 29, + "minNumOfValidators": 30, + "delegationThreshold": 31, + "minDeposit": 32, + "maxCandidateMetadataSize": 33, + "era": 34 + }"#; + + let deserialized: Params = serde_json::from_str(s).unwrap(); + assert_eq!(deserialized.max_extra_data_size, 0x20.into()); + assert_eq!(deserialized.max_asset_scheme_metadata_size, 0x0400.into()); + assert_eq!(deserialized.max_transfer_metadata_size, 0x0100.into()); + assert_eq!(deserialized.max_text_content_size, 0x0200.into()); + assert_eq!(deserialized.network_id, "tc".into()); + assert_eq!(deserialized.min_pay_cost, 10.into()); + assert_eq!(deserialized.min_set_regular_key_cost, 11.into()); + assert_eq!(deserialized.min_create_shard_cost, 12.into()); + assert_eq!(deserialized.min_set_shard_owners_cost, 13.into()); + assert_eq!(deserialized.min_set_shard_users_cost, 14.into()); + assert_eq!(deserialized.min_wrap_ccc_cost, 15.into()); + assert_eq!(deserialized.min_custom_cost, 16.into()); + assert_eq!(deserialized.min_store_cost, 17.into()); + assert_eq!(deserialized.min_remove_cost, 18.into()); + assert_eq!(deserialized.min_mint_asset_cost, 19.into()); + assert_eq!(deserialized.min_transfer_asset_cost, 20.into()); + assert_eq!(deserialized.min_change_asset_scheme_cost, 21.into()); + assert_eq!(deserialized.min_compose_asset_cost, 22.into()); + assert_eq!(deserialized.min_decompose_asset_cost, 23.into()); + assert_eq!(deserialized.min_unwrap_ccc_cost, 24.into()); + assert_eq!(deserialized.min_increase_asset_supply_cost, 25.into()); + assert_eq!(deserialized.max_body_size, 4_194_304.into()); + assert_eq!(deserialized.snapshot_period, 16_384.into()); + assert_eq!(deserialized.term_seconds, Some(3600.into())); + assert_eq!(deserialized.nomination_expiration, Some(26.into())); + assert_eq!(deserialized.custody_period, Some(27.into())); + assert_eq!(deserialized.release_period, Some(28.into())); + assert_eq!(deserialized.max_num_of_validators, Some(29.into())); + assert_eq!(deserialized.min_num_of_validators, Some(30.into())); + assert_eq!(deserialized.delegation_threshold, Some(31.into())); + assert_eq!(deserialized.min_deposit, Some(32.into())); + assert_eq!(deserialized.max_candidate_metadata_size, Some(33.into())); + assert_eq!(deserialized.era, Some(34.into())); + } } diff --git a/test/src/e2e.dynval/1/dv.era.test.ts b/test/src/e2e.dynval/1/dv.era.test.ts new file mode 100644 index 0000000000..6323d6db51 --- /dev/null +++ b/test/src/e2e.dynval/1/dv.era.test.ts @@ -0,0 +1,86 @@ +// 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 * as chai from "chai"; +import { expect } from "chai"; +import * as chaiAsPromised from "chai-as-promised"; +import "mocha"; + +import { validators } from "../../../tendermint.dynval/constants"; +import { PromiseExpect } from "../../helper/promise"; +import { changeParams, setTermTestTimeout, withNodes } from "../setup"; + +chai.use(chaiAsPromised); + +describe("Change era", function() { + const promiseExpect = new PromiseExpect(); + const { nodes, initialParams } = withNodes(this, { + promiseExpect, + overrideParams: { + minNumOfValidators: 3, + maxNumOfValidators: 5 + }, + validators: validators.slice(0, 3).map(signer => ({ + signer, + delegation: 5_000, + deposit: 10_000_000 + })) + }); + + it("should be enabled", async function() { + const termWaiter = setTermTestTimeout(this, { + terms: 3 + }); + + const checkingNode = nodes[0]; + const changeTxHash = await changeParams(checkingNode, 1, { + ...initialParams, + era: 1 + }); + + await checkingNode.waitForTx(changeTxHash); + + await termWaiter.waitNodeUntilTerm(checkingNode, { + target: 3, + termPeriods: 2 + }); + }); + + it("must increase monotonically", async function() { + const termWaiter = setTermTestTimeout(this, { + terms: 2 + }); + + const checkingNode = nodes[0]; + const changeTxHash = await changeParams(checkingNode, 1, { + ...initialParams, + era: 1 + }); + + await checkingNode.waitForTx(changeTxHash); + + await expect( + changeParams(checkingNode, 2, { + ...initialParams, + era: 0 + }) + ).eventually.rejected; + }); + + afterEach(function() { + promiseExpect.checkFulfilled(); + }); +}); diff --git a/test/src/e2e.dynval/setup.ts b/test/src/e2e.dynval/setup.ts index aa509e6685..493f6ef602 100644 --- a/test/src/e2e.dynval/setup.ts +++ b/test/src/e2e.dynval/setup.ts @@ -44,7 +44,7 @@ export function withNodes( options: { promiseExpect: PromiseExpect; validators: ValidatorConfig[]; - overrideParams?: Partial; + overrideParams?: Partial; onBeforeEnable?: (nodes: CodeChain[]) => Promise; } ) { @@ -98,7 +98,7 @@ export function findNode(nodes: CodeChain[], signer: Signer) { async function createNodes(options: { promiseExpect: PromiseExpect; validators: ValidatorConfig[]; - initialParams: typeof defaultParams; + initialParams: CommonParams; onBeforeEnable?: (nodes: CodeChain[]) => Promise; }): Promise { const chain = `${__dirname}/../scheme/tendermint-dynval.json`; @@ -363,12 +363,14 @@ export const defaultParams = { maxCandidateMetadataSize: 128 }; -export async function changeParams( - node: CodeChain, - metadataSeq: number, - params: typeof defaultParams -) { - const newParams: any[] = [ +interface EraCommonParams { + era: number; +} + +type CommonParams = typeof defaultParams & Partial; + +function encodeParams(params: CommonParams): any[] { + const result = [ params.maxExtraDataSize, params.maxAssetSchemeMetadataSize, params.maxTransferMetadataSize, @@ -402,12 +404,23 @@ export async function changeParams( params.minDeposit, params.maxCandidateMetadataSize ]; + if (params.era) { + result.push(params.era); + } + return result; +} + +export async function changeParams( + node: CodeChain, + metadataSeq: number, + params: CommonParams +) { const changeParamsActionRlp: [ number, number, (number | string)[], ...string[] - ] = [0xff, metadataSeq, newParams]; + ] = [0xff, metadataSeq, encodeParams(params)]; const message = blake256(RLP.encode(changeParamsActionRlp).toString("hex")); changeParamsActionRlp.push( `0x${SDK.util.signEcdsa(message, faucetSecret)}` diff --git a/types/src/common_params.rs b/types/src/common_params.rs index f81e690f7d..e59c1eab9a 100644 --- a/types/src/common_params.rs +++ b/types/src/common_params.rs @@ -64,6 +64,8 @@ pub struct CommonParams { delegation_threshold: u64, min_deposit: u64, max_candidate_metadata_size: usize, + + era: u64, } impl CommonParams { @@ -170,6 +172,10 @@ impl CommonParams { self.max_candidate_metadata_size } + pub fn era(&self) -> u64 { + self.era + } + pub fn verify(&self) -> Result<(), String> { if self.term_seconds != 0 { if self.nomination_expiration == 0 { @@ -214,17 +220,41 @@ impl CommonParams { } Ok(()) } + + pub fn verify_change(&self, current_params: &Self) -> Result<(), String> { + self.verify()?; + let current_network_id = current_params.network_id(); + let transaction_network_id = self.network_id(); + if current_network_id != transaction_network_id { + return Err(format!( + "The current network id is {} but the transaction tries to change the network id to {}", + current_network_id, transaction_network_id + )) + } + if self.era < current_params.era { + return Err(format!("The era({}) shouldn't be less than the current era({})", self.era, current_params.era)) + } + Ok(()) + } } const DEFAULT_PARAMS_SIZE: usize = 23; const NUMBER_OF_STAKE_PARAMS: usize = 9; +const NUMBER_OF_ERA_PARAMS: usize = 1; +const STAKE_PARAM_SIZE: usize = DEFAULT_PARAMS_SIZE + NUMBER_OF_STAKE_PARAMS; +const ERA_PARAM_SIZE: usize = STAKE_PARAM_SIZE + NUMBER_OF_ERA_PARAMS; + +const VALID_SIZE: &[usize] = &[DEFAULT_PARAMS_SIZE, STAKE_PARAM_SIZE, ERA_PARAM_SIZE]; impl From for CommonParams { fn from(p: Params) -> Self { - let mut size = DEFAULT_PARAMS_SIZE; - if p.term_seconds.is_some() { - size += NUMBER_OF_STAKE_PARAMS; - } + let size = if p.era.is_some() { + ERA_PARAM_SIZE + } else if p.term_seconds.is_some() { + STAKE_PARAM_SIZE + } else { + DEFAULT_PARAMS_SIZE + }; Self { size, max_extra_data_size: p.max_extra_data_size.into(), @@ -259,6 +289,7 @@ impl From for CommonParams { delegation_threshold: p.delegation_threshold.map(From::from).unwrap_or_default(), min_deposit: p.min_deposit.map(From::from).unwrap_or_default(), max_candidate_metadata_size: p.max_candidate_metadata_size.map(From::from).unwrap_or_default(), + era: p.era.map(From::from).unwrap_or_default(), } } } @@ -292,7 +323,7 @@ impl From for Params { snapshot_period: p.snapshot_period().into(), ..Default::default() }; - if p.size == DEFAULT_PARAMS_SIZE + NUMBER_OF_STAKE_PARAMS { + if p.size >= STAKE_PARAM_SIZE { result.term_seconds = Some(p.term_seconds().into()); result.nomination_expiration = Some(p.nomination_expiration().into()); result.custody_period = Some(p.custody_period().into()); @@ -303,13 +334,15 @@ impl From for Params { result.min_deposit = Some(p.min_deposit().into()); result.max_candidate_metadata_size = Some(p.max_candidate_metadata_size().into()); } + if p.size >= ERA_PARAM_SIZE { + result.era = Some(p.era().into()); + } result } } impl Encodable for CommonParams { fn rlp_append(&self, s: &mut RlpStream) { - const VALID_SIZE: &[usize] = &[DEFAULT_PARAMS_SIZE, DEFAULT_PARAMS_SIZE + NUMBER_OF_STAKE_PARAMS]; assert!(VALID_SIZE.contains(&self.size), "{} must be in {:?}", self.size, VALID_SIZE); s.begin_list(self.size) .append(&self.max_extra_data_size) @@ -335,7 +368,7 @@ impl Encodable for CommonParams { .append(&self.min_asset_unwrap_ccc_cost) .append(&self.max_body_size) .append(&self.snapshot_period); - if self.size == DEFAULT_PARAMS_SIZE + NUMBER_OF_STAKE_PARAMS { + if self.size >= STAKE_PARAM_SIZE { s.append(&self.term_seconds) .append(&self.nomination_expiration) .append(&self.custody_period) @@ -346,13 +379,15 @@ impl Encodable for CommonParams { .append(&self.min_deposit) .append(&self.max_candidate_metadata_size); } + if self.size >= ERA_PARAM_SIZE { + s.append(&self.era); + } } } impl Decodable for CommonParams { fn decode(rlp: &Rlp) -> Result { let size = rlp.item_count()?; - const VALID_SIZE: &[usize] = &[DEFAULT_PARAMS_SIZE, DEFAULT_PARAMS_SIZE + NUMBER_OF_STAKE_PARAMS]; if !VALID_SIZE.contains(&size) { return Err(DecoderError::RlpIncorrectListLen { expected: DEFAULT_PARAMS_SIZE, @@ -394,7 +429,7 @@ impl Decodable for CommonParams { delegation_threshold, min_deposit, max_candidate_metadata_size, - ) = if size >= DEFAULT_PARAMS_SIZE + NUMBER_OF_STAKE_PARAMS { + ) = if size >= STAKE_PARAM_SIZE { ( rlp.val_at(23)?, rlp.val_at(24)?, @@ -409,6 +444,13 @@ impl Decodable for CommonParams { } else { Default::default() }; + + let era = if size >= ERA_PARAM_SIZE { + rlp.val_at(32)? + } else { + Default::default() + }; + Ok(Self { size, max_extra_data_size, @@ -443,6 +485,7 @@ impl Decodable for CommonParams { delegation_threshold, min_deposit, max_candidate_metadata_size, + era, }) } } @@ -514,7 +557,7 @@ mod tests { #[test] fn rlp_with_extra_fields() { let mut params = CommonParams::default_for_test(); - params.size = DEFAULT_PARAMS_SIZE + NUMBER_OF_STAKE_PARAMS; + params.size = ERA_PARAM_SIZE; params.term_seconds = 100; params.min_deposit = 123; rlp_encode_and_decode_test!(params); @@ -524,7 +567,7 @@ mod tests { fn rlp_encoding_are_different_if_the_size_are_different() { let origin = CommonParams::default_for_test(); let mut params = origin; - params.size = DEFAULT_PARAMS_SIZE + NUMBER_OF_STAKE_PARAMS; + params.size = ERA_PARAM_SIZE; assert_ne!(rlp::encode(&origin), rlp::encode(¶ms)); } @@ -591,6 +634,7 @@ mod tests { assert_eq!(deserialized.delegation_threshold, 0); assert_eq!(deserialized.min_deposit, 0); assert_eq!(deserialized.max_candidate_metadata_size, 0); + assert_eq!(deserialized.era, 0); assert_eq!(params, deserialized.into()); } @@ -627,6 +671,7 @@ mod tests { let params = serde_json::from_str::(s).unwrap(); let deserialized = CommonParams::from(params.clone()); + assert_eq!(deserialized.size, STAKE_PARAM_SIZE); assert_eq!(deserialized.max_extra_data_size, 0x20); assert_eq!(deserialized.max_asset_scheme_metadata_size, 0x0400); assert_eq!(deserialized.max_transfer_metadata_size, 0x0100); @@ -659,6 +704,7 @@ mod tests { assert_eq!(deserialized.delegation_threshold, 0); assert_eq!(deserialized.min_deposit, 0); assert_eq!(deserialized.max_candidate_metadata_size, 0); + assert_eq!(deserialized.era, 0); assert_eq!( Params { @@ -670,6 +716,7 @@ mod tests { delegation_threshold: Some(0.into()), min_deposit: Some(0.into()), max_candidate_metadata_size: Some(0.into()), + era: None, ..params }, deserialized.into(), @@ -716,6 +763,85 @@ mod tests { }"#; let params = serde_json::from_str::(s).unwrap(); let deserialized = CommonParams::from(params.clone()); + assert_eq!(deserialized.size, STAKE_PARAM_SIZE); + assert_eq!(deserialized.max_extra_data_size, 0x20); + assert_eq!(deserialized.max_asset_scheme_metadata_size, 0x0400); + assert_eq!(deserialized.max_transfer_metadata_size, 0x0100); + assert_eq!(deserialized.max_text_content_size, 0x0200); + assert_eq!(deserialized.network_id, "tc".into()); + assert_eq!(deserialized.min_pay_transaction_cost, 10); + assert_eq!(deserialized.min_set_regular_key_transaction_cost, 11); + assert_eq!(deserialized.min_create_shard_transaction_cost, 12); + assert_eq!(deserialized.min_set_shard_owners_transaction_cost, 13); + assert_eq!(deserialized.min_set_shard_users_transaction_cost, 14); + assert_eq!(deserialized.min_wrap_ccc_transaction_cost, 15); + assert_eq!(deserialized.min_custom_transaction_cost, 16); + assert_eq!(deserialized.min_store_transaction_cost, 17); + assert_eq!(deserialized.min_remove_transaction_cost, 18); + assert_eq!(deserialized.min_asset_mint_cost, 19); + assert_eq!(deserialized.min_asset_transfer_cost, 20); + assert_eq!(deserialized.min_asset_scheme_change_cost, 21); + assert_eq!(deserialized.min_asset_compose_cost, 22); + assert_eq!(deserialized.min_asset_decompose_cost, 23); + assert_eq!(deserialized.min_asset_unwrap_ccc_cost, 24); + assert_eq!(deserialized.min_asset_supply_increase_cost, 25); + assert_eq!(deserialized.max_body_size, 4_194_304); + assert_eq!(deserialized.snapshot_period, 16_384); + assert_eq!(deserialized.term_seconds, 3600); + assert_eq!(deserialized.nomination_expiration, 26); + assert_eq!(deserialized.custody_period, 27); + assert_eq!(deserialized.release_period, 28); + assert_eq!(deserialized.max_num_of_validators, 29); + assert_eq!(deserialized.min_num_of_validators, 30); + assert_eq!(deserialized.delegation_threshold, 31); + assert_eq!(deserialized.min_deposit, 32); + assert_eq!(deserialized.max_candidate_metadata_size, 33); + assert_eq!(deserialized.era, 0); + + assert_eq!(params, deserialized.into()); + } + + #[test] + #[allow(clippy::cognitive_complexity)] + fn params_from_json_with_era() { + let s = r#"{ + "maxExtraDataSize": "0x20", + "maxAssetSchemeMetadataSize": "0x0400", + "maxTransferMetadataSize": "0x0100", + "maxTextContentSize": "0x0200", + "networkID" : "tc", + "minPayCost" : 10, + "minSetRegularKeyCost" : 11, + "minCreateShardCost" : 12, + "minSetShardOwnersCost" : 13, + "minSetShardUsersCost" : 14, + "minWrapCccCost" : 15, + "minCustomCost" : 16, + "minStoreCost" : 17, + "minRemoveCost" : 18, + "minMintAssetCost" : 19, + "minTransferAssetCost" : 20, + "minChangeAssetSchemeCost" : 21, + "minComposeAssetCost" : 22, + "minDecomposeAssetCost" : 23, + "minUnwrapCccCost" : 24, + "minIncreaseAssetSupplyCost": 25, + "maxBodySize" : 4194304, + "snapshotPeriod": 16384, + "termSeconds": 3600, + "nominationExpiration": 26, + "custodyPeriod": 27, + "releasePeriod": 28, + "maxNumOfValidators": 29, + "minNumOfValidators": 30, + "delegationThreshold": 31, + "minDeposit": 32, + "maxCandidateMetadataSize": 33, + "era": 34 + }"#; + let params = serde_json::from_str::(s).unwrap(); + let deserialized = CommonParams::from(params.clone()); + assert_eq!(deserialized.size, ERA_PARAM_SIZE); assert_eq!(deserialized.max_extra_data_size, 0x20); assert_eq!(deserialized.max_asset_scheme_metadata_size, 0x0400); assert_eq!(deserialized.max_transfer_metadata_size, 0x0100); @@ -748,6 +874,7 @@ mod tests { assert_eq!(deserialized.delegation_threshold, 31); assert_eq!(deserialized.min_deposit, 32); assert_eq!(deserialized.max_candidate_metadata_size, 33); + assert_eq!(deserialized.era, 34); assert_eq!(params, deserialized.into()); }