Skip to content

Commit

Permalink
feat: add version and affiliates to UTXO encoding (#5385)
Browse files Browse the repository at this point in the history
* feat: add version and affiliates to UTXO encoding

* fix: scale-encode version byte

---------

Co-authored-by: dandanlen <3168260+dandanlen@users.noreply.github.com>
  • Loading branch information
msgmaxim and dandanlen authored Nov 7, 2024
1 parent 32580f8 commit 9842dea
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 22 deletions.
7 changes: 6 additions & 1 deletion api/lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ use serde::{Deserialize, Serialize};
use sp_consensus_aura::sr25519::AuthorityId as AuraId;
use sp_consensus_grandpa::AuthorityId as GrandpaId;
pub use sp_core::crypto::AccountId32;
use sp_core::{ed25519::Public as EdPublic, sr25519::Public as SrPublic, Bytes, Pair, H256, U256};
use sp_core::{
bounded_vec, ed25519::Public as EdPublic, sr25519::Public as SrPublic, Bytes, Pair, H256, U256,
};
pub use state_chain_runtime::chainflip::BlockUpdate;
use state_chain_runtime::{opaque::SessionKeys, RuntimeCall};
use zeroize::Zeroize;
Expand Down Expand Up @@ -579,6 +581,9 @@ pub trait BrokerApi: SignedExtrinsicApi + StorageApi + Sized + Send + Sync + 'st
.unwrap_or(2)
.try_into()?,
boost_fee: boost_fee.unwrap_or_default().try_into()?,
broker_fee: broker_commission.try_into()?,
// TODO: lookup affiliate mapping to convert affiliate ids and use them here
affiliates: bounded_vec![],
},
};
Ok(SwapPayload::Bitcoin {
Expand Down
8 changes: 6 additions & 2 deletions engine/src/witness/btc/vault_swaps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ pub fn try_extract_vault_swap_call(
#[cfg(test)]
mod tests {

use std::sync::LazyLock;

use bitcoin::{
blockdata::script::{witness_program::WitnessProgram, witness_version::WitnessVersion},
hashes::Hash,
Expand All @@ -177,7 +179,7 @@ mod tests {

const MOCK_DOT_ADDRESS: [u8; 32] = [9u8; 32];

const MOCK_SWAP_PARAMS: UtxoEncodedData = UtxoEncodedData {
static MOCK_SWAP_PARAMS: LazyLock<UtxoEncodedData> = LazyLock::new(|| UtxoEncodedData {
output_asset: cf_primitives::Asset::Dot,
output_address: EncodedAddress::Dot(MOCK_DOT_ADDRESS),
parameters: SharedCfParameters {
Expand All @@ -186,8 +188,10 @@ mod tests {
number_of_chunks: 0x0ffff,
chunk_interval: 2,
boost_fee: 5,
broker_fee: 10,
affiliates: bounded_vec![],
},
};
});

#[test]
fn script_buf_to_script_pubkey_conversion() {
Expand Down
76 changes: 57 additions & 19 deletions state-chain/chains/src/btc/vault_swap_encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ use crate::address::EncodedAddress;
use cf_primitives::{Asset, AssetAmount, ForeignChain};
use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_core::ConstU32;
use sp_runtime::BoundedVec;
use sp_std::vec::Vec;

use super::{BitcoinOp, BitcoinScript};

// The maximum length of data that can be encoded in a nulldata utxo
const MAX_NULLDATA_LENGTH: usize = 80;
const CURRENT_VERSION: u8 = 0;

#[derive(Clone, PartialEq, Debug, TypeInfo)]
pub struct UtxoEncodedData {
Expand All @@ -20,6 +23,8 @@ impl Encode for UtxoEncodedData {
fn encode(&self) -> Vec<u8> {
let mut r = Vec::with_capacity(MAX_NULLDATA_LENGTH);

CURRENT_VERSION.encode_to(&mut r);

self.output_asset.encode_to(&mut r);

// Note that we don't encode the variant since we know the
Expand All @@ -40,6 +45,15 @@ impl Encode for UtxoEncodedData {

impl Decode for UtxoEncodedData {
fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
let version = u8::decode(input)?;

if version != CURRENT_VERSION {
log::warn!(
"Unexpected version of utxo encoding: {version} (expected: {CURRENT_VERSION})"
);
return Err("unexpected version".into());
}

let output_asset = Asset::decode(input)?;

let output_address = match ForeignChain::from(output_asset) {
Expand All @@ -56,17 +70,31 @@ impl Decode for UtxoEncodedData {
}
}

#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, PartialEq, Debug)]
pub struct AffiliateAndFee {
pub affiliate: u8,
pub fee: u8,
}

// We limit the number of affiliates in btc vault swaps to ensure that we
// can always encode them inside a UTXO
const MAX_AFFILIATES: u32 = 2;

// The encoding of these parameters is the same across chains
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, PartialEq, Debug)]
pub struct SharedCfParameters {
// FoK fields (refund address is stored externally):
// --- FoK fields (refund address is stored externally) ---
pub retry_duration: u16,
pub min_output_amount: AssetAmount,
// DCA fields:
// --- DCA field ---
pub number_of_chunks: u16,
pub chunk_interval: u16,
// Boost fields:
// --- Boost fields ---
pub boost_fee: u8,
// --- Broker fields ---
// Primary's broker fee:
pub broker_fee: u8,
pub affiliates: BoundedVec<AffiliateAndFee, ConstU32<MAX_AFFILIATES>>,
}

pub fn encode_data_in_nulldata_utxo(data: &[u8]) -> Option<BitcoinScript> {
Expand All @@ -86,29 +114,37 @@ pub fn encode_swap_params_in_nulldata_utxo(params: UtxoEncodedData) -> BitcoinSc

#[cfg(test)]
mod tests {
use sp_core::bounded_vec;

use super::*;

const MOCK_DOT_ADDRESS: [u8; 32] = [9u8; 32];

const MOCK_SWAP_PARAMS: UtxoEncodedData = UtxoEncodedData {
output_asset: Asset::Dot,
output_address: EncodedAddress::Dot(MOCK_DOT_ADDRESS),
parameters: SharedCfParameters {
retry_duration: 5,
min_output_amount: u128::MAX,
number_of_chunks: 0x0ffff,
chunk_interval: 2,
boost_fee: 5,
},
};

#[test]
fn check_utxo_encoding() {
let mock_swap_params = UtxoEncodedData {
output_asset: Asset::Dot,
output_address: EncodedAddress::Dot(MOCK_DOT_ADDRESS),
parameters: SharedCfParameters {
retry_duration: 5,
min_output_amount: u128::MAX,
number_of_chunks: 0x0ffff,
chunk_interval: 2,
boost_fee: 5,
broker_fee: 0xa,
affiliates: bounded_vec![
AffiliateAndFee { affiliate: 6, fee: 7 },
AffiliateAndFee { affiliate: 8, fee: 9 }
],
},
};
// The following encoding is expected for MOCK_SWAP_PARAMS:
// (not using "insta" because we want to be precise about how the data
// is encoded exactly, rather than simply that the encoding doesn't change)
let expected_encoding: Vec<u8> = [0x04] // Asset
let expected_encoding: Vec<u8> = []
.into_iter()
.chain([0x00]) // Version
.chain([0x04]) // Asset
.chain(MOCK_DOT_ADDRESS) // Polkadot address
.chain([0x05, 0x00]) // Retry duration
.chain([
Expand All @@ -118,11 +154,13 @@ mod tests {
.chain([0xff, 0xff]) // Number of chunks
.chain([0x02, 0x00]) // Chunk interval
.chain([0x5]) // Boost fee
.chain([0xa]) // Broker fee
.chain([0x8, 0x6, 0x7, 0x8, 0x9]) // Affiliate fees (1 byte length + 2 bytes per affiliate)
.collect();

assert_eq!(MOCK_SWAP_PARAMS.encode(), expected_encoding);
assert_eq!(expected_encoding.len(), 56);
assert_eq!(mock_swap_params.encode(), expected_encoding);
assert_eq!(expected_encoding.len(), 63);

assert_eq!(UtxoEncodedData::decode(&mut expected_encoding.as_ref()), Ok(MOCK_SWAP_PARAMS));
assert_eq!(UtxoEncodedData::decode(&mut expected_encoding.as_ref()), Ok(mock_swap_params));
}
}

0 comments on commit 9842dea

Please sign in to comment.