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}