Skip to content

Commit

Permalink
Initial take on implementing Burn Redemption Memo (#1862)
Browse files Browse the repository at this point in the history
* burn redemption memo type and builder

* another test

* misc fixes

* update comment

* Update transaction/std/src/memo_builder/burn_redemption_memo_builder.rs

Co-authored-by: Mike Turner <zaphrod.beeblebrox@gmail.com>

* fmt

* use const

* Update transaction/std/src/memo_builder/burn_redemption_memo_builder.rs

Co-authored-by: Nick Santana <nick@mobilecoin.com>

* pr review fixes

* merge fixes

* lint

* remove address from InvalidRecipient

* review fixes

Co-authored-by: Mike Turner <zaphrod.beeblebrox@gmail.com>
Co-authored-by: Nick Santana <nick@mobilecoin.com>
  • Loading branch information
3 people committed Apr 28, 2022
1 parent 512d1ff commit a26b06f
Show file tree
Hide file tree
Showing 10 changed files with 586 additions and 6 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions fog/sample-paykit/src/cached_tx_data/memo_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ impl MemoHandler {
log::trace!(self.logger, "Obtained a memo: {:?}", memo_type);
match memo_type.clone() {
MemoType::Unused(_) => Ok(None),
MemoType::BurnRedemption(_) => {
// TODO: For now we are not validating anything with burn redemption memos.
// Right now the memo data is unstructured, so there's nothing
// to verify there. In theory we should only find this type of
// memo on a the burn account, which cannot be used with the sample paykit since
// the spend key is unknown.
Ok(Some(memo_type))
}
MemoType::AuthenticatedSender(memo) => {
if let Some(addr) = self.contacts.get(&memo.sender_address_hash()) {
if bool::from(memo.validate(
Expand Down
6 changes: 6 additions & 0 deletions transaction/core/src/tx_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ pub enum NewMemoError {
OutputsAfterChange,
/// Changing the fee after the change output is not supported
FeeAfterChange,
/// Invalid recipient address
InvalidRecipient,
/// Multiple outputs are not supported
MultipleOutputs,
/// Missing output
MissingOutput,
/// Mixed Token Ids are not supported in these memos
MixedTokenIds,
/// Other: {0}
Expand Down
1 change: 1 addition & 0 deletions transaction/std/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ curve25519-dalek = { version = "4.0.0-pre.2", default-features = false, features
curve25519-dalek = { version = "4.0.0-pre.2", default-features = false, features = ["nightly", "u64_backend"] }

[dev-dependencies]
assert_matches = "1.5"
maplit = "1.0"
yaml-rust = "0.4"

Expand Down
8 changes: 4 additions & 4 deletions transaction/std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ pub use change_destination::ChangeDestination;
pub use error::TxBuilderError;
pub use input_credentials::InputCredentials;
pub use memo::{
AuthenticatedSenderMemo, AuthenticatedSenderWithPaymentRequestIdMemo, DestinationMemo,
DestinationMemoError, MemoDecodingError, MemoType, RegisteredMemoType, SenderMemoCredential,
UnusedMemo,
AuthenticatedSenderMemo, AuthenticatedSenderWithPaymentRequestIdMemo, BurnRedemptionMemo,
DestinationMemo, DestinationMemoError, MemoDecodingError, MemoType, RegisteredMemoType,
SenderMemoCredential, UnusedMemo,
};
pub use memo_builder::{EmptyMemoBuilder, MemoBuilder, RTHMemoBuilder};
pub use memo_builder::{BurnRedemptionMemoBuilder, EmptyMemoBuilder, MemoBuilder, RTHMemoBuilder};
pub use transaction_builder::{DefaultTxOutputsOrdering, TransactionBuilder, TxOutputsOrdering};

// Re-export this to help the exported macros work
Expand Down
57 changes: 57 additions & 0 deletions transaction/std/src/memo/burn_redemption.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2022 The MobileCoin Foundation

//! Object for 0x0001 Burn Redemption memo type
//!
//! TODO: Link to MCIP
//! This was proposed for standardization in mobilecoinfoundation/mcips/pull/TBD

use super::RegisteredMemoType;
use crate::impl_memo_type_conversions;

/// A memo that the sender writes to associate a burn of an assert on the
/// MobileCoin blockchain with a redemption of another asset on a different
/// blockchain. The main intended use-case for this is burning of tokens that
/// are correlated with redemption of some other asset on a different
/// blockchain.
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct BurnRedemptionMemo {
/// The memo data.
/// The contents of the memo depend on the token being burnt, and as such do
/// not have a strict schema.
memo_data: [u8; Self::MEMO_DATA_LEN],
}

impl RegisteredMemoType for BurnRedemptionMemo {
const MEMO_TYPE_BYTES: [u8; 2] = [0x00, 0x01];
}

impl BurnRedemptionMemo {
/// The length of the custom memo data.
pub const MEMO_DATA_LEN: usize = 64;

/// Create a new BurnRedemptionMemo.
pub fn new(memo_data: [u8; Self::MEMO_DATA_LEN]) -> Self {
BurnRedemptionMemo { memo_data }
}

/// Get the memo data
pub fn memo_data(&self) -> &[u8; Self::MEMO_DATA_LEN] {
&self.memo_data
}
}

impl From<&[u8; Self::MEMO_DATA_LEN]> for BurnRedemptionMemo {
fn from(src: &[u8; Self::MEMO_DATA_LEN]) -> Self {
let mut memo_data = [0u8; Self::MEMO_DATA_LEN];
memo_data.copy_from_slice(src);
Self { memo_data }
}
}

impl From<BurnRedemptionMemo> for [u8; BurnRedemptionMemo::MEMO_DATA_LEN] {
fn from(src: BurnRedemptionMemo) -> [u8; BurnRedemptionMemo::MEMO_DATA_LEN] {
src.memo_data
}
}

impl_memo_type_conversions! { BurnRedemptionMemo }
22 changes: 22 additions & 0 deletions transaction/std/src/memo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@
//! implement a new `MemoBuilder`. See the `memo_builder` module for examples.
//! Or, if you don't want to use the `TransactionBuilder`, you can call
//! `TxOut::new_with_memo` directly.
//!
//! The following memo types are natively supported by this module:
//! | Memo type bytes | Name |
//! | ----------- | ----------- |
//! | 0x0000 | Unused |
//! | 0x0001 | Burn Redemption Memo |
//! | 0x0100 | Authenticated Sender Memo |
//! | 0x0101 | Authenticated Sender With Payment Request Id Memo |
//! | 0x0200 | Destination Memo |

use crate::impl_memo_enum;
use core::{convert::TryFrom, fmt::Debug};
Expand All @@ -38,6 +47,7 @@ use displaydoc::Display;
mod authenticated_common;
mod authenticated_sender;
mod authenticated_sender_with_payment_request_id;
mod burn_redemption;
mod credential;
mod destination;
mod macros;
Expand All @@ -46,6 +56,7 @@ mod unused;
pub use authenticated_common::compute_category1_hmac;
pub use authenticated_sender::AuthenticatedSenderMemo;
pub use authenticated_sender_with_payment_request_id::AuthenticatedSenderWithPaymentRequestIdMemo;
pub use burn_redemption::BurnRedemptionMemo;
pub use credential::SenderMemoCredential;
pub use destination::{DestinationMemo, DestinationMemoError};
pub use unused::UnusedMemo;
Expand Down Expand Up @@ -73,6 +84,7 @@ pub enum MemoDecodingError {

impl_memo_enum! { MemoType,
Unused(UnusedMemo),
BurnRedemption(BurnRedemptionMemo),
AuthenticatedSender(AuthenticatedSenderMemo),
AuthenticatedSenderWithPaymentRequestId(AuthenticatedSenderWithPaymentRequestIdMemo),
Destination(DestinationMemo),
Expand Down Expand Up @@ -160,6 +172,16 @@ mod tests {
}
},
}

let memo6 = BurnRedemptionMemo::new([2; 64]);
match MemoType::try_from(&MemoPayload::from(memo6.clone())).unwrap() {
MemoType::BurnRedemption(memo) => {
assert_eq!(memo6, memo);
}
_ => {
panic!("unexpected deserialization");
}
}
}

#[test]
Expand Down
145 changes: 145 additions & 0 deletions transaction/std/src/memo_builder/burn_redemption_memo_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (c) 2022 The MobileCoin Foundation

//! Defines the BurnRedemptionMemoBuilder.
//! This MemoBuilder policy implements Burn Redemption tracking using memos, as
//! envisioned in MCIP #TODO.

use super::{
memo::{BurnRedemptionMemo, DestinationMemo, DestinationMemoError, UnusedMemo},
MemoBuilder,
};
use crate::ChangeDestination;
use mc_account_keys::{burn_address, PublicAddress, ShortAddressHash};
use mc_transaction_core::{tokens::Mob, Amount, MemoContext, MemoPayload, NewMemoError, Token};

/// This memo builder attaches 0x0001 Burn Redemption Memos to an output going
/// to the designated burn address, and 0x0200 Destination Memos to change
/// outputs. Only a single non-change output is allowed, and it must go to the
/// designated burn address.
///
/// Usage:
/// You should usually use this like:
///
/// let memo_data = [1; BurnRedemptionMemo::MEMO_DATA_LEN];
/// let mut mb = BurnRedemptionMemoBuilder::new(memo_data);
/// mb.enable_destination_memo();
///
/// Then use it to construct a transaction builder.
///
/// A memo builder configured this way will use 0x0001 Burn Redemption Memo
/// on the burn output and 0x0200 Destination Memo on the change output.
///
/// If mb.enable_destination_memo() is not called 0x0000 Unused will appear on
/// change output, instead of 0x0200 Destination Memo.
///
/// When invoking the transaction builder, the change output must be created
/// last. If the burn output is created after the change output, an error will
/// occur.
///
/// If more than one burn output is created, an error will be returned.
#[derive(Clone, Debug)]
pub struct BurnRedemptionMemoBuilder {
// The memo data we will attach to the burn output.
memo_data: [u8; BurnRedemptionMemo::MEMO_DATA_LEN],
// Whether destination memos are enabled.
destination_memo_enabled: bool,
// Tracks if we already wrote a destination memo, for error reporting
wrote_destination_memo: bool,
// Tracks the amount being burned
burn_amount: Option<Amount>,
// Tracks the fee
fee: Amount,
}

impl BurnRedemptionMemoBuilder {
/// Construct a new BurnRedemptionMemoBuilder.
pub fn new(memo_data: [u8; BurnRedemptionMemo::MEMO_DATA_LEN]) -> Self {
Self {
memo_data,
destination_memo_enabled: false,
wrote_destination_memo: false,
burn_amount: None,
fee: Amount::new(Mob::MINIMUM_FEE, Mob::ID),
}
}
/// Enable destination memos
pub fn enable_destination_memo(&mut self) {
self.destination_memo_enabled = true;
}

/// Disable destination memos
pub fn disable_destination_memo(&mut self) {
self.destination_memo_enabled = false;
}
}

impl MemoBuilder for BurnRedemptionMemoBuilder {
/// Set the fee
fn set_fee(&mut self, fee: Amount) -> Result<(), NewMemoError> {
if self.wrote_destination_memo {
return Err(NewMemoError::FeeAfterChange);
}
self.fee = fee;
Ok(())
}

/// Build a memo for the burn output.
fn make_memo_for_output(
&mut self,
amount: Amount,
recipient: &PublicAddress,
_memo_context: MemoContext,
) -> Result<MemoPayload, NewMemoError> {
if *recipient != burn_address() {
return Err(NewMemoError::InvalidRecipient);
}
if self.burn_amount.is_some() {
return Err(NewMemoError::MultipleOutputs);
}
if self.wrote_destination_memo {
return Err(NewMemoError::OutputsAfterChange);
}
self.burn_amount = Some(amount);
Ok(BurnRedemptionMemo::new(self.memo_data).into())
}

/// Build a memo for a change output (to ourselves).
fn make_memo_for_change_output(
&mut self,
change_amount: Amount,
_change_destination: &ChangeDestination,
_memo_context: MemoContext,
) -> Result<MemoPayload, NewMemoError> {
if !self.destination_memo_enabled {
return Ok(UnusedMemo {}.into());
}
if self.wrote_destination_memo {
return Err(NewMemoError::MultipleChangeOutputs);
}
let burn_amount = self.burn_amount.ok_or(NewMemoError::MissingOutput)?;
if burn_amount.token_id != self.fee.token_id
|| burn_amount.token_id != change_amount.token_id
{
return Err(NewMemoError::MixedTokenIds);
}

let total_outlay = burn_amount
.value
.checked_add(self.fee.value)
.ok_or(NewMemoError::LimitsExceeded("total_outlay"))?;
match DestinationMemo::new(
ShortAddressHash::from(&burn_address()),
total_outlay,
self.fee.value,
) {
Ok(mut d_memo) => {
self.wrote_destination_memo = true;
d_memo.set_num_recipients(1);
Ok(d_memo.into())
}
Err(err) => match err {
DestinationMemoError::FeeTooLarge => Err(NewMemoError::LimitsExceeded("fee")),
},
}
}
}
3 changes: 3 additions & 0 deletions transaction/std/src/memo_builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ use core::fmt::Debug;
use mc_account_keys::PublicAddress;
use mc_transaction_core::{Amount, MemoContext, MemoPayload, NewMemoError};

mod burn_redemption_memo_builder;
mod rth_memo_builder;

pub use burn_redemption_memo_builder::BurnRedemptionMemoBuilder;
pub use rth_memo_builder::RTHMemoBuilder;

/// The MemoBuilder trait defines the API that the transaction builder uses
Expand Down
Loading

0 comments on commit a26b06f

Please sign in to comment.