From 19278f171603ef352e35ae50506f7ba6aa554b56 Mon Sep 17 00:00:00 2001 From: Stanimal Date: Mon, 13 Dec 2021 17:42:32 +0400 Subject: [PATCH] feat: mimblewimble covenants implementation --- .../tests/blockchain_database.rs | 2 +- .../core/src/consensus/consensus_encoding.rs | 166 ++++++++- base_layer/core/src/consensus/mod.rs | 2 +- base_layer/core/src/covenants/arguments.rs | 310 +++++++++++++++++ base_layer/core/src/covenants/byte_codes.rs | 65 ++++ base_layer/core/src/covenants/context.rs | 82 +++++ base_layer/core/src/covenants/covenant.rs | 160 +++++++++ base_layer/core/src/covenants/decoder.rs | 167 +++++++++ base_layer/core/src/covenants/encoder.rs | 65 ++++ base_layer/core/src/covenants/error.rs | 45 +++ base_layer/core/src/covenants/fields.rs | 329 ++++++++++++++++++ .../src/covenants/filters/absolute_height.rs | 104 ++++++ base_layer/core/src/covenants/filters/and.rs | 69 ++++ .../core/src/covenants/filters/field_eq.rs | 218 ++++++++++++ .../src/covenants/filters/fields_hashed_eq.rs | 97 ++++++ .../src/covenants/filters/fields_preserved.rs | 73 ++++ .../core/src/covenants/filters/filter.rs | 165 +++++++++ .../core/src/covenants/filters/identity.rs | 32 ++ base_layer/core/src/covenants/filters/mod.rs | 49 +++ base_layer/core/src/covenants/filters/not.rs | 64 ++++ base_layer/core/src/covenants/filters/or.rs | 69 ++++ .../src/covenants/filters/output_hash_eq.rs | 68 ++++ base_layer/core/src/covenants/filters/test.rs | 47 +++ base_layer/core/src/covenants/filters/xor.rs | 69 ++++ base_layer/core/src/covenants/macros.rs | 228 ++++++++++++ base_layer/core/src/covenants/mod.rs | 52 +++ base_layer/core/src/covenants/output_set.rs | 172 +++++++++ base_layer/core/src/covenants/test.rs | 52 +++ base_layer/core/src/covenants/token.rs | 220 ++++++++++++ base_layer/core/src/lib.rs | 3 +- .../core/src/transactions/aggregated_body.rs | 7 +- .../core/src/transactions/test_helpers.rs | 5 +- .../transaction/output_features.rs | 15 +- .../transaction/unblinded_output.rs | 5 +- .../transaction_initializer.rs | 11 +- base_layer/core/src/validation/helpers.rs | 10 +- .../src/output_manager_service/service.rs | 25 +- .../tests/output_manager_service/service.rs | 5 +- 38 files changed, 3260 insertions(+), 67 deletions(-) create mode 100644 base_layer/core/src/covenants/arguments.rs create mode 100644 base_layer/core/src/covenants/byte_codes.rs create mode 100644 base_layer/core/src/covenants/context.rs create mode 100644 base_layer/core/src/covenants/covenant.rs create mode 100644 base_layer/core/src/covenants/decoder.rs create mode 100644 base_layer/core/src/covenants/encoder.rs create mode 100644 base_layer/core/src/covenants/error.rs create mode 100644 base_layer/core/src/covenants/fields.rs create mode 100644 base_layer/core/src/covenants/filters/absolute_height.rs create mode 100644 base_layer/core/src/covenants/filters/and.rs create mode 100644 base_layer/core/src/covenants/filters/field_eq.rs create mode 100644 base_layer/core/src/covenants/filters/fields_hashed_eq.rs create mode 100644 base_layer/core/src/covenants/filters/fields_preserved.rs create mode 100644 base_layer/core/src/covenants/filters/filter.rs create mode 100644 base_layer/core/src/covenants/filters/identity.rs create mode 100644 base_layer/core/src/covenants/filters/mod.rs create mode 100644 base_layer/core/src/covenants/filters/not.rs create mode 100644 base_layer/core/src/covenants/filters/or.rs create mode 100644 base_layer/core/src/covenants/filters/output_hash_eq.rs create mode 100644 base_layer/core/src/covenants/filters/test.rs create mode 100644 base_layer/core/src/covenants/filters/xor.rs create mode 100644 base_layer/core/src/covenants/macros.rs create mode 100644 base_layer/core/src/covenants/mod.rs create mode 100644 base_layer/core/src/covenants/output_set.rs create mode 100644 base_layer/core/src/covenants/test.rs create mode 100644 base_layer/core/src/covenants/token.rs diff --git a/base_layer/core/src/chain_storage/tests/blockchain_database.rs b/base_layer/core/src/chain_storage/tests/blockchain_database.rs index b663af15ad..5045a91895 100644 --- a/base_layer/core/src/chain_storage/tests/blockchain_database.rs +++ b/base_layer/core/src/chain_storage/tests/blockchain_database.rs @@ -32,7 +32,7 @@ use crate::{ chain_storage::{BlockchainDatabase, ChainStorageError}, proof_of_work::{AchievedTargetDifficulty, Difficulty, PowAlgorithm}, test_helpers::{ - blockchain::{create_new_blockchain, create_store_with_consensus_and_validators, TempDatabase}, + blockchain::{create_new_blockchain, TempDatabase}, create_block, BlockSpec, }, diff --git a/base_layer/core/src/consensus/consensus_encoding.rs b/base_layer/core/src/consensus/consensus_encoding.rs index 6094d2c3c4..26dd2b2b68 100644 --- a/base_layer/core/src/consensus/consensus_encoding.rs +++ b/base_layer/core/src/consensus/consensus_encoding.rs @@ -24,8 +24,8 @@ use std::io; /// Abstracts the ability of a type to canonically encode itself for the purposes of consensus pub trait ConsensusEncoding { - /// Encode to the given writer returning the number of bytes writter. - /// If writing to this Writer is infallible, this implementation must always succeed. + /// Encode to the given writer returning the number of bytes written. + /// If writing to this Writer is infallible, this implementation MUST always succeed. fn consensus_encode(&self, writer: &mut W) -> Result; } @@ -41,33 +41,81 @@ pub trait ConsensusDecoding: Sized { fn consensus_decode(reader: &mut R) -> Result; } -pub struct ConsensusEncodingWrapper<'a, T> { - inner: &'a T, +pub trait ToConsensusBytes { + fn to_consensus_bytes(&self) -> Vec; } -impl<'a, T> ConsensusEncodingWrapper<'a, T> { - pub fn wrap(inner: &'a T) -> Self { - Self { inner } +impl ToConsensusBytes for T { + fn to_consensus_bytes(&self) -> Vec { + let mut buf = Vec::with_capacity(self.consensus_encode_exact_size()); + // Vec's write impl is infallible, as per the ConsensusEncoding contract, consensus_encode is infallible + self.consensus_encode(&mut buf).expect("unreachable panic"); + buf } } -// TODO: move traits and implement consensus encoding for TariScript -// for now, this wrapper will do that job -mod tariscript_impl { +impl ConsensusEncoding for Vec { + fn consensus_encode(&self, writer: &mut W) -> Result { + writer.write(self) + } +} + +impl ConsensusEncodingSized for Vec { + fn consensus_encode_exact_size(&self) -> usize { + self.len() + } +} + +macro_rules! consensus_encoding_varint_impl { + ($ty:ty) => { + impl ConsensusEncoding for $ty { + fn consensus_encode(&self, writer: &mut W) -> Result { + use integer_encoding::VarIntWriter; + let bytes_written = writer.write_varint(*self)?; + Ok(bytes_written) + } + } + + impl ConsensusDecoding for $ty { + fn consensus_decode(reader: &mut R) -> Result { + use integer_encoding::VarIntReader; + let value = reader.read_varint()?; + Ok(value) + } + } + + impl ConsensusEncodingSized for $ty { + fn consensus_encode_exact_size(&self) -> usize { + use integer_encoding::VarInt; + self.required_space() + } + } + }; +} + +consensus_encoding_varint_impl!(u8); +consensus_encoding_varint_impl!(u64); + +// Keep separate the dependencies of the impls that may in future be implemented in tari crypto +mod impls { + use std::io::Read; + + use tari_common_types::types::{Commitment, PrivateKey, PublicKey, Signature}; use tari_crypto::script::TariScript; + use tari_utilities::ByteArray; use super::*; use crate::common::byte_counter::ByteCounter; - impl<'a> ConsensusEncoding for ConsensusEncodingWrapper<'a, TariScript> { + //---------------------------------- TariScript --------------------------------------------// + + impl ConsensusEncoding for TariScript { fn consensus_encode(&self, writer: &mut W) -> Result { - let bytes = self.inner.as_bytes(); - writer.write_all(&bytes)?; - Ok(bytes.len()) + self.as_bytes().consensus_encode(writer) } } - impl<'a> ConsensusEncodingSized for ConsensusEncodingWrapper<'a, TariScript> { + impl ConsensusEncodingSized for TariScript { fn consensus_encode_exact_size(&self) -> usize { let mut counter = ByteCounter::new(); // TODO: consensus_encode_exact_size must be cheap to run @@ -76,4 +124,92 @@ mod tariscript_impl { counter.get() } } + + //---------------------------------- PublicKey --------------------------------------------// + + impl ConsensusEncoding for PublicKey { + fn consensus_encode(&self, writer: &mut W) -> Result { + writer.write(self.as_bytes()) + } + } + + impl ConsensusEncodingSized for PublicKey { + fn consensus_encode_exact_size(&self) -> usize { + let mut counter = ByteCounter::new(); + // TODO: consensus_encode_exact_size must be cheap to run + // unreachable panic: ByteCounter is infallible + self.consensus_encode(&mut counter).expect("unreachable"); + counter.get() + } + } + + impl ConsensusDecoding for PublicKey { + fn consensus_decode(reader: &mut R) -> Result { + let mut buf = [0u8; 32]; + reader.read_exact(&mut buf)?; + let pk = PublicKey::from_bytes(&buf[..]).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + Ok(pk) + } + } + + //---------------------------------- Commitment --------------------------------------------// + + impl ConsensusEncoding for Commitment { + fn consensus_encode(&self, writer: &mut W) -> Result { + let buf = self.as_bytes(); + let len = buf.len(); + writer.write_all(buf)?; + Ok(len) + } + } + + impl ConsensusEncodingSized for Commitment { + fn consensus_encode_exact_size(&self) -> usize { + 32 + } + } + + impl ConsensusDecoding for Commitment { + fn consensus_decode(reader: &mut R) -> Result { + let mut buf = [0u8; 32]; + reader.read_exact(&mut buf)?; + let commitment = + Commitment::from_bytes(&buf[..]).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + Ok(commitment) + } + } + + //---------------------------------- Signature --------------------------------------------// + + impl ConsensusEncoding for Signature { + fn consensus_encode(&self, writer: &mut W) -> Result { + let pub_nonce = self.get_public_nonce().as_bytes(); + let mut written = pub_nonce.len(); + writer.write_all(pub_nonce)?; + let sig = self.get_signature().as_bytes(); + written += sig.len(); + writer.write_all(sig)?; + Ok(written) + } + } + + impl ConsensusEncodingSized for Signature { + fn consensus_encode_exact_size(&self) -> usize { + 96 + } + } + + impl ConsensusDecoding for Signature { + fn consensus_decode(reader: &mut R) -> Result { + let mut buf = [0u8; 32]; + reader.read_exact(&mut buf)?; + let pub_nonce = + PublicKey::from_bytes(&buf[..]).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + let mut buf = [0u8; 64]; + reader.read_exact(&mut buf)?; + let sig = + PrivateKey::from_bytes(&buf[..]).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + Ok(Signature::new(pub_nonce, sig)) + } + } } diff --git a/base_layer/core/src/consensus/mod.rs b/base_layer/core/src/consensus/mod.rs index e1a2e95e84..dbed7dca39 100644 --- a/base_layer/core/src/consensus/mod.rs +++ b/base_layer/core/src/consensus/mod.rs @@ -30,7 +30,7 @@ mod consensus_manager; pub use consensus_manager::{ConsensusManager, ConsensusManagerBuilder, ConsensusManagerError}; mod consensus_encoding; -pub use consensus_encoding::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, ConsensusEncodingWrapper}; +pub use consensus_encoding::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, ToConsensusBytes}; mod network; pub use network::NetworkConsensus; diff --git a/base_layer/core/src/covenants/arguments.rs b/base_layer/core/src/covenants/arguments.rs new file mode 100644 index 0000000000..fd664dd5d3 --- /dev/null +++ b/base_layer/core/src/covenants/arguments.rs @@ -0,0 +1,310 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 std::{ + fmt::{Display, Formatter}, + io, +}; + +use integer_encoding::{VarIntReader, VarIntWriter}; +use tari_common_types::types::{Commitment, PublicKey}; +use tari_crypto::script::TariScript; +use tari_utilities::hex::{to_hex, Hex}; + +use crate::{ + consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}, + covenants::{ + byte_codes, + covenant::Covenant, + decoder::{CovenantDecodeError, CovenentReadExt}, + encoder::CovenentWriteExt, + error::CovenantError, + fields::{OutputField, OutputFields}, + }, +}; + +const MAX_TARISCRIPT_ARG_SIZE: usize = 4096; +const MAX_COVENANT_ARG_SIZE: usize = 4096; +const MAX_BYTES_ARG_SIZE: usize = 4096; + +pub type Hash = [u8; 32]; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CovenantArg { + Hash(Hash), + PublicKey(PublicKey), + Commitment(Commitment), + TariScript(TariScript), + Covenant(Covenant), + Uint(u64), + OutputField(OutputField), + OutputFields(OutputFields), + Bytes(Vec), +} + +impl CovenantArg { + pub fn is_valid_code(code: u8) -> bool { + byte_codes::is_valid_arg_code(code) + } + + pub fn read_from(reader: &mut R, code: u8) -> Result { + use byte_codes::*; + match code { + ARG_HASH => { + let mut hash = [0u8; 32]; + reader.read_exact(&mut hash)?; + Ok(CovenantArg::Hash(hash)) + }, + ARG_PUBLIC_KEY => { + let pk = PublicKey::consensus_decode(reader)?; + Ok(CovenantArg::PublicKey(pk)) + }, + ARG_COMMITMENT => Ok(CovenantArg::Commitment(Commitment::consensus_decode(reader)?)), + ARG_TARI_SCRIPT => { + let buf = reader.read_variable_length_bytes(MAX_TARISCRIPT_ARG_SIZE)?; + let script = TariScript::from_bytes(&buf[..])?; + Ok(CovenantArg::TariScript(script)) + }, + ARG_COVENANT => { + let buf = reader.read_variable_length_bytes(MAX_COVENANT_ARG_SIZE)?; + let covenant = Covenant::consensus_decode(&mut buf.as_slice())?; + Ok(CovenantArg::Covenant(covenant)) + }, + ARG_UINT => { + let v = reader.read_varint::()?; + Ok(CovenantArg::Uint(v)) + }, + ARG_OUTPUT_FIELD => { + let v = reader + .read_next_byte_code()? + .ok_or(CovenantDecodeError::UnexpectedEof { + expected: "Output field byte code", + })?; + let field = OutputField::from_byte(v)?; + Ok(CovenantArg::OutputField(field)) + }, + ARG_OUTPUT_FIELDS => { + // Each field code is a byte + let fields = OutputFields::read_from(reader)?; + Ok(CovenantArg::OutputFields(fields)) + }, + ARG_BYTES => { + let buf = reader.read_variable_length_bytes(MAX_BYTES_ARG_SIZE)?; + Ok(CovenantArg::Bytes(buf)) + }, + + _ => Err(CovenantDecodeError::UnknownArgByteCode { code }), + } + } + + pub fn write_to(&self, writer: &mut W) -> Result { + use byte_codes::*; + use CovenantArg::*; + + let mut written = 0; + match self { + Hash(hash) => { + written += writer.write_u8_fixed(ARG_HASH)?; + written += hash.len(); + writer.write_all(&hash[..])?; + }, + PublicKey(pk) => { + written += writer.write_u8_fixed(ARG_PUBLIC_KEY)?; + written += pk.consensus_encode(writer)?; + }, + Commitment(commitment) => { + written += writer.write_u8_fixed(ARG_COMMITMENT)?; + written += commitment.consensus_encode(writer)?; + }, + TariScript(script) => { + written += writer.write_u8_fixed(ARG_TARI_SCRIPT)?; + written += writer.write_variable_length_bytes(&script.as_bytes())?; + }, + Covenant(covenant) => { + written += writer.write_u8_fixed(ARG_COVENANT)?; + let len = covenant.consensus_encode_exact_size(); + written += writer.write_varint(len)?; + written += covenant.write_to(writer)?; + }, + Uint(int) => { + written += writer.write_u8_fixed(ARG_UINT)?; + written += writer.write_varint(*int)?; + }, + OutputField(field) => { + written += writer.write_u8_fixed(ARG_OUTPUT_FIELD)?; + written += writer.write_varint(field.as_byte())?; + }, + OutputFields(fields) => { + written += writer.write_u8_fixed(ARG_OUTPUT_FIELDS)?; + written += fields.write_to(writer)?; + }, + Bytes(bytes) => { + written += writer.write_u8_fixed(ARG_BYTES)?; + written += writer.write_variable_length_bytes(bytes)?; + }, + } + + Ok(written) + } +} + +macro_rules! require_x_impl { + ($name:ident, $output:ident, $expected: expr) => { + #[allow(dead_code)] + pub(super) fn $name(self) -> Result<$output, CovenantError> { + match self { + CovenantArg::$output(obj) => Ok(obj), + got => Err(CovenantError::UnexpectedArgument { + expected: $expected, + got: got.to_string(), + }), + } + } + }; +} + +impl CovenantArg { + require_x_impl!(require_hash, Hash, "hash"); + + require_x_impl!(require_publickey, PublicKey, "publickey"); + + require_x_impl!(require_commitment, Commitment, "commitment"); + + require_x_impl!(require_tariscript, TariScript, "script"); + + require_x_impl!(require_covenant, Covenant, "covenant"); + + require_x_impl!(require_outputfield, OutputField, "outputfield"); + + require_x_impl!(require_outputfields, OutputFields, "outputfields"); + + pub fn require_bytes(self) -> Result, CovenantError> { + match self { + CovenantArg::Bytes(val) => Ok(val), + got => Err(CovenantError::UnexpectedArgument { + expected: "bytes", + got: got.to_string(), + }), + } + } + + pub fn require_uint(self) -> Result { + match self { + CovenantArg::Uint(val) => Ok(val), + got => Err(CovenantError::UnexpectedArgument { + expected: "uint", + got: got.to_string(), + }), + } + } +} + +impl Display for CovenantArg { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + use CovenantArg::*; + match self { + Hash(hash) => write!(f, "Hash({})", to_hex(&hash[..])), + PublicKey(public_key) => write!(f, "PublicKey({})", public_key.to_hex()), + Commitment(commitment) => write!(f, "Commitment({})", commitment.to_hex()), + TariScript(_) => write!(f, "TariScript(...)"), + Covenant(_) => write!(f, "Covenant(...)"), + Uint(v) => write!(f, "Uint({})", v), + OutputField(field) => write!(f, "OutputField({})", field.as_byte()), + OutputFields(fields) => write!(f, "OutputFields({} field(s))", fields.len()), + Bytes(bytes) => write!(f, "Bytes({} byte(s))", bytes.len()), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + mod require_x_impl { + use super::*; + + #[test] + fn test() { + // This is mostly to remove unused function warnings + let arg = CovenantArg::Uint(123); + arg.clone().require_bytes().unwrap_err(); + let v = arg.clone().require_uint().unwrap(); + assert_eq!(v, 123); + arg.clone().require_hash().unwrap_err(); + arg.clone().require_outputfield().unwrap_err(); + arg.clone().require_covenant().unwrap_err(); + arg.clone().require_commitment().unwrap_err(); + arg.clone().require_outputfields().unwrap_err(); + arg.clone().require_publickey().unwrap_err(); + arg.require_tariscript().unwrap_err(); + } + } + + mod write_to { + use tari_common_types::types::Commitment; + use tari_crypto::script; + use tari_utilities::hex::from_hex; + + use super::*; + use crate::{covenant, covenants::byte_codes::*}; + + fn test_case(arg: CovenantArg, expected: &[u8]) { + let mut buf = Vec::new(); + arg.write_to(&mut buf).unwrap(); + assert_eq!(buf, expected); + } + + #[test] + fn test() { + test_case(CovenantArg::Uint(2048), &[ARG_UINT, 0x80, 0x10][..]); + test_case( + CovenantArg::Covenant(covenant!(identity())), + &[ARG_COVENANT, 0x01, 0x20][..], + ); + test_case( + CovenantArg::Bytes(vec![0x01, 0x02, 0xaa]), + &[ARG_BYTES, 0x03, 0x01, 0x02, 0xaa][..], + ); + test_case( + CovenantArg::Commitment(Commitment::default()), + &from_hex("030000000000000000000000000000000000000000000000000000000000000000").unwrap(), + ); + test_case( + CovenantArg::PublicKey(PublicKey::default()), + &from_hex("020000000000000000000000000000000000000000000000000000000000000000").unwrap(), + ); + test_case( + CovenantArg::Hash([0u8; 32]), + &from_hex("010000000000000000000000000000000000000000000000000000000000000000").unwrap(), + ); + test_case(CovenantArg::TariScript(script!(Nop)), &[ARG_TARI_SCRIPT, 0x01, 0x73]); + test_case(CovenantArg::OutputField(OutputField::Covenant), &[ + ARG_OUTPUT_FIELD, + FIELD_COVENANT, + ]); + test_case( + CovenantArg::OutputFields(OutputFields::from(vec![OutputField::Features, OutputField::Commitment])), + &[ARG_OUTPUT_FIELDS, 0x02, FIELD_FEATURES, FIELD_COMMITMENT], + ); + } + } +} diff --git a/base_layer/core/src/covenants/byte_codes.rs b/base_layer/core/src/covenants/byte_codes.rs new file mode 100644 index 0000000000..03fa068433 --- /dev/null +++ b/base_layer/core/src/covenants/byte_codes.rs @@ -0,0 +1,65 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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. + +//---------------------------------- ARG byte codes --------------------------------------------// +pub(super) fn is_valid_arg_code(code: u8) -> bool { + (0x01..=0x0a).contains(&code) +} +pub const ARG_HASH: u8 = 0x01; +pub const ARG_PUBLIC_KEY: u8 = 0x02; +pub const ARG_COMMITMENT: u8 = 0x03; +pub const ARG_TARI_SCRIPT: u8 = 0x04; +pub const ARG_COVENANT: u8 = 0x05; +pub const ARG_UINT: u8 = 0x06; +pub const ARG_OUTPUT_FIELD: u8 = 0x07; +pub const ARG_OUTPUT_FIELDS: u8 = 0x08; +pub const ARG_BYTES: u8 = 0x09; + +//---------------------------------- FILTER byte codes --------------------------------------------// + +pub(super) fn is_valid_filter_code(code: u8) -> bool { + (0x20..=0x24).contains(&code) || (0x30..=0x34).contains(&code) +} + +pub const FILTER_IDENTITY: u8 = 0x20; +pub const FILTER_AND: u8 = 0x21; +pub const FILTER_OR: u8 = 0x22; +pub const FILTER_XOR: u8 = 0x23; +pub const FILTER_NOT: u8 = 0x24; + +pub const FILTER_OUTPUT_HASH_EQ: u8 = 0x30; +pub const FILTER_FIELDS_PRESERVED: u8 = 0x31; +pub const FILTER_FIELDS_HASHED_EQ: u8 = 0x32; +pub const FILTER_FIELD_EQ: u8 = 0x33; +pub const FILTER_ABSOLUTE_HEIGHT: u8 = 0x34; + +//---------------------------------- FIELD byte codes --------------------------------------------// +pub const FIELD_COMMITMENT: u8 = 0x00; +pub const FIELD_SCRIPT: u8 = 0x01; +pub const FIELD_SENDER_OFFSET_PUBLIC_KEY: u8 = 0x02; +pub const FIELD_COVENANT: u8 = 0x03; +pub const FIELD_FEATURES: u8 = 0x04; +pub const FIELD_FEATURES_FLAGS: u8 = 0x05; +pub const FIELD_FEATURES_MATURITY: u8 = 0x06; +pub const FIELD_FEATURES_UNIQUE_ID: u8 = 0x07; +pub const FIELD_FEATURES_PARENT_PUBLIC_KEY: u8 = 0x08; +pub const FIELD_FEATURES_METADATA: u8 = 0x09; diff --git a/base_layer/core/src/covenants/context.rs b/base_layer/core/src/covenants/context.rs new file mode 100644 index 0000000000..c3a17b3df0 --- /dev/null +++ b/base_layer/core/src/covenants/context.rs @@ -0,0 +1,82 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::{ + covenants::{ + arguments::CovenantArg, + error::CovenantError, + filters::CovenantFilter, + token::{CovenantToken, CovenantTokenCollection}, + }, + transactions::transaction::TransactionInput, +}; + +pub struct CovenantContext<'a> { + input: &'a TransactionInput, + tokens: CovenantTokenCollection, + block_height: u64, +} + +impl<'a> CovenantContext<'a> { + pub fn new(tokens: CovenantTokenCollection, input: &'a TransactionInput, block_height: u64) -> Self { + Self { + input, + tokens, + block_height, + } + } + + pub fn has_more_tokens(&self) -> bool { + !self.tokens.is_empty() + } + + pub fn next_arg(&mut self) -> Result { + match self.tokens.next().ok_or(CovenantError::UnexpectedEndOfTokens)? { + CovenantToken::Arg(arg) => Ok(arg), + CovenantToken::Filter(_) => Err(CovenantError::ExpectedArgButGotFilter), + } + } + + // Only happens to be used in tests for now + #[cfg(test)] + pub fn next_filter(&mut self) -> Option { + match self.tokens.next()? { + CovenantToken::Filter(filter) => Some(filter), + CovenantToken::Arg(_) => None, + } + } + + pub fn require_next_filter(&mut self) -> Result { + match self.tokens.next().ok_or(CovenantError::UnexpectedEndOfTokens)? { + CovenantToken::Filter(filter) => Ok(filter), + CovenantToken::Arg(_) => Err(CovenantError::ExpectedFilterButGotArg), + } + } + + pub fn block_height(&self) -> u64 { + self.block_height + } + + pub fn input(&self) -> &TransactionInput { + self.input + } +} diff --git a/base_layer/core/src/covenants/covenant.rs b/base_layer/core/src/covenants/covenant.rs new file mode 100644 index 0000000000..c48607c8c3 --- /dev/null +++ b/base_layer/core/src/covenants/covenant.rs @@ -0,0 +1,160 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 std::{io, iter::FromIterator}; + +use crate::{ + common::byte_counter::ByteCounter, + consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}, + covenants::{ + context::CovenantContext, + decoder::{CovenantDecodeError, CovenantTokenDecoder}, + encoder::CovenantTokenEncoder, + error::CovenantError, + filters::Filter, + output_set::OutputSet, + token::{CovenantToken, CovenantTokenCollection}, + }, + transactions::transaction::{TransactionInput, TransactionOutput}, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Covenant { + tokens: Vec, +} + +impl Covenant { + pub fn new() -> Self { + Self { tokens: Vec::new() } + } + + pub fn from_bytes(mut bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Ok(Self::new()); + } + CovenantTokenDecoder::new(&mut bytes).collect() + } + + pub(super) fn write_to(&self, writer: &mut W) -> Result { + CovenantTokenEncoder::new(self.tokens.as_slice()).write_to(writer) + } + + pub fn validate(&self) -> Result<(), CovenantError> { + // TODO + Ok(()) + } + + pub fn execute<'a>( + &self, + block_height: u64, + input: &TransactionInput, + outputs: &'a [TransactionOutput], + ) -> Result { + if self.tokens.is_empty() { + // Empty covenants always pass + return Ok(outputs.len()); + } + + let tokens = CovenantTokenCollection::from_iter(self.tokens.clone()); + let mut cx = CovenantContext::new(tokens, input, block_height); + let root = cx.require_next_filter()?; + let mut output_set = OutputSet::new(outputs); + root.filter(&mut cx, &mut output_set)?; + if cx.has_more_tokens() { + return Err(CovenantError::RemainingTokens); + } + if output_set.is_empty() { + return Err(CovenantError::NoMatchingOutputs); + } + + Ok(output_set.len()) + } + + pub fn push_token(&mut self, token: CovenantToken) { + self.tokens.push(token); + } + + #[cfg(test)] + pub(super) fn tokens(&self) -> &[CovenantToken] { + &self.tokens + } +} + +impl ConsensusEncoding for Covenant { + fn consensus_encode(&self, writer: &mut W) -> Result { + self.write_to(writer) + } +} + +impl ConsensusEncodingSized for Covenant { + fn consensus_encode_exact_size(&self) -> usize { + let mut byte_counter = ByteCounter::new(); + self.write_to(&mut byte_counter).expect("unreachable panic"); + byte_counter.get() + } +} + +impl ConsensusDecoding for Covenant { + fn consensus_decode(reader: &mut R) -> Result { + CovenantTokenDecoder::new(reader) + .collect::>() + .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err)) + } +} + +impl FromIterator for Covenant { + fn from_iter>(iter: T) -> Self { + Self { + tokens: iter.into_iter().collect(), + } + } +} + +#[cfg(test)] +mod test { + use crate::{ + covenant, + covenants::test::{create_input, create_outputs}, + }; + + #[test] + fn it_succeeds_when_empty() { + let outputs = create_outputs(10, Default::default()); + let input = create_input(); + let covenant = covenant!(); + let num_matching_outputs = covenant.execute(0, &input, &outputs).unwrap(); + assert_eq!(num_matching_outputs, 10); + } + + #[test] + fn it_executes_the_covenant() { + let mut outputs = create_outputs(10, Default::default()); + outputs[4].features.maturity = 42; + outputs[5].features.maturity = 42; + outputs[7].features.maturity = 42; + let mut input = create_input(); + input.features.maturity = 42; + let covenant = covenant!(fields_preserved(@fields(@field::features))); + let num_matching_outputs = covenant.execute(0, &input, &outputs).unwrap(); + assert_eq!(num_matching_outputs, 3); + } +} diff --git a/base_layer/core/src/covenants/decoder.rs b/base_layer/core/src/covenants/decoder.rs new file mode 100644 index 0000000000..3ff465796e --- /dev/null +++ b/base_layer/core/src/covenants/decoder.rs @@ -0,0 +1,167 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 std::io; + +use integer_encoding::VarIntReader; +use tari_crypto::script::ScriptError; + +use crate::covenants::token::CovenantToken; + +pub struct CovenantTokenDecoder<'a, R> { + buf: &'a mut R, + is_complete: bool, +} + +impl<'a, R: io::Read> CovenantTokenDecoder<'a, R> { + pub fn new(buf: &'a mut R) -> Self { + Self { + buf, + is_complete: false, + } + } +} + +impl Iterator for CovenantTokenDecoder<'_, R> { + type Item = Result; + + fn next(&mut self) -> Option { + if self.is_complete { + return None; + } + + match CovenantToken::read_from(self.buf) { + Ok(Some(token)) => Some(Ok(token)), + Ok(None) => { + self.is_complete = true; + None + }, + Err(err) => { + self.is_complete = true; + Some(Err(err)) + }, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum CovenantDecodeError { + #[error("Unknown filter byte code {code}")] + UnknownFilterByteCode { code: u8 }, + #[error("Unknown arg byte code {code}")] + UnknownArgByteCode { code: u8 }, + #[error("Unknown byte code {code}")] + UnknownByteCode { code: u8 }, + #[error("Unexpected EoF, expected {expected}")] + UnexpectedEof { expected: &'static str }, + // #[error("Byte array error: {0}")] + // ByteArrayError(#[from] ByteArrayError), + #[error("Tari script error: {0}")] + ScriptError(#[from] ScriptError), + #[error(transparent)] + Io(#[from] io::Error), +} + +pub(super) trait CovenentReadExt: io::Read { + fn read_next_byte_code(&mut self) -> Result, io::Error>; + fn read_variable_length_bytes(&mut self, size: usize) -> Result, io::Error>; +} + +impl CovenentReadExt for R { + fn read_next_byte_code(&mut self) -> Result, io::Error> { + let mut buf = [0u8; 1]; + loop { + // This is what read_exact does, except that if we read 0 bytes, we return None instead of an UnexpectedEof + // error + match self.read(&mut buf) { + Ok(0) => return Ok(None), + Ok(1) => return Ok(Some(buf[0])), + Ok(_) => unreachable!("buffer size is 1 but more bytes were read!?"), + Err(ref err) if err.kind() == io::ErrorKind::Interrupted => {}, + Err(err) => return Err(err), + } + } + } + + fn read_variable_length_bytes(&mut self, max_size: usize) -> Result, io::Error> { + let len = self.read_varint::()? as usize; + if len > max_size { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "Received variable length bytes that exceed {} bytes (max: {})", + len, max_size + ), + )); + } + let mut buf = vec![0u8; len]; + self.read_exact(&mut buf)?; + Ok(buf) + } +} + +#[cfg(test)] +mod test { + use tari_test_utils::unpack_enum; + use tari_utilities::hex::{from_hex, to_hex}; + + use super::*; + use crate::{ + consensus::ToConsensusBytes, + covenant, + covenants::{arguments::CovenantArg, fields::OutputField, filters::CovenantFilter}, + }; + + #[test] + fn it_decodes_from_well_formed_bytes() { + let hash = from_hex("53563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd").unwrap(); + let mut hash_buf = [0u8; 32]; + hash_buf.copy_from_slice(hash.as_slice()); + let bytes = covenant!(fields_hashed_eq( + @fields(@field::commitment, @field::features_metadata), + @hash(hash_buf), + )) + .to_consensus_bytes(); + let mut buf = bytes.as_slice(); + let mut decoder = CovenantTokenDecoder::new(&mut buf); + let token = decoder.next().unwrap().unwrap(); + assert!(matches!( + token, + CovenantToken::Filter(CovenantFilter::FieldsHashedEq(_)) + )); + let token = decoder.next().unwrap().unwrap(); + unpack_enum!(CovenantArg::OutputFields(fields) = token.as_arg().unwrap()); + assert_eq!(fields.fields(), &[ + OutputField::Commitment, + OutputField::FeaturesMetadata + ]); + + let token = decoder.next().unwrap().unwrap(); + unpack_enum!(CovenantArg::Hash(hash) = token.as_arg().unwrap()); + assert_eq!( + to_hex(&hash[..]), + "53563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd" + ); + + assert!(decoder.next().is_none()); + } +} diff --git a/base_layer/core/src/covenants/encoder.rs b/base_layer/core/src/covenants/encoder.rs new file mode 100644 index 0000000000..b4269fe3b9 --- /dev/null +++ b/base_layer/core/src/covenants/encoder.rs @@ -0,0 +1,65 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 std::{convert::TryFrom, io}; + +use integer_encoding::VarIntWriter; + +use crate::covenants::token::CovenantToken; + +pub struct CovenantTokenEncoder<'a> { + tokens: &'a [CovenantToken], +} + +impl<'a> CovenantTokenEncoder<'a> { + pub fn new(tokens: &'a [CovenantToken]) -> Self { + Self { tokens } + } + + pub fn write_to(&self, writer: &mut W) -> Result { + let mut written = 0; + for token in self.tokens { + written += token.write_to(writer)?; + } + Ok(written) + } +} + +pub(super) trait CovenentWriteExt: io::Write { + fn write_u8_fixed(&mut self, v: u8) -> Result; + fn write_variable_length_bytes(&mut self, buf: &[u8]) -> Result; +} + +impl CovenentWriteExt for W { + fn write_u8_fixed(&mut self, v: u8) -> Result { + self.write_all(&[v])?; + Ok(1) + } + + fn write_variable_length_bytes(&mut self, buf: &[u8]) -> Result { + let len = u16::try_from(buf.len()).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + let mut written = self.write_varint(len)?; + written += buf.len(); + self.write_all(buf)?; + Ok(written) + } +} diff --git a/base_layer/core/src/covenants/error.rs b/base_layer/core/src/covenants/error.rs new file mode 100644 index 0000000000..462a6de603 --- /dev/null +++ b/base_layer/core/src/covenants/error.rs @@ -0,0 +1,45 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::covenants::decoder::CovenantDecodeError; + +#[derive(Debug, thiserror::Error)] +pub enum CovenantError { + #[error("Reached the end of tokens but another token was expected")] + UnexpectedEndOfTokens, + #[error("Covenant decode error: {0}")] + CovenantDecodeError(#[from] CovenantDecodeError), + #[error("Expected an argument but got a filter")] + ExpectedArgButGotFilter, + #[error("Expected a filter but got an argument")] + ExpectedFilterButGotArg, + #[error("Encountered an unexpected argument. Expected {expected} but got {got}")] + UnexpectedArgument { expected: &'static str, got: String }, + #[error("Covenant failed: no matching outputs found")] + NoMatchingOutputs, + #[error("Covenant failed: unused tokens remain after execution")] + RemainingTokens, + #[error("Invalid argument for filter {filter}: {details}")] + InvalidArgument { filter: &'static str, details: String }, + #[error("Unsupported argument {arg}: {details}")] + UnsupportedArgument { arg: &'static str, details: String }, +} diff --git a/base_layer/core/src/covenants/fields.rs b/base_layer/core/src/covenants/fields.rs new file mode 100644 index 0000000000..49f19bc06b --- /dev/null +++ b/base_layer/core/src/covenants/fields.rs @@ -0,0 +1,329 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 std::{ + any::Any, + fmt::{Display, Formatter}, + io, + iter::FromIterator, +}; + +use digest::Digest; +use integer_encoding::VarIntWriter; +use tari_common_types::types::Challenge; + +use crate::{ + consensus::ToConsensusBytes, + covenants::{ + byte_codes, + decoder::{CovenantDecodeError, CovenentReadExt}, + encoder::CovenentWriteExt, + error::CovenantError, + }, + transactions::transaction::{TransactionInput, TransactionOutput}, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum OutputField { + Commitment = byte_codes::FIELD_COMMITMENT, + Script = byte_codes::FIELD_SCRIPT, + SenderOffsetPublicKey = byte_codes::FIELD_SENDER_OFFSET_PUBLIC_KEY, + Covenant = byte_codes::FIELD_COVENANT, + Features = byte_codes::FIELD_FEATURES, + FeaturesFlags = byte_codes::FIELD_FEATURES_FLAGS, + FeaturesMaturity = byte_codes::FIELD_FEATURES_MATURITY, + FeaturesUniqueId = byte_codes::FIELD_FEATURES_UNIQUE_ID, + FeaturesParentPublicKey = byte_codes::FIELD_FEATURES_PARENT_PUBLIC_KEY, + FeaturesMetadata = byte_codes::FIELD_FEATURES_METADATA, +} + +impl OutputField { + pub fn from_byte(byte: u8) -> Result { + use byte_codes::*; + use OutputField::*; + match byte { + FIELD_COMMITMENT => Ok(Commitment), + FIELD_SCRIPT => Ok(Script), + FIELD_SENDER_OFFSET_PUBLIC_KEY => Ok(SenderOffsetPublicKey), + FIELD_COVENANT => Ok(Covenant), + FIELD_FEATURES => Ok(Features), + FIELD_FEATURES_FLAGS => Ok(FeaturesFlags), + FIELD_FEATURES_MATURITY => Ok(FeaturesMaturity), + FIELD_FEATURES_UNIQUE_ID => Ok(FeaturesUniqueId), + FIELD_FEATURES_PARENT_PUBLIC_KEY => Ok(FeaturesParentPublicKey), + FIELD_FEATURES_METADATA => Ok(FeaturesMetadata), + + _ => Err(CovenantDecodeError::UnknownByteCode { code: byte }), + } + } + + pub fn as_byte(&self) -> u8 { + *self as u8 + } + + pub fn get_field_value_ref<'a, T: 'static>(&self, output: &'a TransactionOutput) -> Option<&'a T> { + use OutputField::*; + let val = match self { + Commitment => &output.commitment as &dyn Any, + Script => &output.script as &dyn Any, + SenderOffsetPublicKey => &output.sender_offset_public_key as &dyn Any, + Covenant => unimplemented!(), + Features => &output.features as &dyn Any, + FeaturesFlags => &output.features.flags as &dyn Any, + FeaturesMaturity => &output.features.maturity as &dyn Any, + FeaturesUniqueId => &output.features.unique_id as &dyn Any, + FeaturesParentPublicKey => &output.features.parent_public_key as &dyn Any, + FeaturesMetadata => &output.features.metadata as &dyn Any, + }; + val.downcast_ref::() + } + + pub fn get_field_value_bytes(&self, output: &TransactionOutput) -> Vec { + use OutputField::*; + match self { + Commitment => output.commitment.to_consensus_bytes(), + Script => output.script.to_consensus_bytes(), + SenderOffsetPublicKey => output.sender_offset_public_key.to_consensus_bytes(), + Covenant => unimplemented!(), + Features => output.features.to_consensus_bytes(), + FeaturesFlags => output.features.flags.to_consensus_bytes(), + FeaturesMaturity => output.features.maturity.to_consensus_bytes(), + FeaturesUniqueId => output + .features + .unique_id + .as_ref() + .map(|unique_id| unique_id.to_consensus_bytes()) + .unwrap_or_default(), + FeaturesParentPublicKey => output + .features + .parent_public_key + .as_ref() + .map(|pk| pk.to_consensus_bytes()) + .unwrap_or_default(), + FeaturesMetadata => output.features.metadata.to_consensus_bytes(), + } + } + + pub fn is_eq_input(&self, input: &TransactionInput, output: &TransactionOutput) -> bool { + use OutputField::*; + match self { + Commitment => input.commitment == output.commitment, + Script => input.script == output.script, + SenderOffsetPublicKey => input.sender_offset_public_key == output.sender_offset_public_key, + Covenant => unimplemented!(), + Features => input.features == output.features, + FeaturesFlags => input.features.flags == output.features.flags, + FeaturesMaturity => input.features.maturity == output.features.maturity, + FeaturesUniqueId => input.features.unique_id == output.features.unique_id, + FeaturesParentPublicKey => input.features.parent_public_key == output.features.parent_public_key, + FeaturesMetadata => input.features.metadata == output.features.metadata, + } + } + + pub fn is_eq(&self, output: &TransactionOutput, val: &T) -> Result { + use OutputField::*; + match self { + // Handle edge cases + FeaturesParentPublicKey | FeaturesUniqueId => match self.get_field_value_ref::>(output) { + Some(Some(field_val)) => Ok(field_val == val), + _ => Ok(false), + }, + Features => Err(CovenantError::UnsupportedArgument { + arg: "features", + details: "OutputFeatures is not supported for operation is_eq".to_string(), + }), + _ => match self.get_field_value_ref::(output) { + Some(field_val) => Ok(field_val == val), + None => Err(CovenantError::InvalidArgument { + filter: "is_eq", + details: format!("Invalid type for field {}", self), + }), + }, + } + } + + //---------------------------------- Macro helpers --------------------------------------------// + #[allow(dead_code)] + pub(super) fn commitment() -> Self { + OutputField::Commitment + } + + #[allow(dead_code)] + pub(super) fn script() -> Self { + OutputField::Script + } + + #[allow(dead_code)] + pub(super) fn sender_offset_public_key() -> Self { + OutputField::SenderOffsetPublicKey + } + + #[allow(dead_code)] + pub(super) fn covenant() -> Self { + OutputField::Covenant + } + + #[allow(dead_code)] + pub(super) fn features() -> Self { + OutputField::Features + } + + #[allow(dead_code)] + pub(super) fn features_flags() -> Self { + OutputField::FeaturesFlags + } + + #[allow(dead_code)] + pub(super) fn features_maturity() -> Self { + OutputField::FeaturesMaturity + } + + #[allow(dead_code)] + pub(super) fn features_unique_id() -> Self { + OutputField::FeaturesUniqueId + } + + #[allow(dead_code)] + pub(super) fn features_parent_public_key() -> Self { + OutputField::FeaturesParentPublicKey + } + + #[allow(dead_code)] + pub(super) fn features_metadata() -> Self { + OutputField::FeaturesMetadata + } +} + +impl Display for OutputField { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + use OutputField::*; + match self { + Commitment => write!(f, "field::commitment"), + SenderOffsetPublicKey => write!(f, "field::sender_offset_public_key"), + Script => write!(f, "field::script"), + Covenant => write!(f, "field::covenant"), + Features => write!(f, "field::features"), + FeaturesFlags => write!(f, "field::features_flags"), + FeaturesUniqueId => write!(f, "field::features_unique_id"), + FeaturesMetadata => write!(f, "field::features_metadata"), + FeaturesParentPublicKey => write!(f, "field::features_parent_public_key"), + FeaturesMaturity => write!(f, "field::features_maturity"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct OutputFields { + fields: Vec, +} + +impl OutputFields { + /// The number of unique fields available. This always matches the number of variants in `OutputField`. + pub const NUM_FIELDS: usize = 10; + + pub fn new() -> Self { + Default::default() + } + + pub fn push(&mut self, field: OutputField) { + self.fields.push(field); + } + + pub fn read_from(reader: &mut R) -> Result { + // Each field is a byte + let buf = reader.read_variable_length_bytes(Self::NUM_FIELDS)?; + buf.iter().map(|byte| OutputField::from_byte(*byte)).collect() + } + + pub fn write_to(&self, writer: &mut W) -> Result { + let len = self.fields.len(); + if len > Self::NUM_FIELDS { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "tried to write more than maximum number of fields", + )); + } + let mut written = writer.write_varint(len)?; + for byte in self.iter().map(|f| f.as_byte()) { + written += writer.write_u8_fixed(byte)?; + } + Ok(written) + } + + pub fn iter(&self) -> impl Iterator + '_ { + self.fields.iter() + } + + pub fn len(&self) -> usize { + self.fields.len() + } + + pub fn is_empty(&self) -> bool { + self.fields.is_empty() + } + + pub fn construct_challenge_from(&self, output: &TransactionOutput) -> Challenge { + let mut challenge = Challenge::new(); + for field in self.fields.iter() { + challenge = challenge.chain(field.get_field_value_bytes(output)); + } + challenge + } + + pub fn fields(&self) -> &[OutputField] { + &self.fields + } +} + +impl From> for OutputFields { + fn from(fields: Vec) -> Self { + OutputFields { fields } + } +} +impl FromIterator for OutputFields { + fn from_iter>(iter: T) -> Self { + Self { + fields: iter.into_iter().collect(), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::transactions::transaction::OutputFeatures; + + #[test] + fn get_field_value_ref() { + let output = TransactionOutput::new( + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ); + let r = OutputField::Features.get_field_value_ref::(&output); + assert_eq!(*r.unwrap(), OutputFeatures::default()); + } +} diff --git a/base_layer/core/src/covenants/filters/absolute_height.rs b/base_layer/core/src/covenants/filters/absolute_height.rs new file mode 100644 index 0000000000..63d8cb9e05 --- /dev/null +++ b/base_layer/core/src/covenants/filters/absolute_height.rs @@ -0,0 +1,104 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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. + +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::covenants::{context::CovenantContext, error::CovenantError, filters::Filter, output_set::OutputSet}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AbsoluteHeightFilter; + +impl Filter for AbsoluteHeightFilter { + 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(); + if current_height < abs_height { + output_set.clear(); + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + covenant, + covenants::{filters::test::setup_filter_test, test::create_input}, + }; + + #[test] + fn it_filters_all_out_if_height_not_reached() { + let covenant = covenant!(absolute_height(@uint(100))); + let input = create_input(); + let (mut context, outputs) = setup_filter_test(&covenant, &input, 42, |_| {}); + + let mut output_set = OutputSet::new(&outputs); + AbsoluteHeightFilter.filter(&mut context, &mut output_set).unwrap(); + + assert!(output_set.is_empty()); + } + + #[test] + fn it_filters_all_in_if_height_reached() { + let covenant = covenant!(absolute_height(@uint(100))); + let input = create_input(); + let (mut context, outputs) = setup_filter_test(&covenant, &input, 100, |_| {}); + + let mut output_set = OutputSet::new(&outputs); + AbsoluteHeightFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 10); + } + + #[test] + fn it_filters_all_in_if_height_exceeded() { + let covenant = covenant!(absolute_height(@uint(42))); + let input = create_input(); + let (mut context, outputs) = setup_filter_test(&covenant, &input, 100, |_| {}); + + let mut output_set = OutputSet::new(&outputs); + AbsoluteHeightFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 10); + } +} diff --git a/base_layer/core/src/covenants/filters/and.rs b/base_layer/core/src/covenants/filters/and.rs new file mode 100644 index 0000000000..4c32a704f1 --- /dev/null +++ b/base_layer/core/src/covenants/filters/and.rs @@ -0,0 +1,69 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::covenants::{context::CovenantContext, error::CovenantError, filters::Filter, output_set::OutputSet}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AndFilter; + +impl Filter for AndFilter { + fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> { + let a = context.require_next_filter()?; + a.filter(context, output_set)?; + let b = context.require_next_filter()?; + b.filter(context, output_set)?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + + use super::*; + use crate::{ + covenant, + covenants::{filters::test::setup_filter_test, test::create_input}, + }; + + #[test] + fn it_filters_outputset_using_intersection() { + let bytes = vec![0xab, 0xcd, 0xef]; + let covenant = covenant!(and(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::features_unique_id, @bytes(bytes.clone())))); + let input = create_input(); + let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |outputs| { + outputs[5].features.maturity = 42; + outputs[5].features.unique_id = Some(bytes.clone()); + outputs[7].features.maturity = 42; + outputs[7].features.unique_id = Some(bytes.clone()); + // Does not have maturity = 42 + outputs[8].features.maturity = 123; + outputs[8].features.unique_id = Some(bytes.clone()); + }); + + let mut output_set = OutputSet::new(&outputs); + AndFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 2); + assert_eq!(output_set.get_selected_indexes(), vec![5, 7]); + } +} diff --git a/base_layer/core/src/covenants/filters/field_eq.rs b/base_layer/core/src/covenants/filters/field_eq.rs new file mode 100644 index 0000000000..cc1994838c --- /dev/null +++ b/base_layer/core/src/covenants/filters/field_eq.rs @@ -0,0 +1,218 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::covenants::{ + arguments::CovenantArg, + context::CovenantContext, + error::CovenantError, + filters::Filter, + output_set::OutputSet, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FieldEqFilter; + +impl Filter for FieldEqFilter { + fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> { + let field = context.next_arg()?.require_outputfield()?; + let arg = context.next_arg()?; + output_set.retain(|output| { + use CovenantArg::*; + match &arg { + Hash(hash) => field.is_eq(output, hash), + PublicKey(pk) => field.is_eq(output, pk), + Commitment(commitment) => field.is_eq(output, commitment), + TariScript(script) => field.is_eq(output, script), + Covenant(covenant) => field.is_eq(output, covenant), + Uint(int) => { + let val = field + .get_field_value_ref::(output) + .copied() + .or_else(|| field.get_field_value_ref::(output).map(|v| *v as u64)); + + match val { + Some(val) => Ok(val == *int), + None => Err(CovenantError::InvalidArgument { + filter: "fields_eq", + details: "Uint argument cannot be compared to non-numeric field".to_string(), + }), + } + }, + Bytes(bytes) => field.is_eq(output, bytes), + OutputField(_) | OutputFields(_) => Err(CovenantError::InvalidArgument { + filter: "field_eq", + details: "Invalid argument: fields are not a valid argument for field_eq".to_string(), + }), + } + })?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use tari_common_types::types::{Commitment, PublicKey}; + use tari_crypto::script; + use tari_test_utils::unpack_enum; + use tari_utilities::hex::Hex; + + use super::*; + use crate::{ + covenant, + covenants::test::{create_context, create_input, create_outputs}, + }; + + #[test] + fn it_filters_uint() { + let covenant = covenant!(field_eq(@field::features_maturity, @uint(42))); + let input = create_input(); + let mut context = create_context(&covenant, &input, 0); + // Remove `field_eq` + context.next_filter().unwrap(); + let mut outputs = create_outputs(10, Default::default()); + outputs[5].features.maturity = 42; + let mut output_set = OutputSet::new(&outputs); + FieldEqFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 1); + assert_eq!(output_set.get(5).unwrap().features.maturity, 42); + } + + #[test] + fn it_filters_sender_offset_public_key() { + let pk = PublicKey::from_hex("5615a327e1d19da34e5aa8bbd2ecc97addf29b158844b885bfc4efa0dab17052").unwrap(); + let covenant = covenant!(field_eq( + @field::features_parent_public_key, + @public_key(pk.clone()) + )); + let input = create_input(); + let mut context = create_context(&covenant, &input, 0); + // Remove `field_eq` + context.next_filter().unwrap(); + let mut outputs = create_outputs(10, Default::default()); + outputs[5].features.parent_public_key = Some(pk.clone()); + let mut output_set = OutputSet::new(&outputs); + FieldEqFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 1); + assert_eq!( + *output_set.get(5).unwrap().features.parent_public_key.as_ref().unwrap(), + pk + ); + } + + #[test] + fn it_filters_commitment() { + let commitment = + Commitment::from_hex("7ca31ba517d8b563609ed6707fedde5a2be64ac1d67b254cb5348bc2f680557f").unwrap(); + let covenant = covenant!(field_eq( + @field::commitment, + @commitment(commitment.clone()) + )); + let input = create_input(); + let mut context = create_context(&covenant, &input, 0); + // Remove `field_eq` + context.next_filter().unwrap(); + let mut outputs = create_outputs(10, Default::default()); + outputs[5].commitment = commitment.clone(); + outputs[7].commitment = commitment.clone(); + let mut output_set = OutputSet::new(&outputs); + FieldEqFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 2); + assert_eq!(output_set.get_selected_indexes(), vec![5, 7]); + } + + #[test] + fn it_filters_tari_script() { + let script = script!(CheckHeight(100)); + let covenant = covenant!(field_eq( + @field::script, + @script(script.clone()) + )); + let input = create_input(); + let mut context = create_context(&covenant, &input, 0); + // Remove `field_eq` + context.next_filter().unwrap(); + let mut outputs = create_outputs(10, Default::default()); + outputs[5].script = script.clone(); + outputs[7].script = script.clone(); + let mut output_set = OutputSet::new(&outputs); + FieldEqFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 2); + assert_eq!(output_set.get_selected_indexes(), vec![5, 7]); + } + + // #[test] + // fn it_filters_covenant() { + // // TODO: Covenant field is not in output yet + // let covenant = covenant!(identity()); + // let covenant = covenant!(field_eq( + // @field::covenant, + // @covenant(covenant.clone()) + // )); + // let input = create_input(); + // let mut context = create_context(&covenant, &input, 0); + // // Remove `field_eq` + // context.next_filter().unwrap(); + // let mut outputs = create_outputs(10, Default::default()); + // outputs[5].covenant = covenant.clone(); + // outputs[7].covenant = covenant.clone(); + // let mut output_set = OutputSet::new(&outputs); + // FieldEqFilter.filter(&mut context, &mut output_set).unwrap(); + // + // assert_eq!(output_set.len(), 2); + // assert_eq!(output_set.get_selected_indexes(), vec![5, 7]); + // } + + #[test] + fn it_errors_for_unsupported_features_field() { + let covenant = covenant!(field_eq( + @field::features, + @bytes(vec![]) + )); + let input = create_input(); + let mut context = create_context(&covenant, &input, 0); + // Remove `field_eq` + context.next_filter().unwrap(); + let outputs = create_outputs(10, Default::default()); + let mut output_set = OutputSet::new(&outputs); + let err = FieldEqFilter.filter(&mut context, &mut output_set).unwrap_err(); + unpack_enum!(CovenantError::UnsupportedArgument { arg, .. } = err); + assert_eq!(arg, "features"); + } + + #[test] + fn it_errors_if_field_has_an_incorrect_type() { + let covenant = covenant!(field_eq(@field::features, @uint(42))); + let input = create_input(); + let mut context = create_context(&covenant, &input, 0); + // Remove `field_eq` + context.next_filter().unwrap(); + let outputs = create_outputs(10, Default::default()); + let mut output_set = OutputSet::new(&outputs); + let err = FieldEqFilter.filter(&mut context, &mut output_set).unwrap_err(); + unpack_enum!(CovenantError::InvalidArgument { .. } = err); + } +} diff --git a/base_layer/core/src/covenants/filters/fields_hashed_eq.rs b/base_layer/core/src/covenants/filters/fields_hashed_eq.rs new file mode 100644 index 0000000000..b6a3f1f6bf --- /dev/null +++ b/base_layer/core/src/covenants/filters/fields_hashed_eq.rs @@ -0,0 +1,97 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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. + +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 digest::Digest; + +use crate::covenants::{context::CovenantContext, error::CovenantError, filters::Filter, output_set::OutputSet}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FieldsHashedEqFilter; + +impl Filter for FieldsHashedEqFilter { + fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> { + let fields = context.next_arg()?.require_outputfields()?; + let hash = context.next_arg()?.require_hash()?; + output_set.retain(|output| { + let challenge = fields.construct_challenge_from(output); + Ok(challenge.finalize()[..] == hash) + })?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use tari_common_types::types::Challenge; + + use super::*; + use crate::{ + covenant, + covenants::{filters::test::setup_filter_test, test::create_input}, + transactions::transaction::OutputFeatures, + }; + + #[test] + fn it_filters_outputs_with_fields_that_hash_to_given_hash() { + let features = OutputFeatures { + maturity: 42, + unique_id: Some(vec![0xab, 0xcd, 0xef]), + ..Default::default() + }; + let hashed = Challenge::new().chain(features.to_consensus_bytes()).finalize(); + let mut hash = [0u8; 32]; + hash.copy_from_slice(hashed.as_slice()); + let covenant = covenant!(fields_hashed_eq(@fields(@field::features), @hash(hash))); + let input = create_input(); + let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |outputs| { + outputs[5].features = features.clone(); + outputs[7].features = features; + }); + let mut output_set = OutputSet::new(&outputs); + FieldsHashedEqFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 2); + assert_eq!(output_set.get_selected_indexes(), vec![5, 7]); + } +} diff --git a/base_layer/core/src/covenants/filters/fields_preserved.rs b/base_layer/core/src/covenants/filters/fields_preserved.rs new file mode 100644 index 0000000000..998cf0d28b --- /dev/null +++ b/base_layer/core/src/covenants/filters/fields_preserved.rs @@ -0,0 +1,73 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::covenants::{context::CovenantContext, error::CovenantError, filters::Filter, output_set::OutputSet}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FieldsPreservedFilter; + +impl Filter for FieldsPreservedFilter { + fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> { + let fields = context.next_arg()?.require_outputfields()?; + let input = context.input(); + output_set.retain(|output| Ok(fields.iter().all(|field| field.is_eq_input(input, output))))?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + + use super::*; + use crate::{ + covenant, + covenants::{filters::test::setup_filter_test, test::create_input}, + transactions::transaction::OutputFlags, + }; + + #[test] + fn it_filters_outputs_that_match_input_fields() { + let bytes = vec![0xab, 0xcd, 0xef]; + let covenant = covenant!(fields_preserved(@fields(@field::features_maturity, @field::features_unique_id, @field::features_flags))); + let mut input = create_input(); + input.features.maturity = 42; + input.features.unique_id = Some(bytes.clone()); + input.features.flags = OutputFlags::SIDECHAIN_CHECKPOINT; + let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |outputs| { + outputs[5].features.maturity = 42; + outputs[5].features.unique_id = Some(bytes.clone()); + outputs[5].features.flags = OutputFlags::SIDECHAIN_CHECKPOINT; + outputs[7].features.maturity = 42; + outputs[7].features.flags = OutputFlags::SIDECHAIN_CHECKPOINT; + outputs[7].features.unique_id = Some(vec![0x01, 0x02]); + outputs[8].features.maturity = 42; + outputs[8].features.unique_id = Some(bytes.clone()); + outputs[8].features.flags = OutputFlags::SIDECHAIN_CHECKPOINT | OutputFlags::COINBASE_OUTPUT; + }); + let mut output_set = OutputSet::new(&outputs); + + FieldsPreservedFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 1); + assert_eq!(output_set.get_selected_indexes(), vec![5]); + } +} diff --git a/base_layer/core/src/covenants/filters/filter.rs b/base_layer/core/src/covenants/filters/filter.rs new file mode 100644 index 0000000000..fe91c3d15e --- /dev/null +++ b/base_layer/core/src/covenants/filters/filter.rs @@ -0,0 +1,165 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 std::io; + +use super::{ + absolute_height::AbsoluteHeightFilter, + and::AndFilter, + field_eq::FieldEqFilter, + fields_hashed_eq::FieldsHashedEqFilter, + fields_preserved::FieldsPreservedFilter, + identity::IdentityFilter, + not::NotFilter, + or::OrFilter, + output_hash_eq::OutputHashEqFilter, + xor::XorFilter, +}; +use crate::covenants::{ + byte_codes, + context::CovenantContext, + decoder::CovenantDecodeError, + encoder::CovenentWriteExt, + error::CovenantError, + output_set::OutputSet, +}; + +pub trait Filter { + fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError>; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CovenantFilter { + Identity(IdentityFilter), + And(AndFilter), + Or(OrFilter), + Xor(XorFilter), + Not(NotFilter), + OutputHashEq(OutputHashEqFilter), + FieldsPreserved(FieldsPreservedFilter), + FieldEq(FieldEqFilter), + FieldsHashedEq(FieldsHashedEqFilter), + AbsoluteHeight(AbsoluteHeightFilter), +} + +impl CovenantFilter { + pub fn is_valid_code(code: u8) -> bool { + byte_codes::is_valid_filter_code(code) + } + + pub fn write_to(&self, writer: &mut W) -> Result { + writer.write_u8_fixed(self.as_byte_code()) + } + + fn as_byte_code(&self) -> u8 { + use byte_codes::*; + use CovenantFilter::*; + + match self { + Identity(_) => FILTER_IDENTITY, + And(_) => FILTER_AND, + Or(_) => FILTER_OR, + Xor(_) => FILTER_XOR, + Not(_) => FILTER_NOT, + OutputHashEq(_) => FILTER_OUTPUT_HASH_EQ, + FieldsPreserved(_) => FILTER_FIELDS_PRESERVED, + FieldEq(_) => FILTER_FIELD_EQ, + FieldsHashedEq(_) => FILTER_FIELDS_HASHED_EQ, + AbsoluteHeight(_) => FILTER_ABSOLUTE_HEIGHT, + } + } + + pub fn try_from_byte_code(code: u8) -> Result { + use byte_codes::*; + match code { + FILTER_IDENTITY => Ok(Self::identity()), + FILTER_AND => Ok(Self::and()), + FILTER_OR => Ok(Self::or()), + FILTER_XOR => Ok(Self::xor()), + FILTER_NOT => Ok(Self::not()), + FILTER_OUTPUT_HASH_EQ => Ok(Self::output_hash_eq()), + FILTER_FIELDS_PRESERVED => Ok(Self::fields_preserved()), + FILTER_FIELD_EQ => Ok(Self::field_eq()), + FILTER_FIELDS_HASHED_EQ => Ok(Self::fields_hashed_eq()), + FILTER_ABSOLUTE_HEIGHT => Ok(Self::absolute_height()), + _ => Err(CovenantDecodeError::UnknownFilterByteCode { code }), + } + } + + pub fn identity() -> Self { + CovenantFilter::Identity(IdentityFilter) + } + + pub fn and() -> Self { + CovenantFilter::And(AndFilter) + } + + pub fn or() -> Self { + CovenantFilter::Or(OrFilter) + } + + pub fn xor() -> Self { + CovenantFilter::Xor(XorFilter) + } + + pub fn not() -> Self { + CovenantFilter::Not(NotFilter) + } + + pub fn output_hash_eq() -> Self { + CovenantFilter::OutputHashEq(OutputHashEqFilter) + } + + pub fn fields_preserved() -> Self { + CovenantFilter::FieldsPreserved(FieldsPreservedFilter) + } + + pub fn field_eq() -> Self { + CovenantFilter::FieldEq(FieldEqFilter) + } + + pub fn fields_hashed_eq() -> Self { + CovenantFilter::FieldsHashedEq(FieldsHashedEqFilter) + } + + pub fn absolute_height() -> Self { + CovenantFilter::AbsoluteHeight(AbsoluteHeightFilter) + } +} + +impl Filter for CovenantFilter { + fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> { + use CovenantFilter::*; + match self { + Identity(identity) => identity.filter(context, output_set), + And(and) => and.filter(context, output_set), + Or(or) => or.filter(context, output_set), + Xor(xor) => xor.filter(context, output_set), + Not(not) => not.filter(context, output_set), + OutputHashEq(output_hash_eq) => output_hash_eq.filter(context, output_set), + FieldsPreserved(fields_preserved) => fields_preserved.filter(context, output_set), + FieldEq(fields_eq) => fields_eq.filter(context, output_set), + FieldsHashedEq(fields_hashed_eq) => fields_hashed_eq.filter(context, output_set), + AbsoluteHeight(abs_height) => abs_height.filter(context, output_set), + } + } +} diff --git a/base_layer/core/src/covenants/filters/identity.rs b/base_layer/core/src/covenants/filters/identity.rs new file mode 100644 index 0000000000..1748c97810 --- /dev/null +++ b/base_layer/core/src/covenants/filters/identity.rs @@ -0,0 +1,32 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::covenants::{context::CovenantContext, error::CovenantError, filters::Filter, output_set::OutputSet}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IdentityFilter; + +impl Filter for IdentityFilter { + fn filter(&self, _: &mut CovenantContext<'_>, _: &mut OutputSet<'_>) -> Result<(), CovenantError> { + Ok(()) + } +} diff --git a/base_layer/core/src/covenants/filters/mod.rs b/base_layer/core/src/covenants/filters/mod.rs new file mode 100644 index 0000000000..59cb3979d2 --- /dev/null +++ b/base_layer/core/src/covenants/filters/mod.rs @@ -0,0 +1,49 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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. + +mod absolute_height; +mod and; +mod field_eq; +mod fields_hashed_eq; +mod fields_preserved; +mod identity; +mod not; +mod or; +mod output_hash_eq; +mod xor; + +pub use absolute_height::AbsoluteHeightFilter; +pub use and::AndFilter; +pub use field_eq::FieldEqFilter; +pub use fields_hashed_eq::FieldsHashedEqFilter; +pub use fields_preserved::FieldsPreservedFilter; +pub use identity::IdentityFilter; +pub use not::NotFilter; +pub use or::OrFilter; +pub use output_hash_eq::OutputHashEqFilter; +pub use xor::XorFilter; + +mod filter; +pub use filter::{CovenantFilter, Filter}; + +#[cfg(test)] +mod test; diff --git a/base_layer/core/src/covenants/filters/not.rs b/base_layer/core/src/covenants/filters/not.rs new file mode 100644 index 0000000000..5c02587728 --- /dev/null +++ b/base_layer/core/src/covenants/filters/not.rs @@ -0,0 +1,64 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::covenants::{context::CovenantContext, error::CovenantError, filters::Filter, output_set::OutputSet}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NotFilter; + +impl Filter for NotFilter { + fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> { + let filter = context.require_next_filter()?; + let mut output_set_copy = output_set.clone(); + filter.filter(context, &mut output_set_copy)?; + output_set.set(output_set.difference(&output_set_copy)); + Ok(()) + } +} + +#[cfg(test)] +mod test { + + use super::*; + use crate::{ + covenant, + covenants::{filters::test::setup_filter_test, test::create_input}, + }; + + #[test] + fn it_filters_compliment_of_filter() { + let bytes = vec![0xab, 0xcd, 0xef]; + let covenant = covenant!(not(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::features_unique_id, @bytes(bytes.clone()))))); + let input = create_input(); + let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |outputs| { + outputs[5].features.maturity = 42; + outputs[5].features.unique_id = Some(bytes.clone()); + outputs[7].features.maturity = 42; + outputs[8].features.unique_id = Some(bytes.clone()); + }); + let mut output_set = OutputSet::new(&outputs); + NotFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 7); + assert_eq!(output_set.get_selected_indexes(), vec![0, 1, 2, 3, 4, 6, 9]); + } +} diff --git a/base_layer/core/src/covenants/filters/or.rs b/base_layer/core/src/covenants/filters/or.rs new file mode 100644 index 0000000000..53a8ba053b --- /dev/null +++ b/base_layer/core/src/covenants/filters/or.rs @@ -0,0 +1,69 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::covenants::{context::CovenantContext, error::CovenantError, filters::Filter, output_set::OutputSet}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OrFilter; + +impl Filter for OrFilter { + fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> { + let a = context.require_next_filter()?; + let mut output_set_a = output_set.clone(); + a.filter(context, &mut output_set_a)?; + + let b = context.require_next_filter()?; + let mut output_set_b = output_set.clone(); + b.filter(context, &mut output_set_b)?; + + output_set.set(output_set_a.union(&output_set_b)); + Ok(()) + } +} + +#[cfg(test)] +mod test { + + use super::*; + use crate::{ + covenant, + covenants::{filters::test::setup_filter_test, test::create_input}, + }; + + #[test] + fn it_filters_outputset_using_union() { + let bytes = vec![0xab, 0xcd, 0xef]; + let covenant = covenant!(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::features_unique_id, @bytes(bytes.clone())))); + let input = create_input(); + let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |outputs| { + outputs[5].features.maturity = 42; + outputs[5].features.unique_id = Some(bytes.clone()); + outputs[7].features.maturity = 42; + outputs[8].features.unique_id = Some(bytes.clone()); + }); + let mut output_set = OutputSet::new(&outputs); + OrFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 3); + assert_eq!(output_set.get_selected_indexes(), vec![5, 7, 8]); + } +} diff --git a/base_layer/core/src/covenants/filters/output_hash_eq.rs b/base_layer/core/src/covenants/filters/output_hash_eq.rs new file mode 100644 index 0000000000..a1a292dc0a --- /dev/null +++ b/base_layer/core/src/covenants/filters/output_hash_eq.rs @@ -0,0 +1,68 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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_utilities::Hashable; + +use crate::covenants::{context::CovenantContext, error::CovenantError, filters::Filter, output_set::OutputSet}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OutputHashEqFilter; + +impl Filter for OutputHashEqFilter { + fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> { + let hash = context.next_arg()?.require_hash()?; + // An output's hash is unique so the output set is either 1 or 0 outputs will match + output_set.find_inplace(|output| output.hash() == hash); + Ok(()) + } +} + +#[cfg(test)] +mod test { + + use super::*; + use crate::{ + covenant, + covenants::{ + filters::test::setup_filter_test, + test::{create_input, create_outputs}, + }, + }; + + #[test] + fn it_filters_output_with_specific_hash() { + let output = create_outputs(1, Default::default()).remove(0); + 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))); + let input = create_input(); + let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, move |outputs| { + outputs.insert(5, output); + }); + let mut output_set = OutputSet::new(&outputs); + OutputHashEqFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 1); + assert_eq!(output_set.get_selected_indexes(), vec![5]); + } +} diff --git a/base_layer/core/src/covenants/filters/test.rs b/base_layer/core/src/covenants/filters/test.rs new file mode 100644 index 0000000000..66066daa8a --- /dev/null +++ b/base_layer/core/src/covenants/filters/test.rs @@ -0,0 +1,47 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::{ + covenants::{ + context::CovenantContext, + test::{create_context, create_outputs}, + Covenant, + }, + transactions::transaction::{TransactionInput, TransactionOutput}, +}; + +pub fn setup_filter_test<'a, F>( + covenant: &Covenant, + input: &'a TransactionInput, + block_height: u64, + output_mod: F, +) -> (CovenantContext<'a>, Vec) +where + F: FnOnce(&mut Vec), +{ + let mut context = create_context(&covenant, input, block_height); + // Consume root token (i.e the filter we're testing), args for filter presumably come next + context.next_filter().unwrap(); + let mut outputs = create_outputs(10, Default::default()); + output_mod(&mut outputs); + (context, outputs) +} diff --git a/base_layer/core/src/covenants/filters/xor.rs b/base_layer/core/src/covenants/filters/xor.rs new file mode 100644 index 0000000000..b93f946a3f --- /dev/null +++ b/base_layer/core/src/covenants/filters/xor.rs @@ -0,0 +1,69 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 crate::covenants::{context::CovenantContext, error::CovenantError, filters::Filter, output_set::OutputSet}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct XorFilter; + +impl Filter for XorFilter { + fn filter(&self, context: &mut CovenantContext<'_>, output_set: &mut OutputSet<'_>) -> Result<(), CovenantError> { + let a = context.require_next_filter()?; + let mut output_set_a = output_set.clone(); + a.filter(context, &mut output_set_a)?; + + let b = context.require_next_filter()?; + let mut output_set_b = output_set.clone(); + b.filter(context, &mut output_set_b)?; + + output_set.set(output_set_a.symmetric_difference(output_set_b)); + Ok(()) + } +} + +#[cfg(test)] +mod test { + + use super::*; + use crate::{ + covenant, + covenants::{filters::test::setup_filter_test, test::create_input}, + }; + + #[test] + fn it_filters_outputset_using_symmetric_difference() { + let bytes = vec![0xab, 0xcd, 0xef]; + let covenant = covenant!(or(field_eq(@field::features_maturity, @uint(42),), field_eq(@field::features_unique_id, @bytes(bytes.clone())))); + let input = create_input(); + let (mut context, outputs) = setup_filter_test(&covenant, &input, 0, |outputs| { + outputs[5].features.maturity = 42; + outputs[5].features.unique_id = Some(bytes.clone()); + outputs[7].features.maturity = 42; + outputs[8].features.unique_id = Some(bytes.clone()); + }); + let mut output_set = OutputSet::new(&outputs); + XorFilter.filter(&mut context, &mut output_set).unwrap(); + + assert_eq!(output_set.len(), 2); + assert_eq!(output_set.get_selected_indexes(), vec![7, 8]); + } +} diff --git a/base_layer/core/src/covenants/macros.rs b/base_layer/core/src/covenants/macros.rs new file mode 100644 index 0000000000..c3b4a087b4 --- /dev/null +++ b/base_layer/core/src/covenants/macros.rs @@ -0,0 +1,228 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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. + +/// Simple syntax for expressing covenants. +/// +/// ```rust,ignore +/// // Before height 42, this may only be spent into an output with flag 8 (NON_FUNGIBLE) +/// let covenant = covenant!(or(absolute_height(@uint(42)), field_eq(@field::features_flags, @uint(8))); +/// covenant.execute(...)?; +/// ``` +#[macro_export] +macro_rules! covenant { + ($token:ident($($args:tt)*)) => {{ + let mut covenant = $crate::covenants::Covenant::new(); + $crate::__covenant_inner!(@ { covenant } $token($($args)*)); + covenant + }}; + + ($token:ident()) => {{ + let mut covenant = $crate::covenants::Covenant::new(); + $crate::__covenant_inner!(@ { covenant } $token()); + covenant + }}; + + () => { $crate::covenants::Covenant::new() }; +} + +#[macro_export] +macro_rules! __covenant_inner { + (@ { $covenant:ident }) => {}; + + // token() + (@ { $covenant:ident } $token:ident() $(,)?) => { + $covenant.push_token($crate::covenants::CovenantToken::$token()); + }; + + // @field::name, ... + (@ { $covenant:ident } @field::$field:ident, $($tail:tt)*) => { + $covenant.push_token($crate::covenants::CovenantToken::field($crate::covenants::OutputField::$field())); + $crate::__covenant_inner!(@ { $covenant } $($tail)*) + }; + + // @field::name + (@ { $covenant:ident } @field::$field:ident $(,)?) => { + $crate::__covenant_inner!(@ { $covenant } @field::$field,) + }; + + // @fields(@field::name, ...) + (@ { $covenant:ident } @fields($(@field::$field:ident),+ $(,)?)) => { + $crate::__covenant_inner!(@ { $covenant } @fields($(@field::$field),+),) + }; + + // @fields(@field::name, ...), ... + (@ { $covenant:ident } @fields($(@field::$field:ident),+ $(,)?), $($tail:tt)*) => { + $covenant.push_token($crate::covenants::CovenantToken::fields(vec![ + $($crate::covenants::OutputField::$field()),+ + ])); + $crate::__covenant_inner!(@ { $covenant } $($tail)*) + }; + + // @covenant(...), ... + (@ { $covenant:ident } @covenant($($inner:tt)*), $($tail:tt)*) => { + let inner = $crate::covenant!($($inner)*); + $covenant.push_token($crate::covenants::CovenantToken::covenant(inner)); + $crate::__covenant_inner!(@ { $covenant } $($tail)*) + }; + + // @covenant(...) + (@ { $covenant:ident } @covenant($($inner:tt)*) $(,)?) => { + $crate::__covenant_inner!(@ { $covenant } @covenant($($inner)*),) + }; + + // @arg(expr1, expr2, ...), ... + (@ { $covenant:ident } @$arg:ident($($args:expr),* $(,)?), $($tail:tt)*) => { + $covenant.push_token($crate::covenants::CovenantToken::$arg($($args),*)); + $crate::__covenant_inner!(@ { $covenant } $($tail)*) + }; + + // @arg(expr1, expr2, ...) + (@ { $covenant:ident } @$arg:ident($($args:expr),* $(,)?) $(,)?) => { + $crate::__covenant_inner!(@ { $covenant } @$arg($($args),*),) + }; + + // token(), ... + (@ { $covenant:ident } $token:ident(), $($tail:tt)*) => { + $covenant.push_token($crate::covenants::CovenantToken::$token()); + $crate::__covenant_inner!(@ { $covenant } $($tail)*) + }; + // token(filter1, filter2, ...) + (@ { $covenant:ident } $token:ident($($args:tt)+)) => { + $crate::__covenant_inner!(@ { $covenant } $token($($args)+),) + }; + + // token(filter1, filter2, ...), ... + (@ { $covenant:ident } $token:ident($($args:tt)+), $($tail:tt)*) => { + $covenant.push_token($crate::covenants::CovenantToken::$token()); + $crate::__covenant_inner!(@ { $covenant } $($args)+ $($tail)*) + }; + + // token(...) + (@ { $covenant:ident } $token:ident($($args:tt)+)) => { + $covenant.push_token($crate::covenants::CovenantToken::$token()); + $crate::__covenant_inner!(@ { $covenant } $($args)+) + }; +} + +#[cfg(test)] +mod test { + use tari_common_types::types::PublicKey; + use tari_crypto::script; + use tari_test_utils::unpack_enum; + use tari_utilities::hex::{from_hex, Hex}; + + use crate::{ + consensus::{ConsensusDecoding, ToConsensusBytes}, + covenants::{arguments::CovenantArg, filters::CovenantFilter, token::CovenantToken, Covenant}, + }; + + #[test] + fn simple() { + let covenant = covenant!(identity()); + assert_eq!(covenant.tokens().len(), 1); + assert!(matches!( + covenant.tokens()[0], + CovenantToken::Filter(CovenantFilter::Identity(_)) + )); + } + + #[test] + fn fields() { + let covenant = + covenant!(and(identity(), fields_preserved(@fields(@field::commitment, @field::sender_offset_public_key)))); + assert_eq!(covenant.to_consensus_bytes().to_hex(), "21203108020002"); + } + + #[test] + fn hash() { + let hash_str = "53563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd"; + let hash_vec = from_hex(hash_str).unwrap(); + let mut hash = [0u8; 32]; + hash.copy_from_slice(hash_vec.as_slice()); + let covenant = covenant!(output_hash_eq(@hash(hash.clone()))); + assert_eq!(covenant.to_consensus_bytes().to_hex(), format!("3001{}", hash_str)); + + let covenant = covenant!(and( + identity(), + or( + identity(), + fields_preserved(@hash(hash),) + ) + )); + assert_eq!( + covenant.to_consensus_bytes().to_hex(), + "21202220310153563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd" + ); + } + + #[test] + fn nested() { + let covenant = covenant!(xor( + identity(), + and(identity(), and(not(identity(),), and(identity(), identity()))) + )); + assert_eq!(covenant.to_consensus_bytes().to_hex(), "23202120212420212020"); + let h = from_hex("53563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd").unwrap(); + let mut hash = [0u8; 32]; + hash.copy_from_slice(h.as_slice()); + let covenant = covenant!(and( + or( + identity(), + fields_hashed_eq( + @fields(@field::commitment, @field::features_metadata), + @hash(hash), + ), + ), + field_eq(@field::features_maturity, @uint(42)) + )); + assert_eq!( + covenant.to_consensus_bytes().to_hex(), + "21222032080200090153563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd330706062a" + ); + } + + #[test] + fn covenant() { + let bytes = vec![0xba, 0xda, 0x55]; + let covenant = covenant!(field_eq(@field::covenant, @covenant(and(field_eq(@field::features_unique_id, @bytes(bytes), identity()))))); + assert_eq!(covenant.to_consensus_bytes().to_hex(), "330703050a213307070903bada5520"); + } + + #[test] + fn script() { + let hash = "53563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd"; + let hash = { + let mut buf = [0u8; 32]; + buf.copy_from_slice(from_hex(hash).unwrap().as_slice()); + buf + }; + let dest_pk = PublicKey::from_hex("b0c1f788f137ba0cdc0b61e89ee43b80ebf5cca4136d3229561bf11eba347849").unwrap(); + let sender_pk = dest_pk.clone(); + let script = script!(HashSha256 PushHash(Box::new(hash)) Equal IfThen PushPubKey(Box::new(dest_pk)) Else CheckHeightVerify(100) PushPubKey(Box::new(sender_pk)) EndIf); + let covenant = covenant!(field_eq(@field::script, @script(script.clone()))); + + let decoded = Covenant::consensus_decode(&mut covenant.to_consensus_bytes().as_slice()).unwrap(); + assert_eq!(covenant, decoded); + unpack_enum!(CovenantArg::TariScript(decoded_script) = decoded.tokens()[2].as_arg().unwrap()); + assert_eq!(script, *decoded_script); + } +} diff --git a/base_layer/core/src/covenants/mod.rs b/base_layer/core/src/covenants/mod.rs new file mode 100644 index 0000000000..fff53dbfb2 --- /dev/null +++ b/base_layer/core/src/covenants/mod.rs @@ -0,0 +1,52 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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. + +//! # Covenants +//! +//! Allows rules to be specified that restrict _future_ spending of subsequent transactions. +//! +//! https://rfc.tari.com/RFC-0250_Covenants.html + +mod arguments; +mod byte_codes; +mod context; +mod covenant; +mod decoder; +mod encoder; +mod error; +mod fields; +mod filters; +mod output_set; +mod token; + +pub use covenant::Covenant; +// Used in macro +#[allow(unused_imports)] +pub(self) use fields::OutputField; +#[allow(unused_imports)] +pub(self) use token::CovenantToken; + +#[macro_use] +mod macros; + +#[cfg(test)] +mod test; diff --git a/base_layer/core/src/covenants/output_set.rs b/base_layer/core/src/covenants/output_set.rs new file mode 100644 index 0000000000..f086214614 --- /dev/null +++ b/base_layer/core/src/covenants/output_set.rs @@ -0,0 +1,172 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 std::{ + cmp::Ordering, + collections::BTreeSet, + iter::FromIterator, + ops::{Deref, DerefMut}, +}; + +use crate::{covenants::error::CovenantError, transactions::transaction::TransactionOutput}; + +#[derive(Debug, Clone)] +pub struct OutputSet<'a>(BTreeSet>); + +impl<'a> OutputSet<'a> { + pub fn new(outputs: &'a [TransactionOutput]) -> Self { + // This sets the internal index for each output + // Note there is no publicly accessible way to modify the indexes + outputs.iter().enumerate().collect() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn set(&mut self, new_set: Self) { + *self = new_set; + } + + pub fn retain(&mut self, mut f: F) -> Result<(), CovenantError> + where F: FnMut(&'a TransactionOutput) -> Result { + let mut err = None; + self.0.retain(|output| match f(**output) { + Ok(b) => b, + Err(e) => { + // Theres no way to stop retain early, so keep the error for when this completes + err = Some(e); + false + }, + }); + match err { + Some(err) => Err(err), + None => Ok(()), + } + } + + pub fn union(&self, other: &Self) -> Self { + self.0.union(&other.0).copied().collect() + } + + pub fn difference(&self, other: &Self) -> Self { + self.0.difference(&other.0).copied().collect() + } + + pub fn symmetric_difference(&self, other: Self) -> Self { + self.0.symmetric_difference(&other.0).copied().collect() + } + + pub fn find_inplace(&mut self, mut pred: F) + where F: FnMut(&TransactionOutput) -> bool { + match self.0.iter().find(|indexed| pred(&**indexed)) { + Some(output) => { + let output = *output; + self.clear(); + self.0.insert(output); + }, + None => { + self.clear(); + }, + } + } + + pub fn clear(&mut self) { + self.0.clear(); + } + + #[cfg(test)] + pub(super) fn get(&self, index: usize) -> Option<&TransactionOutput> { + self.0 + .iter() + .find(|output| output.index == index) + .map(|output| **output) + } + + #[cfg(test)] + pub(super) fn get_selected_indexes(&self) -> Vec { + self.0.iter().map(|idx| idx.index).collect() + } +} + +impl<'a> FromIterator<(usize, &'a TransactionOutput)> for OutputSet<'a> { + fn from_iter>(iter: T) -> Self { + iter.into_iter().map(|(i, output)| Indexed::new(i, output)).collect() + } +} + +impl<'a> FromIterator> for OutputSet<'a> { + fn from_iter>>(iter: T) -> Self { + Self(iter.into_iter().collect()) + } +} + +/// A simple wrapper struct that implements PartialEq and PartialOrd using a numeric index +#[derive(Debug, Clone, Copy)] +struct Indexed { + index: usize, + value: T, +} + +impl Indexed { + pub fn new(index: usize, value: T) -> Self { + Self { index, value } + } +} + +impl PartialEq for Indexed { + fn eq(&self, other: &Self) -> bool { + self.index == other.index + } +} + +impl Eq for Indexed {} + +impl PartialOrd for Indexed { + fn partial_cmp(&self, other: &Self) -> Option { + self.index.partial_cmp(&other.index) + } +} + +impl Ord for Indexed { + fn cmp(&self, other: &Self) -> Ordering { + self.index.cmp(&other.index) + } +} + +impl Deref for Indexed { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl DerefMut for Indexed { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.value + } +} diff --git a/base_layer/core/src/covenants/test.rs b/base_layer/core/src/covenants/test.rs new file mode 100644 index 0000000000..f881bd7445 --- /dev/null +++ b/base_layer/core/src/covenants/test.rs @@ -0,0 +1,52 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 std::iter; + +use crate::{ + covenants::{context::CovenantContext, Covenant}, + transactions::{ + test_helpers::{TestParams, UtxoTestParams}, + transaction::{TransactionInput, TransactionOutput}, + }, +}; + +pub fn create_outputs(n: usize, utxo_params: UtxoTestParams) -> Vec { + iter::repeat_with(|| { + let params = TestParams::new(); + let output = params.create_unblinded_output(utxo_params.clone()); + output.as_transaction_output(&Default::default()).unwrap() + }) + .take(n) + .collect() +} + +pub fn create_input() -> TransactionInput { + let params = TestParams::new(); + let output = params.create_unblinded_output(Default::default()); + output.as_transaction_input(&Default::default()).unwrap() +} + +pub fn create_context<'a>(covenant: &Covenant, input: &'a TransactionInput, block_height: u64) -> CovenantContext<'a> { + let tokens = covenant.tokens().to_vec(); + CovenantContext::new(tokens.into(), input, block_height) +} diff --git a/base_layer/core/src/covenants/token.rs b/base_layer/core/src/covenants/token.rs new file mode 100644 index 0000000000..d93e12e2d5 --- /dev/null +++ b/base_layer/core/src/covenants/token.rs @@ -0,0 +1,220 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 std::{collections::VecDeque, io, iter::FromIterator}; + +use tari_common_types::types::{Commitment, PublicKey}; +use tari_crypto::script::TariScript; + +use crate::covenants::{ + arguments::{CovenantArg, Hash}, + decoder::{CovenantDecodeError, CovenentReadExt}, + fields::OutputField, + filters::{ + AbsoluteHeightFilter, + AndFilter, + CovenantFilter, + FieldEqFilter, + FieldsHashedEqFilter, + FieldsPreservedFilter, + IdentityFilter, + NotFilter, + OrFilter, + OutputHashEqFilter, + XorFilter, + }, + Covenant, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CovenantToken { + Filter(CovenantFilter), + Arg(CovenantArg), +} + +impl CovenantToken { + pub fn read_from(reader: &mut R) -> Result, CovenantDecodeError> { + let code = match reader.read_next_byte_code()? { + Some(c) => c, + // Nothing further to read + None => return Ok(None), + }; + match code { + code if CovenantFilter::is_valid_code(code) => { + let filter = CovenantFilter::try_from_byte_code(code)?; + Ok(Some(CovenantToken::Filter(filter))) + }, + code if CovenantArg::is_valid_code(code) => { + let arg = CovenantArg::read_from(reader, code)?; + Ok(Some(CovenantToken::Arg(arg))) + }, + code => Err(CovenantDecodeError::UnknownByteCode { code }), + } + } + + pub fn write_to(&self, writer: &mut W) -> Result { + match self { + CovenantToken::Filter(filter) => filter.write_to(writer), + CovenantToken::Arg(arg) => arg.write_to(writer), + } + } + + pub fn as_filter(&self) -> Option<&CovenantFilter> { + match self { + CovenantToken::Filter(filter) => Some(filter), + CovenantToken::Arg(_) => None, + } + } + + pub fn as_arg(&self) -> Option<&CovenantArg> { + match self { + CovenantToken::Filter(_) => None, + CovenantToken::Arg(arg) => Some(arg), + } + } + + //---------------------------------- Macro helper functions --------------------------------------------// + + #[allow(dead_code)] + pub(super) fn identity() -> Self { + CovenantToken::Filter(CovenantFilter::Identity(IdentityFilter)) + } + + #[allow(dead_code)] + pub(super) fn and() -> Self { + CovenantToken::Filter(CovenantFilter::And(AndFilter)) + } + + #[allow(dead_code)] + pub(super) fn or() -> Self { + CovenantToken::Filter(CovenantFilter::Or(OrFilter)) + } + + #[allow(dead_code)] + pub(super) fn xor() -> Self { + CovenantToken::Filter(CovenantFilter::Xor(XorFilter)) + } + + #[allow(dead_code)] + pub(super) fn not() -> Self { + CovenantToken::Filter(CovenantFilter::Not(NotFilter)) + } + + #[allow(dead_code)] + pub(super) fn output_hash_eq() -> Self { + CovenantToken::Filter(CovenantFilter::OutputHashEq(OutputHashEqFilter)) + } + + #[allow(dead_code)] + pub(super) fn fields_preserved() -> Self { + CovenantToken::Filter(CovenantFilter::FieldsPreserved(FieldsPreservedFilter)) + } + + #[allow(dead_code)] + pub(super) fn field_eq() -> Self { + CovenantToken::Filter(CovenantFilter::FieldEq(FieldEqFilter)) + } + + #[allow(dead_code)] + pub(super) fn fields_hashed_eq() -> Self { + CovenantToken::Filter(CovenantFilter::FieldsHashedEq(FieldsHashedEqFilter)) + } + + #[allow(dead_code)] + pub(super) fn absolute_height() -> Self { + CovenantToken::Filter(CovenantFilter::AbsoluteHeight(AbsoluteHeightFilter)) + } + + #[allow(dead_code)] + pub(super) fn hash(hash: Hash) -> Self { + CovenantToken::Arg(CovenantArg::Hash(hash)) + } + + #[allow(dead_code)] + pub(super) fn public_key(public_key: PublicKey) -> Self { + CovenantToken::Arg(CovenantArg::PublicKey(public_key)) + } + + #[allow(dead_code)] + pub(super) fn commitment(commitment: Commitment) -> Self { + CovenantToken::Arg(CovenantArg::Commitment(commitment)) + } + + #[allow(dead_code)] + pub(super) fn script(script: TariScript) -> Self { + CovenantToken::Arg(CovenantArg::TariScript(script)) + } + + #[allow(dead_code)] + pub(super) fn covenant(covenant: Covenant) -> Self { + CovenantToken::Arg(CovenantArg::Covenant(covenant)) + } + + #[allow(dead_code)] + pub(super) fn uint(val: u64) -> Self { + CovenantToken::Arg(CovenantArg::Uint(val)) + } + + #[allow(dead_code)] + pub(super) fn field(field: OutputField) -> Self { + CovenantToken::Arg(CovenantArg::OutputField(field)) + } + + #[allow(dead_code)] + pub(super) fn fields(fields: Vec) -> Self { + CovenantToken::Arg(CovenantArg::OutputFields(fields.into())) + } + + #[allow(dead_code)] + pub(super) fn bytes(bytes: Vec) -> Self { + CovenantToken::Arg(CovenantArg::Bytes(bytes)) + } +} + +#[derive(Debug, Clone, Default)] +pub struct CovenantTokenCollection { + tokens: VecDeque, +} + +impl CovenantTokenCollection { + pub fn is_empty(&self) -> bool { + self.tokens.is_empty() + } + + pub fn next(&mut self) -> Option { + self.tokens.pop_front() + } +} + +impl FromIterator for CovenantTokenCollection { + fn from_iter>(iter: T) -> Self { + Self { + tokens: iter.into_iter().collect(), + } + } +} + +impl From> for CovenantTokenCollection { + fn from(tokens: Vec) -> Self { + Self { tokens: tokens.into() } + } +} diff --git a/base_layer/core/src/lib.rs b/base_layer/core/src/lib.rs index 8d4a080483..d4fe3dc091 100644 --- a/base_layer/core/src/lib.rs +++ b/base_layer/core/src/lib.rs @@ -19,7 +19,6 @@ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, // 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. - #![cfg_attr(not(debug_assertions), deny(unused_variables))] #![cfg_attr(not(debug_assertions), deny(unused_imports))] #![cfg_attr(not(debug_assertions), deny(dead_code))] @@ -35,6 +34,8 @@ pub mod blocks; #[cfg(feature = "base_node")] pub mod chain_storage; pub mod consensus; +#[macro_use] +pub mod covenants; #[cfg(feature = "base_node")] pub mod iterators; pub mod proof_of_work; diff --git a/base_layer/core/src/transactions/aggregated_body.rs b/base_layer/core/src/transactions/aggregated_body.rs index b4673b3f5e..5c437e16c7 100644 --- a/base_layer/core/src/transactions/aggregated_body.rs +++ b/base_layer/core/src/transactions/aggregated_body.rs @@ -45,7 +45,7 @@ use tari_crypto::{ }; use crate::{ - consensus::{ConsensusEncodingSized, ConsensusEncodingWrapper}, + consensus::ConsensusEncodingSized, transactions::{ crypto_factories::CryptoFactories, tari_amount::MicroTari, @@ -491,10 +491,7 @@ impl AggregateBody { pub fn sum_metadata_size(&self) -> usize { self.outputs .iter() - .map(|o| { - o.features.consensus_encode_exact_size() + - ConsensusEncodingWrapper::wrap(&o.script).consensus_encode_exact_size() - }) + .map(|o| o.features.consensus_encode_exact_size() + o.script.consensus_encode_exact_size()) .sum() } diff --git a/base_layer/core/src/transactions/test_helpers.rs b/base_layer/core/src/transactions/test_helpers.rs index d3a7390a1b..d80c8b472a 100644 --- a/base_layer/core/src/transactions/test_helpers.rs +++ b/base_layer/core/src/transactions/test_helpers.rs @@ -36,7 +36,7 @@ use tari_crypto::{ }; use crate::{ - consensus::{ConsensusEncodingSized, ConsensusEncodingWrapper, ConsensusManager}, + consensus::{ConsensusEncodingSized, ConsensusManager}, transactions::{ crypto_factories::CryptoFactories, fee::Fee, @@ -344,8 +344,7 @@ pub struct TransactionSchema { } fn default_metadata_byte_size() -> usize { - OutputFeatures::default().consensus_encode_exact_size() + - ConsensusEncodingWrapper::wrap(&script![Nop]).consensus_encode_exact_size() + OutputFeatures::default().consensus_encode_exact_size() + script![Nop].consensus_encode_exact_size() } /// Create an unconfirmed transaction for testing with a valid fee, unique access_sig, random inputs and outputs, the diff --git a/base_layer/core/src/transactions/transaction/output_features.rs b/base_layer/core/src/transactions/transaction/output_features.rs index acb846d919..e5c041fb4e 100644 --- a/base_layer/core/src/transactions/transaction/output_features.rs +++ b/base_layer/core/src/transactions/transaction/output_features.rs @@ -28,7 +28,6 @@ use std::{ io::{Read, Write}, }; -use integer_encoding::{VarInt, VarIntReader, VarIntWriter}; use serde::{Deserialize, Serialize}; use tari_common_types::types::{Commitment, PublicKey}; use tari_utilities::ByteArray; @@ -179,8 +178,8 @@ impl OutputFeatures { impl ConsensusEncoding for OutputFeatures { fn consensus_encode(&self, writer: &mut W) -> Result { - let mut written = writer.write_varint(Self::CONSENSUS_ENCODING_VERSION)?; - written += writer.write_varint(self.maturity)?; + let mut written = Self::CONSENSUS_ENCODING_VERSION.consensus_encode(writer)?; + written += self.maturity.consensus_encode(writer)?; written += self.flags.consensus_encode(writer)?; Ok(written) } @@ -188,16 +187,16 @@ impl ConsensusEncoding for OutputFeatures { impl ConsensusEncodingSized for OutputFeatures { fn consensus_encode_exact_size(&self) -> usize { - Self::CONSENSUS_ENCODING_VERSION.required_space() + + Self::CONSENSUS_ENCODING_VERSION.consensus_encode_exact_size() + self.flags.consensus_encode_exact_size() + - self.maturity.required_space() + self.maturity.consensus_encode_exact_size() } } impl ConsensusDecoding for OutputFeatures { fn consensus_decode(reader: &mut R) -> Result { // Changing the order of these operations is consensus breaking - let version = reader.read_varint::()?; + let version = u8::consensus_decode(reader)?; if version != Self::CONSENSUS_ENCODING_VERSION { return Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -208,8 +207,8 @@ impl ConsensusDecoding for OutputFeatures { ), )); } - // Decode safety: read_varint will stop reading the varint after 10 bytes - let maturity = reader.read_varint()?; + // Decode safety: consensus_decode will stop reading the varint after 10 bytes + let maturity = u64::consensus_decode(reader)?; let flags = OutputFlags::consensus_decode(reader)?; Ok(Self { flags, diff --git a/base_layer/core/src/transactions/transaction/unblinded_output.rs b/base_layer/core/src/transactions/transaction/unblinded_output.rs index ce7caa5237..33d921eea5 100644 --- a/base_layer/core/src/transactions/transaction/unblinded_output.rs +++ b/base_layer/core/src/transactions/transaction/unblinded_output.rs @@ -37,7 +37,7 @@ use tari_crypto::{ }; use crate::{ - consensus::{ConsensusEncodingSized, ConsensusEncodingWrapper}, + consensus::ConsensusEncodingSized, transactions::{ tari_amount::MicroTari, transaction, @@ -191,8 +191,7 @@ impl UnblindedOutput { } pub fn metadata_byte_size(&self) -> usize { - self.features.consensus_encode_exact_size() + - ConsensusEncodingWrapper::wrap(&self.script).consensus_encode_exact_size() + self.features.consensus_encode_exact_size() + self.script.consensus_encode_exact_size() } // Note: The Hashable trait is not used here due to the dependency on `CryptoFactories`, and `commitment` us not diff --git a/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs b/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs index e379f77286..d5d2ee36c6 100644 --- a/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs +++ b/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs @@ -41,7 +41,7 @@ use tari_crypto::{ }; use crate::{ - consensus::{ConsensusConstants, ConsensusEncodingSized, ConsensusEncodingWrapper}, + consensus::{ConsensusConstants, ConsensusEncodingSized}, transactions::{ crypto_factories::CryptoFactories, fee::Fee, @@ -280,10 +280,7 @@ impl SenderTransactionInitializer { size += self .sender_custom_outputs .iter() - .map(|o| { - o.features.consensus_encode_exact_size() + - ConsensusEncodingWrapper::wrap(&o.script).consensus_encode_exact_size() - }) + .map(|o| o.features.consensus_encode_exact_size() + o.script.consensus_encode_exact_size()) .sum::(); // TODO: implement iter for FixedSet to avoid the clone size += self @@ -291,7 +288,7 @@ impl SenderTransactionInitializer { .clone() .into_vec() .iter() - .map(|script| ConsensusEncodingWrapper::wrap(script).consensus_encode_exact_size()) + .map(|script| script.consensus_encode_exact_size()) .sum::(); size @@ -322,7 +319,7 @@ impl SenderTransactionInitializer { let change_metadata_size = self .change_script .as_ref() - .map(|script| ConsensusEncodingWrapper::wrap(script).consensus_encode_exact_size()) + .map(|script| script.consensus_encode_exact_size()) .unwrap_or(0) + output_features.consensus_encode_exact_size(); diff --git a/base_layer/core/src/validation/helpers.rs b/base_layer/core/src/validation/helpers.rs index ce48ca26ea..4ca646fbab 100644 --- a/base_layer/core/src/validation/helpers.rs +++ b/base_layer/core/src/validation/helpers.rs @@ -37,13 +37,7 @@ use tari_crypto::{ use crate::{ blocks::{Block, BlockHeader, BlockHeaderValidationError, BlockValidationError}, chain_storage::{BlockchainBackend, MmrRoots, MmrTree}, - consensus::{ - emission::Emission, - ConsensusConstants, - ConsensusEncodingSized, - ConsensusEncodingWrapper, - ConsensusManager, - }, + consensus::{emission::Emission, ConsensusConstants, ConsensusEncodingSized, ConsensusManager}, proof_of_work::{ monero_difficulty, monero_rx::MoneroPowData, @@ -505,7 +499,7 @@ pub fn check_outputs( /// Checks the byte size of TariScript is less than or equal to the given size, otherwise returns an error. pub fn check_tari_script_byte_size(script: &TariScript, max_script_size: usize) -> Result<(), ValidationError> { - let script_size = ConsensusEncodingWrapper::wrap(script).consensus_encode_exact_size(); + let script_size = script.consensus_encode_exact_size(); if script_size > max_script_size { return Err(ValidationError::TariScriptExceedsMaxSize { max_script_size, diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 4cb9b2e29c..0ef8f0362a 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -33,7 +33,7 @@ use tari_common_types::{ }; use tari_comms::{types::CommsPublicKey, NodeIdentity}; use tari_core::{ - consensus::{ConsensusConstants, ConsensusEncodingSized, ConsensusEncodingWrapper}, + consensus::{ConsensusConstants, ConsensusEncodingSized}, proto::base_node::FetchMatchingUtxos, transactions::{ fee::Fee, @@ -629,8 +629,8 @@ where ); // TODO: Include asset metadata here if required // We assume that default OutputFeatures and Nop TariScript is used - let metadata_byte_size = OutputFeatures::default().consensus_encode_exact_size() + - ConsensusEncodingWrapper::wrap(&script![Nop]).consensus_encode_exact_size(); + let metadata_byte_size = + OutputFeatures::default().consensus_encode_exact_size() + script![Nop].consensus_encode_exact_size(); let utxo_selection = self .select_utxos( @@ -671,8 +671,8 @@ where fee_per_gram, ); let output_features = OutputFeatures::default(); - let metadata_byte_size = output_features.consensus_encode_exact_size() + - ConsensusEncodingWrapper::wrap(&recipient_script).consensus_encode_exact_size(); + let metadata_byte_size = + output_features.consensus_encode_exact_size() + recipient_script.consensus_encode_exact_size(); let input_selection = self .select_utxos(amount, fee_per_gram, 1, metadata_byte_size, None, unique_id.as_ref()) @@ -858,7 +858,10 @@ where let metadata_byte_size = outputs.iter().fold(0usize, |total, output| { total + output.features.consensus_encode_exact_size() + - ConsensusEncodingWrapper::wrap(output.script.as_ref().unwrap_or(&nop_script)) + output + .script + .as_ref() + .unwrap_or(&nop_script) .consensus_encode_exact_size() }); let input_selection = self @@ -1004,8 +1007,7 @@ where unique_id: unique_id.clone(), ..Default::default() }; - let metadata_byte_size = output_features.consensus_encode_exact_size() + - ConsensusEncodingWrapper::wrap(&script).consensus_encode_exact_size(); + let metadata_byte_size = output_features.consensus_encode_exact_size() + script.consensus_encode_exact_size(); let input_selection = self .select_utxos(amount, fee_per_gram, 1, metadata_byte_size, None, unique_id.as_ref()) @@ -1227,8 +1229,8 @@ where trace!(target: LOG_TARGET, "We found {} UTXOs to select from", uo.len()); // Assumes that default Outputfeatures are used for change utxo - let default_metadata_size = OutputFeatures::default().consensus_encode_exact_size() + - ConsensusEncodingWrapper::wrap(&script![Nop]).consensus_encode_exact_size(); + let default_metadata_size = + OutputFeatures::default().consensus_encode_exact_size() + script![Nop].consensus_encode_exact_size(); let mut requires_change_output = false; for o in uo { utxos_total_value += o.unblinded_output.value; @@ -1306,8 +1308,7 @@ where let output_count = split_count; let script = script!(Nop); let output_features = OutputFeatures::default(); - let metadata_byte_size = output_features.consensus_encode_exact_size() + - ConsensusEncodingWrapper::wrap(&script).consensus_encode_exact_size(); + let metadata_byte_size = output_features.consensus_encode_exact_size() + script.consensus_encode_exact_size(); let total_split_amount = amount_per_split * split_count as u64; let input_selection = self diff --git a/base_layer/wallet/tests/output_manager_service/service.rs b/base_layer/wallet/tests/output_manager_service/service.rs index 11e0d065da..c86f3a9089 100644 --- a/base_layer/wallet/tests/output_manager_service/service.rs +++ b/base_layer/wallet/tests/output_manager_service/service.rs @@ -34,7 +34,7 @@ use tari_comms::{ use tari_core::{ base_node::rpc::BaseNodeWalletRpcServer, blocks::BlockHeader, - consensus::{ConsensusConstantsBuilder, ConsensusEncodingSized, ConsensusEncodingWrapper}, + consensus::{ConsensusConstantsBuilder, ConsensusEncodingSized}, proto::base_node::{QueryDeletedResponse, UtxoQueryResponse, UtxoQueryResponses}, transactions::{ fee::Fee, @@ -91,8 +91,7 @@ use crate::support::{ }; fn default_metadata_byte_size() -> usize { - OutputFeatures::default().consensus_encode_exact_size() + - ConsensusEncodingWrapper::wrap(&script![Nop]).consensus_encode_exact_size() + OutputFeatures::default().consensus_encode_exact_size() + script![Nop].consensus_encode_exact_size() } #[allow(clippy::type_complexity)]