avalanche_types/platformvm/txs/
add_permissionless_validator.rs

1use crate::{
2    codec,
3    errors::{Error, Result},
4    hash, ids, key, platformvm, txs,
5};
6use serde::{Deserialize, Serialize};
7
8/// ref. <https://github.com/ava-labs/avalanchego/blob/master/vms/platformvm/txs/add_permissionless_validator_tx.go>
9/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm/txs#AddPermissionlessValidatorTx>
10/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm/txs#Tx>
11/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm/txs#UnsignedTx>
12#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
13#[serde(rename_all = "camelCase")]
14pub struct Tx {
15    /// The transaction ID is empty for unsigned tx
16    /// as long as "avax.BaseTx.Metadata" is "None".
17    /// Once Metadata is updated with signing and "Tx.Initialize",
18    /// Tx.ID() is non-empty.
19    pub base_tx: txs::Tx,
20    pub validator: platformvm::txs::Validator,
21
22    /// ID of the subnet this validator is validating.
23    /// ref. "github.com/ava-labs/avalanchego/utils/constants.PrimaryNetworkID" (ids.Empty).
24    #[serde(rename = "subnetID")]
25    pub subnet_id: ids::Id,
26    /// If the \[subnet_id\] is the primary network,
27    /// \[signer\] is the BLS key for this validator.
28    /// If the \[subnet_id\] is not the primary network,
29    /// \[signer\] is empty.
30    pub signer: Option<key::bls::ProofOfPossession>,
31
32    #[serde(rename = "stake")]
33    pub stake_transferable_outputs: Option<Vec<txs::transferable::Output>>,
34
35    pub validator_rewards_owner: key::secp256k1::txs::OutputOwners,
36    pub delegator_rewards_owner: key::secp256k1::txs::OutputOwners,
37
38    #[serde(rename = "shares")]
39    pub delegation_shares: u32,
40
41    /// To be updated after signing.
42    pub creds: Vec<key::secp256k1::txs::Credential>,
43}
44
45impl Default for Tx {
46    fn default() -> Self {
47        Self {
48            base_tx: txs::Tx::default(),
49            validator: platformvm::txs::Validator::default(),
50            subnet_id: ids::Id::empty(), // primary network
51            signer: None,
52            stake_transferable_outputs: None,
53            validator_rewards_owner: key::secp256k1::txs::OutputOwners::default(),
54            delegator_rewards_owner: key::secp256k1::txs::OutputOwners::default(),
55            delegation_shares: 0,
56            creds: Vec::new(),
57        }
58    }
59}
60
61impl Tx {
62    pub fn new(base_tx: txs::Tx) -> Self {
63        Self {
64            base_tx,
65            ..Self::default()
66        }
67    }
68
69    /// Returns the transaction ID.
70    /// Only non-empty if the embedded metadata is updated
71    /// with the signing process.
72    pub fn tx_id(&self) -> ids::Id {
73        if self.base_tx.metadata.is_some() {
74            let m = self.base_tx.metadata.clone().unwrap();
75            m.id
76        } else {
77            ids::Id::default()
78        }
79    }
80
81    pub fn type_name() -> String {
82        "platformvm.AddPermissionlessValidatorTx".to_string()
83    }
84
85    pub fn type_id() -> u32 {
86        *(codec::P_TYPES.get(&Self::type_name()).unwrap()) as u32
87    }
88
89    /// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm/txs#Tx.Sign>
90    /// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/utils/crypto#PrivateKeyED25519.SignHash>
91    pub async fn sign<T: key::secp256k1::SignOnly + Clone>(
92        &mut self,
93        signers: Vec<Vec<T>>,
94    ) -> Result<()> {
95        // marshal "unsigned tx" with the codec version
96        let type_id = Self::type_id();
97        let packer = self.base_tx.pack(codec::VERSION, type_id)?;
98
99        // "avalanchego" marshals the whole struct again for signed bytes
100        // even when the underlying "unsigned_tx" is already once marshaled
101        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#Tx.Sign
102        //
103        // reuse the underlying packer to avoid marshaling the unsigned tx twice
104        // just marshal the next fields in the struct and pack them all together
105        // in the existing packer
106        let unsigned_tx_bytes = packer.take_bytes();
107        packer.set_bytes(&unsigned_tx_bytes);
108
109        // pack the second field "validator" in the struct
110        packer.pack_bytes(self.validator.node_id.as_ref())?;
111        packer.pack_u64(self.validator.start)?;
112        packer.pack_u64(self.validator.end)?;
113        packer.pack_u64(self.validator.weight)?;
114
115        // pack the third field "subnet_id" in the struct
116        packer.pack_bytes(self.subnet_id.as_ref())?;
117
118        // pack the fourth field "signer"
119        if let Some(signer) = &self.signer {
120            let type_id_signer: u32 = 28;
121            packer.pack_u32(type_id_signer)?;
122            packer.pack_bytes(&signer.public_key)?;
123            packer.pack_bytes(&signer.proof_of_possession)?;
124        } else {
125            // empty signer for non-primary network
126            let type_id_signer: u32 = 27;
127            packer.pack_u32(type_id_signer)?;
128        }
129
130        // pack the third field "stake" in the struct
131        if let Some(stake_transferable_outputs) = &self.stake_transferable_outputs {
132            packer.pack_u32(stake_transferable_outputs.len() as u32)?;
133
134            for transferable_output in stake_transferable_outputs.iter() {
135                // "TransferableOutput.Asset" is struct and serialize:"true"
136                // but embedded inline in the struct "TransferableOutput"
137                // so no need to encode type ID
138                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableOutput
139                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#Asset
140                packer.pack_bytes(transferable_output.asset_id.as_ref())?;
141
142                // fx_id is serialize:"false" thus skipping serialization
143
144                // decide the type
145                // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/components/avax#TransferableOutput
146                if transferable_output.transfer_output.is_none()
147                    && transferable_output.stakeable_lock_out.is_none()
148                {
149                    return Err(Error::Other {
150                        message: "unexpected Nones in TransferableOutput transfer_output and stakeable_lock_out".to_string(),
151                        retryable: false,
152                    });
153                }
154                let type_id_transferable_out = {
155                    if transferable_output.transfer_output.is_some() {
156                        key::secp256k1::txs::transfer::Output::type_id()
157                    } else {
158                        platformvm::txs::StakeableLockOut::type_id()
159                    }
160                };
161                // marshal type ID for "key::secp256k1::txs::transfer::Output" or "platformvm::txs::StakeableLockOut"
162                packer.pack_u32(type_id_transferable_out)?;
163
164                match type_id_transferable_out {
165                    7 => {
166                        // "key::secp256k1::txs::transfer::Output"
167                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput
168                        let transfer_output = transferable_output.transfer_output.clone().unwrap();
169
170                        // marshal "secp256k1fx.TransferOutput.Amt" field
171                        packer.pack_u64(transfer_output.amount)?;
172
173                        // "secp256k1fx.TransferOutput.OutputOwners" is struct and serialize:"true"
174                        // but embedded inline in the struct "TransferOutput"
175                        // so no need to encode type ID
176                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput
177                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#OutputOwners
178                        packer.pack_u64(transfer_output.output_owners.locktime)?;
179                        packer.pack_u32(transfer_output.output_owners.threshold)?;
180                        packer.pack_u32(transfer_output.output_owners.addresses.len() as u32)?;
181                        for addr in transfer_output.output_owners.addresses.iter() {
182                            packer.pack_bytes(addr.as_ref())?;
183                        }
184                    }
185                    22 => {
186                        // "platformvm::txs::StakeableLockOut"
187                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#StakeableLockOut
188                        let stakeable_lock_out =
189                            transferable_output.stakeable_lock_out.clone().unwrap();
190
191                        // marshal "platformvm::txs::StakeableLockOut.locktime" field
192                        packer.pack_u64(stakeable_lock_out.locktime)?;
193
194                        // secp256k1fx.TransferOutput type ID
195                        packer.pack_u32(7)?;
196
197                        // "platformvm.StakeableLockOut.TransferOutput" is struct and serialize:"true"
198                        // but embedded inline in the struct "StakeableLockOut"
199                        // so no need to encode type ID
200                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#StakeableLockOut
201                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#TransferOutput
202                        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#OutputOwners
203                        //
204                        // marshal "secp256k1fx.TransferOutput.Amt" field
205                        packer.pack_u64(stakeable_lock_out.transfer_output.amount)?;
206                        packer
207                            .pack_u64(stakeable_lock_out.transfer_output.output_owners.locktime)?;
208                        packer
209                            .pack_u32(stakeable_lock_out.transfer_output.output_owners.threshold)?;
210                        packer.pack_u32(
211                            stakeable_lock_out
212                                .transfer_output
213                                .output_owners
214                                .addresses
215                                .len() as u32,
216                        )?;
217                        for addr in stakeable_lock_out
218                            .transfer_output
219                            .output_owners
220                            .addresses
221                            .iter()
222                        {
223                            packer.pack_bytes(addr.as_ref())?;
224                        }
225                    }
226                    _ => {
227                        return Err(Error::Other {
228                            message: format!(
229                                "unexpected type ID {} for TransferableOutput",
230                                type_id_transferable_out
231                            ),
232                            retryable: false,
233                        });
234                    }
235                }
236            }
237        } else {
238            packer.pack_u32(0_u32)?;
239        }
240
241        // pack the fourth field "reward_owner" in the struct
242        // not embedded thus encode struct type id
243        let output_owners_type_id = key::secp256k1::txs::OutputOwners::type_id();
244        packer.pack_u32(output_owners_type_id)?;
245        packer.pack_u64(self.validator_rewards_owner.locktime)?;
246        packer.pack_u32(self.validator_rewards_owner.threshold)?;
247        packer.pack_u32(self.validator_rewards_owner.addresses.len() as u32)?;
248        for addr in self.validator_rewards_owner.addresses.iter() {
249            packer.pack_bytes(addr.as_ref())?;
250        }
251
252        packer.pack_u32(output_owners_type_id)?;
253        packer.pack_u64(self.delegator_rewards_owner.locktime)?;
254        packer.pack_u32(self.delegator_rewards_owner.threshold)?;
255        packer.pack_u32(self.delegator_rewards_owner.addresses.len() as u32)?;
256        for addr in self.delegator_rewards_owner.addresses.iter() {
257            packer.pack_bytes(addr.as_ref())?;
258        }
259
260        // pack the fifth field "shares" in the struct
261        packer.pack_u32(self.delegation_shares)?;
262
263        // take bytes just for hashing computation
264        let tx_bytes_with_no_signature = packer.take_bytes();
265        packer.set_bytes(&tx_bytes_with_no_signature);
266
267        // compute sha256 for marshaled "unsigned tx" bytes
268        // IMPORTANT: take the hash only for the type "platformvm.AddPermissionlessValidatorTx" unsigned tx
269        // not other fields -- only hash "platformvm.AddPermissionlessValidatorTx.*" but not "platformvm.Tx.Creds"
270        // ref. https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm#UnsignedAddPermissionlessValidatorTx
271        let tx_bytes_hash = hash::sha256(&tx_bytes_with_no_signature);
272
273        // number of of credentials
274        let creds_len = signers.len() as u32;
275        // pack the fourth field in the struct
276        packer.pack_u32(creds_len)?;
277
278        // sign the hash with the signers (in case of multi-sig)
279        // and combine all signatures into a secp256k1fx credential
280        self.creds = Vec::new();
281        for keys in signers.iter() {
282            let mut sigs: Vec<Vec<u8>> = Vec::new();
283            for k in keys.iter() {
284                let sig = k.sign_digest(&tx_bytes_hash).await?;
285                sigs.push(Vec::from(sig));
286            }
287
288            let cred = key::secp256k1::txs::Credential { signatures: sigs };
289
290            // add a new credential to "Tx"
291            self.creds.push(cred);
292        }
293        if creds_len > 0 {
294            // pack each "cred" which is "secp256k1fx.Credential"
295            // marshal type ID for "secp256k1fx.Credential"
296            let cred_type_id = key::secp256k1::txs::Credential::type_id();
297            for cred in self.creds.iter() {
298                // marshal type ID for "secp256k1fx.Credential"
299                packer.pack_u32(cred_type_id)?;
300
301                // marshal fields for "secp256k1fx.Credential"
302                packer.pack_u32(cred.signatures.len() as u32)?;
303                for sig in cred.signatures.iter() {
304                    packer.pack_bytes(sig)?;
305                }
306            }
307        }
308        let tx_bytes_with_signatures = packer.take_bytes();
309        let tx_id = hash::sha256(&tx_bytes_with_signatures);
310
311        // update "BaseTx.Metadata" with id/unsigned bytes/bytes
312        // ref. "avalanchego/vms/platformvm.Tx.Sign"
313        // ref. "avalanchego/vms/components/avax.BaseTx.Metadata.Initialize"
314        self.base_tx.metadata = Some(txs::Metadata {
315            id: ids::Id::from_slice(&tx_id),
316            tx_bytes_with_no_signature: tx_bytes_with_no_signature.to_vec(),
317            tx_bytes_with_signatures: tx_bytes_with_signatures.to_vec(),
318        });
319
320        Ok(())
321    }
322}
323
324/// RUST_LOG=debug cargo test --package avalanche-types --lib -- platformvm::txs::add_permissionless_validator::test_add_permissionless_validator_tx_serialization_with_one_signer --exact --show-output
325#[test]
326fn test_add_permissionless_validator_tx_serialization_with_one_signer() {
327    use crate::ids::{node, short};
328    use std::str::FromStr;
329
330    let _ = env_logger::builder()
331        .filter_level(log::LevelFilter::Info)
332        .is_test(true)
333        .try_init();
334
335    macro_rules! ab {
336        ($e:expr) => {
337            tokio_test::block_on($e)
338        };
339    }
340
341    let sk = key::secp256k1::private_key::Key::from_cb58(
342        "24jUJ9vZexUM6expyMcT48LBx27k1m7xpraoV62oSQAHdziao5",
343    )
344    .unwrap();
345
346    let mut tx = Tx {
347        base_tx: txs::Tx {
348            network_id: 1000000,
349            transferable_outputs: Some(vec![txs::transferable::Output {
350                asset_id: ids::Id::from_slice(&<Vec<u8>>::from([
351                    0x88, 0xee, 0xc2, 0xe0, 0x99, 0xc6, 0xa5, 0x28, //
352                    0xe6, 0x89, 0x61, 0x8e, 0x87, 0x21, 0xe0, 0x4a, //
353                    0xe8, 0x5e, 0xa5, 0x74, 0xc7, 0xa1, 0x5a, 0x79, //
354                    0x68, 0x64, 0x4d, 0x14, 0xd5, 0x47, 0x80, 0x14, //
355                ])),
356                transfer_output: Some(key::secp256k1::txs::transfer::Output {
357                    amount: 0x2c6874d687fc000,
358                    output_owners: key::secp256k1::txs::OutputOwners {
359                        locktime: 0,
360                        threshold: 1,
361                        addresses: vec![sk.to_public_key().to_short_id().unwrap()],
362                    },
363                }),
364                ..txs::transferable::Output::default()
365            }]),
366            transferable_inputs: Some(vec![txs::transferable::Input {
367                utxo_id: txs::utxo::Id {
368                    tx_id: ids::Id::from_slice(&<Vec<u8>>::from([
369                        0x74, 0x78, 0x49, 0x44,
370                    ])),
371                    output_index: 2,
372                    ..txs::utxo::Id::default()
373                },
374                asset_id: ids::Id::from_slice(&<Vec<u8>>::from([
375                    0x88, 0xee, 0xc2, 0xe0, 0x99, 0xc6, 0xa5, 0x28, //
376                    0xe6, 0x89, 0x61, 0x8e, 0x87, 0x21, 0xe0, 0x4a, //
377                    0xe8, 0x5e, 0xa5, 0x74, 0xc7, 0xa1, 0x5a, 0x79, //
378                    0x68, 0x64, 0x4d, 0x14, 0xd5, 0x47, 0x80, 0x14, //
379                ])),
380                transfer_input: Some(key::secp256k1::txs::transfer::Input {
381                    amount: 5678,
382                    sig_indices: vec![0],
383                }),
384                ..txs::transferable::Input::default()
385            }]),
386            ..txs::Tx::default()
387        },
388        validator: platformvm::txs::Validator {
389            node_id: node::Id::from_slice(&<Vec<u8>>::from([
390                0x9c, 0xd7, 0xb3, 0xe4, 0x79, 0x04, 0xf6, 0x7c, 0xc4, 0x8e, //
391                0xb5, 0xb9, 0xaf, 0xdb, 0x03, 0xe6, 0xd1, 0x8a, 0xcf, 0x6c, //
392            ])),
393            start: 0x623d7267,
394            end: 0x63c91062,
395            weight: 0x7e7,
396        },
397        subnet_id: ids::Id::from_str("2u5EYNkXMDFNi4pL9eGBt2F5DnXLGriecu7Ctje8jK155FFkPx").unwrap(),
398        signer: Some(key::bls::ProofOfPossession{
399            public_key: hex::decode("0x8f95423f7142d00a48e1014a3de8d28907d420dc33b3052a6dee03a3f2941a393c2351e354704ca66a3fc29870282e15".trim_start_matches("0x")).unwrap(),
400            proof_of_possession: hex::decode("0x86a3ab4c45cfe31cae34c1d06f212434ac71b1be6cfe046c80c162e057614a94a5bc9f1ded1a7029deb0ba4ca7c9b71411e293438691be79c2dbf19d1ca7c3eadb9c756246fc5de5b7b89511c7d7302ae051d9e03d7991138299b5ed6a570a98".trim_start_matches("0x")).unwrap(),
401            ..Default::default()
402        }),
403        stake_transferable_outputs: Some(vec![txs::transferable::Output {
404            asset_id: ids::Id::from_slice(&<Vec<u8>>::from([
405                0x88, 0xee, 0xc2, 0xe0, 0x99, 0xc6, 0xa5, 0x28, //
406                0xe6, 0x89, 0x61, 0x8e, 0x87, 0x21, 0xe0, 0x4a, //
407                0xe8, 0x5e, 0xa5, 0x74, 0xc7, 0xa1, 0x5a, 0x79, //
408                0x68, 0x64, 0x4d, 0x14, 0xd5, 0x47, 0x80, 0x14, //
409            ])),
410            stakeable_lock_out: Some(platformvm::txs::StakeableLockOut{
411                locktime:0,
412                transfer_output: key::secp256k1::txs::transfer::Output {
413                    amount: 0x7e7,
414                    output_owners: key::secp256k1::txs::OutputOwners {
415                        locktime: 0,
416                        threshold: 1,
417                        addresses: vec![short::Id::from_slice(&<Vec<u8>>::from([
418                            0xfc,0xed,0xa8,0xf9,0x0f,0xcb,0x5d,0x30,0x61,0x4b, //
419                            0x99,0xd7,0x9f,0xc4,0xba,0xa2,0x93,0x07,0x76,0x26, //
420                        ]))],
421                    },
422                },
423            }),
424            ..txs::transferable::Output::default()
425        }]),
426        validator_rewards_owner: key::secp256k1::txs::OutputOwners {
427            locktime: 0,
428            threshold: 1,
429            addresses: vec![short::Id::from_slice(&<Vec<u8>>::from([
430                            0xfc,0xed,0xa8,0xf9,0x0f,0xcb,0x5d,0x30,0x61,0x4b, //
431                            0x99,0xd7,0x9f,0xc4,0xba,0xa2,0x93,0x07,0x76,0x26, //
432                        ]))],
433        },
434        delegator_rewards_owner: key::secp256k1::txs::OutputOwners {
435            locktime: 0,
436            threshold: 1,
437            addresses: vec![short::Id::from_slice(&<Vec<u8>>::from([
438                            0xfc,0xed,0xa8,0xf9,0x0f,0xcb,0x5d,0x30,0x61,0x4b, //
439                            0x99,0xd7,0x9f,0xc4,0xba,0xa2,0x93,0x07,0x76,0x26, //
440                        ]))],
441        },
442        delegation_shares: 1_000_000,
443        ..Tx::default()
444    };
445
446    let test_key = key::secp256k1::private_key::Key::from_cb58(
447        "PrivateKey-24jUJ9vZexUM6expyMcT48LBx27k1m7xpraoV62oSQAHdziao5",
448    )
449    .expect("failed to load private key");
450    let keys1: Vec<key::secp256k1::private_key::Key> = vec![test_key];
451    let signers: Vec<Vec<key::secp256k1::private_key::Key>> = vec![keys1];
452    ab!(tx.sign(signers)).expect("failed to sign");
453    let tx_metadata = tx.base_tx.metadata.clone().unwrap();
454    let tx_bytes_with_signatures = tx_metadata.tx_bytes_with_signatures;
455    log::info!("tx id {}", tx.tx_id().to_string());
456    assert_eq!(
457        tx.tx_id().to_string(),
458        "22tDNpLuSpTfv8dweokq22KCo8hVTK4o2mgBESg1XQGHJegve5"
459    );
460
461    // for (i, c) in tx_bytes_with_signatures.iter().enumerate() {
462    //     print!("0x{:02x},", *c);
463    //     if i > 0 && i % 20 == 0 {
464    //         println!();
465    //     }
466    // }
467
468    let expected_signed_bytes: &[u8] = &[
469        // codec version
470        0x00, 0x00, //
471        //
472        // platformvm.UnsignedAddPermissionlessValidatorTx type ID
473        0x00, 0x00, 0x00, 0x19, //
474        //
475        // network id
476        0x00, 0x0f, 0x42, 0x40, //
477        //
478        // blockchain id
479        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
480        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
481        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
482        //
483        // outs.len()
484        0x00, 0x00, 0x00, 0x01, //
485        // "outs[0]" TransferableOutput.asset_id
486        0x88, 0xee, 0xc2, 0xe0, 0x99, 0xc6, 0xa5, 0x28, 0xe6, 0x89, //
487        0x61, 0x8e, 0x87, 0x21, 0xe0, 0x4a, 0xe8, 0x5e, 0xa5, 0x74, //
488        0xc7, 0xa1, 0x5a, 0x79, 0x68, 0x64, 0x4d, 0x14, 0xd5, 0x47, //
489        0x80, 0x14, //
490        //
491        // NOTE: fx_id is serialize:"false"
492        //
493        // "outs[0]" secp256k1fx.TransferOutput type ID
494        0x00, 0x00, 0x00, 0x07, //
495        //
496        // "outs[0]" TransferableOutput.out.key::secp256k1::txs::transfer::Output.amount
497        0x02, 0xc6, 0x87, 0x4d, 0x68, 0x7f, 0xc0, 0x00, //
498        //
499        // "outs[0]" TransferableOutput.out.key::secp256k1::txs::transfer::Output.output_owners.locktime
500        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
501        //
502        // "outs[0]" TransferableOutput.out.key::secp256k1::txs::transfer::Output.output_owners.threshold
503        0x00, 0x00, 0x00, 0x01, //
504        //
505        // "outs[0]" TransferableOutput.out.key::secp256k1::txs::transfer::Output.output_owners.addrs.len()
506        0x00, 0x00, 0x00, 0x01, //
507        //
508        // "outs[0]" TransferableOutput.out.key::secp256k1::txs::transfer::Output.output_owners.addrs[0]
509        0xfc, 0xed, 0xa8, 0xf9, 0x0f, 0xcb, 0x5d, 0x30, 0x61, 0x4b, //
510        0x99, 0xd7, 0x9f, 0xc4, 0xba, 0xa2, 0x93, 0x07, 0x76, 0x26, //
511        //
512        // ins.len()
513        0x00, 0x00, 0x00, 0x01, //
514        // "ins[0]" TransferableInput.utxo_id.tx_id
515        0x74, 0x78, 0x49, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
516        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
517        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
518        0x00, 0x00, //
519        //
520        // "ins[0]" TransferableInput.utxo_id.output_index
521        0x00, 0x00, 0x00, 0x02, //
522        // "ins[0]" TransferableInput.asset_id
523        0x88, 0xee, 0xc2, 0xe0, 0x99, 0xc6, 0xa5, 0x28, 0xe6, 0x89, //
524        0x61, 0x8e, 0x87, 0x21, 0xe0, 0x4a, 0xe8, 0x5e, 0xa5, 0x74, //
525        0xc7, 0xa1, 0x5a, 0x79, 0x68, 0x64, 0x4d, 0x14, 0xd5, 0x47, //
526        0x80, 0x14, //
527        //
528        // "ins[0]" secp256k1fx.TransferInput type ID
529        0x00, 0x00, 0x00, 0x05, //
530        //
531        // "ins[0]" TransferableInput.input.key::secp256k1::txs::transfer::Input.amount
532        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x2e, //
533        //
534        // "ins[0]" TransferableInput.input.key::secp256k1::txs::transfer::Input.sig_indices.len()
535        0x00, 0x00, 0x00, 0x01, //
536        //
537        // "ins[0]" TransferableInput.input.key::secp256k1::txs::transfer::Input.sig_indices[0]
538        0x00, 0x00, 0x00, 0x00, //
539        //
540        // memo.len()
541        0x00, 0x00, 0x00, 0x00, //
542        //
543        // Validator.validator.node_id
544        0x9c, 0xd7, 0xb3, 0xe4, 0x79, 0x04, 0xf6, 0x7c, 0xc4, 0x8e, //
545        0xb5, 0xb9, 0xaf, 0xdb, 0x03, 0xe6, 0xd1, 0x8a, 0xcf, 0x6c, //
546        //
547        // Validator.validator.start
548        0x00, 0x00, 0x00, 0x00, 0x62, 0x3d, 0x72, 0x67, //
549        //
550        // Validator.validator.end
551        0x00, 0x00, 0x00, 0x00, 0x63, 0xc9, 0x10, 0x62, //
552        //
553        // Validator.validator.weight
554        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe7, //
555        // subnet id
556        0xf9, 0xef, 0x27, 0x25, 0xf6, 0x61, 0x9b, 0x92, 0x3f, 0x1e, 0x84, 0xbf, 0x34, 0x81, 0xd5,
557        0x3f, 0xd0, 0x7e, 0x2b, 0xa4, 0xbc, 0x49, 0xcc, 0xf5, 0xa6, 0x9e, 0x9a, 0xc7, 0x36, 0x73,
558        0x4e, 0x1a, //
559        // proof of possession type id
560        0x00, 0x00, 0x00, 0x1c, //
561        //
562        // proof of possession public key 48-byte
563        0x8f, 0x95, 0x42, 0x3f, 0x71, 0x42, 0xd0, 0x0a, 0x48, 0xe1, //
564        0x01, 0x4a, 0x3d, 0xe8, 0xd2, 0x89, 0x07, 0xd4, 0x20, 0xdc, //
565        0x33, 0xb3, 0x05, 0x2a, 0x6d, 0xee, 0x03, 0xa3, 0xf2, 0x94, //
566        0x1a, 0x39, 0x3c, 0x23, 0x51, 0xe3, 0x54, 0x70, 0x4c, 0xa6, //
567        0x6a, 0x3f, 0xc2, 0x98, 0x70, 0x28, 0x2e, 0x15, //
568        //
569        // proof of possession, 96-byte
570        0x86, 0xa3, 0xab, 0x4c, 0x45, 0xcf, 0xe3, 0x1c, 0xae, 0x34, //
571        0xc1, 0xd0, 0x6f, 0x21, 0x24, 0x34, 0xac, 0x71, 0xb1, 0xbe, //
572        0x6c, 0xfe, 0x04, 0x6c, 0x80, 0xc1, 0x62, 0xe0, 0x57, 0x61, //
573        0x4a, 0x94, 0xa5, 0xbc, 0x9f, 0x1d, 0xed, 0x1a, 0x70, 0x29, //
574        0xde, 0xb0, 0xba, 0x4c, 0xa7, 0xc9, 0xb7, 0x14, 0x11, 0xe2, //
575        0x93, 0x43, 0x86, 0x91, 0xbe, 0x79, 0xc2, 0xdb, 0xf1, 0x9d, //
576        0x1c, 0xa7, 0xc3, 0xea, 0xdb, 0x9c, 0x75, 0x62, 0x46, 0xfc, //
577        0x5d, 0xe5, 0xb7, 0xb8, 0x95, 0x11, 0xc7, 0xd7, 0x30, 0x2a, //
578        0xe0, 0x51, 0xd9, 0xe0, 0x3d, 0x79, 0x91, 0x13, 0x82, 0x99, //
579        0xb5, 0xed, 0x6a, 0x57, 0x0a, 0x98, //
580        //
581        // stake_outputs.len
582        0x00, 0x00, 0x00, 0x01, //
583        //
584        // stake_outputs[0].asset_id
585        0x88, 0xee, 0xc2, 0xe0, 0x99, 0xc6, 0xa5, 0x28, 0xe6, 0x89, //
586        0x61, 0x8e, 0x87, 0x21, 0xe0, 0x4a, 0xe8, 0x5e, 0xa5, 0x74, //
587        0xc7, 0xa1, 0x5a, 0x79, 0x68, 0x64, 0x4d, 0x14, 0xd5, 0x47, //
588        0x80, 0x14, //
589        //
590        // platformvm.StakeableLockOut type ID
591        0x00, 0x00, 0x00, 0x16, //
592        //
593        // locktime
594        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
595        //
596        // secp256k1fx.TransferOutput type ID
597        0x00, 0x00, 0x00, 0x07, //
598        // amount
599        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xe7, //
600        //
601        // secp256k1fx.OutputOwners.locktime
602        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
603        //
604        // secp256k1fx.OutputOwners.threshold
605        0x00, 0x00, 0x00, 0x01, //
606        //
607        // secp256k1fx.OutputOwners.addrs.len()
608        0x00, 0x00, 0x00, 0x01, //
609        //
610        // secp256k1fx.OutputOwners.addrs[0]
611        0xfc, 0xed, 0xa8, 0xf9, 0x0f, 0xcb, 0x5d, 0x30, 0x61, 0x4b, //
612        0x99, 0xd7, 0x9f, 0xc4, 0xba, 0xa2, 0x93, 0x07, 0x76, 0x26, //
613        //
614        // secp256k1fx.OutputOwners type id
615        0x00, 0x00, 0x00, 0x0b, //
616        //
617        // secp256k1fx.OutputOwners locktime
618        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
619        //
620        // secp256k1fx.OutputOwners threshold
621        0x00, 0x00, 0x00, 0x01, //
622        //
623        // secp256k1fx.OutputOwners.addrs.len()
624        0x00, 0x00, 0x00, 0x01, //
625        //
626        // secp256k1fx.OutputOwners.addrs[0]
627        0xfc, 0xed, 0xa8, 0xf9, 0x0f, 0xcb, 0x5d, 0x30, 0x61, 0x4b, //
628        0x99, 0xd7, 0x9f, 0xc4, 0xba, 0xa2, 0x93, 0x07, 0x76, 0x26, //
629        //
630        // secp256k1fx.OutputOwners type id
631        0x00, 0x00, 0x00, 0x0b, //
632        // secp256k1fx.OutputOwners locktime
633        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
634        //
635        // secp256k1fx.OutputOwners threshold
636        0x00, 0x00, 0x00, 0x01, //
637        //
638        // secp256k1fx.OutputOwners.addrs.len()
639        0x00, 0x00, 0x00, 0x01, //
640        //
641        // secp256k1fx.OutputOwners.addrs[0]
642        0xfc, 0xed, 0xa8, 0xf9, 0x0f, 0xcb, 0x5d, 0x30, 0x61, 0x4b, //
643        0x99, 0xd7, 0x9f, 0xc4, 0xba, 0xa2, 0x93, 0x07, 0x76, 0x26, //
644        //
645        // delegation_shares
646        0x00, 0x0f, 0x42, 0x40, //
647        // number of credentials
648        0x00, 0x00, 0x00, 0x01, //
649        //
650        // struct field type ID "fx::Credential.cred"
651        // "secp256k1fx.Credential" type ID
652        0x00, 0x00, 0x00, 0x09, //
653        //
654        // number of signers ("fx::Credential.cred.sigs.len()")
655        0x00, 0x00, 0x00, 0x01, //
656        //
657        // first 65-byte signature
658        0xfc, 0x13, 0x6a, 0x2d, 0x14, 0x0d, 0x7e, 0xdf, 0xdc, 0x87, //
659        0xa4, 0x13, 0xcd, 0x8f, 0xdf, 0xa6, 0x80, 0xdd, 0x07, 0x69, //
660        0xf3, 0x61, 0xdc, 0x22, 0x7f, 0xe4, 0x84, 0x53, 0x47, 0xec, //
661        0xda, 0xd7, 0x06, 0x93, 0x96, 0x9a, 0x45, 0x35, 0xe2, 0x51, //
662        0x71, 0x94, 0x84, 0xe2, 0xe5, 0x52, 0xb1, 0x53, 0xe7, 0x66, //
663        0xde, 0x74, 0x2b, 0x3c, 0x24, 0x52, 0x66, 0xc9, 0x29, 0x45, //
664        0xe7, 0x98, 0x99, 0xac, 0x00, //
665    ];
666    assert!(cmp_manager::eq_vectors(
667        expected_signed_bytes,
668        &tx_bytes_with_signatures
669    ));
670}