Skip to content

Commit

Permalink
feat: limit open-ended vectors for covenants (#6497)
Browse files Browse the repository at this point in the history
Description
---
Limited open-ended vectors to guard against malicious network messages
in:
- covenants

Motivation and Context
---
This is a _defense-in-depth_ exercise.

How Has This Been Tested?
---
Existing unit tests pass.

What process can a PR reviewer use to test or verify this change?
---
Code review

<!-- 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 -->
  • Loading branch information
hansieodendaal authored Aug 26, 2024
1 parent f91cffa commit 7a1150d
Show file tree
Hide file tree
Showing 22 changed files with 98 additions and 66 deletions.
16 changes: 10 additions & 6 deletions base_layer/core/src/covenants/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ use crate::{
const MAX_COVENANT_ARG_SIZE: usize = 4096;
const MAX_BYTES_ARG_SIZE: usize = 4096;

pub(crate) type BytesArg = MaxSizeBytes<MAX_BYTES_ARG_SIZE>;

#[derive(Debug, Clone, PartialEq, Eq)]
/// Covenant arguments
pub enum CovenantArg {
Expand All @@ -60,7 +62,7 @@ pub enum CovenantArg {
Uint(u64),
OutputField(OutputField),
OutputFields(OutputFields),
Bytes(Vec<u8>),
Bytes(BytesArg),
}

impl CovenantArg {
Expand Down Expand Up @@ -117,8 +119,8 @@ impl CovenantArg {
Ok(CovenantArg::OutputFields(fields))
},
ARG_BYTES => {
let buf = MaxSizeBytes::<MAX_BYTES_ARG_SIZE>::deserialize(reader)?;
Ok(CovenantArg::Bytes(buf.into()))
let buf = BytesArg::deserialize(reader)?;
Ok(CovenantArg::Bytes(buf))
},

_ => Err(CovenantDecodeError::UnknownArgByteCode { code }),
Expand Down Expand Up @@ -224,7 +226,7 @@ impl CovenantArg {

require_x_impl!(require_outputfields, OutputFields, "outputfields");

require_x_impl!(require_bytes, Bytes, "bytes", Vec<u8>);
require_x_impl!(require_bytes, Bytes, "bytes", BytesArg);

require_x_impl!(require_uint, Uint, "u64", u64);
}
Expand Down Expand Up @@ -278,6 +280,8 @@ mod test {
}

mod write_to_and_read_from {
use std::convert::TryFrom;

use super::*;

fn test_case(argument: CovenantArg, mut data: &[u8]) {
Expand All @@ -295,11 +299,11 @@ mod test {
fn test() {
test_case(CovenantArg::Uint(2048), &[ARG_UINT, 0, 8, 0, 0, 0, 0, 0, 0][..]);
test_case(
CovenantArg::Covenant(covenant!(identity())),
CovenantArg::Covenant(covenant!(identity()).unwrap()),
&[ARG_COVENANT, 0x01, 0x20][..],
);
test_case(
CovenantArg::Bytes(vec![0x01, 0x02, 0xaa]),
CovenantArg::Bytes(BytesArg::try_from(vec![0x01, 0x02, 0xaa]).unwrap()),
&[ARG_BYTES, 0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0xaa][..],
);
test_case(
Expand Down
1 change: 1 addition & 0 deletions base_layer/core/src/covenants/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use crate::{

/// The covenant execution context provides a reference to the transaction input being verified, the tokenized covenant
/// and other relevant context e.g current block height
#[derive(Debug)]
pub struct CovenantContext<'a> {
input: &'a TransactionInput,
tokens: CovenantTokenCollection,
Expand Down
25 changes: 16 additions & 9 deletions base_layer/core/src/covenants/covenant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use std::{

use borsh::{BorshDeserialize, BorshSerialize};
use integer_encoding::{VarIntReader, VarIntWriter};
use tari_max_size::MaxSizeVec;

use super::decoder::CovenantDecodeError;
use crate::{
Expand All @@ -45,11 +46,13 @@ use crate::{

const MAX_COVENANT_BYTES: usize = 4096;

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) type CovenantTokens = MaxSizeVec<CovenantToken, 128>;

#[derive(Debug, Default, Clone, PartialEq, Eq)]
/// A covenant allows a UTXO to specify some restrictions on how it is spent in a future transaction.
/// See https://rfc.tari.com/RFC-0250_Covenants.html for details.
pub struct Covenant {
tokens: Vec<CovenantToken>,
tokens: CovenantTokens,
}

impl BorshSerialize for Covenant {
Expand Down Expand Up @@ -85,7 +88,9 @@ impl BorshDeserialize for Covenant {

impl Covenant {
pub fn new() -> Self {
Self { tokens: Vec::new() }
Self {
tokens: CovenantTokens::default(),
}
}

/// Produces a new `Covenant` instance, out of a byte buffer. It errors
Expand All @@ -110,7 +115,7 @@ impl Covenant {

/// Writes a `Covenant` instance byte to a writer.
pub(super) fn write_to<W: io::Write>(&self, writer: &mut W) -> Result<(), io::Error> {
CovenantTokenEncoder::new(self.tokens.as_slice()).write_to(writer)
CovenantTokenEncoder::new(&self.tokens).write_to(writer)
}

/// Gets the byte lenght of the underlying byte buffer
Expand Down Expand Up @@ -149,8 +154,8 @@ impl Covenant {
}

/// Adds a new `CovenantToken` to the current `tokens` vector field.
pub fn push_token(&mut self, token: CovenantToken) {
self.tokens.push(token);
pub fn push_token(&mut self, token: CovenantToken) -> Result<(), CovenantError> {
Ok(self.tokens.push(token)?)
}

#[cfg(test)]
Expand Down Expand Up @@ -197,7 +202,7 @@ mod test {
let key_manager = create_memory_db_key_manager().unwrap();
let outputs = create_outputs(10, UtxoTestParams::default(), &key_manager).await;
let input = create_input(&key_manager).await;
let covenant = covenant!();
let covenant = covenant!().unwrap();
let num_matching_outputs = covenant.execute(0, &input, &outputs).unwrap();
assert_eq!(num_matching_outputs, 10);
}
Expand All @@ -214,7 +219,8 @@ mod test {
let covenant = covenant!(fields_preserved(@fields(
@field::features_output_type,
@field::features_maturity))
);
)
.unwrap();
let num_matching_outputs = covenant.execute(0, &input, &outputs).unwrap();
assert_eq!(num_matching_outputs, 3);
}
Expand All @@ -231,7 +237,8 @@ mod test {
let covenant = covenant!(fields_preserved(@fields(
@field::features_output_type,
@field::features_maturity))
);
)
.unwrap();
let mut buf = Vec::new();
covenant.serialize(&mut buf).unwrap();
buf.extend_from_slice(&[1, 2, 3]);
Expand Down
1 change: 1 addition & 0 deletions base_layer/core/src/covenants/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ mod test {
@fields(@field::commitment),
@hash(hash),
))
.unwrap()
.write_to(&mut bytes)
.unwrap();
let mut buf = bytes.as_slice();
Expand Down
4 changes: 2 additions & 2 deletions base_layer/core/src/covenants/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ mod tests {

#[test]
fn it_encodes_tokens_correctly() {
let covenant = covenant!(and(identity(), or(identity())));
let covenant = covenant!(and(identity(), or(identity()))).unwrap();
let encoder = CovenantTokenEncoder::new(covenant.tokens());
let mut buf = Vec::<u8>::new();
encoder.write_to(&mut buf).unwrap();
Expand All @@ -85,7 +85,7 @@ mod tests {
#[test]
fn it_encodes_args_correctly() {
let dummy = FixedHash::zero();
let covenant = covenant!(field_eq(@field::features, @hash(dummy)));
let covenant = covenant!(field_eq(@field::features, @hash(dummy))).unwrap();
let encoder = CovenantTokenEncoder::new(covenant.tokens());
let mut buf = Vec::<u8>::new();
encoder.write_to(&mut buf).unwrap();
Expand Down
4 changes: 4 additions & 0 deletions base_layer/core/src/covenants/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use tari_max_size::MaxSizeVecError;

#[derive(Debug, thiserror::Error)]
pub enum CovenantError {
#[error("Reached the end of tokens but another token was expected")]
Expand All @@ -36,4 +38,6 @@ pub enum CovenantError {
RemainingTokens,
#[error("Invalid argument for filter {filter}: {details}")]
InvalidArgument { filter: &'static str, details: String },
#[error("Max sized vector error: {0}")]
MaxSizeVecError(#[from] MaxSizeVecError),
}
2 changes: 1 addition & 1 deletion base_layer/core/src/covenants/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ mod test {
.is_eq(&output, &PublicKey::default())
.unwrap());
assert!(!OutputField::Covenant
.is_eq(&output, &covenant!(and(identity(), identity())))
.is_eq(&output, &covenant!(and(identity(), identity())).unwrap())
.unwrap());
assert!(!OutputField::Features
.is_eq(&output, &OutputFeatures::default())
Expand Down
6 changes: 3 additions & 3 deletions base_layer/core/src/covenants/filters/absolute_height.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ mod test {
#[tokio::test]
async fn it_filters_all_out_if_height_not_reached() {
let key_manager = create_memory_db_key_manager().unwrap();
let covenant = covenant!(absolute_height(@uint(100)));
let covenant = covenant!(absolute_height(@uint(100))).unwrap();
let input = create_input(&key_manager).await;
let (mut context, outputs) = setup_filter_test(&covenant, &input, 42, |_| {}, &key_manager).await;

Expand All @@ -86,7 +86,7 @@ mod test {
#[tokio::test]
async fn it_filters_all_in_if_height_reached() {
let key_manager = create_memory_db_key_manager().unwrap();
let covenant = covenant!(absolute_height(@uint(100)));
let covenant = covenant!(absolute_height(@uint(100))).unwrap();
let input = create_input(&key_manager).await;
let (mut context, outputs) = setup_filter_test(&covenant, &input, 100, |_| {}, &key_manager).await;

Expand All @@ -99,7 +99,7 @@ mod test {
#[tokio::test]
async fn it_filters_all_in_if_height_exceeded() {
let key_manager = create_memory_db_key_manager().unwrap();
let covenant = covenant!(absolute_height(@uint(42)));
let covenant = covenant!(absolute_height(@uint(42))).unwrap();
let input = create_input(&key_manager).await;
let (mut context, outputs) = setup_filter_test(&covenant, &input, 100, |_| {}, &key_manager).await;

Expand Down
2 changes: 1 addition & 1 deletion base_layer/core/src/covenants/filters/and.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ mod test {
async fn it_filters_outputset_using_intersection() {
let key_manager = create_memory_db_key_manager().unwrap();
let script = script!(CheckHeight(101)).unwrap();
let covenant = covenant!(and(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::script, @script(script.clone()))));
let covenant = covenant!(and(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::script, @script(script.clone())))).unwrap();
let input = create_input(&key_manager).await;
let (mut context, outputs) = setup_filter_test(
&covenant,
Expand Down
19 changes: 11 additions & 8 deletions base_layer/core/src/covenants/filters/field_eq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ mod test {
#[tokio::test]
async fn it_filters_uint() {
let key_manager = create_memory_db_key_manager().unwrap();
let covenant = covenant!(field_eq(@field::features_maturity, @uint(42)));
let covenant = covenant!(field_eq(@field::features_maturity, @uint(42))).unwrap();
let input = create_input(&key_manager).await;
let mut context = create_context(&covenant, &input, 0);
// Remove `field_eq`
Expand All @@ -112,7 +112,8 @@ mod test {
let covenant = covenant!(field_eq(
@field::sender_offset_public_key,
@public_key(pk.clone())
));
))
.unwrap();
let input = create_input(&key_manager).await;
let mut context = create_context(&covenant, &input, 0);
// Remove `field_eq`
Expand All @@ -134,7 +135,8 @@ mod test {
let covenant = covenant!(field_eq(
@field::commitment,
@commitment(commitment.clone())
));
))
.unwrap();
let input = create_input(&key_manager).await;
let mut context = create_context(&covenant, &input, 0);
// Remove `field_eq`
Expand All @@ -156,7 +158,8 @@ mod test {
let covenant = covenant!(field_eq(
@field::script,
@script(script.clone())
));
))
.unwrap();
let input = create_input(&key_manager).await;
let mut context = create_context(&covenant, &input, 0);
// Remove `field_eq`
Expand All @@ -174,8 +177,8 @@ mod test {
#[tokio::test]
async fn it_filters_covenant() {
let key_manager = create_memory_db_key_manager().unwrap();
let next_cov = covenant!(and(identity(), or(field_eq(@field::features_maturity, @uint(42)))));
let covenant = covenant!(field_eq(@field::covenant, @covenant(next_cov.clone())));
let next_cov = covenant!(and(identity(), or(field_eq(@field::features_maturity, @uint(42))))).unwrap();
let covenant = covenant!(field_eq(@field::covenant, @covenant(next_cov.clone()))).unwrap();
let input = create_input(&key_manager).await;
let mut context = create_context(&covenant, &input, 0);
// Remove `field_eq`
Expand All @@ -193,7 +196,7 @@ mod test {
#[tokio::test]
async fn it_filters_output_type() {
let key_manager = create_memory_db_key_manager().unwrap();
let covenant = covenant!(field_eq(@field::features_output_type, @output_type(Coinbase)));
let covenant = covenant!(field_eq(@field::features_output_type, @output_type(Coinbase))).unwrap();
let input = create_input(&key_manager).await;
let mut context = create_context(&covenant, &input, 0);
// Remove `field_eq`
Expand All @@ -211,7 +214,7 @@ mod test {
#[tokio::test]
async fn it_errors_if_field_has_an_incorrect_type() {
let key_manager = create_memory_db_key_manager().unwrap();
let covenant = covenant!(field_eq(@field::features, @uint(42)));
let covenant = covenant!(field_eq(@field::features, @uint(42))).unwrap();
let input = create_input(&key_manager).await;
let mut context = create_context(&covenant, &input, 0);
// Remove `field_eq`
Expand Down
2 changes: 1 addition & 1 deletion base_layer/core/src/covenants/filters/fields_hashed_eq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ mod test {
let mut hasher = Blake2b::<U32>::new();
BaseLayerCovenantsDomain::add_domain_separation_tag(&mut hasher, COVENANTS_FIELD_HASHER_LABEL);
let hash = hasher.chain(borsh::to_vec(&features).unwrap()).finalize();
let covenant = covenant!(fields_hashed_eq(@fields(@field::features), @hash(hash.into())));
let covenant = covenant!(fields_hashed_eq(@fields(@field::features), @hash(hash.into()))).unwrap();
let input = create_input(&key_manager).await;
let (mut context, outputs) = setup_filter_test(
&covenant,
Expand Down
3 changes: 2 additions & 1 deletion base_layer/core/src/covenants/filters/fields_preserved.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ mod test {

#[tokio::test]
async fn it_filters_outputs_that_match_input_fields() {
let covenant = covenant!(fields_preserved(@fields(@field::features_maturity, @field::features_output_type)));
let covenant =
covenant!(fields_preserved(@fields(@field::features_maturity, @field::features_output_type))).unwrap();
let key_manager = create_memory_db_key_manager().unwrap();
let mut input = create_input(&key_manager).await;
input.set_maturity(42).unwrap();
Expand Down
2 changes: 1 addition & 1 deletion base_layer/core/src/covenants/filters/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ mod tests {
#[tokio::test]
async fn it_returns_the_outputset_unchanged() {
let key_manager = create_memory_db_key_manager().unwrap();
let covenant = covenant!(identity());
let covenant = covenant!(identity()).unwrap();
let input = create_input(&key_manager).await;
let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |_| {}, &key_manager).await;
let mut output_set = OutputSet::new(&outputs);
Expand Down
2 changes: 1 addition & 1 deletion base_layer/core/src/covenants/filters/not.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ mod test {
async fn it_filters_compliment_of_filter() {
let key_manager = create_memory_db_key_manager().unwrap();
let script = script!(CheckHeight(100)).unwrap();
let covenant = covenant!(not(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::script, @script(script.clone())))));
let covenant = covenant!(not(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::script, @script(script.clone()))))).unwrap();
let input = create_input(&key_manager).await;
let (mut context, outputs) = setup_filter_test(
&covenant,
Expand Down
2 changes: 1 addition & 1 deletion base_layer/core/src/covenants/filters/or.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ mod test {
async fn it_filters_outputset_using_union() {
let key_manager = create_memory_db_key_manager().unwrap();
let script = script!(CheckHeight(100)).unwrap();
let covenant = covenant!(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::script, @script(script.clone()))));
let covenant = covenant!(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::script, @script(script.clone())))).unwrap();
let input = create_input(&key_manager).await;
let (mut context, outputs) = setup_filter_test(
&covenant,
Expand Down
2 changes: 1 addition & 1 deletion base_layer/core/src/covenants/filters/output_hash_eq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ mod test {
let output_hash = output.hash();
let mut hash = [0u8; 32];
hash.copy_from_slice(output_hash.as_slice());
let covenant = covenant!(output_hash_eq(@hash(hash.into())));
let covenant = covenant!(output_hash_eq(@hash(hash.into()))).unwrap();
let input = create_input(&key_manager).await;
let (mut context, outputs) = setup_filter_test(
&covenant,
Expand Down
2 changes: 1 addition & 1 deletion base_layer/core/src/covenants/filters/xor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ mod test {
async fn it_filters_outputset_using_symmetric_difference() {
let key_manager = create_memory_db_key_manager().unwrap();
let script = script!(CheckHeight(100)).unwrap();
let covenant = covenant!(and(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::script, @script(script.clone()))));
let covenant = covenant!(and(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::script, @script(script.clone())))).unwrap();
let input = create_input(&key_manager).await;
let (mut context, outputs) = setup_filter_test(
&covenant,
Expand Down
Loading

0 comments on commit 7a1150d

Please sign in to comment.