diff --git a/Cargo.lock b/Cargo.lock index e973fb2536..44006f5f0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -722,6 +722,7 @@ dependencies = [ name = "ckb-jsonrpc-types" version = "0.44.0-pre" dependencies = [ + "ckb-constant", "ckb-types", "faster-hex", "lazy_static", @@ -1426,6 +1427,7 @@ dependencies = [ "bit-vec", "bytes 1.0.1", "ckb-channel", + "ckb-constant", "ckb-error", "ckb-fixed-hash", "ckb-hash", diff --git a/script/src/error.rs b/script/src/error.rs index f8b70dce2f..4dd7e70ca5 100644 --- a/script/src/error.rs +++ b/script/src/error.rs @@ -33,7 +33,7 @@ pub enum ScriptError { InvalidScriptHashType(String), /// InvalidVmVersion - #[error("Invalid vm version {0}")] + #[error("Invalid VM Version: {0}")] InvalidVmVersion(u8), /// Known bugs are detected in transaction script outputs diff --git a/test/Cargo.toml b/test/Cargo.toml index b23e21bfd4..f4086c9e7f 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -39,6 +39,13 @@ lazy_static = "1.4.0" byteorder = "1.3.1" jsonrpc-core = "17.1" +[features] +default = [ + "ckb-constant/test-only", + "ckb-types/test-only", + "ckb-jsonrpc-types/test-only" +] + # Prevent this from interfering with workspaces [workspace] members = ["."] diff --git a/test/src/main.rs b/test/src/main.rs index b02c6798f1..7cf8eb1297 100644 --- a/test/src/main.rs +++ b/test/src/main.rs @@ -493,6 +493,7 @@ fn all_specs() -> Vec> { Box::new(CheckAbsoluteEpochSince), Box::new(CheckRelativeEpochSince), Box::new(CheckBlockExtension), + Box::new(CheckVmVersion), Box::new(DuplicateCellDepsForDataHashTypeLockScript), Box::new(DuplicateCellDepsForDataHashTypeTypeScript), Box::new(DuplicateCellDepsForTypeHashTypeLockScript), diff --git a/test/src/specs/hardfork/v2021/mod.rs b/test/src/specs/hardfork/v2021/mod.rs index 0b4de99a0b..75162edd34 100644 --- a/test/src/specs/hardfork/v2021/mod.rs +++ b/test/src/specs/hardfork/v2021/mod.rs @@ -1,6 +1,7 @@ mod cell_deps; mod extension; mod since; +mod vm_version; pub use cell_deps::{ DuplicateCellDepsForDataHashTypeLockScript, DuplicateCellDepsForDataHashTypeTypeScript, @@ -8,3 +9,4 @@ pub use cell_deps::{ }; pub use extension::CheckBlockExtension; pub use since::{CheckAbsoluteEpochSince, CheckRelativeEpochSince}; +pub use vm_version::CheckVmVersion; diff --git a/test/src/specs/hardfork/v2021/vm_version.rs b/test/src/specs/hardfork/v2021/vm_version.rs new file mode 100644 index 0000000000..28dbf0551e --- /dev/null +++ b/test/src/specs/hardfork/v2021/vm_version.rs @@ -0,0 +1,387 @@ +use crate::{ + util::{ + cell::gen_spendable, + check::{assert_epoch_should_less_than, is_transaction_committed}, + mining::{mine, mine_until_bool, mine_until_epoch}, + }, + utils::{assert_send_transaction_fail, wait_until}, + Node, Spec, +}; +use ckb_constant::MAX_VM_VERSION as RPC_MAX_VM_VERSION; +use ckb_jsonrpc_types as rpc; +use ckb_logger::{debug, info}; +use ckb_types::{ + core::{Capacity, DepType, ScriptHashType, TransactionView}, + packed, + prelude::*, +}; +use std::fmt; + +const MAX_VM_VERSION: u8 = RPC_MAX_VM_VERSION - 1; + +const GENESIS_EPOCH_LENGTH: u64 = 10; +const CKB2021_START_EPOCH: u64 = 10; + +const TEST_CASES_COUNT: usize = (RPC_MAX_VM_VERSION as usize + 1 + 1) * 2; +const INITIAL_INPUTS_COUNT: usize = 1 + TEST_CASES_COUNT * 2; + +pub struct CheckVmVersion; + +struct NewScript { + cell_dep: packed::CellDep, + data_hash: packed::Byte32, + type_hash: packed::Byte32, +} + +#[derive(Debug, Clone, Copy)] +enum ExpectedResult { + ShouldBePassed, + RpcInvalidVmVersion, + LockInvalidVmVersion, + TypeInvalidVmVersion, +} + +struct CheckVmVersionTestRunner<'a> { + node: &'a Node, +} + +impl Spec for CheckVmVersion { + crate::setup!(num_nodes: 2); + + fn run(&self, nodes: &mut Vec) { + let epoch_length = GENESIS_EPOCH_LENGTH; + let ckb2019_last_epoch = CKB2021_START_EPOCH - 1; + + let node = &nodes[0]; + let node1 = &nodes[1]; + + mine(node, 1); + node1.connect(node); + + { + let mut inputs = gen_spendable(node, INITIAL_INPUTS_COUNT) + .into_iter() + .map(|input| packed::CellInput::new(input.out_point, 0)); + let script = NewScript::new_with_id(node, 0, &mut inputs); + let runner = CheckVmVersionTestRunner::new(node); + + info!("CKB v2019:"); + runner.run_all_tests(&mut inputs, &script, 0); + + assert_epoch_should_less_than(node, ckb2019_last_epoch, epoch_length - 4, epoch_length); + mine_until_epoch(node, ckb2019_last_epoch, epoch_length - 4, epoch_length); + + info!("CKB v2021:"); + runner.run_all_tests(&mut inputs, &script, 1); + } + + { + info!("Test Sync:"); + let (rpc_client0, rpc_client1) = (node.rpc_client(), node1.rpc_client()); + + let ret = wait_until(20, || { + let header0 = rpc_client0.get_tip_header(); + let header1 = rpc_client1.get_tip_header(); + header0 == header1 + }); + assert!( + ret, + "Nodes should sync with each other until same tip chain", + ); + } + } + + fn modify_chain_spec(&self, spec: &mut ckb_chain_spec::ChainSpec) { + spec.params.permanent_difficulty_in_dummy = Some(true); + spec.params.genesis_epoch_length = Some(GENESIS_EPOCH_LENGTH); + if spec.params.hardfork.is_none() { + spec.params.hardfork = Some(Default::default()); + } + if let Some(mut switch) = spec.params.hardfork.as_mut() { + switch.rfc_pr_0237 = Some(CKB2021_START_EPOCH); + } + } +} + +impl NewScript { + fn new_with_id( + node: &Node, + id: u8, + inputs: &mut impl Iterator, + ) -> Self { + let original_data = node.always_success_raw_data(); + let data = packed::Bytes::new_builder() + .extend(original_data.as_ref().iter().map(|x| (*x).into())) + .push(id.into()) + .build(); + let tx = Self::deploy(node, &data, inputs); + let cell_dep = packed::CellDep::new_builder() + .out_point(packed::OutPoint::new(tx.hash(), 0)) + .dep_type(DepType::Code.into()) + .build(); + let data_hash = packed::CellOutput::calc_data_hash(&data.raw_data()); + let type_hash = tx + .output(0) + .unwrap() + .type_() + .to_opt() + .unwrap() + .calc_script_hash(); + Self { + cell_dep, + data_hash, + type_hash, + } + } + + fn deploy( + node: &Node, + data: &packed::Bytes, + inputs: &mut impl Iterator, + ) -> TransactionView { + let type_script = node.always_success_script(); + let tx_template = TransactionView::new_advanced_builder(); + let cell_input = inputs.next().unwrap(); + let cell_output = packed::CellOutput::new_builder() + .type_(Some(type_script).pack()) + .build_exact_capacity(Capacity::bytes(data.len()).unwrap()) + .unwrap(); + let tx = tx_template + .cell_dep(node.always_success_cell_dep()) + .input(cell_input) + .output(cell_output) + .output_data(data.clone()) + .build(); + node.submit_transaction(&tx); + mine_until_bool(node, || is_transaction_committed(node, &tx)); + tx + } + + fn cell_dep(&self) -> packed::CellDep { + self.cell_dep.clone() + } + + fn as_data_script(&self, vm_version: u8) -> packed::Script { + packed::Script::new_builder() + .code_hash(self.data_hash.clone()) + .hash_type(ScriptHashType::Data(vm_version).into()) + .build() + } + + fn as_type_script(&self) -> packed::Script { + packed::Script::new_builder() + .code_hash(self.type_hash.clone()) + .hash_type(ScriptHashType::Type.into()) + .build() + } +} + +impl fmt::Display for ExpectedResult { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::ShouldBePassed => write!(f, " allowed"), + _ => write!(f, "not allowed"), + } + } +} + +impl ExpectedResult { + fn error_message(self) -> Option<&'static str> { + match self { + Self::ShouldBePassed => None, + Self::RpcInvalidVmVersion => Some( + "{\"code\":-32602,\"message\":\"\ + Invalid params: the maximum vm version currently supported is", + ), + Self::LockInvalidVmVersion => Some( + "{\"code\":-302,\"message\":\"TransactionFailedToVerify: \ + Verification failed Script(TransactionScriptError \ + { source: Inputs[0].Lock, cause: Invalid VM Version:", + ), + Self::TypeInvalidVmVersion => Some( + "{\"code\":-302,\"message\":\"TransactionFailedToVerify: \ + Verification failed Script(TransactionScriptError { \ + source: Outputs[0].Type, cause: Invalid VM Version: ", + ), + } + } +} + +impl<'a> CheckVmVersionTestRunner<'a> { + fn new(node: &'a Node) -> Self { + Self { node } + } + + fn test_create( + &self, + inputs: &mut impl Iterator, + cell_dep_opt: Option, + script: packed::Script, + expected: ExpectedResult, + ) -> Option { + let (tx_builder, co_builder) = if let Some(cell_dep) = cell_dep_opt { + ( + TransactionView::new_advanced_builder().cell_dep(cell_dep), + packed::CellOutput::new_builder() + .lock(self.node.always_success_script()) + .type_(Some(script).pack()), + ) + } else { + ( + TransactionView::new_advanced_builder(), + packed::CellOutput::new_builder().lock(script), + ) + }; + let cell_input = inputs.next().unwrap(); + let input_cell = self.get_previous_output(&cell_input); + let cell_output = co_builder + .capacity((input_cell.capacity.value() - 1).pack()) + .build(); + let tx = tx_builder + .cell_dep(self.node.always_success_cell_dep()) + .input(cell_input) + .output(cell_output) + .output_data(Default::default()) + .build(); + if let Some(errmsg) = expected.error_message() { + assert_send_transaction_fail(self.node, &tx, &errmsg); + None + } else { + self.submit_transaction_until_committed(&tx); + Some(tx) + } + } + + fn test_spend( + &self, + tx: TransactionView, + cell_dep: packed::CellDep, + has_always_success: bool, + expected: ExpectedResult, + ) { + let out_point = packed::OutPoint::new(tx.hash(), 0); + let input = packed::CellInput::new(out_point, 0); + let output = packed::CellOutput::new_builder() + .build_exact_capacity(Capacity::shannons(0)) + .unwrap(); + let tx = if has_always_success { + TransactionView::new_advanced_builder().cell_dep(self.node.always_success_cell_dep()) + } else { + TransactionView::new_advanced_builder() + } + .cell_dep(cell_dep) + .input(input) + .output(output) + .output_data(Default::default()) + .build(); + if let Some(errmsg) = expected.error_message() { + assert_send_transaction_fail(self.node, &tx, &errmsg); + } else { + self.submit_transaction_until_committed(&tx); + } + } + + fn get_previous_output(&self, cell_input: &packed::CellInput) -> rpc::CellOutput { + let previous_output = cell_input.previous_output(); + let previous_output_index: usize = previous_output.index().unpack(); + self.node + .rpc_client() + .get_transaction(previous_output.tx_hash()) + .unwrap() + .transaction + .inner + .outputs[previous_output_index] + .clone() + } + + fn submit_transaction_until_committed(&self, tx: &TransactionView) { + debug!(">>> >>> submit: transaction {:#x}.", tx.hash()); + self.node.submit_transaction(tx); + mine_until_bool(self.node, || is_transaction_committed(self.node, tx)); + } + + fn run_all_tests( + &self, + inputs: &mut impl Iterator, + script: &NewScript, + max_vm_version: u8, + ) { + for vm_version in 0..=RPC_MAX_VM_VERSION { + // TODO ckb2021 Should NOT pass if vm_version is greater than the max_vm_version? + let res = if vm_version <= MAX_VM_VERSION { + ExpectedResult::ShouldBePassed + } else { + ExpectedResult::RpcInvalidVmVersion + }; + info!( + ">>> Create a cell with Data({:2}) lock script is {}", + vm_version, res + ); + let s = script.as_data_script(vm_version); + if let Some(tx) = self.test_create(inputs, None, s, res) { + let res = if vm_version <= max_vm_version { + ExpectedResult::ShouldBePassed + } else { + ExpectedResult::LockInvalidVmVersion + }; + info!( + ">>> Spend the cell with Data({:2}) lock script is {}", + vm_version, res + ); + let dep = script.cell_dep(); + self.test_spend(tx, dep, false, res); + } + } + { + let res = ExpectedResult::ShouldBePassed; + info!(">>> Create a cell with Type lock script is {}", res); + let s = script.as_type_script(); + if let Some(tx) = self.test_create(inputs, None, s, res) { + let res = ExpectedResult::ShouldBePassed; + info!(">>> Spend the cell with Type lock script is {}", res); + let dep = script.cell_dep(); + self.test_spend(tx, dep, false, res); + } + } + for vm_version in 0..=RPC_MAX_VM_VERSION { + let res = if vm_version <= max_vm_version { + ExpectedResult::ShouldBePassed + } else if vm_version <= MAX_VM_VERSION { + ExpectedResult::TypeInvalidVmVersion + } else { + ExpectedResult::RpcInvalidVmVersion + }; + info!( + ">>> Create a cell with Data({:2}) type script is {}", + vm_version, res + ); + let dep = Some(script.cell_dep()); + let s = script.as_data_script(vm_version); + if let Some(tx) = self.test_create(inputs, dep, s, res) { + let res = if vm_version <= max_vm_version { + ExpectedResult::ShouldBePassed + } else { + ExpectedResult::TypeInvalidVmVersion + }; + info!( + ">>> Spend the cell with Data({:2}) type script is {}", + vm_version, res + ); + let dep = script.cell_dep(); + self.test_spend(tx, dep, true, res); + } + } + { + let res = ExpectedResult::ShouldBePassed; + info!(">>> Create a cell with Type type script is {}", res); + let dep = Some(script.cell_dep()); + let s = script.as_type_script(); + if let Some(tx) = self.test_create(inputs, dep, s, res) { + let res = ExpectedResult::ShouldBePassed; + info!(">>> Spend the cell with Type type script is {}", res); + let dep = script.cell_dep(); + self.test_spend(tx, dep, true, res); + } + } + } +} diff --git a/util/constant/Cargo.toml b/util/constant/Cargo.toml index 6ceff83383..51ee9fd689 100644 --- a/util/constant/Cargo.toml +++ b/util/constant/Cargo.toml @@ -11,3 +11,6 @@ repository = "https://github.com/nervosnetwork/ckb" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + +[features] +test-only = [] diff --git a/util/constant/src/lib.rs b/util/constant/src/lib.rs index 85eb438468..0c2330f108 100644 --- a/util/constant/src/lib.rs +++ b/util/constant/src/lib.rs @@ -6,3 +6,13 @@ pub mod hardfork; pub mod store; /// sync constant pub mod sync; + +/// The maximum vm version number. +#[cfg(not(feature = "test-only"))] +pub const MAX_VM_VERSION: u8 = 1; + +/// The fake maximum vm version number for test only. +/// +/// This number should be 1 larger than the real maximum vm version number. +#[cfg(feature = "test-only")] +pub const MAX_VM_VERSION: u8 = 2; diff --git a/util/jsonrpc-types/Cargo.toml b/util/jsonrpc-types/Cargo.toml index ca645f2b83..052ed0db40 100644 --- a/util/jsonrpc-types/Cargo.toml +++ b/util/jsonrpc-types/Cargo.toml @@ -9,11 +9,15 @@ homepage = "https://github.com/nervosnetwork/ckb" repository = "https://github.com/nervosnetwork/ckb" [dependencies] +ckb-constant = { path = "../constant", version = "= 0.44.0-pre" } ckb-types = { path = "../types", version = "= 0.44.0-pre" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" faster-hex = "0.6" +[features] +test-only = ["ckb-constant/test-only", "ckb-types/test-only"] + [dev-dependencies] proptest = "0.9" regex = "1.1" diff --git a/util/jsonrpc-types/src/blockchain.rs b/util/jsonrpc-types/src/blockchain.rs index 881430f81d..b54eac20f6 100644 --- a/util/jsonrpc-types/src/blockchain.rs +++ b/util/jsonrpc-types/src/blockchain.rs @@ -3,6 +3,7 @@ use crate::{ BlockNumber, Byte32, Capacity, Cycle, EpochNumber, EpochNumberWithFraction, ProposalShortId, Timestamp, Uint128, Uint32, Uint64, Version, VmVersion, }; +use ckb_constant::MAX_VM_VERSION; use ckb_types::{core, packed, prelude::*, H256}; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; @@ -46,15 +47,15 @@ pub enum ScriptHashTypeKind { Type, } -#[derive(Serialize, Deserialize)] +#[derive(Deserialize)] #[serde(deny_unknown_fields)] struct ScriptHashTypeShadow { kind: ScriptHashTypeKind, - #[serde(rename = "vm_version", skip_serializing_if = "Option::is_none")] + #[serde(rename = "vm_version")] vm_version_opt: Option, } -struct ScriptHashTypeValidationError(&'static str); +struct ScriptHashTypeValidationError(String); impl std::fmt::Display for ScriptHashTypeValidationError { fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -72,17 +73,24 @@ impl std::convert::TryFrom for ScriptHashType { match kind { ScriptHashTypeKind::Data => { if let Some(vm_version) = vm_version_opt { - Ok(ScriptHashType::Data { vm_version }) + if vm_version > MAX_VM_VERSION { + Err(ScriptHashTypeValidationError(format!( + "the maximum vm version currently supported is {}, but got {}.", + MAX_VM_VERSION, vm_version + ))) + } else { + Ok(ScriptHashType::Data { vm_version }) + } } else { Err(ScriptHashTypeValidationError( - "vm version should be provided for hash-type \"data\".", + "vm version should be provided for hash-type \"data\".".to_owned(), )) } } ScriptHashTypeKind::Type => { if vm_version_opt.is_some() { Err(ScriptHashTypeValidationError( - "vm version is not allowed for hash-type \"type\".", + "vm version is not allowed for hash-type \"type\".".to_owned(), )) } else { Ok(ScriptHashType::Type) @@ -1401,12 +1409,12 @@ mod tests { \"args\":\"0x\",\ \"hash_type\":{\ \"kind\":\"data\",\ - \"vm_version\":2\ + \"vm_version\":1\ }\ }", Script { code_hash: h256!("0x1"), - hash_type: ScriptHashType::Data { vm_version: 2 }, + hash_type: ScriptHashType::Data { vm_version: 1 }, args: JsonBytes::default(), }, ), @@ -1473,6 +1481,15 @@ mod tests { \"unknown_field\":0,\ \"args\":\"0x\"\ }", + "{\ + \"code_hash\":\"0x00000000000000000000000000000000\ + 00000000000000000000000000000001\",\ + \"args\":\"0x\",\ + \"hash_type\":{\ + \"kind\":\"data\",\ + \"vm_version\":2\ + }\ + }", ] { let result: Result = serde_json::from_str(&malformed); assert!( diff --git a/util/types/Cargo.toml b/util/types/Cargo.toml index dba28aa9e8..38110a3479 100644 --- a/util/types/Cargo.toml +++ b/util/types/Cargo.toml @@ -9,6 +9,7 @@ homepage = "https://github.com/nervosnetwork/ckb" repository = "https://github.com/nervosnetwork/ckb" [dependencies] +ckb-constant = { path = "../constant", version = "= 0.44.0-pre" } molecule = "=0.7.1" ckb-fixed-hash = { path = "../fixed-hash", version = "= 0.44.0-pre" } numext-fixed-uint = { version = "0.1", features = ["support_rand", "support_heapsize", "support_serde"] } @@ -23,5 +24,8 @@ ckb-rational = { path = "../rational", version = "= 0.44.0-pre" } once_cell = "1.3.1" derive_more = { version = "0.99.0", default-features=false, features = ["display"] } +[features] +test-only = ["ckb-constant/test-only"] + [dev-dependencies] proptest = "0.9" diff --git a/util/types/src/core/blockchain.rs b/util/types/src/core/blockchain.rs index 4ae9bd83cd..5ec4fba454 100644 --- a/util/types/src/core/blockchain.rs +++ b/util/types/src/core/blockchain.rs @@ -1,3 +1,4 @@ +use ckb_constant::MAX_VM_VERSION; use ckb_error::OtherError; use std::convert::TryFrom; @@ -23,7 +24,17 @@ impl TryFrom for ScriptHashType { fn try_from(v: packed::Byte) -> Result { match Into::::into(v) { - x if x % 2 == 0 => Ok(ScriptHashType::Data(x / 2)), + x if x % 2 == 0 => { + let vm_version = x / 2; + if vm_version > MAX_VM_VERSION { + Err(OtherError::new(format!( + "The maximum vm version currently supported is {}, but got {}", + MAX_VM_VERSION, vm_version + ))) + } else { + Ok(ScriptHashType::Data(vm_version)) + } + } 1 => Ok(ScriptHashType::Type), _ => Err(OtherError::new(format!("Invalid script hash type {}", v))), } @@ -33,7 +44,7 @@ impl TryFrom for ScriptHashType { impl ScriptHashType { #[inline] pub(crate) fn verify_value(v: u8) -> bool { - v % 2 == 0 || v == 1 + v == 1 || ((v % 2 == 0) && (v / 2 <= MAX_VM_VERSION)) } }