From 22adb37db42d6ead1b7d28020602be8c5cf2b820 Mon Sep 17 00:00:00 2001 From: Xuejie Xiao Date: Fri, 4 Jan 2019 13:18:53 +0800 Subject: [PATCH] feat: Add block level script cycle limit --- Cargo.lock | 6 +- core/src/lib.rs | 1 + nodes_template/spec/dev.json | 3 +- pool/src/txs_pool/pool.rs | 5 +- script/Cargo.toml | 2 +- script/src/cost_model.rs | 79 ++++++++++++++++++++++ script/src/lib.rs | 1 + script/src/verify.rs | 86 +++++++++++++++++++----- spec/src/consensus.rs | 16 ++++- spec/src/lib.rs | 4 +- src/setup.rs | 3 +- verification/src/block_verifier.rs | 27 +++++--- verification/src/error.rs | 3 + verification/src/transaction_verifier.rs | 11 +-- 14 files changed, 205 insertions(+), 42 deletions(-) create mode 100644 script/src/cost_model.rs diff --git a/Cargo.lock b/Cargo.lock index 13aa51493a..78c58ad9d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -577,7 +577,7 @@ dependencies = [ "byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", "ckb-core 0.4.0-pre", "ckb-protocol 0.4.0-pre", - "ckb-vm 0.1.0 (git+https://github.com/nervosnetwork/ckb-vm?rev=f32bdcb)", + "ckb-vm 0.1.0 (git+https://github.com/nervosnetwork/ckb-vm?rev=dd90ed1)", "crypto 0.4.0-pre", "faster-hex 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "flatbuffers 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -670,7 +670,7 @@ dependencies = [ [[package]] name = "ckb-vm" version = "0.1.0" -source = "git+https://github.com/nervosnetwork/ckb-vm?rev=f32bdcb#f32bdcbed6067cbb3d88933126f65db50adfb29b" +source = "git+https://github.com/nervosnetwork/ckb-vm?rev=dd90ed1#dd90ed1ea23dfc72e25031314ce56a19dbe4ac47" dependencies = [ "byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3559,7 +3559,7 @@ dependencies = [ "checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4" "checksum chashmap 2.2.1 (git+https://github.com/redox-os/tfs)" = "" "checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" -"checksum ckb-vm 0.1.0 (git+https://github.com/nervosnetwork/ckb-vm?rev=f32bdcb)" = "" +"checksum ckb-vm 0.1.0 (git+https://github.com/nervosnetwork/ckb-vm?rev=dd90ed1)" = "" "checksum clang-sys 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e414af9726e1d11660801e73ccc7fb81803fb5f49e5903a25b348b2b3b480d2e" "checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e" "checksum clicolors-control 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "51872be694bb3bcbd1ea95c6dd467c2c46c6c64d287e1c9084ace7c3116ae9c0" diff --git a/core/src/lib.rs b/core/src/lib.rs index 00deb432d3..2f28190c73 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -20,3 +20,4 @@ pub use crate::error::Error; pub type PublicKey = numext_fixed_hash::H512; pub type BlockNumber = u64; pub type Capacity = u64; +pub type Cycle = u64; diff --git a/nodes_template/spec/dev.json b/nodes_template/spec/dev.json index aa848a97cc..76549dd2c5 100644 --- a/nodes_template/spec/dev.json +++ b/nodes_template/spec/dev.json @@ -32,7 +32,8 @@ "uncles_hash": "0x0000000000000000000000000000000000000000000000000000000000000000" }, "params": { - "initial_block_reward": 50000 + "initial_block_reward": 50000, + "max_block_cycles": 100000000 }, "system_cells": [ {"path": "cells/always_success"} diff --git a/pool/src/txs_pool/pool.rs b/pool/src/txs_pool/pool.rs index 19b0e5d880..cc203b4617 100644 --- a/pool/src/txs_pool/pool.rs +++ b/pool/src/txs_pool/pool.rs @@ -457,7 +457,7 @@ where if unknowns.is_empty() { // TODO: Parallel TransactionVerifier::new(&rtx) - .verify() + .verify(self.shared.consensus().max_block_cycles()) .map_err(PoolError::InvalidTx)?; } } @@ -480,7 +480,8 @@ where for tx in txs { let rtx = self.resolve_transaction(&tx); - let rs = TransactionVerifier::new(&rtx).verify(); + let rs = + TransactionVerifier::new(&rtx).verify(self.shared.consensus().max_block_cycles()); if rs.is_ok() { self.pool.add_transaction(tx); } else if rs == Err(TransactionError::DoubleSpent) { diff --git a/script/Cargo.toml b/script/Cargo.toml index f2138d5785..8ea7cfb8c6 100644 --- a/script/Cargo.toml +++ b/script/Cargo.toml @@ -11,7 +11,7 @@ byteorder = "1.2.2" crypto = {path = "../util/crypto"} ckb-core = { path = "../core" } hash = {path = "../util/hash"} -ckb-vm = { git = "https://github.com/nervosnetwork/ckb-vm", rev = "f32bdcb" } +ckb-vm = { git = "https://github.com/nervosnetwork/ckb-vm", rev = "dd90ed1" } faster-hex = "0.3" fnv = "1.0.3" flatbuffers = "0.5.0" diff --git a/script/src/cost_model.rs b/script/src/cost_model.rs new file mode 100644 index 0000000000..b645e16fb3 --- /dev/null +++ b/script/src/cost_model.rs @@ -0,0 +1,79 @@ +use ckb_vm::{ + instructions::{i, m, rvc}, + Instruction, +}; + +pub fn instruction_cycles(i: &Instruction) -> u64 { + match i { + Instruction::I(i) => match i { + i::Instruction::I(i) => match i.inst() { + i::ItypeInstruction::JALR => 3, + i::ItypeInstruction::LD => 2, + i::ItypeInstruction::LW => 3, + i::ItypeInstruction::LH => 3, + i::ItypeInstruction::LB => 3, + i::ItypeInstruction::LWU => 3, + i::ItypeInstruction::LHU => 3, + i::ItypeInstruction::LBU => 3, + _ => 1, + }, + i::Instruction::S(s) => match s.inst() { + // Here we choose to be explicit so as to avoid potential confusions. + i::StypeInstruction::SB => 3, + i::StypeInstruction::SH => 3, + i::StypeInstruction::SW => 3, + i::StypeInstruction::SD => 2, + }, + i::Instruction::B(_) => 3, + // Cycles for Env instructions will be processed in the Env code. + i::Instruction::Env(_) => 0, + i::Instruction::JAL { .. } => 3, + _ => 1, + }, + Instruction::RVC(i) => match i { + rvc::Instruction::Iu(i) => match i.inst() { + rvc::ItypeUInstruction::LW => 3, + rvc::ItypeUInstruction::LD => 2, + _ => 1, + }, + rvc::Instruction::Su(s) => match s.inst() { + rvc::StypeUInstruction::SW => 3, + rvc::StypeUInstruction::SD => 2, + _ => 1, + }, + rvc::Instruction::Uu(u) => match u.inst() { + rvc::UtypeUInstruction::LWSP => 3, + rvc::UtypeUInstruction::LDSP => 2, + _ => 1, + }, + rvc::Instruction::CSS(c) => match c.inst() { + rvc::CSSformatInstruction::SWSP => 3, + rvc::CSSformatInstruction::SDSP => 2, + _ => 1, + }, + rvc::Instruction::BEQZ { .. } => 3, + rvc::Instruction::BNEZ { .. } => 3, + rvc::Instruction::JAL { .. } => 3, + rvc::Instruction::J { .. } => 3, + rvc::Instruction::JR { .. } => 3, + rvc::Instruction::JALR { .. } => 3, + rvc::Instruction::EBREAK => 0, + _ => 1, + }, + Instruction::M(m::Instruction(i)) => match i.inst() { + m::RtypeInstruction::MUL => 5, + m::RtypeInstruction::MULW => 5, + m::RtypeInstruction::MULH => 5, + m::RtypeInstruction::MULHU => 5, + m::RtypeInstruction::MULHSU => 5, + m::RtypeInstruction::DIV => 16, + m::RtypeInstruction::DIVW => 16, + m::RtypeInstruction::DIVU => 16, + m::RtypeInstruction::DIVUW => 16, + m::RtypeInstruction::REM => 16, + m::RtypeInstruction::REMW => 16, + m::RtypeInstruction::REMU => 16, + m::RtypeInstruction::REMUW => 16, + }, + } +} diff --git a/script/src/lib.rs b/script/src/lib.rs index 18f13b4837..24a3c0a37e 100644 --- a/script/src/lib.rs +++ b/script/src/lib.rs @@ -1,3 +1,4 @@ +mod cost_model; mod syscalls; mod verify; diff --git a/script/src/verify.rs b/script/src/verify.rs index 1f519e4328..2c040e1fdd 100644 --- a/script/src/verify.rs +++ b/script/src/verify.rs @@ -1,10 +1,14 @@ -use crate::syscalls::{build_tx, Debugger, LoadCell, LoadCellByField, LoadInputByField, LoadTx}; -use crate::ScriptError; +use crate::{ + cost_model::instruction_cycles, + syscalls::{build_tx, Debugger, LoadCell, LoadCellByField, LoadInputByField, LoadTx}, + ScriptError, +}; use ckb_core::cell::ResolvedTransaction; use ckb_core::script::Script; use ckb_core::transaction::{CellInput, CellOutput}; +use ckb_core::Cycle; use ckb_protocol::{FlatbuffersVectorIterator, Script as FbsScript}; -use ckb_vm::{DefaultMachine, SparseMemory}; +use ckb_vm::{CoreMachine, DefaultMachine, SparseMemory}; use flatbuffers::{get_root, FlatBufferBuilder}; use fnv::FnvHashMap; use log::info; @@ -141,13 +145,17 @@ impl<'a> TransactionScriptsVerifier<'a> { prefix: &str, current_cell: &'a CellOutput, current_input: Option<&'a CellInput>, - ) -> Result<(), ScriptError> { + max_cycles: Cycle, + ) -> Result { let mut args = vec![b"verify".to_vec()]; self.extract_script(script, &mut args) .and_then(|script_binary| { args.extend_from_slice(&script.args.as_slice()); - let mut machine = DefaultMachine::::default(); + let mut machine = DefaultMachine::::new_with_cost_model( + Box::new(instruction_cycles), + max_cycles, + ); machine.add_syscall_module(Box::new(self.build_load_tx())); machine.add_syscall_module(Box::new(self.build_load_cell(current_cell))); machine.add_syscall_module(Box::new(self.build_load_cell_by_field(current_cell))); @@ -158,7 +166,7 @@ impl<'a> TransactionScriptsVerifier<'a> { .map_err(ScriptError::VMError) .and_then(|code| { if code == 0 { - Ok(()) + Ok(machine.cycles()) } else { Err(ScriptError::ValidationFailure(code)) } @@ -166,10 +174,11 @@ impl<'a> TransactionScriptsVerifier<'a> { }) } - pub fn verify(&self) -> Result<(), ScriptError> { + pub fn verify(&self, max_cycles: Cycle) -> Result { + let mut cycles = 0; for (i, input) in self.inputs.iter().enumerate() { let prefix = format!("Transaction {}, input {}", self.hash, i); - self.verify_script(&input.unlock, &prefix, self.input_cells[i], Some(input)).map_err(|e| { + cycles += self.verify_script(&input.unlock, &prefix, self.input_cells[i], Some(input), max_cycles - cycles).map_err(|e| { info!(target: "script", "Error validating input {} of transaction {}: {:?}", i, self.hash, e); e })?; @@ -177,13 +186,13 @@ impl<'a> TransactionScriptsVerifier<'a> { for (i, output) in self.outputs.iter().enumerate() { if let Some(ref type_) = output.type_ { let prefix = format!("Transaction {}, output {}", self.hash, i); - self.verify_script(type_, &prefix, output, None).map_err(|e| { + cycles += self.verify_script(type_, &prefix, output, None, max_cycles - cycles).map_err(|e| { info!(target: "script", "Error validating output {} of transaction {}: {:?}", i, self.hash, e); e })?; } } - Ok(()) + Ok(cycles) } } @@ -255,7 +264,52 @@ mod tests { let verifier = TransactionScriptsVerifier::new(&rtx); - assert!(verifier.verify().is_ok()); + assert!(verifier.verify(100000000).is_ok()); + } + + #[test] + fn check_signature_with_not_enough_cycles() { + let mut file = open_cell_verify(); + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer).unwrap(); + + let gen = Generator::new(); + let privkey = gen.random_privkey(); + let mut args = vec![b"foo".to_vec(), b"bar".to_vec()]; + + let mut bytes = vec![]; + for argument in &args { + bytes.write(argument).unwrap(); + } + let hash1 = sha3_256(&bytes); + let hash2 = sha3_256(hash1); + let signature = privkey.sign_recoverable(&hash2.into()).unwrap(); + + let signature_der = signature.serialize_der(); + let mut hex_signature = vec![0; signature_der.len() * 2]; + hex_encode(&signature_der, &mut hex_signature).expect("hex privkey"); + args.insert(0, hex_signature); + + let privkey = privkey.pubkey().unwrap().serialize(); + let mut hex_privkey = vec![0; privkey.len() * 2]; + hex_encode(&privkey, &mut hex_privkey).expect("hex privkey"); + + let script = Script::new(0, args, None, Some(buffer), vec![hex_privkey]); + let input = CellInput::new(OutPoint::null(), script); + + let transaction = TransactionBuilder::default().input(input.clone()).build(); + + let dummy_cell = CellOutput::new(100, vec![], H256::default(), None); + + let rtx = ResolvedTransaction { + transaction, + dep_cells: vec![], + input_cells: vec![CellStatus::Live(dummy_cell)], + }; + + let verifier = TransactionScriptsVerifier::new(&rtx); + + assert!(verifier.verify(100).is_err()); } #[test] @@ -302,7 +356,7 @@ mod tests { let verifier = TransactionScriptsVerifier::new(&rtx); - assert!(verifier.verify().is_err()); + assert!(verifier.verify(100000000).is_err()); } #[test] @@ -357,7 +411,7 @@ mod tests { let verifier = TransactionScriptsVerifier::new(&rtx); - assert!(verifier.verify().is_ok()); + assert!(verifier.verify(100000000).is_ok()); } #[test] @@ -408,7 +462,7 @@ mod tests { let verifier = TransactionScriptsVerifier::new(&rtx); - assert!(verifier.verify().is_err()); + assert!(verifier.verify(100000000).is_err()); } fn create_always_success_script() -> Script { @@ -465,7 +519,7 @@ mod tests { let verifier = TransactionScriptsVerifier::new(&rtx); - assert!(verifier.verify().is_ok()); + assert!(verifier.verify(100000000).is_ok()); } #[test] @@ -516,6 +570,6 @@ mod tests { let verifier = TransactionScriptsVerifier::new(&rtx); - assert!(verifier.verify().is_err()); + assert!(verifier.verify(100000000).is_err()); } } diff --git a/spec/src/consensus.rs b/spec/src/consensus.rs index 30a26cade0..45a7d51d64 100644 --- a/spec/src/consensus.rs +++ b/spec/src/consensus.rs @@ -1,7 +1,7 @@ use ckb_core::block::{Block, BlockBuilder}; use ckb_core::header::HeaderBuilder; use ckb_core::transaction::Capacity; -use ckb_core::BlockNumber; +use ckb_core::{BlockNumber, Cycle}; use ckb_pow::{Pow, PowEngine}; use numext_fixed_uint::U256; use std::sync::Arc; @@ -20,6 +20,8 @@ pub const ORPHAN_RATE_TARGET: f32 = 0.1; pub const POW_TIME_SPAN: u64 = 12 * 60 * 60 * 1000; // 12 hours pub const POW_SPACING: u64 = 15 * 1000; //15s +pub const MAX_BLOCK_CYCLES: Cycle = 100000000; + #[derive(Clone, PartialEq, Debug)] pub struct Consensus { pub id: String, @@ -40,6 +42,8 @@ pub struct Consensus { pub cellbase_maturity: usize, // This parameter indicates the count of past blocks used in the median time calculation pub median_time_block_count: usize, + // Maximum cycles that all the scripts in all the commit transactions can take + pub max_block_cycles: Cycle, } // genesis difficulty should not be zero @@ -63,6 +67,7 @@ impl Default for Consensus { verification: true, cellbase_maturity: CELLBASE_MATURITY, median_time_block_count: MEDIAN_TIME_BLOCK_COUNT, + max_block_cycles: MAX_BLOCK_CYCLES, } } } @@ -93,6 +98,11 @@ impl Consensus { self } + pub fn set_max_block_cycles(mut self, max_block_cycles: Cycle) -> Self { + self.max_block_cycles = max_block_cycles; + self + } + pub fn genesis_block(&self) -> &Block { &self.genesis_block } @@ -132,4 +142,8 @@ impl Consensus { pub fn median_time_block_count(&self) -> usize { self.median_time_block_count } + + pub fn max_block_cycles(&self) -> Cycle { + self.max_block_cycles + } } diff --git a/spec/src/lib.rs b/spec/src/lib.rs index 39c35bb346..99c04d9ac4 100644 --- a/spec/src/lib.rs +++ b/spec/src/lib.rs @@ -10,7 +10,7 @@ use ckb_core::block::BlockBuilder; use ckb_core::header::HeaderBuilder; use ckb_core::script::Script; use ckb_core::transaction::{CellOutput, Transaction, TransactionBuilder}; -use ckb_core::Capacity; +use ckb_core::{Capacity, Cycle}; use ckb_pow::{Pow, PowEngine}; use ckb_protocol::Script as FbsScript; use flatbuffers::FlatBufferBuilder; @@ -37,6 +37,7 @@ pub struct ChainSpec { #[derive(Clone, PartialEq, Eq, Debug, Deserialize)] pub struct Params { pub initial_block_reward: Capacity, + pub max_block_cycles: Cycle, } #[derive(Clone, PartialEq, Eq, Debug, Deserialize)] @@ -125,6 +126,7 @@ impl ChainSpec { .set_id(self.name.clone()) .set_genesis_block(genesis_block) .set_initial_block_reward(self.params.initial_block_reward) + .set_max_block_cycles(self.params.max_block_cycles) .set_pow(self.pow.clone()); Ok(consensus) diff --git a/src/setup.rs b/src/setup.rs index 0e64895eb8..f5668f578a 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -170,7 +170,8 @@ pub mod test { "uncles_hash": "0x0000000000000000000000000000000000000000000000000000000000000000" }, "params": { - "initial_block_reward": 233 + "initial_block_reward": 233, + "max_block_cycles": 100000000 }, "system_cells": [ {"path": "verify"}, diff --git a/verification/src/block_verifier.rs b/verification/src/block_verifier.rs index c6a17c26b5..06d85cad6e 100644 --- a/verification/src/block_verifier.rs +++ b/verification/src/block_verifier.rs @@ -6,6 +6,7 @@ use ckb_core::block::Block; use ckb_core::cell::{CellProvider, CellStatus}; use ckb_core::header::Header; use ckb_core::transaction::{Capacity, CellInput, OutPoint}; +use ckb_core::Cycle; use ckb_shared::shared::ChainProvider; use fnv::{FnvHashMap, FnvHashSet}; use merkle_root::merkle_root; @@ -447,26 +448,30 @@ impl TransactionsVerifier

{ }; let parent_hash = block.header().parent_hash(); + let max_cycles = self.provider.consensus().max_block_cycles(); // make verifiers orthogonal // skip first tx, assume the first is cellbase, other verifier will verify cellbase - let err: Vec<(usize, TransactionError)> = block + let results: Vec> = block .commit_transactions() .par_iter() .skip(1) .map(|x| wrapper.resolve_transaction_at(x, &parent_hash)) + .map(|tx| TransactionVerifier::new(&tx).verify(max_cycles)) + .collect(); + let err: Vec<(usize, TransactionError)> = results + .iter() .enumerate() - .filter_map(|(index, tx)| { - TransactionVerifier::new(&tx) - .verify() - .err() - .map(|e| (index, e)) - }) + .filter_map(|(index, result)| result.err().map(|e| (index, e))) .collect(); - if err.is_empty() { - Ok(()) - } else { - Err(Error::Transactions(err)) + if !err.is_empty() { + return Err(Error::Transactions(err)); + } + // Verify that sum of actual transaction cycles do not exceed max_cycles + let cycles: Cycle = results.iter().map(|result| result.unwrap_or(0)).sum(); + if cycles > max_cycles { + return Err(Error::ExceededMaximumCycles); } + Ok(()) } } diff --git a/verification/src/error.rs b/verification/src/error.rs index 7ef2a23df1..f7079b1503 100644 --- a/verification/src/error.rs +++ b/verification/src/error.rs @@ -40,6 +40,9 @@ pub enum Error { /// This error is returned when the committed transactions does not meet the 2-phases /// propose-then-commit consensus rule. Commit(CommitError), + /// Cycles consumed by all scripts in all commit transactions of the block exceed + /// the maximum allowed cycles in consensus rules + ExceededMaximumCycles, } #[derive(Debug, PartialEq, Clone, Eq)] diff --git a/verification/src/transaction_verifier.rs b/verification/src/transaction_verifier.rs index 18244a4ed5..8a52aff926 100644 --- a/verification/src/transaction_verifier.rs +++ b/verification/src/transaction_verifier.rs @@ -1,6 +1,7 @@ use crate::error::TransactionError; use ckb_core::cell::ResolvedTransaction; use ckb_core::transaction::{Capacity, Transaction}; +use ckb_core::Cycle; use ckb_script::TransactionScriptsVerifier; use std::collections::HashSet; @@ -25,15 +26,15 @@ impl<'a> TransactionVerifier<'a> { } } - pub fn verify(&self) -> Result<(), TransactionError> { + pub fn verify(&self, max_cycles: Cycle) -> Result { self.empty.verify()?; self.null.verify()?; self.capacity.verify()?; self.duplicate_inputs.verify()?; // InputVerifier should be executed before ScriptVerifier self.inputs.verify()?; - self.script.verify()?; - Ok(()) + let cycles = self.script.verify(max_cycles)?; + Ok(cycles) } } @@ -88,9 +89,9 @@ impl<'a> ScriptVerifier<'a> { } } - pub fn verify(&self) -> Result<(), TransactionError> { + pub fn verify(&self, max_cycles: Cycle) -> Result { TransactionScriptsVerifier::new(&self.resolved_transaction) - .verify() + .verify(max_cycles) .map_err(TransactionError::ScriptFailure) } }