diff --git a/base_layer/core/src/consensus/consensus_constants.rs b/base_layer/core/src/consensus/consensus_constants.rs index 02ba61a3ca..11e8c8e7d0 100644 --- a/base_layer/core/src/consensus/consensus_constants.rs +++ b/base_layer/core/src/consensus/consensus_constants.rs @@ -38,6 +38,7 @@ use crate::{ transaction_components::{ OutputFeatures, OutputFeaturesVersion, + OutputType, TransactionInputVersion, TransactionKernelVersion, TransactionOutputVersion, @@ -85,11 +86,13 @@ pub struct ConsensusConstants { /// Maximum byte size of TariScript max_script_byte_size: usize, /// Range of valid transaction input versions - pub(crate) input_version_range: RangeInclusive, + input_version_range: RangeInclusive, /// Range of valid transaction output (and features) versions - pub(crate) output_version_range: OutputVersionRange, + output_version_range: OutputVersionRange, /// Range of valid transaction kernel versions - pub(crate) kernel_version_range: RangeInclusive, + kernel_version_range: RangeInclusive, + /// An allowlist of output types + permitted_output_types: &'static [OutputType], } // todo: remove this once OutputFeaturesVersion is removed in favor of just TransactionOutputVersion @@ -277,6 +280,11 @@ impl ConsensusConstants { &self.kernel_version_range } + /// Returns the permitted OutputTypes + pub fn permitted_output_types(&self) -> &[OutputType] { + self.permitted_output_types + } + pub fn localnet() -> Vec { let difficulty_block_window = 90; let mut algos = HashMap::new(); @@ -313,6 +321,7 @@ impl ConsensusConstants { input_version_range, output_version_range, kernel_version_range, + permitted_output_types: OutputType::all(), }] } @@ -352,6 +361,7 @@ impl ConsensusConstants { input_version_range, output_version_range, kernel_version_range, + permitted_output_types: Self::current_permitted_output_types(), }] } @@ -394,6 +404,7 @@ impl ConsensusConstants { input_version_range, output_version_range, kernel_version_range, + permitted_output_types: Self::current_permitted_output_types(), }] } @@ -443,6 +454,7 @@ impl ConsensusConstants { input_version_range: input_version_range.clone(), output_version_range: output_version_range.clone(), kernel_version_range: kernel_version_range.clone(), + permitted_output_types: Self::current_permitted_output_types(), }, ConsensusConstants { effective_from_height: 23000, @@ -465,6 +477,7 @@ impl ConsensusConstants { input_version_range, output_version_range, kernel_version_range, + permitted_output_types: Self::current_permitted_output_types(), }, ] } @@ -512,6 +525,7 @@ impl ConsensusConstants { input_version_range, output_version_range, kernel_version_range, + permitted_output_types: Self::current_permitted_output_types(), }] } @@ -552,8 +566,13 @@ impl ConsensusConstants { input_version_range, output_version_range, kernel_version_range, + permitted_output_types: Self::current_permitted_output_types(), }] } + + const fn current_permitted_output_types() -> &'static [OutputType] { + &[OutputType::Coinbase, OutputType::Standard, OutputType::Burn] + } } static EMISSION_DECAY: [u64; 5] = [22, 23, 24, 26, 27]; @@ -628,6 +647,11 @@ impl ConsensusConstantsBuilder { self } + pub fn with_permitted_output_types(mut self, permitted_output_types: &'static [OutputType]) -> Self { + self.consensus.permitted_output_types = permitted_output_types; + self + } + pub fn build(self) -> ConsensusConstants { self.consensus } diff --git a/base_layer/core/src/transactions/transaction_components/output_type.rs b/base_layer/core/src/transactions/transaction_components/output_type.rs index 3f74f3ba1b..a611c82268 100644 --- a/base_layer/core/src/transactions/transaction_components/output_type.rs +++ b/base_layer/core/src/transactions/transaction_components/output_type.rs @@ -57,6 +57,10 @@ impl OutputType { pub fn from_byte(value: u8) -> Option { FromPrimitive::from_u8(value) } + + pub const fn all() -> &'static [Self] { + &[OutputType::Standard, OutputType::Coinbase, OutputType::Burn] + } } impl Default for OutputType { @@ -107,5 +111,7 @@ mod tests { fn it_converts_from_byte_to_output_type() { assert_eq!(OutputType::from_byte(0), Some(OutputType::Standard)); assert_eq!(OutputType::from_byte(1), Some(OutputType::Coinbase)); + assert_eq!(OutputType::from_byte(2), Some(OutputType::Burn)); + assert_eq!(OutputType::from_byte(255), None); } } diff --git a/base_layer/core/src/validation/block_validators/async_validator.rs b/base_layer/core/src/validation/block_validators/async_validator.rs index 770a7846d7..4358997bb9 100644 --- a/base_layer/core/src/validation/block_validators/async_validator.rs +++ b/base_layer/core/src/validation/block_validators/async_validator.rs @@ -346,6 +346,7 @@ impl BlockValidator { .into() } + #[allow(clippy::too_many_lines)] fn start_output_validation( &self, header: &BlockHeader, @@ -372,11 +373,12 @@ impl BlockValidator { .map(|outputs| { let range_proof_prover = self.factories.range_proof.clone(); let db = self.db.inner().clone(); - let max_script_size = self.rules.consensus_constants(height).get_max_script_byte_size(); + let constants = self.rules.consensus_constants(height).clone(); task::spawn_blocking(move || { let db = db.db_read_access()?; let mut aggregate_sender_offset = PublicKey::default(); let mut commitment_sum = Commitment::default(); + let max_script_size = constants.get_max_script_byte_size(); let mut coinbase_index = None; debug!( target: LOG_TARGET, @@ -400,6 +402,7 @@ impl BlockValidator { aggregate_sender_offset = aggregate_sender_offset + &output.sender_offset_public_key; } + helpers::check_permitted_output_types(&constants, output)?; helpers::check_tari_script_byte_size(&output.script, max_script_size)?; output.verify_metadata_signature()?; helpers::check_not_duplicate_txo(&*db, output)?; diff --git a/base_layer/core/src/validation/block_validators/orphan.rs b/base_layer/core/src/validation/block_validators/orphan.rs index 63416a53d8..aa593de140 100644 --- a/base_layer/core/src/validation/block_validators/orphan.rs +++ b/base_layer/core/src/validation/block_validators/orphan.rs @@ -34,6 +34,7 @@ use crate::{ check_coinbase_output, check_kernel_lock_height, check_maturity, + check_permitted_output_types, check_sorting_and_duplicates, check_total_burned, }, @@ -86,7 +87,8 @@ impl OrphanValidation for OrphanBlockValidator { }; trace!(target: LOG_TARGET, "Validating {}", block_id); - check_block_weight(block, self.rules.consensus_constants(height))?; + let constants = self.rules.consensus_constants(height); + check_block_weight(block, constants)?; trace!(target: LOG_TARGET, "SV - Block weight is ok for {} ", &block_id); trace!( @@ -100,6 +102,10 @@ impl OrphanValidation for OrphanBlockValidator { "SV - No duplicate inputs / outputs for {} ", &block_id ); + for output in block.body.outputs() { + check_permitted_output_types(constants, output)?; + } + trace!(target: LOG_TARGET, "SV - Permitted output type ok for {} ", &block_id); check_total_burned(&block.body)?; trace!(target: LOG_TARGET, "SV - Burned outputs ok for {} ", &block_id); diff --git a/base_layer/core/src/validation/block_validators/test.rs b/base_layer/core/src/validation/block_validators/test.rs index 278dfdc1bd..0be1c4527b 100644 --- a/base_layer/core/src/validation/block_validators/test.rs +++ b/base_layer/core/src/validation/block_validators/test.rs @@ -295,7 +295,7 @@ mod body_only { mod orphan_validator { use super::*; - use crate::txn_schema; + use crate::{transactions::transaction_components::OutputType, txn_schema}; #[test] fn it_rejects_zero_conf_double_spends() { @@ -330,4 +330,29 @@ mod orphan_validator { let err = validator.validate(&unmined).unwrap_err(); assert!(matches!(err, ValidationError::UnsortedOrDuplicateInput)); } + + #[test] + fn it_rejects_unpermitted_output_types() { + let rules = ConsensusManager::builder(Network::LocalNet) + .add_consensus_constants( + ConsensusConstantsBuilder::new(Network::LocalNet) + .with_permitted_output_types(&[OutputType::Coinbase]) + .with_coinbase_lockheight(0) + .build(), + ) + .build(); + let mut blockchain = TestBlockchain::create(rules.clone()); + let validator = OrphanBlockValidator::new(rules, false, CryptoFactories::default()); + let (_, coinbase) = blockchain.append(block_spec!("1", parent: "GB")).unwrap(); + + let schema = txn_schema!(from: vec![coinbase], to: vec![201 * T]); + let (tx, _) = schema_to_transaction(&[schema]); + + let transactions = tx.into_iter().map(|b| Arc::try_unwrap(b).unwrap()).collect::>(); + + let (unmined, _) = blockchain.create_unmined_block(block_spec!("2", parent: "1", transactions: transactions)); + let err = validator.validate(&unmined).unwrap_err(); + unpack_enum!(ValidationError::OutputTypeNotPermitted { output_type } = err); + assert_eq!(output_type, OutputType::Standard); + } } diff --git a/base_layer/core/src/validation/error.rs b/base_layer/core/src/validation/error.rs index a96d71025e..6900b3bcde 100644 --- a/base_layer/core/src/validation/error.rs +++ b/base_layer/core/src/validation/error.rs @@ -128,6 +128,8 @@ pub enum ValidationError { }, #[error("Contains Invalid Burn: {0}")] InvalidBurnError(String), + #[error("Output type '{output_type}' is not permitted")] + OutputTypeNotPermitted { output_type: OutputType }, } // ChainStorageError has a ValidationError variant, so to prevent a cyclic dependency we use a string representation in diff --git a/base_layer/core/src/validation/helpers.rs b/base_layer/core/src/validation/helpers.rs index 853bb42623..90d2c89b90 100644 --- a/base_layer/core/src/validation/helpers.rs +++ b/base_layer/core/src/validation/helpers.rs @@ -460,8 +460,9 @@ pub fn check_input_is_utxo(db: &B, input: &TransactionInpu } /// This function checks: -/// 1. the byte size of TariScript does not exceed the maximum -/// 2. that the outputs do not already exist in the UTxO set. +/// 1. that the output type is permitted +/// 2. the byte size of TariScript does not exceed the maximum +/// 3. that the outputs do not already exist in the UTxO set. pub fn check_outputs( db: &B, constants: &ConsensusConstants, @@ -469,6 +470,7 @@ pub fn check_outputs( ) -> Result<(), ValidationError> { let max_script_size = constants.get_max_script_byte_size(); for output in body.outputs() { + check_permitted_output_types(constants, output)?; check_tari_script_byte_size(&output.script, max_script_size)?; check_not_duplicate_txo(db, output)?; } @@ -741,6 +743,22 @@ pub fn check_blockchain_version(constants: &ConsensusConstants, version: u16) -> } } +pub fn check_permitted_output_types( + constants: &ConsensusConstants, + output: &TransactionOutput, +) -> Result<(), ValidationError> { + if !constants + .permitted_output_types() + .contains(&output.features.output_type) + { + return Err(ValidationError::OutputTypeNotPermitted { + output_type: output.features.output_type, + }); + } + + Ok(()) +} + #[cfg(test)] mod test { use tari_test_utils::unpack_enum; diff --git a/base_layer/core/src/validation/transaction_validators.rs b/base_layer/core/src/validation/transaction_validators.rs index 34674a44ef..de9d308422 100644 --- a/base_layer/core/src/validation/transaction_validators.rs +++ b/base_layer/core/src/validation/transaction_validators.rs @@ -28,7 +28,7 @@ use crate::{ consensus::ConsensusConstants, transactions::{transaction_components::Transaction, CryptoFactories}, validation::{ - helpers::{check_inputs_are_utxos, check_outputs, check_total_burned}, + helpers::{check_inputs_are_utxos, check_outputs, check_permitted_output_types, check_total_burned}, MempoolTransactionValidation, ValidationError, }, @@ -105,7 +105,7 @@ impl TxConsensusValidator { ) -> Result<(), ValidationError> { // validate input version for input in tx.body().inputs() { - if !consensus_constants.input_version_range.contains(&input.version) { + if !consensus_constants.input_version_range().contains(&input.version) { let msg = format!( "Transaction input contains a version not allowed by consensus ({:?})", input.version @@ -117,12 +117,12 @@ impl TxConsensusValidator { // validate output version and output features version for output in tx.body().outputs() { let valid_output_version = consensus_constants - .output_version_range + .output_version_range() .outputs .contains(&output.version); let valid_features_version = consensus_constants - .output_version_range + .output_version_range() .features .contains(&output.features.version); @@ -141,11 +141,13 @@ impl TxConsensusValidator { ); return Err(ValidationError::ConsensusError(msg)); } + + check_permitted_output_types(consensus_constants, output)?; } // validate kernel version for kernel in tx.body().kernels() { - if !consensus_constants.kernel_version_range.contains(&kernel.version) { + if !consensus_constants.kernel_version_range().contains(&kernel.version) { let msg = format!( "Transaction kernel version is not allowed by consensus ({:?})", kernel.version