Skip to main content

avalanche_types/txs/
mod.rs

1//! Definitions of Avalanche transaction types.
2pub mod raw;
3pub mod transferable;
4pub mod utxo;
5
6use super::{
7    codec::{self, serde::hex_0x_bytes::Hex0xBytes},
8    errors::{Error, Result},
9    hash, ids, key, packer, platformvm,
10};
11use serde::{Deserialize, Serialize};
12use serde_with::serde_as;
13
14/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#BaseTx>
15#[serde_as]
16#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
17pub struct Tx {
18    #[serde(skip)]
19    pub metadata: Option<Metadata>, // skip serialization due to serialize:"false"
20
21    #[serde(rename = "networkID")]
22    pub network_id: u32,
23    #[serde(rename = "blockchainID")]
24    pub blockchain_id: ids::Id,
25
26    #[serde(rename = "inputs")]
27    pub transferable_inputs: Option<Vec<transferable::Input>>,
28    #[serde(rename = "outputs")]
29    pub transferable_outputs: Option<Vec<transferable::Output>>,
30
31    #[serde_as(as = "Option<Hex0xBytes>")]
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub memo: Option<Vec<u8>>,
34}
35
36impl Default for Tx {
37    fn default() -> Self {
38        Self {
39            metadata: None,
40            network_id: 0,
41            blockchain_id: ids::Id::empty(),
42            transferable_inputs: None,
43            transferable_outputs: None,
44            memo: None,
45        }
46    }
47}
48
49impl Tx {
50    pub fn type_name() -> String {
51        "avm.BaseTx".to_string()
52    }
53
54    pub fn type_id() -> u32 {
55        *(codec::X_TYPES.get(&Self::type_name()).unwrap()) as u32
56    }
57
58    /// "Tx.Unsigned" is implemented by "avax.BaseTx"
59    /// but for marshal, it's passed as an interface.
60    /// Then marshaled via "avalanchego/codec/linearcodec.linearCodec"
61    /// which then calls "genericCodec.marshal".
62    /// ref. "avalanchego/vms/avm.Tx.SignSECP256K1Fx"
63    /// ref. "avalanchego/codec.manager.Marshal"
64    /// ref. "avalanchego/codec.manager.Marshal(codecVersion, &t.UnsignedTx)"
65    /// ref. "avalanchego/codec/linearcodec.linearCodec.MarshalInto"
66    /// ref. "avalanchego/codec/reflectcodec.genericCodec.MarshalInto"
67    /// ref. "avalanchego/codec/reflectcodec.genericCodec.marshal"
68    ///
69    /// Returns the packer itself so that the following marshals can reuse.
70    ///
71    /// "BaseTx" is an interface in Go (reflect.Interface)
72    /// thus the underlying type must be specified by the caller
73    /// TODO: can we do better in Rust? Go does so with reflect...
74    /// e.g., pack prefix with the type ID for "avm.BaseTx" (linearCodec.PackPrefix)
75    /// ref. "avalanchego/codec/linearcodec.linearCodec.MarshalInto"
76    /// ref. "avalanchego/codec/reflectcodec.genericCodec.MarshalInto"
77    pub fn pack(&self, codec_version: u16, type_id: u32) -> Result<packer::Packer> {
78        // ref. "avalanchego/codec.manager.Marshal", "vms/avm.newCustomCodecs"
79        // ref. "math.MaxInt32" and "constants.DefaultByteSliceCap" in Go
80        let packer = packer::Packer::new((1 << 31) - 1, 128);
81
82        // codec version
83        // ref. "avalanchego/codec.manager.Marshal"
84        packer.pack_u16(codec_version)?;
85        packer.pack_u32(type_id)?;
86
87        // marshal the actual struct "avm.BaseTx"
88        // "BaseTx.Metadata" is not serialize:"true" thus skipping serialization!!!
89        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#BaseTx
90        // ref. "avalanchego/codec/reflectcodec.structFielder"
91        packer.pack_u32(self.network_id)?;
92        packer.pack_bytes(self.blockchain_id.as_ref())?;
93
94        // "transferable_outputs" field; pack the number of slice elements
95        if self.transferable_outputs.is_some() {
96            let transferable_outputs = self.transferable_outputs.as_ref().unwrap();
97            packer.pack_u32(transferable_outputs.len() as u32)?;
98
99            for transferable_output in transferable_outputs.iter() {
100                // "TransferableOutput.Asset" is struct and serialize:"true"
101                // but embedded inline in the struct "TransferableOutput"
102                // so no need to encode type ID
103                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableOutput
104                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#Asset
105                packer.pack_bytes(transferable_output.asset_id.as_ref())?;
106
107                // fx_id is serialize:"false" thus skipping serialization
108
109                // decide the type
110                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableOutput
111                if transferable_output.transfer_output.is_none()
112                    && transferable_output.stakeable_lock_out.is_none()
113                {
114                    return Err(Error::Other {
115                        message: "unexpected Nones in TransferableOutput transfer_output and stakeable_lock_out".to_string(),
116                        retryable: false,
117                    });
118                }
119                let type_id_transferable_out = {
120                    if transferable_output.transfer_output.is_some() {
121                        key::secp256k1::txs::transfer::Output::type_id()
122                    } else {
123                        platformvm::txs::StakeableLockOut::type_id()
124                    }
125                };
126                // marshal type ID for "key::secp256k1::txs::transfer::Output" or "platformvm::txs::StakeableLockOut"
127                packer.pack_u32(type_id_transferable_out)?;
128
129                match type_id_transferable_out {
130                    7 => {
131                        // "key::secp256k1::txs::transfer::Output"
132                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput
133                        let transfer_output = transferable_output.transfer_output.clone().unwrap();
134
135                        // marshal "secp256k1fx.TransferOutput.Amt" field
136                        packer.pack_u64(transfer_output.amount)?;
137
138                        // "secp256k1fx.TransferOutput.OutputOwners" is struct and serialize:"true"
139                        // but embedded inline in the struct "TransferOutput"
140                        // so no need to encode type ID
141                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput
142                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#OutputOwners
143                        packer.pack_u64(transfer_output.output_owners.locktime)?;
144                        packer.pack_u32(transfer_output.output_owners.threshold)?;
145                        packer.pack_u32(transfer_output.output_owners.addresses.len() as u32)?;
146                        for addr in transfer_output.output_owners.addresses.iter() {
147                            packer.pack_bytes(addr.as_ref())?;
148                        }
149                    }
150                    22 => {
151                        // "platformvm::txs::StakeableLockOut"
152                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#StakeableLockOut
153                        let stakeable_lock_out =
154                            transferable_output.stakeable_lock_out.clone().unwrap();
155
156                        // marshal "platformvm::txs::StakeableLockOut.locktime" field
157                        packer.pack_u64(stakeable_lock_out.locktime)?;
158
159                        // secp256k1fx.TransferOutput type ID
160                        packer.pack_u32(7)?;
161
162                        // "platformvm.StakeableLockOut.TransferOutput" is struct and serialize:"true"
163                        // but embedded inline in the struct "StakeableLockOut"
164                        // so no need to encode type ID
165                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#StakeableLockOut
166                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput
167                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#OutputOwners
168                        //
169                        // marshal "secp256k1fx.TransferOutput.Amt" field
170                        packer.pack_u64(stakeable_lock_out.transfer_output.amount)?;
171                        packer
172                            .pack_u64(stakeable_lock_out.transfer_output.output_owners.locktime)?;
173                        packer
174                            .pack_u32(stakeable_lock_out.transfer_output.output_owners.threshold)?;
175                        packer.pack_u32(
176                            stakeable_lock_out
177                                .transfer_output
178                                .output_owners
179                                .addresses
180                                .len() as u32,
181                        )?;
182                        for addr in stakeable_lock_out
183                            .transfer_output
184                            .output_owners
185                            .addresses
186                            .iter()
187                        {
188                            packer.pack_bytes(addr.as_ref())?;
189                        }
190                    }
191                    _ => {
192                        return Err(Error::Other {
193                            message: format!(
194                                "unexpected type ID {} for TransferableOutput",
195                                type_id_transferable_out
196                            ),
197                            retryable: false,
198                        })
199                    }
200                }
201            }
202        } else {
203            packer.pack_u32(0_u32)?;
204        }
205
206        // "transferable_inputs" field; pack the number of slice elements
207        if self.transferable_inputs.is_some() {
208            let transferable_inputs = self.transferable_inputs.as_ref().unwrap();
209            packer.pack_u32(transferable_inputs.len() as u32)?;
210
211            for transferable_input in transferable_inputs.iter() {
212                // "TransferableInput.UTXOID" is struct and serialize:"true"
213                // but embedded inline in the struct "TransferableInput"
214                // so no need to encode type ID
215                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableInput
216                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#UTXOID
217                packer.pack_bytes(transferable_input.utxo_id.tx_id.as_ref())?;
218                packer.pack_u32(transferable_input.utxo_id.output_index)?;
219
220                // "TransferableInput.Asset" is struct and serialize:"true"
221                // but embedded inline in the struct "TransferableInput"
222                // so no need to encode type ID
223                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableInput
224                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#Asset
225                packer.pack_bytes(transferable_input.asset_id.as_ref())?;
226
227                // fx_id is serialize:"false" thus skipping serialization
228
229                // decide the type
230                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableInput
231                if transferable_input.transfer_input.is_none()
232                    && transferable_input.stakeable_lock_in.is_none()
233                {
234                    return Err(Error::Other {
235                        message: "unexpected Nones in TransferableInput transfer_input and stakeable_lock_in".to_string(),
236                        retryable: false,
237                    });
238                }
239                let type_id_transferable_in = {
240                    if transferable_input.transfer_input.is_some() {
241                        key::secp256k1::txs::transfer::Input::type_id()
242                    } else {
243                        platformvm::txs::StakeableLockIn::type_id()
244                    }
245                };
246                // marshal type ID for "key::secp256k1::txs::transfer::Input" or "platformvm::txs::StakeableLockIn"
247                packer.pack_u32(type_id_transferable_in)?;
248
249                match type_id_transferable_in {
250                    5 => {
251                        // "key::secp256k1::txs::transfer::Input"
252                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferInput
253                        let transfer_input = transferable_input.transfer_input.clone().unwrap();
254
255                        // marshal "secp256k1fx.TransferInput.Amt" field
256                        packer.pack_u64(transfer_input.amount)?;
257
258                        // "secp256k1fx.TransferInput.Input" is struct and serialize:"true"
259                        // but embedded inline in the struct "TransferInput"
260                        // so no need to encode type ID
261                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferInput
262                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#Input
263                        packer.pack_u32(transfer_input.sig_indices.len() as u32)?;
264                        for idx in transfer_input.sig_indices.iter() {
265                            packer.pack_u32(*idx)?;
266                        }
267                    }
268                    21 => {
269                        // "platformvm::txs::StakeableLockIn"
270                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#StakeableLockIn
271                        let stakeable_lock_in =
272                            transferable_input.stakeable_lock_in.clone().unwrap();
273
274                        // marshal "platformvm::txs::StakeableLockIn.locktime" field
275                        packer.pack_u64(stakeable_lock_in.locktime)?;
276
277                        // "platformvm.StakeableLockIn.TransferableIn" is struct and serialize:"true"
278                        // but embedded inline in the struct "StakeableLockIn"
279                        // so no need to encode type ID
280                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#StakeableLockIn
281                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferInput
282                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#Input
283                        //
284                        // marshal "secp256k1fx.TransferInput.Amt" field
285                        packer.pack_u64(stakeable_lock_in.transfer_input.amount)?;
286                        //
287                        // "secp256k1fx.TransferInput.Input" is struct and serialize:"true"
288                        // but embedded inline in the struct "TransferInput"
289                        // so no need to encode type ID
290                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferInput
291                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#Input
292                        packer
293                            .pack_u32(stakeable_lock_in.transfer_input.sig_indices.len() as u32)?;
294                        for idx in stakeable_lock_in.transfer_input.sig_indices.iter() {
295                            packer.pack_u32(*idx)?;
296                        }
297                    }
298                    _ => {
299                        return Err(Error::Other {
300                            message: format!(
301                                "unexpected type ID {} for TransferableInput",
302                                type_id_transferable_in
303                            ),
304                            retryable: false,
305                        })
306                    }
307                }
308            }
309        } else {
310            packer.pack_u32(0_u32)?;
311        }
312
313        // marshal "BaseTx.memo"
314        if self.memo.is_some() {
315            let memo = self.memo.as_ref().unwrap();
316            packer.pack_u32(memo.len() as u32)?;
317            packer.pack_bytes(memo)?;
318        } else {
319            packer.pack_u32(0_u32)?;
320        }
321
322        Ok(packer)
323    }
324}
325
326/// RUST_LOG=debug cargo test --package avalanche-types --lib -- txs::test_base_tx_serialization --exact --show-output
327/// ref. "avalanchego/vms/avm.TestBaseTxSerialization"
328#[test]
329fn test_base_tx_serialization() {
330    use crate::{ids::short, key};
331
332    // ref. "avalanchego/vms/avm/vm_test.go"
333    let test_key = key::secp256k1::private_key::Key::from_cb58(
334        "PrivateKey-24jUJ9vZexUM6expyMcT48LBx27k1m7xpraoV62oSQAHdziao5",
335    )
336    .expect("failed to load private key");
337    let test_key_short_addr = test_key
338        .to_public_key()
339        .to_short_bytes()
340        .expect("failed to_short_bytes");
341    let test_key_short_addr = short::Id::from_slice(&test_key_short_addr);
342
343    let unsigned_tx = Tx {
344        network_id: 10,
345        blockchain_id: ids::Id::from_slice(&<Vec<u8>>::from([5, 4, 3, 2, 1])),
346        transferable_outputs: Some(vec![transferable::Output {
347            asset_id: ids::Id::from_slice(&<Vec<u8>>::from([1, 2, 3])),
348            transfer_output: Some(key::secp256k1::txs::transfer::Output {
349                amount: 12345,
350                output_owners: key::secp256k1::txs::OutputOwners {
351                    locktime: 0,
352                    threshold: 1,
353                    addresses: vec![test_key_short_addr],
354                },
355            }),
356            ..transferable::Output::default()
357        }]),
358        transferable_inputs: Some(vec![transferable::Input {
359            utxo_id: utxo::Id {
360                tx_id: ids::Id::from_slice(&<Vec<u8>>::from([
361                    0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, //
362                    0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf2, 0xf1, 0xf0, //
363                    0xef, 0xee, 0xed, 0xec, 0xeb, 0xea, 0xe9, 0xe8, //
364                    0xe7, 0xe6, 0xe5, 0xe4, 0xe3, 0xe2, 0xe1, 0xe0, //
365                ])),
366                output_index: 1,
367                ..utxo::Id::default()
368            },
369            asset_id: ids::Id::from_slice(&<Vec<u8>>::from([1, 2, 3])),
370            transfer_input: Some(key::secp256k1::txs::transfer::Input {
371                amount: 54321,
372                sig_indices: vec![2],
373            }),
374            ..transferable::Input::default()
375        }]),
376        memo: Some(vec![0x00, 0x01, 0x02, 0x03]),
377        ..Tx::default()
378    };
379    let unsigned_tx_packer = unsigned_tx
380        .pack(0, Tx::type_id())
381        .expect("failed to pack unsigned_tx");
382    let unsigned_tx_bytes = unsigned_tx_packer.take_bytes();
383
384    let expected_unsigned_tx_bytes: Vec<u8> = vec![
385        // codec version
386        0x00, 0x00, //
387        //
388        // avm.BaseTx type ID
389        0x00, 0x00, 0x00, 0x00, //
390        //
391        // network id
392        0x00, 0x00, 0x00, 0x0a, //
393        //
394        // blockchain id
395        0x05, 0x04, 0x03, 0x02, 0x01, 0x00, 0x00, 0x00, //
396        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
397        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
398        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
399        //
400        // outs.len()
401        0x00, 0x00, 0x00, 0x01, //
402        //
403        // "outs[0]" TransferableOutput.asset_id
404        0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, //
405        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
406        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
407        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
408        //
409        // NOTE: fx_id is serialize:"false"
410        //
411        // "outs[0]" secp256k1fx.TransferOutput type ID
412        0x00, 0x00, 0x00, 0x07, //
413        //
414        // "outs[0]" TransferableOutput.out.key::secp256k1::txs::transfer::Output.amount
415        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x39, //
416        //
417        // "outs[0]" TransferableOutput.out.key::secp256k1::txs::transfer::Output.output_owners.locktime
418        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
419        //
420        // "outs[0]" TransferableOutput.out.key::secp256k1::txs::transfer::Output.output_owners.threshold
421        0x00, 0x00, 0x00, 0x01, //
422        //
423        // "outs[0]" TransferableOutput.out.key::secp256k1::txs::transfer::Output.output_owners.addrs.len()
424        0x00, 0x00, 0x00, 0x01, //
425        //
426        // "outs[0]" TransferableOutput.out.key::secp256k1::txs::transfer::Output.output_owners.addrs[0]
427        0xfc, 0xed, 0xa8, 0xf9, 0x0f, 0xcb, 0x5d, 0x30, //
428        0x61, 0x4b, 0x99, 0xd7, 0x9f, 0xc4, 0xba, 0xa2, //
429        0x93, 0x07, 0x76, 0x26, //
430        //
431        // ins.len()
432        0x00, 0x00, 0x00, 0x01, //
433        //
434        // "ins[0]" TransferableInput.utxo_id.tx_id
435        0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, //
436        0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf2, 0xf1, 0xf0, //
437        0xef, 0xee, 0xed, 0xec, 0xeb, 0xea, 0xe9, 0xe8, //
438        0xe7, 0xe6, 0xe5, 0xe4, 0xe3, 0xe2, 0xe1, 0xe0, //
439        //
440        // "ins[0]" TransferableInput.utxo_id.output_index
441        0x00, 0x00, 0x00, 0x01, //
442        //
443        // "ins[0]" TransferableInput.asset_id
444        0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, //
445        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
446        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
447        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
448        //
449        // "ins[0]" secp256k1fx.TransferInput type ID
450        0x00, 0x00, 0x00, 0x05, //
451        //
452        // "ins[0]" TransferableInput.input.key::secp256k1::txs::transfer::Input.amount
453        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd4, 0x31, //
454        //
455        // "ins[0]" TransferableInput.input.key::secp256k1::txs::transfer::Input.sig_indices.len()
456        0x00, 0x00, 0x00, 0x01, //
457        //
458        // "ins[0]" TransferableInput.input.key::secp256k1::txs::transfer::Input.sig_indices[0]
459        0x00, 0x00, 0x00, 0x02, //
460        //
461        // memo.len()
462        0x00, 0x00, 0x00, 0x04, //
463        //
464        // memo
465        0x00, 0x01, 0x02, 0x03, //
466    ];
467    // for c in &unsigned_bytes {
468    //     print!("{:#02x},", *c);
469    // }
470    assert!(cmp_manager::eq_vectors(
471        &expected_unsigned_tx_bytes,
472        &unsigned_tx_bytes
473    ));
474}
475
476/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#Metadata>
477#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
478pub struct Metadata {
479    pub id: ids::Id,
480    pub tx_bytes_with_no_signature: Vec<u8>,
481    pub tx_bytes_with_signatures: Vec<u8>,
482}
483
484impl Default for Metadata {
485    fn default() -> Self {
486        Self {
487            id: ids::Id::empty(),
488            tx_bytes_with_no_signature: Vec::new(),
489            tx_bytes_with_signatures: Vec::new(),
490        }
491    }
492}
493
494impl Metadata {
495    pub fn new(tx_bytes_with_no_signature: &[u8], tx_bytes_with_signatures: &[u8]) -> Self {
496        let id = hash::sha256(tx_bytes_with_signatures);
497        let id = ids::Id::from_slice(&id);
498        Self {
499            id,
500            tx_bytes_with_no_signature: Vec::from(tx_bytes_with_no_signature),
501            tx_bytes_with_signatures: Vec::from(tx_bytes_with_signatures),
502        }
503    }
504
505    pub fn verify(&self) -> Result<()> {
506        if self.id.is_empty() {
507            return Err(Error::Other {
508                message: "metadata was never initialized and is not valid".to_string(), // ref. "errMetadataNotInitialize"
509                retryable: false,
510            });
511        }
512        Ok(())
513    }
514}