Skip to content

Commit

Permalink
feat: add documentation to covenant crate (#5524)
Browse files Browse the repository at this point in the history
Description
---
Adds documentation to covenant crate.

Motivation and Context
---
Auditing.

How Has This Been Tested?
---

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

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


Breaking Changes
---

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

<!-- Does this include a breaking change? If so, include this line as a
footer -->
<!-- BREAKING CHANGE: Description what the user should do, e.g. delete a
database, resync the chain -->
  • Loading branch information
jorgeantonio21 authored Jun 28, 2023
1 parent 84d068e commit 442d75b
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 21 deletions.
35 changes: 16 additions & 19 deletions base_layer/core/src/covenants/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const MAX_COVENANT_ARG_SIZE: usize = 4096;
const MAX_BYTES_ARG_SIZE: usize = 4096;

#[derive(Debug, Clone, PartialEq, Eq)]
/// Covenant arguments
pub enum CovenantArg {
Hash(FixedHash),
PublicKey(PublicKey),
Expand All @@ -66,10 +67,12 @@ pub enum CovenantArg {
}

impl CovenantArg {
/// Checks if a stream of bytes results in valid argument code
pub fn is_valid_code(code: u8) -> bool {
byte_codes::is_valid_arg_code(code)
}

/// Reads a `CovenantArg` from a buffer of bytes
pub fn read_from(reader: &mut &[u8], code: u8) -> Result<Self, CovenantDecodeError> {
use byte_codes::*;
match code {
Expand Down Expand Up @@ -126,6 +129,7 @@ impl CovenantArg {
}
}

/// Parses the `CovenantArg` data to bytes and writes it to an IO writer
pub fn write_to<W: io::Write>(&self, writer: &mut W) -> Result<(), io::Error> {
use byte_codes::*;
#[allow(clippy::enum_glob_use)]
Expand Down Expand Up @@ -180,8 +184,17 @@ impl CovenantArg {
}
}

/// `require_x_impl!` is a helper macro that generates an implementation of a function with a specific signature
/// based on the provided input parameters. Functionality:
/// The macro expects to receive either three or four arguments.
/// $name, represents the name of the function to be generated.
/// $output, represents the name of the enum variant that the function will match against.
/// $expected, represents an expression that will be used in the error message when the provided argument
/// does not match the expected variant.
/// (optional) $output_type, represents the type that the function will return. If
/// not provided, it defaults to the same as $output.
macro_rules! require_x_impl {
($name:ident, $output:ident, $expected: expr, $output_type:ident) => {
($name:ident, $output:ident, $expected: expr, $output_type:ty) => {
#[allow(dead_code)]
pub(super) fn $name(self) -> Result<$output_type, CovenantError> {
match self {
Expand Down Expand Up @@ -215,25 +228,9 @@ impl CovenantArg {

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

pub fn require_bytes(self) -> Result<Vec<u8>, CovenantError> {
match self {
CovenantArg::Bytes(val) => Ok(val),
got => Err(CovenantError::UnexpectedArgument {
expected: "bytes",
got: got.to_string(),
}),
}
}
require_x_impl!(require_bytes, Bytes, "bytes", Vec<u8>);

pub fn require_uint(self) -> Result<u64, CovenantError> {
match self {
CovenantArg::Uint(val) => Ok(val),
got => Err(CovenantError::UnexpectedArgument {
expected: "uint",
got: got.to_string(),
}),
}
}
require_x_impl!(require_uint, Uint, "u64", u64);
}

impl Display for CovenantArg {
Expand Down
34 changes: 33 additions & 1 deletion base_layer/core/src/covenants/byte_codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub(super) fn is_valid_arg_code(code: u8) -> bool {
ALL_ARGS.contains(&code)
}

/// Array with all possible covenant arg byte codes.
pub(super) const ALL_ARGS: [u8; 10] = [
ARG_HASH,
ARG_PUBLIC_KEY,
Expand All @@ -38,23 +39,34 @@ pub(super) const ALL_ARGS: [u8; 10] = [
ARG_OUTPUT_TYPE,
];

/// Covenant arg hash byte code.
pub const ARG_HASH: u8 = 0x01;
/// Covenant arg public key byte code.
pub const ARG_PUBLIC_KEY: u8 = 0x02;
/// Covenant arg commitment byte code.
pub const ARG_COMMITMENT: u8 = 0x03;
/// Covenant arg tari script byte code.
pub const ARG_TARI_SCRIPT: u8 = 0x04;
/// Covenant arg covenant byte code.
pub const ARG_COVENANT: u8 = 0x05;
/// Covenant arg uint byte code.
pub const ARG_UINT: u8 = 0x06;
/// Covenant arg output field byte code.
pub const ARG_OUTPUT_FIELD: u8 = 0x07;
/// Covenant arg output fields byte code.
pub const ARG_OUTPUT_FIELDS: u8 = 0x08;
/// Covenant arg bytes byte code.
pub const ARG_BYTES: u8 = 0x09;
/// Covenant arg output type byte code.
pub const ARG_OUTPUT_TYPE: u8 = 0x0a;

//---------------------------------- FILTER byte codes --------------------------------------------//

/// Checks if a byte value results in a valid argument byte code
pub(super) fn is_valid_filter_code(code: u8) -> bool {
ALL_FILTERS.contains(&code)
}

/// Array with all possible covenant filter bytecodes.
pub(super) const ALL_FILTERS: [u8; 10] = [
FILTER_IDENTITY,
FILTER_AND,
Expand All @@ -68,28 +80,48 @@ pub(super) const ALL_FILTERS: [u8; 10] = [
FILTER_ABSOLUTE_HEIGHT,
];

/// Identity filter.
pub const FILTER_IDENTITY: u8 = 0x20;
/// And filter.
pub const FILTER_AND: u8 = 0x21;
/// Or filter.
pub const FILTER_OR: u8 = 0x22;
/// Xor Filter.
pub const FILTER_XOR: u8 = 0x23;
/// Not filter.
pub const FILTER_NOT: u8 = 0x24;

/// Output hash equality filter.
pub const FILTER_OUTPUT_HASH_EQ: u8 = 0x30;
/// Fields preserved filter.
pub const FILTER_FIELDS_PRESERVED: u8 = 0x31;
/// Fields hashed equality filter.
pub const FILTER_FIELDS_HASHED_EQ: u8 = 0x32;
/// Field equality filter.
pub const FILTER_FIELD_EQ: u8 = 0x33;
/// Absolute height filter.
pub const FILTER_ABSOLUTE_HEIGHT: u8 = 0x34;

//---------------------------------- FIELD byte codes --------------------------------------------//
/// Field commitment.
pub const FIELD_COMMITMENT: u8 = 0x00;
/// Field script.
pub const FIELD_SCRIPT: u8 = 0x01;
/// Field sender offset public key.
pub const FIELD_SENDER_OFFSET_PUBLIC_KEY: u8 = 0x02;
/// Field covenant.
pub const FIELD_COVENANT: u8 = 0x03;
/// Field features.
pub const FIELD_FEATURES: u8 = 0x04;
/// Field features output type.
pub const FIELD_FEATURES_OUTPUT_TYPE: u8 = 0x05;
/// Field features maturity.
pub const FIELD_FEATURES_MATURITY: u8 = 0x06;
/// Field features side chain features.
pub const FIELD_FEATURES_SIDE_CHAIN_FEATURES: u8 = 0x07;
/// Field features range proof type.
pub const FIELD_FEATURES_RANGE_PROOF_TYPE: u8 = 0x08;
/// Field minimum value promise.
pub const MINIMUM_VALUE_PROMISE: u8 = 0x09;

#[cfg(test)]
Expand Down
8 changes: 8 additions & 0 deletions base_layer/core/src/covenants/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ use crate::{
transactions::transaction_components::TransactionInput,
};

/// The covenant execution context provides a reference to the transaction input being verified, the tokenized covenant
/// and other relevant context e.g current block height
pub struct CovenantContext<'a> {
input: &'a TransactionInput,
tokens: CovenantTokenCollection,
Expand All @@ -45,10 +47,12 @@ impl<'a> CovenantContext<'a> {
}
}

/// Returns true if there are more tokens to consume, otherwise false
pub fn has_more_tokens(&self) -> bool {
!self.tokens.is_empty()
}

/// Outputs the next token argument
pub fn next_arg(&mut self) -> Result<CovenantArg, CovenantError> {
match self.tokens.next().ok_or(CovenantError::UnexpectedEndOfTokens)? {
CovenantToken::Arg(arg) => Ok(*arg),
Expand All @@ -65,17 +69,21 @@ impl<'a> CovenantContext<'a> {
}
}

/// Outputs next `CovenantFilter`, if it happens to be the next token in the current instance,
/// otherwise it errors
pub fn require_next_filter(&mut self) -> Result<CovenantFilter, CovenantError> {
match self.tokens.next().ok_or(CovenantError::UnexpectedEndOfTokens)? {
CovenantToken::Filter(filter) => Ok(filter),
CovenantToken::Arg(_) => Err(CovenantError::ExpectedFilterButGotArg),
}
}

/// Block height
pub fn block_height(&self) -> u64 {
self.block_height
}

/// Transaction input
pub fn input(&self) -> &TransactionInput {
self.input
}
Expand Down
15 changes: 15 additions & 0 deletions base_layer/core/src/covenants/covenant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ use crate::{
const MAX_COVENANT_BYTES: usize = 4096;

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

/// Produces a new `Covenant` instance, out of a byte buffer. It errors
/// if the byte buffer length is higher than `MAX_COVENANT_BYTES`.
pub fn from_bytes(bytes: &mut &[u8]) -> Result<Self, CovenantDecodeError> {
if bytes.is_empty() {
return Ok(Self::new());
Expand All @@ -89,22 +93,28 @@ impl Covenant {
CovenantTokenDecoder::new(bytes).collect()
}

/// Given a `Covenant` instance, it writes its bytes content to a
/// new byte buffer.
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(self.get_byte_length());
self.write_to(&mut buf).unwrap();
buf
}

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

/// Gets the byte lenght of the underlying byte buffer
pub(super) fn get_byte_length(&self) -> usize {
let mut counter = ByteCounter::new();
self.write_to(&mut counter).unwrap();
counter.get()
}

/// It executes the covenant on the transaction input being spent, it filters the transaction outputs which should
/// generate at least one match. An empty covenant is an identity and matches all outputs.
pub fn execute<'a>(
&self,
block_height: u64,
Expand All @@ -131,25 +141,30 @@ impl Covenant {
Ok(output_set.len())
}

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

#[cfg(test)]
/// Outputs a slice of the instance existing `CovenantToken`'s.
pub(super) fn tokens(&self) -> &[CovenantToken] {
&self.tokens
}

/// Outputs the length of `tokens` field.
pub fn num_tokens(&self) -> usize {
self.tokens.len()
}

/// Checks if the `tokens` field is empty.
pub fn is_empty(&self) -> bool {
self.tokens.is_empty()
}
}

impl FromIterator<CovenantToken> for Covenant {
/// Creates a new `CovenantToken` instance from an iterator with `Item = CovenantToken`.
fn from_iter<T: IntoIterator<Item = CovenantToken>>(iter: T) -> Self {
Self {
tokens: iter.into_iter().collect(),
Expand Down
9 changes: 9 additions & 0 deletions base_layer/core/src/covenants/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ use tari_script::ScriptError;

use crate::covenants::token::CovenantToken;

/// Covenant Token decoder.
pub struct CovenantTokenDecoder<'a, R> {
buf: &'a mut R,
is_complete: bool,
}

impl<'a, R: io::Read> CovenantTokenDecoder<'a, R> {
/// Given a read buffer, it creates a new instance of `CovenantTokenDecoder`.
pub fn new(buf: &'a mut R) -> Self {
Self {
buf,
Expand All @@ -44,6 +46,8 @@ impl<'a, R: io::Read> CovenantTokenDecoder<'a, R> {
impl Iterator for CovenantTokenDecoder<'_, &[u8]> {
type Item = Result<CovenantToken, CovenantDecodeError>;

/// Returns the next item in `CovenantTokenDecoder`'s buffer. If it is complete,
/// it returns `None`.
fn next(&mut self) -> Option<Self::Item> {
if self.is_complete {
return None;
Expand All @@ -64,6 +68,7 @@ impl Iterator for CovenantTokenDecoder<'_, &[u8]> {
}

#[derive(Debug, thiserror::Error)]
/// Error enum for covenant decoding possible failure scenarios.
pub enum CovenantDecodeError {
#[error("Unknown filter byte code {code}")]
UnknownFilterByteCode { code: u8 },
Expand All @@ -81,12 +86,15 @@ pub enum CovenantDecodeError {
Io(#[from] io::Error),
}

/// Trait `CovenantReadExt`. Contains two interface methods, `read_next_byte_code`
/// and `read_variable_length_bytes`.
pub(super) trait CovenantReadExt: io::Read {
fn read_next_byte_code(&mut self) -> Result<Option<u8>, io::Error>;
fn read_variable_length_bytes(&mut self, size: usize) -> Result<Vec<u8>, io::Error>;
}

impl<R: io::Read> CovenantReadExt for R {
/// Reads next byte code
fn read_next_byte_code(&mut self) -> Result<Option<u8>, io::Error> {
let mut buf = [0u8; 1];
loop {
Expand All @@ -102,6 +110,7 @@ impl<R: io::Read> CovenantReadExt for R {
}
}

/// Reads a variable length byte array
fn read_variable_length_bytes(&mut self, max_size: usize) -> Result<Vec<u8>, io::Error> {
let len = self.read_varint::<u16>()? as usize;
if len > max_size {
Expand Down
Loading

0 comments on commit 442d75b

Please sign in to comment.