Skip to content

Commit

Permalink
feat: covenants audit (#5526)
Browse files Browse the repository at this point in the history
Description
---
- Added missing docs
- Expanded some test cases
- Added `FeaturesRangeProofType` and `MinimumValuePromise` to `pub enum
OutputField` with associated implementations and test cases as those
were missing.

Motivation and Context
---
Preparation for code audit

How Has This Been Tested?
---
Expanded unit tests for the additional output fields.

What process can a PR reviewer use to test or verify this change?
---
Review additions
Review docs

<!-- Checklist -->
<!-- 1. Is the title of your PR in the form that would make nice release
notes? The title, excluding the conventional commit
tag, will be included exactly as is in the CHANGELOG, so please think
about it carefully. -->


Breaking Changes
---

- [x] None
- [ ] Requires data directory on base node to be deleted
- [ ] Requires hard fork
- [ ] Other - Please specify

<!-- Does this include a breaking change? If so, include this line as a
footer -->
<!-- BREAKING CHANGE: Description what the user should do, e.g. delete a
database, resync the chain -->

---------

Co-authored-by: SW van Heerden <[email protected]>
  • Loading branch information
hansieodendaal and SWvheerden authored Jun 28, 2023
1 parent 70763dd commit dbb5975
Show file tree
Hide file tree
Showing 14 changed files with 161 additions and 29 deletions.
4 changes: 3 additions & 1 deletion base_layer/core/src/covenants/byte_codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ pub const FIELD_COVENANT: u8 = 0x03;
pub const FIELD_FEATURES: u8 = 0x04;
pub const FIELD_FEATURES_OUTPUT_TYPE: u8 = 0x05;
pub const FIELD_FEATURES_MATURITY: u8 = 0x06;
pub const FIELD_FEATURES_SIDE_CHAIN_FEATURES: u8 = 0x08;
pub const FIELD_FEATURES_SIDE_CHAIN_FEATURES: u8 = 0x07;
pub const FIELD_FEATURES_RANGE_PROOF_TYPE: u8 = 0x08;
pub const MINIMUM_VALUE_PROMISE: u8 = 0x09;

#[cfg(test)]
mod tests {
Expand Down
111 changes: 90 additions & 21 deletions base_layer/core/src/covenants/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ pub enum OutputField {
FeaturesOutputType = byte_codes::FIELD_FEATURES_OUTPUT_TYPE,
FeaturesMaturity = byte_codes::FIELD_FEATURES_MATURITY,
FeaturesSideChainFeatures = byte_codes::FIELD_FEATURES_SIDE_CHAIN_FEATURES,
FeaturesRangeProofType = byte_codes::FIELD_FEATURES_RANGE_PROOF_TYPE,
MinimumValuePromise = byte_codes::MINIMUM_VALUE_PROMISE,
}

impl OutputField {
Expand All @@ -70,6 +72,8 @@ impl OutputField {
FIELD_FEATURES_OUTPUT_TYPE => Ok(FeaturesOutputType),
FIELD_FEATURES_MATURITY => Ok(FeaturesMaturity),
FIELD_FEATURES_SIDE_CHAIN_FEATURES => Ok(FeaturesSideChainFeatures),
FIELD_FEATURES_RANGE_PROOF_TYPE => Ok(FeaturesRangeProofType),
MINIMUM_VALUE_PROMISE => Ok(MinimumValuePromise),

_ => Err(CovenantDecodeError::UnknownByteCode { code: byte }),
}
Expand All @@ -91,6 +95,8 @@ impl OutputField {
FeaturesOutputType => &output.features.output_type as &dyn Any,
FeaturesMaturity => &output.features.maturity as &dyn Any,
FeaturesSideChainFeatures => &output.features.sidechain_feature as &dyn Any,
FeaturesRangeProofType => &output.features.range_proof_type as &dyn Any,
MinimumValuePromise => &output.minimum_value_promise as &dyn Any,
};
val.downcast_ref::<T>()
}
Expand All @@ -109,6 +115,8 @@ impl OutputField {
FeaturesOutputType => BorshSerialize::serialize(&output.features.output_type, &mut writer),
FeaturesMaturity => BorshSerialize::serialize(&output.features.maturity, &mut writer),
FeaturesSideChainFeatures => BorshSerialize::serialize(&output.features.sidechain_feature, &mut writer),
FeaturesRangeProofType => BorshSerialize::serialize(&output.features.range_proof_type, &mut writer),
MinimumValuePromise => BorshSerialize::serialize(&output.minimum_value_promise, &mut writer),
}
.unwrap();
writer
Expand Down Expand Up @@ -147,6 +155,14 @@ impl OutputField {
.features()
.map(|features| features.sidechain_feature == output.features.sidechain_feature)
.unwrap_or(false),
FeaturesRangeProofType => input
.features()
.map(|features| features.range_proof_type == output.features.range_proof_type)
.unwrap_or(false),
MinimumValuePromise => input
.minimum_value_promise()
.map(|minimum_value_promise| *minimum_value_promise == output.minimum_value_promise)
.unwrap_or(false),
}
}

Expand Down Expand Up @@ -222,11 +238,20 @@ impl OutputField {
OutputField::FeaturesMaturity
}

#[allow(dead_code)]
#[allow(dead_code)]
pub fn features_sidechain_feature() -> Self {
OutputField::FeaturesSideChainFeatures
}

#[allow(dead_code)]
pub fn features_range_proof_type() -> Self {
OutputField::FeaturesRangeProofType
}

#[allow(dead_code)]
pub fn minimum_value_promise() -> Self {
OutputField::MinimumValuePromise
}
}

impl Display for OutputField {
Expand All @@ -242,6 +267,8 @@ impl Display for OutputField {
FeaturesOutputType => write!(f, "field::features_flags"),
FeaturesSideChainFeatures => write!(f, "field::features_sidechain_feature"),
FeaturesMaturity => write!(f, "field::features_maturity"),
FeaturesRangeProofType => write!(f, "field::features_range_proof_type"),
MinimumValuePromise => write!(f, "field::minimum_value_promise"),
}
}
}
Expand Down Expand Up @@ -344,7 +371,11 @@ mod test {

mod is_eq {
use super::*;
use crate::transactions::test_helpers::create_test_core_key_manager_with_memory_db;
use crate::transactions::{
tari_amount::MicroTari,
test_helpers::create_test_core_key_manager_with_memory_db,
transaction_components::RangeProofType,
};

#[tokio::test]
async fn it_returns_true_if_eq() {
Expand All @@ -366,20 +397,26 @@ mod test {
.remove(0);

assert!(OutputField::Commitment.is_eq(&output, &output.commitment).unwrap());
assert!(OutputField::Features.is_eq(&output, &output.features).unwrap());
assert!(OutputField::Script.is_eq(&output, &output.script).unwrap());
assert!(OutputField::Covenant.is_eq(&output, &output.covenant).unwrap());
assert!(OutputField::FeaturesMaturity
.is_eq(&output, &output.features.maturity)
assert!(OutputField::SenderOffsetPublicKey
.is_eq(&output, &output.sender_offset_public_key)
.unwrap());
assert!(OutputField::Covenant.is_eq(&output, &output.covenant).unwrap());
assert!(OutputField::Features.is_eq(&output, &output.features).unwrap());
assert!(OutputField::FeaturesOutputType
.is_eq(&output, &output.features.output_type)
.unwrap());
assert!(OutputField::FeaturesMaturity
.is_eq(&output, &output.features.maturity)
.unwrap());
assert!(OutputField::FeaturesSideChainFeatures
.is_eq(&output, output.features.sidechain_feature.as_ref().unwrap())
.unwrap());
assert!(OutputField::SenderOffsetPublicKey
.is_eq(&output, &output.sender_offset_public_key)
assert!(OutputField::FeaturesRangeProofType
.is_eq(&output, &output.features.range_proof_type)
.unwrap());
assert!(OutputField::MinimumValuePromise
.is_eq(&output, &output.minimum_value_promise)
.unwrap());
}

Expand All @@ -392,9 +429,13 @@ mod test {
UtxoTestParams {
features: OutputFeatures {
sidechain_feature: Some(side_chain_features),
range_proof_type: RangeProofType::RevealedValue,
output_type: OutputType::Burn,
..Default::default()
},
script: script![Drop Nop],
minimum_value_promise: MicroTari(123456),
value: MicroTari(123456),
..Default::default()
},
&key_manager,
Expand All @@ -403,19 +444,25 @@ mod test {
.remove(0);

assert!(!OutputField::Commitment.is_eq(&output, &Commitment::default()).unwrap());
assert!(!OutputField::Features
.is_eq(&output, &OutputFeatures::default())
.unwrap());
assert!(!OutputField::Script.is_eq(&output, &script![Nop Drop]).unwrap());
assert!(!OutputField::SenderOffsetPublicKey
.is_eq(&output, &PublicKey::default())
.unwrap());
assert!(!OutputField::Covenant
.is_eq(&output, &covenant!(and(identity(), identity())))
.unwrap());
assert!(!OutputField::Features
.is_eq(&output, &OutputFeatures::default())
.unwrap());
assert!(!OutputField::FeaturesMaturity.is_eq(&output, &123u64).unwrap());
assert!(!OutputField::FeaturesOutputType
.is_eq(&output, &OutputType::Coinbase)
.unwrap());
assert!(!OutputField::SenderOffsetPublicKey
.is_eq(&output, &PublicKey::default())
assert!(!OutputField::FeaturesRangeProofType
.is_eq(&output, &RangeProofType::BulletProofPlus)
.unwrap());
assert!(!OutputField::MinimumValuePromise
.is_eq(&output, &MicroTari::default())
.unwrap());
}
}
Expand Down Expand Up @@ -459,27 +506,31 @@ mod test {
}

assert!(OutputField::Commitment.is_eq_input(&input, &output));
assert!(OutputField::Features.is_eq_input(&input, &output));
assert!(OutputField::Script.is_eq_input(&input, &output));
assert!(OutputField::SenderOffsetPublicKey.is_eq_input(&input, &output));
assert!(OutputField::Covenant.is_eq_input(&input, &output));
assert!(OutputField::Features.is_eq_input(&input, &output));
assert!(OutputField::FeaturesMaturity.is_eq_input(&input, &output));
assert!(OutputField::FeaturesOutputType.is_eq_input(&input, &output));
assert!(OutputField::FeaturesSideChainFeatures.is_eq_input(&input, &output));
assert!(OutputField::SenderOffsetPublicKey.is_eq_input(&input, &output));
assert!(OutputField::FeaturesRangeProofType.is_eq_input(&input, &output));
assert!(OutputField::MinimumValuePromise.is_eq_input(&input, &output));
}
}

#[test]
fn display() {
let output_fields = [
OutputField::Commitment,
OutputField::Script,
OutputField::SenderOffsetPublicKey,
OutputField::Covenant,
OutputField::Features,
OutputField::FeaturesMaturity,
OutputField::FeaturesOutputType,
OutputField::FeaturesSideChainFeatures,
OutputField::FeaturesMaturity,
OutputField::SenderOffsetPublicKey,
OutputField::Script,
OutputField::Covenant,
OutputField::FeaturesRangeProofType,
OutputField::MinimumValuePromise,
];
output_fields.iter().for_each(|f| {
assert!(f.to_string().starts_with("field::"));
Expand All @@ -495,21 +546,28 @@ mod test {
use tari_crypto::hashing::DomainSeparation;

use super::*;
use crate::transactions::test_helpers::create_test_core_key_manager_with_memory_db;
use crate::transactions::{
tari_amount::MicroTari,
test_helpers::create_test_core_key_manager_with_memory_db,
transaction_components::RangeProofType,
};

#[tokio::test]
async fn it_constructs_challenge_using_consensus_encoding() {
let key_manager = create_test_core_key_manager_with_memory_db();
let features = OutputFeatures {
maturity: 42,
output_type: OutputType::Coinbase,
range_proof_type: RangeProofType::RevealedValue,
..Default::default()
};
let output = create_outputs(
1,
UtxoTestParams {
features,
script: script![Drop Nop],
minimum_value_promise: MicroTari(123456),
value: MicroTari(123456),
..Default::default()
},
&key_manager,
Expand All @@ -521,6 +579,7 @@ mod test {
fields.push(OutputField::Features);
fields.push(OutputField::Commitment);
fields.push(OutputField::Script);
fields.push(OutputField::MinimumValuePromise);
let hash = fields.construct_challenge_from(&output).finalize();
let hash = hash.to_vec();

Expand All @@ -530,6 +589,7 @@ mod test {
.chain(output.features.try_to_vec().unwrap())
.chain(output.commitment.try_to_vec().unwrap())
.chain(output.script.try_to_vec().unwrap())
.chain(output.minimum_value_promise.try_to_vec().unwrap())
.finalize()
.to_vec();
assert_eq!(hash, expected_hash);
Expand All @@ -538,19 +598,26 @@ mod test {

mod get_field_value_ref {
use super::*;
use crate::transactions::test_helpers::create_test_core_key_manager_with_memory_db;
use crate::transactions::{
tari_amount::MicroTari,
test_helpers::create_test_core_key_manager_with_memory_db,
transaction_components::RangeProofType,
};

#[tokio::test]
async fn it_retrieves_the_value_as_ref() {
let key_manager = create_test_core_key_manager_with_memory_db();
let features = OutputFeatures {
maturity: 42,
range_proof_type: RangeProofType::RevealedValue,
..Default::default()
};
let output = create_outputs(
1,
UtxoTestParams {
features: features.clone(),
minimum_value_promise: MicroTari(123456),
value: MicroTari(123456),
..Default::default()
},
&key_manager,
Expand All @@ -560,6 +627,8 @@ mod test {
.unwrap();
let r = OutputField::Features.get_field_value_ref::<OutputFeatures>(&output);
assert_eq!(*r.unwrap(), features);
let r = OutputField::MinimumValuePromise.get_field_value_ref::<MicroTari>(&output);
assert_eq!(*r.unwrap(), MicroTari(123456));
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions base_layer/core/src/covenants/filters/absolute_height.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,13 @@

use crate::covenants::{context::CovenantContext, error::CovenantError, filters::Filter, output_set::OutputSet};

/// Holding struct for the "absolute height" filter
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AbsoluteHeightFilter;

impl Filter for AbsoluteHeightFilter {
// The absolute height filter removes all outputs in the mutable output set if the current block height is less than
// the absolute height provided in the covenant context.
fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> {
let abs_height = context.next_arg()?.require_uint()?;
let current_height = context.block_height();
Expand Down
18 changes: 14 additions & 4 deletions base_layer/core/src/covenants/filters/and.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@

use crate::covenants::{context::CovenantContext, error::CovenantError, filters::Filter, output_set::OutputSet};

/// Holding struct for the "and" filter
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AndFilter;

impl Filter for AndFilter {
// The and filter removes all outputs in the mutable output set based on the next two filters in the covenant
// context - the filters are applied in order.
fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> {
let a = context.require_next_filter()?;
a.filter(context, output_set)?;
Expand All @@ -51,19 +54,26 @@ mod test {
#[tokio::test]
async fn it_filters_outputset_using_intersection() {
let key_manager = create_test_core_key_manager_with_memory_db();
let script = script!(Nop);
let covenant =
covenant!(and(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::script, @script(script))));
let script = script!(CheckHeight(101));
let covenant = covenant!(and(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::script, @script(script.clone()))));
let input = create_input(&key_manager).await;
let (mut context, outputs) = setup_filter_test(
&covenant,
&input,
0,
|outputs| {
// output satisfying maturity only
outputs[2].features.maturity = 42;
outputs[2].script = script!(CheckHeight(102));
// output satisfying maturity and script
outputs[5].features.maturity = 42;
outputs[5].script = script.clone();
// output satisfying maturity and script
outputs[7].features.maturity = 42;
// Does not have maturity = 42
outputs[7].script = script.clone();
// output satisfying script only
outputs[8].features.maturity = 123;
outputs[8].script = script.clone();
},
&key_manager,
)
Expand Down
3 changes: 3 additions & 0 deletions base_layer/core/src/covenants/filters/field_eq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ use crate::covenants::{
output_set::OutputSet,
};

/// Holding struct for the "fields equal" filter
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldEqFilter;

impl Filter for FieldEqFilter {
// Filters out all outputs that do not have the specified output field equal to the specified value based on the
// next two arguments in the covenant context.
fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> {
let field = context.next_arg()?.require_outputfield()?;
let arg = context.next_arg()?;
Expand Down
Loading

0 comments on commit dbb5975

Please sign in to comment.