diff --git a/tx-pool/src/chunk_process.rs b/tx-pool/src/chunk_process.rs index d8dc6d3273..34c38caf2b 100644 --- a/tx-pool/src/chunk_process.rs +++ b/tx-pool/src/chunk_process.rs @@ -15,8 +15,9 @@ use ckb_types::{ use ckb_verification::cache::TxVerificationCache; use ckb_verification::{ cache::{CacheEntry, Completed}, - ContextualWithoutScriptTransactionVerifier, ScriptError, ScriptVerifier, ScriptVerifyResult, - ScriptVerifyState, TimeRelativeTransactionVerifier, TransactionSnapshot, TxVerifyEnv, + ContextualWithoutScriptTransactionVerifier, DaoScriptSizeVerifier, ScriptError, ScriptVerifier, + ScriptVerifyResult, ScriptVerifyState, TimeRelativeTransactionVerifier, TransactionSnapshot, + TxVerifyEnv, }; use std::sync::Arc; use tokio::sync::watch; @@ -269,6 +270,15 @@ impl ChunkProcess { Arc::clone(&tx_env), ) .verify() + .and_then(|result| { + DaoScriptSizeVerifier::new( + Arc::clone(&rtx), + consensus.dao_type_hash(), + data_loader.clone(), + ) + .verify()?; + Ok(result) + }) .map_err(Reject::Verification); let fee = try_or_return_with_snapshot!(ret, snapshot); diff --git a/tx-pool/src/process.rs b/tx-pool/src/process.rs index c386bd0524..5880ae128e 100644 --- a/tx-pool/src/process.rs +++ b/tx-pool/src/process.rs @@ -27,8 +27,8 @@ use ckb_types::{ use ckb_util::LinkedHashSet; use ckb_verification::{ cache::{CacheEntry, Completed}, - ContextualTransactionVerifier, ScriptVerifyResult, TimeRelativeTransactionVerifier, - TxVerifyEnv, + ContextualTransactionVerifier, DaoScriptSizeVerifier, ScriptVerifyResult, + TimeRelativeTransactionVerifier, TxVerifyEnv, }; use std::collections::HashSet; use std::collections::{HashMap, VecDeque}; @@ -654,6 +654,15 @@ impl TxPoolService { match ret { ScriptVerifyResult::Completed(cycles) => { + if let Err(e) = DaoScriptSizeVerifier::new( + Arc::clone(&rtx), + self.consensus.dao_type_hash(), + snapshot.as_data_loader(), + ) + .verify() + { + return Err(Reject::Verification(e)); + } if let Some((declared, _)) = remote { if declared != cycles { return Err(Reject::DeclaredWrongCycles(declared, cycles)); diff --git a/tx-pool/src/util.rs b/tx-pool/src/util.rs index bfbd9e028f..28f141e2f1 100644 --- a/tx-pool/src/util.rs +++ b/tx-pool/src/util.rs @@ -11,7 +11,7 @@ use ckb_types::core::{ }; use ckb_verification::{ cache::{CacheEntry, Completed}, - ContextualTransactionVerifier, NonContextualTransactionVerifier, + ContextualTransactionVerifier, DaoScriptSizeVerifier, NonContextualTransactionVerifier, TimeRelativeTransactionVerifier, TxVerifyEnv, }; use std::sync::Arc; @@ -101,15 +101,33 @@ pub(crate) fn verify_rtx( .map_err(Reject::Verification) } CacheEntry::Suspended(suspended) => { - ContextualTransactionVerifier::new(rtx, consensus, data_loader, tx_env) + ContextualTransactionVerifier::new(Arc::clone(&rtx), consensus, data_loader, tx_env) .complete(max_tx_verify_cycles, false, &suspended.snap) + .and_then(|result| { + DaoScriptSizeVerifier::new( + rtx, + snapshot.cloned_consensus().dao_type_hash(), + snapshot.as_data_loader(), + ) + .verify()?; + Ok(result) + }) .map_err(Reject::Verification) } } } else { block_in_place(|| { - ContextualTransactionVerifier::new(rtx, consensus, data_loader, tx_env) + ContextualTransactionVerifier::new(Arc::clone(&rtx), consensus, data_loader, tx_env) .verify(max_tx_verify_cycles, false) + .and_then(|result| { + DaoScriptSizeVerifier::new( + rtx, + snapshot.cloned_consensus().dao_type_hash(), + snapshot.as_data_loader(), + ) + .verify()?; + Ok(result) + }) .map_err(Reject::Verification) }) } diff --git a/util/types/src/core/error.rs b/util/types/src/core/error.rs index 16cfbccd8d..9e807ed03d 100644 --- a/util/types/src/core/error.rs +++ b/util/types/src/core/error.rs @@ -177,6 +177,13 @@ pub enum TransactionError { feature: &'static str, }, + /// Nervos DAO lock size mismatch. + #[error("The lock script size of deposit cell at index {} does not match the withdrawing cell at the same index", index)] + DaoLockSizeMismatch { + /// The index of mismatched DAO cells. + index: usize, + }, + /// The internal error. #[error("Internal: {description}, this error shouldn't happen, please report this bug to developers.")] Internal { @@ -204,6 +211,7 @@ impl TransactionError { | TransactionError::CellbaseImmaturity { .. } | TransactionError::MismatchedVersion { .. } | TransactionError::Compatible { .. } + | TransactionError::DaoLockSizeMismatch { .. } | TransactionError::Internal { .. } => false, } } diff --git a/verification/contextual/src/contextual_block_verifier.rs b/verification/contextual/src/contextual_block_verifier.rs index 15d41e4075..a3ed5c002c 100644 --- a/verification/contextual/src/contextual_block_verifier.rs +++ b/verification/contextual/src/contextual_block_verifier.rs @@ -27,7 +27,7 @@ use ckb_verification::cache::{ }; use ckb_verification::{ BlockErrorKind, CellbaseError, CommitError, ContextualTransactionVerifier, - TimeRelativeTransactionVerifier, UnknownParentError, + DaoScriptSizeVerifier, TimeRelativeTransactionVerifier, UnknownParentError, }; use ckb_verification::{BlockTransactionsError, EpochError, TxVerifyEnv}; use ckb_verification_traits::Switch; @@ -327,25 +327,28 @@ impl<'a, 'b, 'c, CS: ChainStore + VersionbitsIndexer> DaoHeaderVerifier<'a, 'b, } } -struct BlockTxsVerifier<'a, CS> { +struct BlockTxsVerifier<'a, 'b, CS> { context: VerifyContext, header: HeaderView, handle: &'a Handle, txs_verify_cache: &'a Arc>, + parent: &'b HeaderView, } -impl<'a, CS: ChainStore + VersionbitsIndexer + 'static> BlockTxsVerifier<'a, CS> { +impl<'a, 'b, CS: ChainStore + VersionbitsIndexer + 'static> BlockTxsVerifier<'a, 'b, CS> { pub fn new( context: VerifyContext, header: HeaderView, handle: &'a Handle, txs_verify_cache: &'a Arc>, + parent: &'b HeaderView, ) -> Self { BlockTxsVerifier { context, header, handle, txs_verify_cache, + parent, } } @@ -465,7 +468,16 @@ impl<'a, CS: ChainStore + VersionbitsIndexer + 'static> BlockTxsVerifier<'a, CS> .into() }) .map(|completed| (tx_hash, completed)) - } + }.and_then(|result| { + if self.context.versionbits_active(DeploymentPos::LightClient, self.parent) { + DaoScriptSizeVerifier::new( + Arc::clone(tx), + self.context.consensus.dao_type_hash(), + self.context.store.as_data_loader(), + ).verify()?; + } + Ok(result) + }) }) .skip(1) // skip cellbase tx .collect::, Error>>()?; @@ -697,6 +709,7 @@ impl<'a, CS: ChainStore + VersionbitsIndexer + 'static, MS: MMRStore Option { + None + } + + fn get_cell_data_hash(&self, _out_point: &OutPoint) -> Option { + None + } +} + +#[test] +fn test_dao_disables_different_lock_script_size() { + let dao_type_script = build_genesis_type_id_script(OUTPUT_INDEX_DAO); + let transaction = TransactionBuilder::default() + .outputs(vec![ + CellOutput::new_builder() + .capacity(capacity_bytes!(50).pack()) + .build(), + CellOutput::new_builder() + .capacity(capacity_bytes!(200).pack()) + .lock( + Script::new_builder() + .args(Bytes::from(vec![1; 20]).pack()) + .build(), + ) + .type_(Some(dao_type_script.clone()).pack()) + .build(), + ]) + .outputs_data(vec![Bytes::new().pack(); 2]) + .build(); + + let rtx = Arc::new(ResolvedTransaction { + transaction, + resolved_cell_deps: Vec::new(), + resolved_inputs: vec![ + CellMetaBuilder::from_cell_output( + CellOutput::new_builder() + .capacity(capacity_bytes!(50).pack()) + .build(), + Bytes::new(), + ) + .build(), + CellMetaBuilder::from_cell_output( + CellOutput::new_builder() + .capacity(capacity_bytes!(201).pack()) + .lock(Script::new_builder().args(Bytes::new().pack()).build()) + .type_(Some(dao_type_script.clone()).pack()) + .build(), + Bytes::from(vec![0; 8]), + ) + .build(), + ], + resolved_dep_groups: vec![], + }); + let verifier = + DaoScriptSizeVerifier::new(rtx, Some(dao_type_script.code_hash()), EmptyDataProvider {}); + + assert_error_eq!( + verifier.verify().unwrap_err(), + TransactionError::DaoLockSizeMismatch { index: 1 }, + ); +} + +#[test] +fn test_non_dao_allows_lock_script_size() { + let dao_type_script = build_genesis_type_id_script(OUTPUT_INDEX_DAO); + let transaction = TransactionBuilder::default() + .outputs(vec![ + CellOutput::new_builder() + .capacity(capacity_bytes!(50).pack()) + .build(), + CellOutput::new_builder() + .capacity(capacity_bytes!(200).pack()) + .lock( + Script::new_builder() + .args(Bytes::from(vec![1; 20]).pack()) + .build(), + ) + .build(), + ]) + .outputs_data(vec![Bytes::new().pack(); 2]) + .build(); + + let rtx = Arc::new(ResolvedTransaction { + transaction, + resolved_cell_deps: Vec::new(), + resolved_inputs: vec![ + CellMetaBuilder::from_cell_output( + CellOutput::new_builder() + .capacity(capacity_bytes!(50).pack()) + .build(), + Bytes::new(), + ) + .build(), + CellMetaBuilder::from_cell_output( + CellOutput::new_builder() + .capacity(capacity_bytes!(201).pack()) + .lock(Script::new_builder().args(Bytes::new().pack()).build()) + .build(), + Bytes::from(vec![0; 8]), + ) + .build(), + ], + resolved_dep_groups: vec![], + }); + let verifier = + DaoScriptSizeVerifier::new(rtx, Some(dao_type_script.code_hash()), EmptyDataProvider {}); + + assert!(verifier.verify().is_ok()); +} + +#[test] +fn test_dao_allows_different_lock_script_size_in_withdraw_phase_2() { + let dao_type_script = build_genesis_type_id_script(OUTPUT_INDEX_DAO); + let transaction = TransactionBuilder::default() + .outputs(vec![ + CellOutput::new_builder() + .capacity(capacity_bytes!(50).pack()) + .build(), + CellOutput::new_builder() + .capacity(capacity_bytes!(200).pack()) + .lock( + Script::new_builder() + .args(Bytes::from(vec![1; 20]).pack()) + .build(), + ) + .type_(Some(dao_type_script.clone()).pack()) + .build(), + ]) + .outputs_data(vec![Bytes::new().pack(); 2]) + .build(); + + let rtx = Arc::new(ResolvedTransaction { + transaction, + resolved_cell_deps: Vec::new(), + resolved_inputs: vec![ + CellMetaBuilder::from_cell_output( + CellOutput::new_builder() + .capacity(capacity_bytes!(50).pack()) + .build(), + Bytes::new(), + ) + .build(), + CellMetaBuilder::from_cell_output( + CellOutput::new_builder() + .capacity(capacity_bytes!(201).pack()) + .lock(Script::new_builder().args(Bytes::new().pack()).build()) + .type_(Some(dao_type_script.clone()).pack()) + .build(), + Bytes::from(vec![1; 8]), + ) + .build(), + ], + resolved_dep_groups: vec![], + }); + let verifier = + DaoScriptSizeVerifier::new(rtx, Some(dao_type_script.code_hash()), EmptyDataProvider {}); + + assert!(verifier.verify().is_ok()); +} + +#[test] +fn test_dao_allows_different_lock_script_size_using_normal_cells_in_withdraw_phase_2() { + let dao_type_script = build_genesis_type_id_script(OUTPUT_INDEX_DAO); + let transaction = TransactionBuilder::default() + .outputs(vec![ + CellOutput::new_builder() + .capacity(capacity_bytes!(50).pack()) + .build(), + CellOutput::new_builder() + .capacity(capacity_bytes!(200).pack()) + .lock( + Script::new_builder() + .args(Bytes::from(vec![1; 20]).pack()) + .build(), + ) + .type_(Some(dao_type_script.clone()).pack()) + .build(), + ]) + .outputs_data(vec![]) + .build(); + + let rtx = Arc::new(ResolvedTransaction { + transaction, + resolved_cell_deps: Vec::new(), + resolved_inputs: vec![ + CellMetaBuilder::from_cell_output( + CellOutput::new_builder() + .capacity(capacity_bytes!(50).pack()) + .build(), + Bytes::new(), + ) + .build(), + CellMetaBuilder::from_cell_output( + CellOutput::new_builder() + .capacity(capacity_bytes!(201).pack()) + .lock(Script::new_builder().args(Bytes::new().pack()).build()) + .build(), + Bytes::from(vec![1; 8]), + ) + .build(), + ], + resolved_dep_groups: vec![], + }); + let verifier = + DaoScriptSizeVerifier::new(rtx, Some(dao_type_script.code_hash()), EmptyDataProvider {}); + + assert!(verifier.verify().is_ok()); +} diff --git a/verification/src/transaction_verifier.rs b/verification/src/transaction_verifier.rs index 8c4e11e734..22468e862a 100644 --- a/verification/src/transaction_verifier.rs +++ b/verification/src/transaction_verifier.rs @@ -14,7 +14,7 @@ use ckb_types::{ cell::{CellMeta, ResolvedTransaction}, Capacity, Cycle, EpochNumberWithFraction, ScriptHashType, TransactionView, Version, }, - packed::Byte32, + packed::{Byte32, CellOutput}, prelude::*, }; use std::collections::HashSet; @@ -585,20 +585,25 @@ impl CapacityVerifier { .resolved_inputs .iter() .any(|cell_meta| { - cell_meta - .cell_output - .type_() - .to_opt() - .map(|t| { - Into::::into(t.hash_type()) == Into::::into(ScriptHashType::Type) - && &t.code_hash() - == self.dao_type_hash.as_ref().expect("No dao system cell") - }) - .unwrap_or(false) + cell_uses_dao_type_script( + &cell_meta.cell_output, + self.dao_type_hash.as_ref().expect("No dao system cell"), + ) }) } } +fn cell_uses_dao_type_script(cell_output: &CellOutput, dao_type_hash: &Byte32) -> bool { + cell_output + .type_() + .to_opt() + .map(|t| { + Into::::into(t.hash_type()) == Into::::into(ScriptHashType::Type) + && &t.code_hash() == dao_type_hash + }) + .unwrap_or(false) +} + const LOCK_TYPE_FLAG: u64 = 1 << 63; const METRIC_TYPE_FLAG_MASK: u64 = 0x6000_0000_0000_0000; const VALUE_MASK: u64 = 0x00ff_ffff_ffff_ffff; @@ -962,3 +967,68 @@ where Ok(fee) } } + +/// Verifies that deposit cell and withdrawing cell in Nervos DAO use same sized lock scripts. +/// It provides a temporary solution till Nervos DAO script can be properly upgraded. +pub struct DaoScriptSizeVerifier
{ + resolved_transaction: Arc, + // It's Option because genesis block might not always have dao system cell + dao_type_hash: Option, + data_loader: DL, +} + +impl DaoScriptSizeVerifier
{ + /// Create a new `DaoScriptSizeVerifier` + pub fn new( + resolved_transaction: Arc, + dao_type_hash: Option, + data_loader: DL, + ) -> Self { + DaoScriptSizeVerifier { + resolved_transaction, + dao_type_hash, + data_loader, + } + } + + /// Verifies that for all Nervos DAO transactions, withdrawing cells must use lock scripts + /// of the same size as corresponding deposit cells + pub fn verify(&self) -> Result<(), Error> { + if self.dao_type_hash.is_none() { + return Ok(()); + } + let dao_type_hash = self.dao_type_hash.as_ref().unwrap(); + for (i, (input_meta, cell_output)) in self + .resolved_transaction + .resolved_inputs + .iter() + .zip(self.resolved_transaction.transaction.outputs()) + .enumerate() + { + // Both the input and output cell must use Nervos DAO as type script + if !(cell_uses_dao_type_script(&input_meta.cell_output, dao_type_hash) + && cell_uses_dao_type_script(&cell_output, dao_type_hash)) + { + continue; + } + + // A Nervos DAO deposit cell must have input data + let input_data = match self.data_loader.load_cell_data(input_meta) { + Some(data) => data, + None => continue, + }; + + // Only input data with full zeros are counted as deposit cell + if input_data.into_iter().any(|b| b != 0) { + continue; + } + + // Now we have a pair of DAO deposit and withdrawing cells, it is expected + // they have the lock scripts of the same size. + if input_meta.cell_output.lock().total_size() != cell_output.lock().total_size() { + return Err((TransactionError::DaoLockSizeMismatch { index: i }).into()); + } + } + Ok(()) + } +}