diff --git a/api/src/types.rs b/api/src/types.rs index c8d21409b..09a43a8cc 100644 --- a/api/src/types.rs +++ b/api/src/types.rs @@ -509,6 +509,10 @@ impl TxKernelPrintable { KernelFeatures::Plain { fee } => (fee, 0), KernelFeatures::Coinbase => (0, 0), KernelFeatures::HeightLocked { fee, lock_height } => (fee, lock_height), + KernelFeatures::NoRecentDuplicate { + fee, + relative_height, + } => (fee, relative_height.into()), }; TxKernelPrintable { features, diff --git a/chain/tests/chain_test_helper.rs b/chain/tests/chain_test_helper.rs index 1d019adb0..206c2fb6c 100644 --- a/chain/tests/chain_test_helper.rs +++ b/chain/tests/chain_test_helper.rs @@ -50,7 +50,7 @@ pub fn init_chain(dir_name: &str, genesis: Block) -> Chain { } /// Build genesis block with reward (non-empty, like we have in mainnet). -fn genesis_block(keychain: &K) -> Block +pub fn genesis_block(keychain: &K) -> Block where K: Keychain, { @@ -69,6 +69,7 @@ where /// Mine a chain of specified length to assist with automated tests. /// Probably a good idea to call clean_output_dir at the beginning and end of each test. +#[allow(dead_code)] pub fn mine_chain(dir_name: &str, chain_length: u64) -> Chain { global::set_local_chain_type(ChainTypes::AutomatedTesting); let keychain = keychain::ExtKeychain::from_random_seed(false).unwrap(); @@ -78,6 +79,7 @@ pub fn mine_chain(dir_name: &str, chain_length: u64) -> Chain { chain } +#[allow(dead_code)] fn mine_some_on_top(chain: &mut Chain, chain_length: u64, keychain: &K) where K: Keychain, diff --git a/chain/tests/mine_nrd_kernel.rs b/chain/tests/mine_nrd_kernel.rs new file mode 100644 index 000000000..a44b3d169 --- /dev/null +++ b/chain/tests/mine_nrd_kernel.rs @@ -0,0 +1,151 @@ +// Copyright 2020 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod chain_test_helper; + +use grin_chain as chain; +use grin_core as core; +use grin_keychain as keychain; +use grin_util as util; + +use self::chain_test_helper::{clean_output_dir, genesis_block, init_chain}; +use crate::chain::{Chain, Options}; +use crate::core::core::{Block, KernelFeatures, NRDRelativeHeight, Transaction}; +use crate::core::libtx::{build, reward, ProofBuilder}; +use crate::core::{consensus, global, pow}; +use crate::keychain::{ExtKeychain, ExtKeychainPath, Identifier, Keychain}; +use chrono::Duration; + +fn build_block(chain: &Chain, keychain: &K, key_id: &Identifier, txs: Vec) -> Block +where + K: Keychain, +{ + let prev = chain.head_header().unwrap(); + let next_header_info = consensus::next_difficulty(1, chain.difficulty_iter().unwrap()); + let fee = txs.iter().map(|x| x.fee()).sum(); + let reward = + reward::output(keychain, &ProofBuilder::new(keychain), key_id, fee, false).unwrap(); + + let mut block = Block::new(&prev, txs, next_header_info.clone().difficulty, reward).unwrap(); + + block.header.timestamp = prev.timestamp + Duration::seconds(60); + block.header.pow.secondary_scaling = next_header_info.secondary_scaling; + + chain.set_txhashset_roots(&mut block).unwrap(); + + let edge_bits = global::min_edge_bits(); + block.header.pow.proof.edge_bits = edge_bits; + pow::pow_size( + &mut block.header, + next_header_info.difficulty, + global::proofsize(), + edge_bits, + ) + .unwrap(); + + block +} + +#[test] +fn mine_block_with_nrd_kernel_and_nrd_feature_enabled() { + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + global::set_local_nrd_enabled(true); + + util::init_test_logger(); + + let chain_dir = ".grin.nrd_kernel"; + clean_output_dir(chain_dir); + + let keychain = ExtKeychain::from_random_seed(false).unwrap(); + let pb = ProofBuilder::new(&keychain); + let genesis = genesis_block(&keychain); + let chain = init_chain(chain_dir, genesis.clone()); + + for n in 1..9 { + let key_id = ExtKeychainPath::new(1, n, 0, 0, 0).to_identifier(); + let block = build_block(&chain, &keychain, &key_id, vec![]); + chain.process_block(block, Options::MINE).unwrap(); + } + + assert_eq!(chain.head().unwrap().height, 8); + + let key_id1 = ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier(); + let key_id2 = ExtKeychainPath::new(1, 2, 0, 0, 0).to_identifier(); + let tx = build::transaction( + KernelFeatures::NoRecentDuplicate { + fee: 20000, + relative_height: NRDRelativeHeight::new(1440).unwrap(), + }, + vec![ + build::coinbase_input(consensus::REWARD, key_id1.clone()), + build::output(consensus::REWARD - 20000, key_id2.clone()), + ], + &keychain, + &pb, + ) + .unwrap(); + + let key_id9 = ExtKeychainPath::new(1, 9, 0, 0, 0).to_identifier(); + let block = build_block(&chain, &keychain, &key_id9, vec![tx]); + chain.process_block(block, Options::MINE).unwrap(); + chain.validate(false).unwrap(); + + clean_output_dir(chain_dir); +} + +#[test] +fn mine_invalid_block_with_nrd_kernel_and_nrd_feature_enabled_before_hf() { + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + global::set_local_nrd_enabled(true); + + util::init_test_logger(); + + let chain_dir = ".grin.invalid_nrd_kernel"; + clean_output_dir(chain_dir); + + let keychain = ExtKeychain::from_random_seed(false).unwrap(); + let pb = ProofBuilder::new(&keychain); + let genesis = genesis_block(&keychain); + let chain = init_chain(chain_dir, genesis.clone()); + + for n in 1..8 { + let key_id = ExtKeychainPath::new(1, n, 0, 0, 0).to_identifier(); + let block = build_block(&chain, &keychain, &key_id, vec![]); + chain.process_block(block, Options::MINE).unwrap(); + } + + assert_eq!(chain.head().unwrap().height, 7); + + let key_id1 = ExtKeychainPath::new(1, 1, 0, 0, 0).to_identifier(); + let key_id2 = ExtKeychainPath::new(1, 2, 0, 0, 0).to_identifier(); + let tx = build::transaction( + KernelFeatures::NoRecentDuplicate { + fee: 20000, + relative_height: NRDRelativeHeight::new(1440).unwrap(), + }, + vec![ + build::coinbase_input(consensus::REWARD, key_id1.clone()), + build::output(consensus::REWARD - 20000, key_id2.clone()), + ], + &keychain, + &pb, + ) + .unwrap(); + + let key_id8 = ExtKeychainPath::new(1, 8, 0, 0, 0).to_identifier(); + let block = build_block(&chain, &keychain, &key_id8, vec![tx]); + let res = chain.process_block(block, Options::MINE); + assert!(res.is_err()); + clean_output_dir(chain_dir); +} diff --git a/chain/tests/mine_simple_chain.rs b/chain/tests/mine_simple_chain.rs index 01a75c29a..e61476828 100644 --- a/chain/tests/mine_simple_chain.rs +++ b/chain/tests/mine_simple_chain.rs @@ -81,7 +81,7 @@ fn mine_empty_chain() { #[test] fn mine_short_chain() { - let chain_dir = ".grin.genesis"; + let chain_dir = ".grin.short"; clean_output_dir(chain_dir); let chain = mine_chain(chain_dir, 4); assert_eq!(chain.head().unwrap().height, 3); diff --git a/core/src/consensus.rs b/core/src/consensus.rs index 5872e30d7..6701f35e2 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -133,12 +133,15 @@ pub const FLOONET_FIRST_HARD_FORK: u64 = 185_040; /// Floonet second hard fork height, set to happen around 2019-12-19 pub const FLOONET_SECOND_HARD_FORK: u64 = 298_080; -/// AutomatedTesting and UserTesting first hard fork height. +/// AutomatedTesting and UserTesting HF1 height. pub const TESTING_FIRST_HARD_FORK: u64 = 3; -/// AutomatedTesting and UserTesting second hard fork height. +/// AutomatedTesting and UserTesting HF2 height. pub const TESTING_SECOND_HARD_FORK: u64 = 6; +/// AutomatedTesting and UserTesting HF3 height. +pub const TESTING_THIRD_HARD_FORK: u64 = 9; + /// Compute possible block version at a given height, implements /// 6 months interval scheduled hard forks for the first 2 years. pub fn header_version(height: u64) -> HeaderVersion { @@ -162,10 +165,12 @@ pub fn header_version(height: u64) -> HeaderVersion { HeaderVersion(1) } else if height < TESTING_SECOND_HARD_FORK { HeaderVersion(2) - } else if height < 3 * HARD_FORK_INTERVAL { + } else if height < TESTING_THIRD_HARD_FORK { HeaderVersion(3) + } else if height < 4 * HARD_FORK_INTERVAL { + HeaderVersion(4) } else { - HeaderVersion(hf_interval) + HeaderVersion(5) } } } diff --git a/core/src/core/block.rs b/core/src/core/block.rs index e892e2709..bcef277dd 100644 --- a/core/src/core/block.rs +++ b/core/src/core/block.rs @@ -63,6 +63,10 @@ pub enum Error { InvalidPow, /// Kernel not valid due to lock_height exceeding block header height KernelLockHeight(u64), + /// NRD kernels are not valid prior to HF3. + NRDKernelPreHF3, + /// NRD kernels are not valid if disabled locally via "feature flag". + NRDKernelNotEnabled, /// Underlying tx related error Transaction(transaction::Error), /// Underlying Secp256k1 error (signature validation or invalid public key @@ -753,6 +757,7 @@ impl Block { self.body.validate(Weighting::AsBlock, verifier)?; self.verify_kernel_lock_heights()?; + self.verify_nrd_kernels_for_header_version()?; self.verify_coinbase()?; // take the kernel offset for this block (block offset minus previous) and @@ -802,6 +807,7 @@ impl Block { Ok(()) } + // Verify any absolute kernel lock heights. fn verify_kernel_lock_heights(&self) -> Result<(), Error> { for k in &self.body.kernels { // check we have no kernels with lock_heights greater than current height @@ -814,6 +820,21 @@ impl Block { } Ok(()) } + + // NRD kernels are not valid if the global feature flag is disabled. + // NRD kernels were introduced in HF3 and are not valid for block version < 4. + // Blocks prior to HF3 containing any NRD kernel(s) are invalid. + fn verify_nrd_kernels_for_header_version(&self) -> Result<(), Error> { + if self.body.kernels.iter().any(|k| k.is_nrd()) { + if !global::is_nrd_enabled() { + return Err(Error::NRDKernelNotEnabled); + } + if self.header.version < HeaderVersion(4) { + return Err(Error::NRDKernelPreHF3); + } + } + Ok(()) + } } impl From for Block { diff --git a/core/src/core/transaction.rs b/core/src/core/transaction.rs index 1502323dc..b37cee0ed 100644 --- a/core/src/core/transaction.rs +++ b/core/src/core/transaction.rs @@ -17,7 +17,7 @@ use crate::core::hash::{DefaultHashable, Hashed}; use crate::core::verifier_cache::VerifierCache; use crate::core::{committed, Committed}; -use crate::libtx::secp_ser; +use crate::libtx::{aggsig, secp_ser}; use crate::ser::{ self, read_multi, PMMRable, ProtocolVersion, Readable, Reader, VerifySortedAndUnique, Writeable, Writer, @@ -27,7 +27,7 @@ use enum_primitive::FromPrimitive; use keychain::{self, BlindingFactor}; use std::cmp::Ordering; use std::cmp::{max, min}; -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; use std::sync::Arc; use std::{error, fmt}; use util::secp; @@ -36,6 +36,70 @@ use util::static_secp_instance; use util::RwLock; use util::ToHex; +/// Relative height field on NRD kernel variant. +/// u16 representing a height between 1 and MAX (consensus::WEEK_HEIGHT). +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct NRDRelativeHeight(u16); + +impl DefaultHashable for NRDRelativeHeight {} + +impl Writeable for NRDRelativeHeight { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_u16(self.0) + } +} + +impl Readable for NRDRelativeHeight { + fn read(reader: &mut R) -> Result { + let x = reader.read_u16()?; + NRDRelativeHeight::try_from(x).map_err(|_| ser::Error::CorruptedData) + } +} + +/// Conversion from a u16 to a valid NRDRelativeHeight. +/// Valid height is between 1 and WEEK_HEIGHT inclusive. +impl TryFrom for NRDRelativeHeight { + type Error = Error; + + fn try_from(height: u16) -> Result { + if height == 0 { + Err(Error::InvalidNRDRelativeHeight) + } else if height + > NRDRelativeHeight::MAX + .try_into() + .expect("WEEK_HEIGHT const should fit in u16") + { + Err(Error::InvalidNRDRelativeHeight) + } else { + Ok(Self(height)) + } + } +} + +impl TryFrom for NRDRelativeHeight { + type Error = Error; + + fn try_from(height: u64) -> Result { + Self::try_from(u16::try_from(height).map_err(|_| Error::InvalidNRDRelativeHeight)?) + } +} + +impl From for u64 { + fn from(height: NRDRelativeHeight) -> Self { + height.0 as u64 + } +} + +impl NRDRelativeHeight { + const MAX: u64 = consensus::WEEK_HEIGHT; + + /// Create a new NRDRelativeHeight from the provided height. + /// Checks height is valid (between 1 and WEEK_HEIGHT inclusive). + pub fn new(height: u64) -> Result { + NRDRelativeHeight::try_from(height) + } +} + /// Various tx kernel variants. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum KernelFeatures { @@ -53,12 +117,20 @@ pub enum KernelFeatures { /// Height locked kernels have lock heights. lock_height: u64, }, + /// "No Recent Duplicate" (NRD) kernels enforcing relative lock height between instances. + NoRecentDuplicate { + /// These have fees. + fee: u64, + /// Relative lock height. + relative_height: NRDRelativeHeight, + }, } impl KernelFeatures { const PLAIN_U8: u8 = 0; const COINBASE_U8: u8 = 1; const HEIGHT_LOCKED_U8: u8 = 2; + const NO_RECENT_DUPLICATE_U8: u8 = 3; /// Underlying (u8) value representing this kernel variant. /// This is the first byte when we serialize/deserialize the kernel features. @@ -67,6 +139,7 @@ impl KernelFeatures { KernelFeatures::Plain { .. } => KernelFeatures::PLAIN_U8, KernelFeatures::Coinbase => KernelFeatures::COINBASE_U8, KernelFeatures::HeightLocked { .. } => KernelFeatures::HEIGHT_LOCKED_U8, + KernelFeatures::NoRecentDuplicate { .. } => KernelFeatures::NO_RECENT_DUPLICATE_U8, } } @@ -76,18 +149,24 @@ impl KernelFeatures { KernelFeatures::Plain { .. } => String::from("Plain"), KernelFeatures::Coinbase => String::from("Coinbase"), KernelFeatures::HeightLocked { .. } => String::from("HeightLocked"), + KernelFeatures::NoRecentDuplicate { .. } => String::from("NoRecentDuplicate"), } } - /// msg = hash(features) for coinbase kernels - /// hash(features || fee) for plain kernels - /// hash(features || fee || lock_height) for height locked kernels + /// msg = hash(features) for coinbase kernels + /// hash(features || fee) for plain kernels + /// hash(features || fee || lock_height) for height locked kernels + /// hash(features || fee || relative_height) for NRD kernels pub fn kernel_sig_msg(&self) -> Result { let x = self.as_u8(); let hash = match self { KernelFeatures::Plain { fee } => (x, fee).hash(), - KernelFeatures::Coinbase => (x).hash(), + KernelFeatures::Coinbase => x.hash(), KernelFeatures::HeightLocked { fee, lock_height } => (x, fee, lock_height).hash(), + KernelFeatures::NoRecentDuplicate { + fee, + relative_height, + } => (x, fee, relative_height).hash(), }; let msg = secp::Message::from_slice(&hash.as_bytes())?; @@ -97,14 +176,36 @@ impl KernelFeatures { /// Write tx kernel features out in v1 protocol format. /// Always include the fee and lock_height, writing 0 value if unused. fn write_v1(&self, writer: &mut W) -> Result<(), ser::Error> { - let (fee, lock_height) = match self { - KernelFeatures::Plain { fee } => (*fee, 0), - KernelFeatures::Coinbase => (0, 0), - KernelFeatures::HeightLocked { fee, lock_height } => (*fee, *lock_height), - }; writer.write_u8(self.as_u8())?; - writer.write_u64(fee)?; - writer.write_u64(lock_height)?; + match self { + KernelFeatures::Plain { fee } => { + writer.write_u64(*fee)?; + // Write "empty" bytes for feature specific data (8 bytes). + writer.write_empty_bytes(8)?; + } + KernelFeatures::Coinbase => { + // Write "empty" bytes for fee (8 bytes) and feature specific data (8 bytes). + writer.write_empty_bytes(16)?; + } + KernelFeatures::HeightLocked { fee, lock_height } => { + writer.write_u64(*fee)?; + // 8 bytes of feature specific data containing the lock height as big-endian u64. + writer.write_u64(*lock_height)?; + } + KernelFeatures::NoRecentDuplicate { + fee, + relative_height, + } => { + writer.write_u64(*fee)?; + + // 8 bytes of feature specific data. First 6 bytes are empty. + // Last 2 bytes contain the relative lock height as big-endian u16. + // Note: This is effectively the same as big-endian u64. + // We write "empty" bytes explicitly rather than quietly casting the u16 -> u64. + writer.write_empty_bytes(6)?; + relative_height.write(writer)?; + } + }; Ok(()) } @@ -113,48 +214,75 @@ impl KernelFeatures { /// Only write fee out for feature variants that support it. /// Only write lock_height out for feature variants that support it. fn write_v2(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_u8(self.as_u8())?; match self { KernelFeatures::Plain { fee } => { - writer.write_u8(self.as_u8())?; + // Fee only, no additional data on plain kernels. writer.write_u64(*fee)?; } KernelFeatures::Coinbase => { - writer.write_u8(self.as_u8())?; + // No additional data. } KernelFeatures::HeightLocked { fee, lock_height } => { - writer.write_u8(self.as_u8())?; writer.write_u64(*fee)?; + // V2 height locked kernels use 8 bytes for the lock height. writer.write_u64(*lock_height)?; } + KernelFeatures::NoRecentDuplicate { + fee, + relative_height, + } => { + writer.write_u64(*fee)?; + // V2 NRD kernels use 2 bytes for the relative lock height. + relative_height.write(writer)?; + } } Ok(()) } - // Always read feature byte, 8 bytes for fee and 8 bytes for lock height. - // Fee and lock height may be unused for some kernel variants but we need + // Always read feature byte, 8 bytes for fee and 8 bytes for additional data + // representing lock height or relative height. + // Fee and additional data may be unused for some kernel variants but we need // to read these bytes and verify they are 0 if unused. fn read_v1(reader: &mut R) -> Result { let feature_byte = reader.read_u8()?; - let fee = reader.read_u64()?; - let lock_height = reader.read_u64()?; - let features = match feature_byte { KernelFeatures::PLAIN_U8 => { - if lock_height != 0 { - return Err(ser::Error::CorruptedData); - } + let fee = reader.read_u64()?; + // 8 "empty" bytes as additional data is not used. + reader.read_empty_bytes(8)?; KernelFeatures::Plain { fee } } KernelFeatures::COINBASE_U8 => { - if fee != 0 { - return Err(ser::Error::CorruptedData); - } - if lock_height != 0 { - return Err(ser::Error::CorruptedData); - } + // 8 "empty" bytes as fee is not used. + // 8 "empty" bytes as additional data is not used. + reader.read_empty_bytes(16)?; KernelFeatures::Coinbase } - KernelFeatures::HEIGHT_LOCKED_U8 => KernelFeatures::HeightLocked { fee, lock_height }, + KernelFeatures::HEIGHT_LOCKED_U8 => { + let fee = reader.read_u64()?; + // 8 bytes of feature specific data, lock height as big-endian u64. + let lock_height = reader.read_u64()?; + KernelFeatures::HeightLocked { fee, lock_height } + } + KernelFeatures::NO_RECENT_DUPLICATE_U8 => { + // NRD kernels are invalid if NRD feature flag is not enabled. + if !global::is_nrd_enabled() { + return Err(ser::Error::CorruptedData); + } + + let fee = reader.read_u64()?; + + // 8 bytes of feature specific data. + // The first 6 bytes must be "empty". + // The last 2 bytes is the relative height as big-endian u16. + reader.read_empty_bytes(6)?; + let relative_height = NRDRelativeHeight::read(reader)?; + KernelFeatures::NoRecentDuplicate { + fee, + relative_height, + } + } _ => { return Err(ser::Error::CorruptedData); } @@ -176,6 +304,19 @@ impl KernelFeatures { let lock_height = reader.read_u64()?; KernelFeatures::HeightLocked { fee, lock_height } } + KernelFeatures::NO_RECENT_DUPLICATE_U8 => { + // NRD kernels are invalid if NRD feature flag is not enabled. + if !global::is_nrd_enabled() { + return Err(ser::Error::CorruptedData); + } + + let fee = reader.read_u64()?; + let relative_height = NRDRelativeHeight::read(reader)?; + KernelFeatures::NoRecentDuplicate { + fee, + relative_height, + } + } _ => { return Err(ser::Error::CorruptedData); } @@ -246,6 +387,8 @@ pub enum Error { /// Validation error relating to kernel features. /// It is invalid for a transaction to contain a coinbase kernel, for example. InvalidKernelFeatures, + /// NRD kernel relative height is limited to 1 week duration and must be greater than 0. + InvalidNRDRelativeHeight, /// Signature verification error. IncorrectSignature, /// Underlying serialization error. @@ -383,6 +526,14 @@ impl KernelFeatures { _ => false, } } + + /// Is this an NRD kernel? + pub fn is_nrd(&self) -> bool { + match self { + KernelFeatures::NoRecentDuplicate { .. } => true, + _ => false, + } + } } impl TxKernel { @@ -401,6 +552,11 @@ impl TxKernel { self.features.is_height_locked() } + /// Is this an NRD kernel? + pub fn is_nrd(&self) -> bool { + self.features.is_nrd() + } + /// Return the excess commitment for this tx_kernel. pub fn excess(&self) -> Commitment { self.excess @@ -422,14 +578,13 @@ impl TxKernel { let sig = &self.excess_sig; // Verify aggsig directly in libsecp let pubkey = &self.excess.to_pubkey(&secp)?; - if !secp::aggsig::verify_single( + if !aggsig::verify_single( &secp, &sig, &self.msg_to_sign()?, None, &pubkey, Some(&pubkey), - None, false, ) { return Err(Error::IncorrectSignature); @@ -440,9 +595,9 @@ impl TxKernel { /// Batch signature verification. pub fn batch_sig_verify(tx_kernels: &[TxKernel]) -> Result<(), Error> { let len = tx_kernels.len(); - let mut sigs: Vec = Vec::with_capacity(len); - let mut pubkeys: Vec = Vec::with_capacity(len); - let mut msgs: Vec = Vec::with_capacity(len); + let mut sigs = Vec::with_capacity(len); + let mut pubkeys = Vec::with_capacity(len); + let mut msgs = Vec::with_capacity(len); let secp = static_secp_instance(); let secp = secp.lock(); @@ -453,7 +608,7 @@ impl TxKernel { msgs.push(tx_kernel.msg_to_sign()?); } - if !secp::aggsig::verify_batch(&secp, &sigs, &msgs, &pubkeys) { + if !aggsig::verify_batch(&secp, &sigs, &msgs, &pubkeys) { return Err(Error::IncorrectSignature); } @@ -668,9 +823,9 @@ impl TransactionBody { .iter() .filter_map(|k| match k.features { KernelFeatures::Coinbase => None, - KernelFeatures::Plain { fee } | KernelFeatures::HeightLocked { fee, .. } => { - Some(fee) - } + KernelFeatures::Plain { fee } => Some(fee), + KernelFeatures::HeightLocked { fee, .. } => Some(fee), + KernelFeatures::NoRecentDuplicate { fee, .. } => Some(fee), }) .fold(0, |acc, fee| acc.saturating_add(fee)) } @@ -1577,10 +1732,9 @@ mod test { use crate::core::hash::Hash; use crate::core::id::{ShortId, ShortIdentifiable}; use keychain::{ExtKeychain, Keychain, SwitchCommitmentType}; - use util::secp; #[test] - fn test_kernel_ser_deser() { + fn test_plain_kernel_ser_deser() { let keychain = ExtKeychain::from_random_seed(false).unwrap(); let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); let commit = keychain @@ -1596,12 +1750,35 @@ mod test { excess_sig: sig.clone(), }; + // Test explicit protocol version. + for version in vec![ProtocolVersion(1), ProtocolVersion(2)] { + let mut vec = vec![]; + ser::serialize(&mut vec, version, &kernel).expect("serialized failed"); + let kernel2: TxKernel = ser::deserialize(&mut &vec[..], version).unwrap(); + assert_eq!(kernel2.features, KernelFeatures::Plain { fee: 10 }); + assert_eq!(kernel2.excess, commit); + assert_eq!(kernel2.excess_sig, sig.clone()); + } + + // Test with "default" protocol version. let mut vec = vec![]; ser::serialize_default(&mut vec, &kernel).expect("serialized failed"); let kernel2: TxKernel = ser::deserialize_default(&mut &vec[..]).unwrap(); assert_eq!(kernel2.features, KernelFeatures::Plain { fee: 10 }); assert_eq!(kernel2.excess, commit); assert_eq!(kernel2.excess_sig, sig.clone()); + } + + #[test] + fn test_height_locked_kernel_ser_deser() { + let keychain = ExtKeychain::from_random_seed(false).unwrap(); + let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let commit = keychain + .commit(5, &key_id, SwitchCommitmentType::Regular) + .unwrap(); + + // just some bytes for testing ser/deser + let sig = secp::Signature::from_raw_data(&[0; 64]).unwrap(); // now check a kernel with lock_height serialize/deserialize correctly let kernel = TxKernel { @@ -1613,20 +1790,123 @@ mod test { excess_sig: sig.clone(), }; + // Test explicit protocol version. + for version in vec![ProtocolVersion(1), ProtocolVersion(2)] { + let mut vec = vec![]; + ser::serialize(&mut vec, version, &kernel).expect("serialized failed"); + let kernel2: TxKernel = ser::deserialize(&mut &vec[..], version).unwrap(); + assert_eq!(kernel.features, kernel2.features); + assert_eq!(kernel2.excess, commit); + assert_eq!(kernel2.excess_sig, sig.clone()); + } + + // Test with "default" protocol version. let mut vec = vec![]; ser::serialize_default(&mut vec, &kernel).expect("serialized failed"); let kernel2: TxKernel = ser::deserialize_default(&mut &vec[..]).unwrap(); - assert_eq!( - kernel2.features, - KernelFeatures::HeightLocked { - fee: 10, - lock_height: 100 - } - ); + assert_eq!(kernel.features, kernel2.features); assert_eq!(kernel2.excess, commit); assert_eq!(kernel2.excess_sig, sig.clone()); } + #[test] + fn test_nrd_kernel_ser_deser() { + global::set_local_nrd_enabled(true); + + let keychain = ExtKeychain::from_random_seed(false).unwrap(); + let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let commit = keychain + .commit(5, &key_id, SwitchCommitmentType::Regular) + .unwrap(); + + // just some bytes for testing ser/deser + let sig = secp::Signature::from_raw_data(&[0; 64]).unwrap(); + + // now check an NRD kernel will serialize/deserialize correctly + let kernel = TxKernel { + features: KernelFeatures::NoRecentDuplicate { + fee: 10, + relative_height: NRDRelativeHeight(100), + }, + excess: commit, + excess_sig: sig.clone(), + }; + + // Test explicit protocol version. + for version in vec![ProtocolVersion(1), ProtocolVersion(2)] { + let mut vec = vec![]; + ser::serialize(&mut vec, version, &kernel).expect("serialized failed"); + let kernel2: TxKernel = ser::deserialize(&mut &vec[..], version).unwrap(); + assert_eq!(kernel.features, kernel2.features); + assert_eq!(kernel2.excess, commit); + assert_eq!(kernel2.excess_sig, sig.clone()); + } + + // Test with "default" protocol version. + let mut vec = vec![]; + ser::serialize_default(&mut vec, &kernel).expect("serialized failed"); + let kernel2: TxKernel = ser::deserialize_default(&mut &vec[..]).unwrap(); + assert_eq!(kernel.features, kernel2.features); + assert_eq!(kernel2.excess, commit); + assert_eq!(kernel2.excess_sig, sig.clone()); + } + + #[test] + fn nrd_kernel_verify_sig() { + let keychain = ExtKeychain::from_random_seed(false).unwrap(); + let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + + let mut kernel = TxKernel::with_features(KernelFeatures::NoRecentDuplicate { + fee: 10, + relative_height: NRDRelativeHeight(100), + }); + + // Construct the message to be signed. + let msg = kernel.msg_to_sign().unwrap(); + + let excess = keychain + .commit(0, &key_id, SwitchCommitmentType::Regular) + .unwrap(); + let skey = keychain + .derive_key(0, &key_id, SwitchCommitmentType::Regular) + .unwrap(); + let pubkey = excess.to_pubkey(&keychain.secp()).unwrap(); + + let excess_sig = + aggsig::sign_single(&keychain.secp(), &msg, &skey, None, Some(&pubkey)).unwrap(); + + kernel.excess = excess; + kernel.excess_sig = excess_sig; + + // Check the signature verifies. + assert_eq!(kernel.verify(), Ok(())); + + // Modify the fee and check signature no longer verifies. + kernel.features = KernelFeatures::NoRecentDuplicate { + fee: 9, + relative_height: NRDRelativeHeight(100), + }; + assert_eq!(kernel.verify(), Err(Error::IncorrectSignature)); + + // Modify the relative_height and check signature no longer verifies. + kernel.features = KernelFeatures::NoRecentDuplicate { + fee: 10, + relative_height: NRDRelativeHeight(101), + }; + assert_eq!(kernel.verify(), Err(Error::IncorrectSignature)); + + // Swap the features out for something different and check signature no longer verifies. + kernel.features = KernelFeatures::Plain { fee: 10 }; + assert_eq!(kernel.verify(), Err(Error::IncorrectSignature)); + + // Check signature verifies if we use the original features. + kernel.features = KernelFeatures::NoRecentDuplicate { + fee: 10, + relative_height: NRDRelativeHeight(100), + }; + assert_eq!(kernel.verify(), Ok(())); + } + #[test] fn commit_consistency() { let keychain = ExtKeychain::from_seed(&[0; 32], false).unwrap(); @@ -1678,20 +1958,20 @@ mod test { } #[test] - fn kernel_features_serialization() { + fn kernel_features_serialization() -> Result<(), Error> { let mut vec = vec![]; - ser::serialize_default(&mut vec, &(0u8, 10u64, 0u64)).expect("serialized failed"); - let features: KernelFeatures = ser::deserialize_default(&mut &vec[..]).unwrap(); + ser::serialize_default(&mut vec, &(0u8, 10u64, 0u64))?; + let features: KernelFeatures = ser::deserialize_default(&mut &vec[..])?; assert_eq!(features, KernelFeatures::Plain { fee: 10 }); let mut vec = vec![]; - ser::serialize_default(&mut vec, &(1u8, 0u64, 0u64)).expect("serialized failed"); - let features: KernelFeatures = ser::deserialize_default(&mut &vec[..]).unwrap(); + ser::serialize_default(&mut vec, &(1u8, 0u64, 0u64))?; + let features: KernelFeatures = ser::deserialize_default(&mut &vec[..])?; assert_eq!(features, KernelFeatures::Coinbase); let mut vec = vec![]; - ser::serialize_default(&mut vec, &(2u8, 10u64, 100u64)).expect("serialized failed"); - let features: KernelFeatures = ser::deserialize_default(&mut &vec[..]).unwrap(); + ser::serialize_default(&mut vec, &(2u8, 10u64, 100u64))?; + let features: KernelFeatures = ser::deserialize_default(&mut &vec[..])?; assert_eq!( features, KernelFeatures::HeightLocked { @@ -1700,9 +1980,55 @@ mod test { } ); + // NRD kernel support not enabled by default. let mut vec = vec![]; - ser::serialize_default(&mut vec, &(3u8, 0u64, 0u64)).expect("serialized failed"); + ser::serialize_default(&mut vec, &(3u8, 10u64, 100u16)).expect("serialized failed"); let res: Result = ser::deserialize_default(&mut &vec[..]); assert_eq!(res.err(), Some(ser::Error::CorruptedData)); + + // Additional kernel features unsupported. + let mut vec = vec![]; + ser::serialize_default(&mut vec, &(4u8)).expect("serialized failed"); + let res: Result = ser::deserialize_default(&mut &vec[..]); + assert_eq!(res.err(), Some(ser::Error::CorruptedData)); + + Ok(()) + } + + #[test] + fn kernel_features_serialization_nrd_enabled() -> Result<(), Error> { + global::set_local_nrd_enabled(true); + + let mut vec = vec![]; + ser::serialize_default(&mut vec, &(3u8, 10u64, 100u16))?; + let features: KernelFeatures = ser::deserialize_default(&mut &vec[..])?; + assert_eq!( + features, + KernelFeatures::NoRecentDuplicate { + fee: 10, + relative_height: NRDRelativeHeight(100) + } + ); + + // NRD with relative height 0 is invalid. + vec.clear(); + ser::serialize_default(&mut vec, &(3u8, 10u64, 0u16))?; + let res: Result = ser::deserialize_default(&mut &vec[..]); + assert_eq!(res.err(), Some(ser::Error::CorruptedData)); + + // NRD with relative height WEEK_HEIGHT+1 is invalid. + vec.clear(); + let invalid_height = consensus::WEEK_HEIGHT + 1; + ser::serialize_default(&mut vec, &(3u8, 10u64, invalid_height as u16))?; + let res: Result = ser::deserialize_default(&mut &vec[..]); + assert_eq!(res.err(), Some(ser::Error::CorruptedData)); + + // Kernel variant 4 (and above) is invalid. + let mut vec = vec![]; + ser::serialize_default(&mut vec, &(4u8))?; + let res: Result = ser::deserialize_default(&mut &vec[..]); + assert_eq!(res.err(), Some(ser::Error::CorruptedData)); + + Ok(()) } } diff --git a/core/src/global.rs b/core/src/global.rs index 07a079f5f..6377c1e8c 100644 --- a/core/src/global.rs +++ b/core/src/global.rs @@ -140,11 +140,19 @@ lazy_static! { /// This is accessed via get_chain_type() which allows the global value /// to be overridden on a per-thread basis (for testing). pub static ref GLOBAL_CHAIN_TYPE: OneTime = OneTime::new(); + + /// Global feature flag for NRD kernel support. + /// If enabled NRD kernels are treated as valid after HF3 (based on header version). + /// If disabled NRD kernels are invalid regardless of header version or block height. + pub static ref GLOBAL_NRD_FEATURE_ENABLED: OneTime = OneTime::new(); } thread_local! { /// Mainnet|Floonet|UserTesting|AutomatedTesting pub static CHAIN_TYPE: Cell> = Cell::new(None); + + /// Local feature flag for NRD kernel support. + pub static NRD_FEATURE_ENABLED: Cell> = Cell::new(None); } /// Set the chain type on a per-thread basis via thread_local storage. @@ -174,6 +182,36 @@ pub fn init_global_chain_type(new_type: ChainTypes) { GLOBAL_CHAIN_TYPE.init(new_type) } +/// One time initialization of the global chain_type. +/// Will panic if we attempt to re-initialize this (via OneTime). +pub fn init_global_nrd_enabled(enabled: bool) { + GLOBAL_NRD_FEATURE_ENABLED.init(enabled) +} + +/// Explicitly enable the NRD global feature flag. +pub fn set_local_nrd_enabled(enabled: bool) { + NRD_FEATURE_ENABLED.with(|flag| flag.set(Some(enabled))) +} + +/// Is the NRD feature flag enabled? +/// Look at thread local config first. If not set fallback to global config. +/// Default to false if global config unset. +pub fn is_nrd_enabled() -> bool { + NRD_FEATURE_ENABLED.with(|flag| match flag.get() { + None => { + if GLOBAL_NRD_FEATURE_ENABLED.is_init() { + let global_flag = GLOBAL_NRD_FEATURE_ENABLED.borrow(); + flag.set(Some(global_flag)); + global_flag + } else { + // Global config unset, default to false. + false + } + } + Some(flag) => flag, + }) +} + /// Return either a cuckoo context or a cuckatoo context /// Single change point pub fn create_pow_context( diff --git a/core/src/libtx/aggsig.rs b/core/src/libtx/aggsig.rs index 352cefdbd..d241b9204 100644 --- a/core/src/libtx/aggsig.rs +++ b/core/src/libtx/aggsig.rs @@ -441,6 +441,16 @@ pub fn verify_single( ) } +/// Verify a batch of signatures. +pub fn verify_batch( + secp: &Secp256k1, + sigs: &Vec, + msgs: &Vec, + pubkeys: &Vec, +) -> bool { + aggsig::verify_batch(secp, sigs, msgs, pubkeys) +} + /// Just a simple sig, creates its own nonce, etc pub fn sign_with_blinding( secp: &Secp256k1, @@ -449,7 +459,6 @@ pub fn sign_with_blinding( pubkey_sum: Option<&PublicKey>, ) -> Result { let skey = &blinding.secret_key(&secp)?; - //let pubkey_sum = PublicKey::from_secret_key(&secp, &skey)?; let sig = aggsig::sign_single(secp, &msg, skey, None, None, None, pubkey_sum, None)?; Ok(sig) } diff --git a/core/src/ser.rs b/core/src/ser.rs index a99229565..bbc1c1e4e 100644 --- a/core/src/ser.rs +++ b/core/src/ser.rs @@ -186,6 +186,11 @@ pub trait Writer { /// Writes a fixed number of bytes. The reader is expected to know the actual length on read. fn write_fixed_bytes>(&mut self, bytes: T) -> Result<(), Error>; + + /// Writes a fixed length of "empty" bytes. + fn write_empty_bytes(&mut self, length: usize) -> Result<(), Error> { + self.write_fixed_bytes(vec![0u8; length]) + } } /// Implementations defined how different numbers and binary structures are @@ -213,6 +218,17 @@ pub trait Reader { /// Access to underlying protocol version to support /// version specific deserialization logic. fn protocol_version(&self) -> ProtocolVersion; + + /// Read a fixed number of "empty" bytes from the underlying reader. + /// It is an error if any non-empty bytes encountered. + fn read_empty_bytes(&mut self, length: usize) -> Result<(), Error> { + for _ in 0..length { + if self.read_u8()? != 0u8 { + return Err(Error::CorruptedData); + } + } + Ok(()) + } } /// Trait that every type that can be serialized as binary must implement. diff --git a/core/tests/block.rs b/core/tests/block.rs index ff1193f2b..b45963c79 100644 --- a/core/tests/block.rs +++ b/core/tests/block.rs @@ -14,16 +14,15 @@ mod common; use crate::common::{new_block, tx1i2o, tx2i1o, txspend1i1o}; -use crate::core::consensus::BLOCK_OUTPUT_WEIGHT; -use crate::core::core::block::Error; +use crate::core::consensus::{self, BLOCK_OUTPUT_WEIGHT, TESTING_THIRD_HARD_FORK}; +use crate::core::core::block::{Block, BlockHeader, Error, HeaderVersion}; use crate::core::core::hash::Hashed; use crate::core::core::id::ShortIdentifiable; -use crate::core::core::transaction::{self, Transaction}; -use crate::core::core::verifier_cache::{LruVerifierCache, VerifierCache}; -use crate::core::core::Committed; -use crate::core::core::{ - Block, BlockHeader, CompactBlock, HeaderVersion, KernelFeatures, OutputFeatures, +use crate::core::core::transaction::{ + self, KernelFeatures, NRDRelativeHeight, OutputFeatures, Transaction, }; +use crate::core::core::verifier_cache::{LruVerifierCache, VerifierCache}; +use crate::core::core::{Committed, CompactBlock}; use crate::core::libtx::build::{self, input, output}; use crate::core::libtx::ProofBuilder; use crate::core::{global, ser}; @@ -84,6 +83,178 @@ fn very_empty_block() { ); } +#[test] +fn block_with_nrd_kernel_pre_post_hf3() { + // automated testing - HF{1|2|3} at block heights {3, 6, 9} + // Enable the global NRD feature flag. NRD kernels valid at HF3 at height 9. + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + global::set_local_nrd_enabled(true); + + let keychain = ExtKeychain::from_random_seed(false).unwrap(); + let builder = ProofBuilder::new(&keychain); + let key_id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let key_id2 = ExtKeychain::derive_key_id(1, 2, 0, 0, 0); + + let mut tx = build::transaction( + KernelFeatures::NoRecentDuplicate { + fee: 2, + relative_height: NRDRelativeHeight::new(1440).unwrap(), + }, + vec![input(7, key_id1), output(5, key_id2)], + &keychain, + &builder, + ) + .unwrap(); + + let prev_height = TESTING_THIRD_HARD_FORK - 2; + let prev = BlockHeader { + height: prev_height, + version: consensus::header_version(prev_height), + ..BlockHeader::default() + }; + let b = new_block( + vec![&mut tx], + &keychain, + &builder, + &prev, + &ExtKeychain::derive_key_id(1, 1, 0, 0, 0), + ); + + // Block is invalid at header version 3 if it contains an NRD kernel. + assert_eq!(b.header.version, HeaderVersion(3)); + assert_eq!( + b.validate(&BlindingFactor::zero(), verifier_cache()), + Err(Error::NRDKernelPreHF3) + ); + + let prev_height = TESTING_THIRD_HARD_FORK - 1; + let prev = BlockHeader { + height: prev_height, + version: consensus::header_version(prev_height), + ..BlockHeader::default() + }; + let b = new_block( + vec![&mut tx], + &keychain, + &builder, + &prev, + &ExtKeychain::derive_key_id(1, 1, 0, 0, 0), + ); + + // Block is valid at header version 4 (at HF height) if it contains an NRD kernel. + assert_eq!(b.header.height, TESTING_THIRD_HARD_FORK); + assert_eq!(b.header.version, HeaderVersion(4)); + assert!(b + .validate(&BlindingFactor::zero(), verifier_cache()) + .is_ok()); + + let prev_height = TESTING_THIRD_HARD_FORK; + let prev = BlockHeader { + height: prev_height, + version: consensus::header_version(prev_height), + ..BlockHeader::default() + }; + let b = new_block( + vec![&mut tx], + &keychain, + &builder, + &prev, + &ExtKeychain::derive_key_id(1, 1, 0, 0, 0), + ); + + // Block is valid at header version 4 if it contains an NRD kernel. + assert_eq!(b.header.version, HeaderVersion(4)); + assert!(b + .validate(&BlindingFactor::zero(), verifier_cache()) + .is_ok()); +} + +#[test] +fn block_with_nrd_kernel_nrd_not_enabled() { + // automated testing - HF{1|2|3} at block heights {3, 6, 9} + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + + let keychain = ExtKeychain::from_random_seed(false).unwrap(); + let builder = ProofBuilder::new(&keychain); + let key_id1 = ExtKeychain::derive_key_id(1, 1, 0, 0, 0); + let key_id2 = ExtKeychain::derive_key_id(1, 2, 0, 0, 0); + + let mut tx = build::transaction( + KernelFeatures::NoRecentDuplicate { + fee: 2, + relative_height: NRDRelativeHeight::new(1440).unwrap(), + }, + vec![input(7, key_id1), output(5, key_id2)], + &keychain, + &builder, + ) + .unwrap(); + + let prev_height = TESTING_THIRD_HARD_FORK - 2; + let prev = BlockHeader { + height: prev_height, + version: consensus::header_version(prev_height), + ..BlockHeader::default() + }; + let b = new_block( + vec![&mut tx], + &keychain, + &builder, + &prev, + &ExtKeychain::derive_key_id(1, 1, 0, 0, 0), + ); + + // Block is invalid as NRD not enabled. + assert_eq!(b.header.version, HeaderVersion(3)); + assert_eq!( + b.validate(&BlindingFactor::zero(), verifier_cache()), + Err(Error::NRDKernelNotEnabled) + ); + + let prev_height = TESTING_THIRD_HARD_FORK - 1; + let prev = BlockHeader { + height: prev_height, + version: consensus::header_version(prev_height), + ..BlockHeader::default() + }; + let b = new_block( + vec![&mut tx], + &keychain, + &builder, + &prev, + &ExtKeychain::derive_key_id(1, 1, 0, 0, 0), + ); + + // Block is invalid as NRD not enabled. + assert_eq!(b.header.height, TESTING_THIRD_HARD_FORK); + assert_eq!(b.header.version, HeaderVersion(4)); + assert_eq!( + b.validate(&BlindingFactor::zero(), verifier_cache()), + Err(Error::NRDKernelNotEnabled) + ); + + let prev_height = TESTING_THIRD_HARD_FORK; + let prev = BlockHeader { + height: prev_height, + version: consensus::header_version(prev_height), + ..BlockHeader::default() + }; + let b = new_block( + vec![&mut tx], + &keychain, + &builder, + &prev, + &ExtKeychain::derive_key_id(1, 1, 0, 0, 0), + ); + + // Block is invalid as NRD not enabled. + assert_eq!(b.header.version, HeaderVersion(4)); + assert_eq!( + b.validate(&BlindingFactor::zero(), verifier_cache()), + Err(Error::NRDKernelNotEnabled) + ); +} + #[test] // builds a block with a tx spending another and check that cut_through occurred fn block_with_cut_through() { diff --git a/pool/src/transaction_pool.rs b/pool/src/transaction_pool.rs index 5dd4ea7fc..8f330b954 100644 --- a/pool/src/transaction_pool.rs +++ b/pool/src/transaction_pool.rs @@ -20,7 +20,8 @@ use self::core::core::hash::{Hash, Hashed}; use self::core::core::id::ShortId; use self::core::core::verifier_cache::VerifierCache; -use self::core::core::{transaction, Block, BlockHeader, Transaction, Weighting}; +use self::core::core::{transaction, Block, BlockHeader, HeaderVersion, Transaction, Weighting}; +use self::core::global; use self::util::RwLock; use crate::pool::Pool; use crate::types::{BlockChain, PoolAdapter, PoolConfig, PoolEntry, PoolError, TxSource}; @@ -132,6 +133,24 @@ where Ok(()) } + /// Verify the tx kernel variants and ensure they can all be accepted to the txpool/stempool + /// with respect to current header version. + fn verify_kernel_variants( + &self, + tx: &Transaction, + header: &BlockHeader, + ) -> Result<(), PoolError> { + if tx.kernels().iter().any(|k| k.is_nrd()) { + if !global::is_nrd_enabled() { + return Err(PoolError::NRDKernelNotEnabled); + } + if header.version < HeaderVersion(4) { + return Err(PoolError::NRDKernelPreHF3); + } + } + Ok(()) + } + /// Add the given tx to the pool, directing it to either the stempool or /// txpool based on stem flag provided. pub fn add_to_pool( @@ -147,6 +166,9 @@ where return Err(PoolError::DuplicateTx); } + // Check this tx is valid based on current header version. + self.verify_kernel_variants(&tx, header)?; + // Do we have the capacity to accept this transaction? let acceptability = self.is_acceptable(&tx, stem); let mut evict = false; diff --git a/pool/src/types.rs b/pool/src/types.rs index eae0700a2..131b5fed0 100644 --- a/pool/src/types.rs +++ b/pool/src/types.rs @@ -221,6 +221,12 @@ pub enum PoolError { /// Attempt to add a duplicate tx to the pool. #[fail(display = "Duplicate tx")] DuplicateTx, + /// NRD kernels will not be accepted by the txpool/stempool pre-HF3. + #[fail(display = "NRD kernel pre-HF3")] + NRDKernelPreHF3, + /// NRD kernels are not valid if disabled locally via "feature flag". + #[fail(display = "NRD kernel not enabled")] + NRDKernelNotEnabled, /// Other kinds of error (not yet pulled out into meaningful errors). #[fail(display = "General pool error {}", _0)] Other(String), diff --git a/pool/tests/common.rs b/pool/tests/common.rs index 37205bd10..a5b0fc0aa 100644 --- a/pool/tests/common.rs +++ b/pool/tests/common.rs @@ -216,10 +216,26 @@ where { let input_sum = input_values.iter().sum::() as i64; let output_sum = output_values.iter().sum::() as i64; - let fees: i64 = input_sum - output_sum; assert!(fees >= 0); + test_transaction_with_kernel_features( + keychain, + input_values, + output_values, + KernelFeatures::Plain { fee: fees as u64 }, + ) +} + +pub fn test_transaction_with_kernel_features( + keychain: &K, + input_values: Vec, + output_values: Vec, + kernel_features: KernelFeatures, +) -> Transaction +where + K: Keychain, +{ let mut tx_elements = Vec::new(); for input_value in input_values { @@ -233,7 +249,7 @@ where } libtx::build::transaction( - KernelFeatures::Plain { fee: fees as u64 }, + kernel_features, tx_elements, keychain, &libtx::ProofBuilder::new(keychain), diff --git a/pool/tests/nrd_kernels.rs b/pool/tests/nrd_kernels.rs new file mode 100644 index 000000000..75839d31a --- /dev/null +++ b/pool/tests/nrd_kernels.rs @@ -0,0 +1,216 @@ +// Copyright 2020 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod common; + +use self::core::core::hash::Hashed; +use self::core::core::verifier_cache::LruVerifierCache; +use self::core::core::{ + Block, BlockHeader, HeaderVersion, KernelFeatures, NRDRelativeHeight, Transaction, +}; +use self::core::global; +use self::core::pow::Difficulty; +use self::core::{consensus, libtx}; +use self::keychain::{ExtKeychain, Keychain}; +use self::pool::types::PoolError; +use self::util::RwLock; +use crate::common::*; +use grin_core as core; +use grin_keychain as keychain; +use grin_pool as pool; +use grin_util as util; +use std::sync::Arc; + +#[test] +fn test_nrd_kernel_verification_block_version() { + util::init_test_logger(); + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + global::set_local_nrd_enabled(true); + + let keychain: ExtKeychain = Keychain::from_random_seed(false).unwrap(); + + let db_root = ".grin_nrd_kernels"; + clean_output_dir(db_root.into()); + + let mut chain = ChainAdapter::init(db_root.into()).unwrap(); + + let verifier_cache = Arc::new(RwLock::new(LruVerifierCache::new())); + + // Initialize the chain/txhashset with an initial block + // so we have a non-empty UTXO set. + let add_block = |prev_header: BlockHeader, txs: Vec, chain: &mut ChainAdapter| { + let height = prev_header.height + 1; + let key_id = ExtKeychain::derive_key_id(1, height as u32, 0, 0, 0); + let fee = txs.iter().map(|x| x.fee()).sum(); + let reward = libtx::reward::output( + &keychain, + &libtx::ProofBuilder::new(&keychain), + &key_id, + fee, + false, + ) + .unwrap(); + let mut block = Block::new(&prev_header, txs, Difficulty::min(), reward).unwrap(); + + // Set the prev_root to the prev hash for testing purposes (no MMR to obtain a root from). + block.header.prev_root = prev_header.hash(); + + chain.update_db_for_block(&block); + block + }; + + let block = add_block(BlockHeader::default(), vec![], &mut chain); + let header = block.header; + + // Now create tx to spend that first coinbase (now matured). + // Provides us with some useful outputs to test with. + let initial_tx = test_transaction_spending_coinbase(&keychain, &header, vec![10, 20, 30, 40]); + + // Mine that initial tx so we can spend it with multiple txs + let mut block = add_block(header, vec![initial_tx], &mut chain); + let mut header = block.header; + + // Initialize a new pool with our chain adapter. + let mut pool = test_setup(Arc::new(chain.clone()), verifier_cache); + + let tx_1 = test_transaction_with_kernel_features( + &keychain, + vec![10, 20], + vec![24], + KernelFeatures::NoRecentDuplicate { + fee: 6, + relative_height: NRDRelativeHeight::new(1440).unwrap(), + }, + ); + + assert!(header.version < HeaderVersion(4)); + + assert_eq!( + pool.add_to_pool(test_source(), tx_1.clone(), false, &header), + Err(PoolError::NRDKernelPreHF3) + ); + + // Now mine several more blocks out to HF3 + for _ in 0..7 { + block = add_block(header, vec![], &mut chain); + header = block.header; + } + assert_eq!(header.height, consensus::TESTING_THIRD_HARD_FORK); + assert_eq!(header.version, HeaderVersion(4)); + + // Now confirm we can successfully add transaction with NRD kernel to txpool. + assert_eq!( + pool.add_to_pool(test_source(), tx_1.clone(), false, &header), + Ok(()), + ); + + assert_eq!(pool.total_size(), 1); + + let txs = pool.prepare_mineable_transactions().unwrap(); + assert_eq!(txs.len(), 1); + + // Cleanup db directory + clean_output_dir(db_root.into()); +} + +#[test] +fn test_nrd_kernel_verification_nrd_disabled() { + util::init_test_logger(); + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + + let keychain: ExtKeychain = Keychain::from_random_seed(false).unwrap(); + + let db_root = ".grin_nrd_kernel_disabled"; + clean_output_dir(db_root.into()); + + let mut chain = ChainAdapter::init(db_root.into()).unwrap(); + + let verifier_cache = Arc::new(RwLock::new(LruVerifierCache::new())); + + // Initialize the chain/txhashset with an initial block + // so we have a non-empty UTXO set. + let add_block = |prev_header: BlockHeader, txs: Vec, chain: &mut ChainAdapter| { + let height = prev_header.height + 1; + let key_id = ExtKeychain::derive_key_id(1, height as u32, 0, 0, 0); + let fee = txs.iter().map(|x| x.fee()).sum(); + let reward = libtx::reward::output( + &keychain, + &libtx::ProofBuilder::new(&keychain), + &key_id, + fee, + false, + ) + .unwrap(); + let mut block = Block::new(&prev_header, txs, Difficulty::min(), reward).unwrap(); + + // Set the prev_root to the prev hash for testing purposes (no MMR to obtain a root from). + block.header.prev_root = prev_header.hash(); + + chain.update_db_for_block(&block); + block + }; + + let block = add_block(BlockHeader::default(), vec![], &mut chain); + let header = block.header; + + // Now create tx to spend that first coinbase (now matured). + // Provides us with some useful outputs to test with. + let initial_tx = test_transaction_spending_coinbase(&keychain, &header, vec![10, 20, 30, 40]); + + // Mine that initial tx so we can spend it with multiple txs + let mut block = add_block(header, vec![initial_tx], &mut chain); + let mut header = block.header; + + // Initialize a new pool with our chain adapter. + let mut pool = test_setup(Arc::new(chain.clone()), verifier_cache); + + let tx_1 = test_transaction_with_kernel_features( + &keychain, + vec![10, 20], + vec![24], + KernelFeatures::NoRecentDuplicate { + fee: 6, + relative_height: NRDRelativeHeight::new(1440).unwrap(), + }, + ); + + assert!(header.version < HeaderVersion(4)); + + assert_eq!( + pool.add_to_pool(test_source(), tx_1.clone(), false, &header), + Err(PoolError::NRDKernelNotEnabled) + ); + + // Now mine several more blocks out to HF3 + for _ in 0..7 { + block = add_block(header, vec![], &mut chain); + header = block.header; + } + assert_eq!(header.height, consensus::TESTING_THIRD_HARD_FORK); + assert_eq!(header.version, HeaderVersion(4)); + + // NRD kernel support not enabled via feature flag, so not valid. + assert_eq!( + pool.add_to_pool(test_source(), tx_1.clone(), false, &header), + Err(PoolError::NRDKernelNotEnabled) + ); + + assert_eq!(pool.total_size(), 0); + + let txs = pool.prepare_mineable_transactions().unwrap(); + assert_eq!(txs.len(), 0); + + // Cleanup db directory + clean_output_dir(db_root.into()); +} diff --git a/src/bin/grin.rs b/src/bin/grin.rs index c35659c09..84e80873c 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -64,6 +64,10 @@ fn log_build_info() { debug!("{}", detailed_info); } +fn log_feature_flags() { + info!("Feature: NRD kernel enabled: {}", global::is_nrd_enabled()); +} + fn main() { let exit_code = real_main(); std::process::exit(exit_code); @@ -140,9 +144,6 @@ fn real_main() -> i32 { }; init_logger(Some(logging_config), logs_tx); - // One time initialization of the global chain_type. - global::init_global_chain_type(config.members.unwrap().server.chain_type); - if let Some(file_path) = &config.config_file_path { info!( "Using configuration file at {}", @@ -154,6 +155,22 @@ fn real_main() -> i32 { log_build_info(); + // Initialize our global chain_type and feature flags (NRD kernel support currently). + // These are read via global and not read from config beyond this point. + global::init_global_chain_type(config.members.unwrap().server.chain_type); + info!("Chain: {:?}", global::get_chain_type()); + match global::get_chain_type() { + global::ChainTypes::Mainnet => { + // Set various mainnet specific feature flags. + global::init_global_nrd_enabled(false); + } + _ => { + // Set various non-mainnet feature flags. + global::init_global_nrd_enabled(true); + } + } + log_feature_flags(); + // Execute subcommand match args.subcommand() { // server commands and options