diff --git a/chain/src/chain.rs b/chain/src/chain.rs index 88ed2a071c..33698a6ebf 100644 --- a/chain/src/chain.rs +++ b/chain/src/chain.rs @@ -468,6 +468,8 @@ impl ChainService { .map(|x| resolve_transaction(x, &mut seen_inputs, &cell_provider)) .collect(); + let cellbase_maturity = { self.shared.consensus().cellbase_maturity() }; + match txs_verifier.verify( chain_state.mut_txs_verify_cache(), &resolved, @@ -478,6 +480,7 @@ impl ChainService { consensus: self.shared.consensus(), }, b.header().number(), + cellbase_maturity, ) { Ok(_) => { cell_set_diff.push_new(b); diff --git a/chain/src/tests/basic.rs b/chain/src/tests/basic.rs index 43bd8b5b13..ed1e10e080 100644 --- a/chain/src/tests/basic.rs +++ b/chain/src/tests/basic.rs @@ -173,7 +173,7 @@ fn test_transaction_spend_in_same_block() { .chain_state() .lock() .get_cell_status(&OutPoint::new(tx2_hash, 0)), - CellStatus::live_output(tx2_output, Some(4)) + CellStatus::live_output(tx2_output, Some(4), false) ); } diff --git a/chain/src/tests/util.rs b/chain/src/tests/util.rs index ef96d2ab11..e037ccbfee 100644 --- a/chain/src/tests/util.rs +++ b/chain/src/tests/util.rs @@ -22,7 +22,7 @@ pub(crate) fn start_chain( ) -> (ChainController, Shared>) { let builder = SharedBuilder::::new(); let shared = builder - .consensus(consensus.unwrap_or_else(Default::default)) + .consensus(consensus.unwrap_or_else(|| Consensus::default().set_cellbase_maturity(0))) .build(); let notify = NotifyService::default().start::<&str>(None); diff --git a/core/src/cell.rs b/core/src/cell.rs index c38c6c8f06..2f970aa248 100644 --- a/core/src/cell.rs +++ b/core/src/cell.rs @@ -16,9 +16,14 @@ pub enum LiveCell { pub struct CellMeta { pub cell_output: CellOutput, pub block_number: Option, + pub cellbase: bool, } impl CellMeta { + pub fn is_cellbase(&self) -> bool { + self.cellbase + } + pub fn capacity(&self) -> Capacity { self.cell_output.capacity } @@ -39,10 +44,15 @@ impl CellStatus { CellStatus::Live(LiveCell::Null) } - pub fn live_output(cell_output: CellOutput, block_number: Option) -> CellStatus { + pub fn live_output( + cell_output: CellOutput, + block_number: Option, + cellbase: bool, + ) -> CellStatus { CellStatus::Live(LiveCell::Output(CellMeta { cell_output, block_number, + cellbase, })) } @@ -151,7 +161,9 @@ impl<'a> CellProvider for BlockCellProvider<'a> { .outputs() .get(out_point.index as usize) { - Some(x) => CellStatus::live_output(x.clone(), Some(self.block.header().number())), + Some(x) => { + CellStatus::live_output(x.clone(), Some(self.block.header().number()), *i == 0) + } None => CellStatus::Unknown, } } else { @@ -284,6 +296,7 @@ mod tests { lock: Script::default(), type_: None, }, + cellbase: false, }; db.cells.insert(p1.clone(), Some(o.clone())); diff --git a/script/src/verify.rs b/script/src/verify.rs index a980cd3fc9..03fae354d5 100644 --- a/script/src/verify.rs +++ b/script/src/verify.rs @@ -222,6 +222,7 @@ mod tests { let dummy_cell = CellMeta { cell_output: CellOutput::new(100, vec![], Script::always_success(), None), block_number: Some(1), + cellbase: false, }; let input = CellInput::new(OutPoint::null(), 0, vec![]); @@ -272,6 +273,7 @@ mod tests { let dep_cell = CellMeta { cell_output: CellOutput::new(buffer.len() as Capacity, buffer, Script::default(), None), block_number: Some(1), + cellbase: false, }; let script = Script::new(args, binary_hash); @@ -286,6 +288,7 @@ mod tests { let dummy_cell = CellMeta { cell_output: CellOutput::new(100, vec![], script, None), block_number: Some(1), + cellbase: false, }; let rtx = ResolvedTransaction { @@ -333,6 +336,7 @@ mod tests { let dep_cell = CellMeta { cell_output: CellOutput::new(buffer.len() as Capacity, buffer, Script::default(), None), block_number: Some(1), + cellbase: false, }; let script = Script::new(args, binary_hash); @@ -347,6 +351,7 @@ mod tests { let dummy_cell = CellMeta { cell_output: CellOutput::new(100, vec![], script, None), block_number: Some(1), + cellbase: false, }; let rtx = ResolvedTransaction { @@ -396,6 +401,7 @@ mod tests { let dep_cell = CellMeta { cell_output: CellOutput::new(buffer.len() as Capacity, buffer, Script::default(), None), block_number: Some(1), + cellbase: false, }; let script = Script::new(args, binary_hash); @@ -410,6 +416,7 @@ mod tests { let dummy_cell = CellMeta { cell_output: CellOutput::new(100, vec![], script, None), block_number: Some(1), + cellbase: false, }; let rtx = ResolvedTransaction { @@ -466,6 +473,7 @@ mod tests { let dummy_cell = CellMeta { cell_output: CellOutput::new(100, vec![], script, None), block_number: Some(1), + cellbase: false, }; let rtx = ResolvedTransaction { @@ -511,6 +519,7 @@ mod tests { let dummy_cell = CellMeta { cell_output: CellOutput::new(100, vec![], Script::always_success(), None), block_number: Some(1), + cellbase: false, }; let script = Script::new(args, (&blake2b_256(&buffer)).into()); @@ -525,6 +534,7 @@ mod tests { let dep_cell = CellMeta { cell_output: CellOutput::new(buffer.len() as Capacity, buffer, Script::default(), None), block_number: Some(1), + cellbase: false, }; let transaction = TransactionBuilder::default() @@ -578,6 +588,7 @@ mod tests { let dummy_cell = CellMeta { cell_output: CellOutput::new(100, vec![], Script::always_success(), None), block_number: Some(1), + cellbase: false, }; let script = Script::new(args, (&blake2b_256(&buffer)).into()); @@ -587,6 +598,7 @@ mod tests { let dep_cell = CellMeta { cell_output: CellOutput::new(buffer.len() as Capacity, buffer, Script::default(), None), block_number: Some(1), + cellbase: false, }; let transaction = TransactionBuilder::default() diff --git a/shared/src/chain_state.rs b/shared/src/chain_state.rs index a2fc8c9146..a721f66d19 100644 --- a/shared/src/chain_state.rs +++ b/shared/src/chain_state.rs @@ -183,12 +183,16 @@ impl ChainState { Ok(cycles) => { // enqueue tx with cycles let entry = PoolEntry::new(tx, 0, Some(cycles)); - tx_pool.enqueue_tx(entry); + if !tx_pool.enqueue_tx(entry) { + return Err(PoolError::Duplicate); + } Ok(cycles) } Err(TransactionError::UnknownInput) => { let entry = PoolEntry::new(tx, 0, None); - tx_pool.enqueue_tx(entry); + if !tx_pool.enqueue_tx(entry) { + return Err(PoolError::Duplicate); + } Err(PoolError::InvalidTx(TransactionError::UnknownInput)) } Err(err) => Err(PoolError::InvalidTx(err)), @@ -212,8 +216,13 @@ impl ChainState { match ret { Some(cycles) => Ok(cycles), None => { - let cycles = - TransactionVerifier::new(&rtx, &self, self.tip_number()).verify(max_cycles)?; + let cycles = TransactionVerifier::new( + &rtx, + &self, + self.tip_number(), + self.consensus().cellbase_maturity, + ) + .verify(max_cycles)?; // write cache self.txs_verify_cache.borrow_mut().insert(tx_hash, cycles); Ok(cycles) @@ -426,6 +435,7 @@ impl CellProvider for ChainState { CellStatus::live_output( tx.outputs()[out_point.index as usize].clone(), Some(tx_meta.block_number()), + tx_meta.is_cellbase(), ) } } @@ -448,6 +458,7 @@ impl<'a, CS: ChainStore> CellProvider for ChainCellSetOverlay<'a, CS> { CellStatus::live_output( tx.outputs()[out_point.index as usize].clone(), Some(tx_meta.block_number()), + tx_meta.is_cellbase(), ) } } diff --git a/shared/src/tx_pool/types.rs b/shared/src/tx_pool/types.rs index a62a3109cd..97ee3ec79f 100644 --- a/shared/src/tx_pool/types.rs +++ b/shared/src/tx_pool/types.rs @@ -78,6 +78,8 @@ pub enum PoolError { TimeOut, /// BlockNumber is not right InvalidBlockNumber, + /// Duplicate tx + Duplicate, } impl fmt::Display for PoolError { @@ -218,7 +220,7 @@ impl CellProvider for StagingPool { if x.is_some() { CellStatus::Dead } else { - CellStatus::live_output(self.get_output(o).expect("output"), None) + CellStatus::live_output(self.get_output(o).expect("output"), None, false) } } else if self.edges.get_outer(o).is_some() { CellStatus::Dead diff --git a/spec/src/consensus.rs b/spec/src/consensus.rs index cfd2a6b5c8..870e3ef5bc 100644 --- a/spec/src/consensus.rs +++ b/spec/src/consensus.rs @@ -114,6 +114,11 @@ impl Consensus { self } + pub fn set_cellbase_maturity(mut self, cellbase_maturity: usize) -> Self { + self.cellbase_maturity = cellbase_maturity; + self + } + pub fn genesis_block(&self) -> &Block { &self.genesis_block } diff --git a/sync/src/relayer/transaction_process.rs b/sync/src/relayer/transaction_process.rs index b94ba648af..8632390587 100644 --- a/sync/src/relayer/transaction_process.rs +++ b/sync/src/relayer/transaction_process.rs @@ -80,7 +80,8 @@ impl<'a, CS: ChainStore> TransactionProcess<'a, CS> { } Err(PoolError::InvalidTx(TransactionError::UnknownInput)) | Err(PoolError::InvalidTx(TransactionError::Conflict)) - | Err(PoolError::InvalidTx(TransactionError::Immature)) => { + | Err(PoolError::InvalidTx(TransactionError::Immature)) + | Err(PoolError::InvalidTx(TransactionError::CellbaseImmaturity)) => { // this error may occured when peer's tip is different with us, // we can't proof peer is bad so just ignore this debug!(target: "relay", "peer {} relay a conflict or missing input tx: {:?}", self.peer, tx); diff --git a/sync/src/tests/relayer.rs b/sync/src/tests/relayer.rs index 6bb52d0732..869722b8b7 100644 --- a/sync/src/tests/relayer.rs +++ b/sync/src/tests/relayer.rs @@ -339,7 +339,9 @@ fn setup_node( .timestamp(unix_time_as_millis()) .difficulty(U256::from(1000u64)), ); - let consensus = Consensus::default().set_genesis_block(block.clone()); + let consensus = Consensus::default() + .set_genesis_block(block.clone()) + .set_cellbase_maturity(0); let shared = SharedBuilder::::new() .consensus(consensus) diff --git a/util/jsonrpc-types/src/blockchain.rs b/util/jsonrpc-types/src/blockchain.rs index f7ecc7e84b..fdbb21784c 100644 --- a/util/jsonrpc-types/src/blockchain.rs +++ b/util/jsonrpc-types/src/blockchain.rs @@ -114,7 +114,7 @@ impl TryFrom for CoreOutPoint { #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash, Debug)] pub struct CellInput { pub previous_output: OutPoint, - pub valid_since: u64, + pub valid_since: String, pub args: Vec, } @@ -123,7 +123,7 @@ impl From for CellInput { let (previous_output, valid_since, args) = core.destruct(); CellInput { previous_output: previous_output.into(), - valid_since, + valid_since: valid_since.to_string(), args: args.into_iter().map(Bytes::new).collect(), } } @@ -140,7 +140,7 @@ impl TryFrom for CoreCellInput { } = json; Ok(CoreCellInput::new( previous_output.try_into()?, - valid_since, + valid_since.parse::()?, args.into_iter().map(Bytes::into_vec).collect(), )) } diff --git a/verification/src/block_verifier.rs b/verification/src/block_verifier.rs index 84786642d8..fbee485b96 100644 --- a/verification/src/block_verifier.rs +++ b/verification/src/block_verifier.rs @@ -353,6 +353,7 @@ impl TransactionsVerifier { block_reward: Capacity, block_median_time_context: M, tip_number: BlockNumber, + cellbase_maturity: usize, ) -> Result<(), Error> where M: BlockMedianTimeContext + Sync, @@ -377,10 +378,15 @@ impl TransactionsVerifier { .map_err(|e| Error::Transactions((index, e))) .map(|_| (None, *cycles)) } else { - TransactionVerifier::new(&tx, &block_median_time_context, tip_number) - .verify(self.max_cycles) - .map_err(|e| Error::Transactions((index, e))) - .map(|cycles| (Some(tx.transaction.hash()), cycles)) + TransactionVerifier::new( + &tx, + &block_median_time_context, + tip_number, + cellbase_maturity, + ) + .verify(self.max_cycles) + .map_err(|e| Error::Transactions((index, e))) + .map(|cycles| (Some(tx.transaction.hash()), cycles)) } }) .collect::, _>>()?; diff --git a/verification/src/error.rs b/verification/src/error.rs index 4eb8ec1b91..5f841847bd 100644 --- a/verification/src/error.rs +++ b/verification/src/error.rs @@ -149,4 +149,5 @@ pub enum TransactionError { Immature, /// Invalid ValidSince flags InvalidValidSince, + CellbaseImmaturity, } diff --git a/verification/src/tests/transaction_verifier.rs b/verification/src/tests/transaction_verifier.rs index 1e24ab0c3f..a5c57653af 100644 --- a/verification/src/tests/transaction_verifier.rs +++ b/verification/src/tests/transaction_verifier.rs @@ -1,5 +1,6 @@ use super::super::transaction_verifier::{ - CapacityVerifier, DuplicateInputsVerifier, EmptyVerifier, NullVerifier, ValidSinceVerifier, + CapacityVerifier, DuplicateInputsVerifier, EmptyVerifier, MaturityVerifier, NullVerifier, + ValidSinceVerifier, }; use crate::error::TransactionError; use ckb_core::cell::CellStatus; @@ -42,6 +43,7 @@ pub fn test_capacity_outofbound() { input_cells: vec![CellStatus::live_output( CellOutput::new(50, Vec::new(), Script::default(), None), None, + false, )], }; let verifier = CapacityVerifier::new(&rtx); @@ -52,6 +54,37 @@ pub fn test_capacity_outofbound() { ); } +#[test] +pub fn test_cellbase_maturity() { + let transaction = TransactionBuilder::default() + .output(CellOutput::new(50, vec![1; 51], Script::default(), None)) + .build(); + + let rtx = ResolvedTransaction { + transaction, + dep_cells: Vec::new(), + input_cells: vec![CellStatus::live_output( + CellOutput::new(50, Vec::new(), Script::default(), None), + Some(30), + true, + )], + }; + + let tip_number = 70; + let cellbase_maturity = 100; + let verifier = MaturityVerifier::new(&rtx, tip_number, cellbase_maturity); + + assert_eq!( + verifier.verify().err(), + Some(TransactionError::CellbaseImmaturity) + ); + + let tip_number = 130; + let verifier = MaturityVerifier::new(&rtx, tip_number, cellbase_maturity); + + assert!(verifier.verify().is_ok()); +} + #[test] pub fn test_capacity_invalid() { let transaction = TransactionBuilder::default() @@ -68,10 +101,12 @@ pub fn test_capacity_invalid() { CellStatus::live_output( CellOutput::new(49, Vec::new(), Script::default(), None), None, + false, ), CellStatus::live_output( CellOutput::new(100, Vec::new(), Script::default(), None), None, + false, ), ], }; @@ -141,6 +176,7 @@ pub fn test_valid_since() { input_cells: vec![CellStatus::live_output( CellOutput::new(50, Vec::new(), Script::default(), None), Some(1), + false, )], }; @@ -168,6 +204,7 @@ pub fn test_valid_since() { input_cells: vec![CellStatus::live_output( CellOutput::new(50, Vec::new(), Script::default(), None), Some(1), + false, )], }; @@ -195,6 +232,7 @@ pub fn test_valid_since() { input_cells: vec![CellStatus::live_output( CellOutput::new(50, Vec::new(), Script::default(), None), Some(1), + false, )], }; @@ -230,6 +268,7 @@ pub fn test_valid_since() { input_cells: vec![CellStatus::live_output( CellOutput::new(50, Vec::new(), Script::default(), None), Some(1), + false, )], }; diff --git a/verification/src/transaction_verifier.rs b/verification/src/transaction_verifier.rs index 78e8ee31ed..d8f189486e 100644 --- a/verification/src/transaction_verifier.rs +++ b/verification/src/transaction_verifier.rs @@ -15,6 +15,7 @@ pub struct TransactionVerifier<'a, M> { pub version: VersionVerifier<'a>, pub null: NullVerifier<'a>, pub empty: EmptyVerifier<'a>, + pub maturity: MaturityVerifier<'a>, pub capacity: CapacityVerifier<'a>, pub duplicate_inputs: DuplicateInputsVerifier<'a>, pub inputs: InputVerifier<'a>, @@ -30,11 +31,13 @@ where rtx: &'a ResolvedTransaction, median_time_context: &'a M, tip_number: BlockNumber, + cellbase_maturity: usize, ) -> Self { TransactionVerifier { version: VersionVerifier::new(&rtx.transaction), null: NullVerifier::new(&rtx.transaction), empty: EmptyVerifier::new(&rtx.transaction), + maturity: MaturityVerifier::new(&rtx, tip_number, cellbase_maturity), duplicate_inputs: DuplicateInputsVerifier::new(&rtx.transaction), script: ScriptVerifier::new(rtx), capacity: CapacityVerifier::new(rtx), @@ -47,6 +50,7 @@ where self.version.verify()?; self.empty.verify()?; self.null.verify()?; + self.maturity.verify()?; self.inputs.verify()?; self.capacity.verify()?; self.duplicate_inputs.verify()?; @@ -140,6 +144,52 @@ impl<'a> EmptyVerifier<'a> { } } +pub struct MaturityVerifier<'a> { + transaction: &'a ResolvedTransaction, + tip_number: BlockNumber, + cellbase_maturity: usize, +} + +impl<'a> MaturityVerifier<'a> { + pub fn new( + transaction: &'a ResolvedTransaction, + tip_number: BlockNumber, + cellbase_maturity: usize, + ) -> Self { + MaturityVerifier { + transaction, + tip_number, + cellbase_maturity, + } + } + + pub fn verify(&self) -> Result<(), TransactionError> { + let cellbase_immature = |cell_status: &CellStatus| -> bool { + match cell_status.get_live_output() { + Some(ref meta) + if meta.is_cellbase() + && self.tip_number + < meta.block_number.expect( + "cell meta should have block number when transaction verify", + ) + self.cellbase_maturity as u64 => + { + true + } + _ => false, + } + }; + + let input_immature_spend = || self.transaction.input_cells.iter().any(cellbase_immature); + let dep_immature_spend = || self.transaction.dep_cells.iter().any(cellbase_immature); + + if input_immature_spend() || dep_immature_spend() { + Err(TransactionError::CellbaseImmaturity) + } else { + Ok(()) + } + } +} + pub struct DuplicateInputsVerifier<'a> { transaction: &'a Transaction, }