Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix (de)serialization of transferable outputs #181

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 2 additions & 119 deletions crates/avalanche-types/src/avm/txs/export.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
//! Base export transaction type.
use crate::{
avm::txs::fx,
codec,
errors::{Error, Result},
hash, ids, key, platformvm, txs,
};
use crate::{avm::txs::fx, codec, errors::Result, hash, ids, key, txs};
use serde::{Deserialize, Serialize};

/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/avm#Tx>
Expand Down Expand Up @@ -74,119 +69,7 @@ impl Tx {
packer.pack_bytes(self.destination_chain_id.as_ref())?;

// pack the third field in the struct
if self.destination_chain_transferable_outputs.is_some() {
let destination_chain_transferable_outputs = self
.destination_chain_transferable_outputs
.as_ref()
.unwrap();
packer.pack_u32(destination_chain_transferable_outputs.len() as u32)?;

for transferable_output in destination_chain_transferable_outputs.iter() {
// "TransferableOutput.Asset" is struct and serialize:"true"
// but embedded inline in the struct "TransferableOutput"
// so no need to encode type ID
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableOutput
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#Asset
packer.pack_bytes(transferable_output.asset_id.as_ref())?;

// fx_id is serialize:"false" thus skipping serialization

// decide the type
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableOutput
if transferable_output.transfer_output.is_none()
&& transferable_output.stakeable_lock_out.is_none()
{
return Err(Error::Other {
message: "unexpected Nones in TransferableOutput transfer_output and stakeable_lock_out".to_string(),
retryable: false,
});
}
let type_id_transferable_out = {
if transferable_output.transfer_output.is_some() {
key::secp256k1::txs::transfer::Output::type_id()
} else {
platformvm::txs::StakeableLockOut::type_id()
}
};
// marshal type ID for "key::secp256k1::txs::transfer::Output" or "platformvm::txs::StakeableLockOut"
packer.pack_u32(type_id_transferable_out)?;

match type_id_transferable_out {
7 => {
// "key::secp256k1::txs::transfer::Output"
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput
let transfer_output = transferable_output.transfer_output.clone().unwrap();

// marshal "secp256k1fx.TransferOutput.Amt" field
packer.pack_u64(transfer_output.amount)?;

// "secp256k1fx.TransferOutput.OutputOwners" is struct and serialize:"true"
// but embedded inline in the struct "TransferOutput"
// so no need to encode type ID
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#OutputOwners
packer.pack_u64(transfer_output.output_owners.locktime)?;
packer.pack_u32(transfer_output.output_owners.threshold)?;
packer.pack_u32(transfer_output.output_owners.addresses.len() as u32)?;
for addr in transfer_output.output_owners.addresses.iter() {
packer.pack_bytes(addr.as_ref())?;
}
}
22 => {
// "platformvm::txs::StakeableLockOut"
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#StakeableLockOut
let stakeable_lock_out =
transferable_output.stakeable_lock_out.clone().unwrap();

// marshal "platformvm::txs::StakeableLockOut.locktime" field
packer.pack_u64(stakeable_lock_out.locktime)?;

// secp256k1fx.TransferOutput type ID
packer.pack_u32(7)?;

// "platformvm.StakeableLockOut.TransferOutput" is struct and serialize:"true"
// but embedded inline in the struct "StakeableLockOut"
// so no need to encode type ID
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#StakeableLockOut
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#OutputOwners
//
// marshal "secp256k1fx.TransferOutput.Amt" field
packer.pack_u64(stakeable_lock_out.transfer_output.amount)?;
packer
.pack_u64(stakeable_lock_out.transfer_output.output_owners.locktime)?;
packer
.pack_u32(stakeable_lock_out.transfer_output.output_owners.threshold)?;
packer.pack_u32(
stakeable_lock_out
.transfer_output
.output_owners
.addresses
.len() as u32,
)?;
for addr in stakeable_lock_out
.transfer_output
.output_owners
.addresses
.iter()
{
packer.pack_bytes(addr.as_ref())?;
}
}
_ => {
return Err(Error::Other {
message: format!(
"unexpected type ID {} for TransferableOutput",
type_id_transferable_out
),
retryable: false,
});
}
}
}
} else {
packer.pack_u32(0_u32)?;
}
packer.pack(&self.destination_chain_transferable_outputs)?;

// take bytes just for hashing computation
let tx_bytes_with_no_signature = packer.take_bytes();
Expand Down
11 changes: 2 additions & 9 deletions crates/avalanche-types/src/avm/txs/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,7 @@ impl Tx {
// so no need to encode type ID
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferInput
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#Input
packer.pack_u32(transfer_input.sig_indices.len() as u32)?;
for idx in transfer_input.sig_indices.iter() {
packer.pack_u32(*idx)?;
}
packer.pack(&transfer_input.sig_indices)?;
}
21 => {
// "platformvm::txs::StakeableLockIn"
Expand All @@ -157,11 +154,7 @@ impl Tx {
// so no need to encode type ID
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferInput
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#Input
packer
.pack_u32(stakeable_lock_in.transfer_input.sig_indices.len() as u32)?;
for idx in stakeable_lock_in.transfer_input.sig_indices.iter() {
packer.pack_u32(*idx)?;
}
packer.pack(&stakeable_lock_in.transfer_input.sig_indices)?;
}
_ => {
return Err(Error::Other {
Expand Down
11 changes: 8 additions & 3 deletions crates/avalanche-types/src/avm/txs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ pub mod fx;
pub mod import;
pub mod vertex;

use crate::{codec, errors::Result, hash, ids, key, txs};
use crate::{
codec,
errors::Result,
hash, ids, key,
txs::{self},
};
use serde::{Deserialize, Serialize};

/// Base transaction.
Expand Down Expand Up @@ -135,7 +140,7 @@ impl Tx {
/// ref. "avalanchego/vms/avm.TestBaseTxSerialization"
#[test]
fn test_tx_serialization_with_two_signers() {
use crate::ids::short;
use crate::{ids::short, txs::transferable::TransferableOut};

macro_rules! ab {
($e:expr) => {
Expand All @@ -159,7 +164,7 @@ fn test_tx_serialization_with_two_signers() {
blockchain_id: ids::Id::from_slice(&<Vec<u8>>::from([5, 4, 3, 2, 1])),
transferable_outputs: Some(vec![txs::transferable::Output {
asset_id: ids::Id::from_slice(&<Vec<u8>>::from([1, 2, 3])),
transfer_output: Some(key::secp256k1::txs::transfer::Output {
out: TransferableOut::TransferOutput(key::secp256k1::txs::transfer::Output {
amount: 12345,
output_owners: key::secp256k1::txs::OutputOwners {
locktime: 0,
Expand Down
8 changes: 7 additions & 1 deletion crates/avalanche-types/src/ids/short.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::{
str::FromStr,
};

use crate::{formatting, hash, key::secp256k1};
use crate::{formatting, hash, key::secp256k1, packer::{Packable, Packer}, errors};
use lazy_static::lazy_static;
use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
use zerocopy::{AsBytes, FromBytes, FromZeroes, Unaligned};
Expand Down Expand Up @@ -126,6 +126,12 @@ impl<'de> Deserialize<'de> for Id {
}
}

impl Packable for Id {
fn pack(&self, packer: &Packer) -> errors::Result<()> {
packer.pack_bytes(self.as_ref())
}
}

fn fmt_id<'de, D>(deserializer: D) -> Result<Id, D::Error>
where
D: Deserializer<'de>,
Expand Down
30 changes: 29 additions & 1 deletion crates/avalanche-types/src/key/secp256k1/txs/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ use std::{
io::{self, Error, ErrorKind},
};

use crate::{codec, key};
use crate::{
codec,
errors::Result,
key,
packer::{Packable, Packer},
};
use serde::{Deserialize, Serialize};

/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableOutput>
Expand Down Expand Up @@ -36,6 +41,29 @@ impl Output {
}
}

impl Packable for Output {
fn pack(&self, packer: &Packer) -> Result<()> {
// marshal type ID for "key::secp256k1::txs::transfer::Output"
packer.pack_u32(Self::type_id())?;

// "key::secp256k1::txs::transfer::Output"
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput

// marshal "secp256k1fx.TransferOutput.Amt" field
packer.pack_u64(self.amount)?;

// "secp256k1fx.TransferOutput.OutputOwners" is struct and serialize:"true"
// but embedded inline in the struct "TransferOutput"
// so no need to encode type ID
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput
// ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#OutputOwners
packer.pack_u64(self.output_owners.locktime)?;
packer.pack_u32(self.output_owners.threshold)?;
packer.pack(&self.output_owners.addresses)?;
Ok(())
}
}

/// RUST_LOG=debug cargo test --package avalanche-types --lib -- key::secp256k1::txs::transfer::test_transfer_output_custom_de_serializer --exact --show-output
#[test]
fn test_transfer_output_custom_de_serializer() {
Expand Down
58 changes: 58 additions & 0 deletions crates/avalanche-types/src/packer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ impl Packer {
}
}

pub fn new_with_version(codec_version: u16) -> Result<Self> {
// ref. "avalanchego/codec.manager.Marshal", "vms/avm.newCustomCodecs"
// ref. "math.MaxInt32" and "constants.DefaultByteSliceCap" in Go
let packer = Self::new((1 << 31) - 1, 128);

// codec version
// ref. "avalanchego/codec.manager.Marshal"
packer.pack_u16(codec_version)?;
Ok(packer)
}

/// Creates a new Packer with 32-bit message length header.
pub fn new_with_header(max_size: usize, initial_cap: usize) -> Self {
let mut b = BytesMut::with_capacity(initial_cap);
Expand Down Expand Up @@ -621,6 +632,53 @@ impl Packer {
};
Ok(s)
}

/// Packs a packable type
pub fn pack<T: Packable>(&self, v: &T) -> Result<()> {
v.pack(self)
}
}

/// A trait implemented by types that can be packed using [`Packer`]
pub trait Packable {
fn pack(&self, packer: &Packer) -> Result<()>;
}

/// [`Packable`] implementation for [`u8`]
impl Packable for u8 {
fn pack(&self, packer: &Packer) -> Result<()> {
packer.pack_byte(*self)
}
}

/// [`Packable`] implementation for [`u32`]
impl Packable for u32 {
fn pack(&self, packer: &Packer) -> Result<()> {
packer.pack_u32(*self)
}
}

/// Generic [`Packable`] implementation for [`Option<T>`]
impl<T: Packable> Packable for Option<T> {
fn pack(&self, packer: &Packer) -> Result<()> {
if let Some(val) = self {
packer.pack(val)?;
} else {
packer.pack_u32(0_u32)?;
}
Ok(())
}
}

/// Generic [`Packable`] implementation for [`Vec<T>`]
impl<T: Packable> Packable for Vec<T> {
fn pack(&self, packer: &Packer) -> Result<()> {
packer.pack_u32(self.len() as u32)?;
for val in self {
packer.pack(val)?;
}
Ok(())
}
}

/// RUST_LOG=debug cargo test --package avalanche-types --lib -- packer::test_expand --exact --show-output
Expand Down
Loading