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 transaction for adding subnet validators #180

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions crates/avalanche-types/src/key/secp256k1/txs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ fn test_sort_output_owners() {
/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/secp256k1fx#Input>
#[derive(Debug, Serialize, Deserialize, Eq, Clone, Default)]
pub struct Input {
#[serde(rename = "signatureIndices")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:
Generally, it's probably better to do $[serde(rename_all = "camelCase")] so that you don't have to add this to every new field.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, forgot to mention that you would also need to rename the field to signature_indices.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't want to do this, I will still accept the PR

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Generally, it's probably better to do $[serde(rename_all = "camelCase")] so that you don't have to add this to every new field.

Yeah, I've been considering that, but it doesn't quite work for some fields like node_id. Still, may be better to just add it.

pub sig_indices: Vec<u32>,
}

Expand Down
174 changes: 170 additions & 4 deletions crates/avalanche-types/src/platformvm/txs/add_subnet_validator.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
use crate::{codec, errors::Result, hash, ids, key, platformvm, txs};
use crate::{
codec,
errors::Result,
hash, ids, key, packer, platformvm,
txs::{self},
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
pub struct Validator {
#[serde(flatten)]
pub validator: platformvm::txs::Validator,
#[serde(rename = "subnetID")]
pub subnet_id: ids::Id,
}

Expand All @@ -25,11 +32,14 @@ pub struct Tx {
/// as long as "avax.BaseTx.Metadata" is "None".
/// Once Metadata is updated with signing and "Tx.Initialize",
/// Tx.ID() is non-empty.
#[serde(flatten)]
pub base_tx: txs::Tx,
pub validator: Validator,
#[serde(rename = "subnetAuthorization")]
pub subnet_auth: key::secp256k1::txs::Input,

/// To be updated after signing.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub creds: Vec<key::secp256k1::txs::Credential>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the skip_serializing_if actually necessary? Will an empty vector break something?

}

Expand Down Expand Up @@ -61,9 +71,7 @@ impl Tx {
*(codec::P_TYPES.get(&Self::type_name()).unwrap()) as u32
}

/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm/txs#Tx.Sign>
/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/utils/crypto#PrivateKeyED25519.SignHash>
pub async fn sign<T: key::secp256k1::SignOnly>(&mut self, signers: Vec<Vec<T>>) -> Result<()> {
pub fn pack(&self) -> Result<packer::Packer> {
// marshal "unsigned tx" with the codec version
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem like there's any reason to change the public API here

Suggested change
pub fn pack(&self) -> Result<packer::Packer> {
fn pack(&self) -> Result<packer::Packer> {

let type_id = Self::type_id();
let packer = self.base_tx.pack(codec::VERSION, type_id)?;
Expand Down Expand Up @@ -93,6 +101,14 @@ impl Tx {
packer.pack_u32(*sig_idx)?;
}

Ok(packer)
}

/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm/txs#Tx.Sign>
/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/utils/crypto#PrivateKeyED25519.SignHash>
pub async fn sign<T: key::secp256k1::SignOnly>(&mut self, signers: Vec<Vec<T>>) -> Result<()> {
let packer = self.pack()?;

// take bytes just for hashing computation
let tx_bytes_with_no_signature = packer.take_bytes();
packer.set_bytes(&tx_bytes_with_no_signature);
Expand Down Expand Up @@ -405,3 +421,153 @@ fn test_add_subnet_validator_tx_serialization_with_one_signer() {
&tx_bytes_with_signatures
));
}

/// RUST_LOG=debug cargo test --package avalanche-types --lib -- platformvm::txs::add_subnet_validator::test_json_deserialize --exact --show-output
#[test]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you're following a pattern, but I'm really not a fan of this. There are clear docs for how to run tests, there's no point in duplicating them and having to change this if we move any files/modules around.

Suggested change
/// RUST_LOG=debug cargo test --package avalanche-types --lib -- platformvm::txs::add_subnet_validator::test_json_deserialize --exact --show-output
/// RUST_LOG=debug cargo test --package avalanche-types --lib -- platformvm::txs::add_subnet_validator::test_json_deserialize --exact --show-output

fn test_json_deserialize() {
4tXJ7f marked this conversation as resolved.
Show resolved Hide resolved
use serde_json::json;

// Based on <https://github.com/ava-labs/avalanche-rs/issues/177>
let tx_json = json!({
"networkID": 1337,
"blockchainID": "11111111111111111111111111111111LpoYY",
"outputs": [
{
"assetID": "28VTWcsTZ55draGkmjdcS9CFFv4zC3PbvVjkyoqxzNC7Y5msRP",
"fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ",
"output": {
"addresses": ["P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p"],
"amount": 19999999899000000 as u64,
"locktime": 0,
"threshold": 1
}
}
],
"inputs": [
{
"txID": "CjisYCwF4j7zSyC25MWR21e5xxzJgwdaLEuf7oBXSGpe3oaej",
"outputIndex": 0,
"assetID": "28VTWcsTZ55draGkmjdcS9CFFv4zC3PbvVjkyoqxzNC7Y5msRP",
"fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ",
"input": { "amount": 19999999900000000 as u64, "signatureIndices": [0] }
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"amount": 19999999899000000 as u64,
"locktime": 0,
"threshold": 1
}
}
],
"inputs": [
{
"txID": "CjisYCwF4j7zSyC25MWR21e5xxzJgwdaLEuf7oBXSGpe3oaej",
"outputIndex": 0,
"assetID": "28VTWcsTZ55draGkmjdcS9CFFv4zC3PbvVjkyoqxzNC7Y5msRP",
"fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ",
"input": { "amount": 19999999900000000 as u64, "signatureIndices": [0] }
"amount": 19999999899000000_u64,
"locktime": 0,
"threshold": 1
}
}
],
"inputs": [
{
"txID": "CjisYCwF4j7zSyC25MWR21e5xxzJgwdaLEuf7oBXSGpe3oaej",
"outputIndex": 0,
"assetID": "28VTWcsTZ55draGkmjdcS9CFFv4zC3PbvVjkyoqxzNC7Y5msRP",
"fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ",
"input": { "amount": 19999999900000000_64, "signatureIndices": [0] }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I was running into issues with _u64, because it somehow still ended up being treated as an i32. With a u64 suffix, it works now.

],
"memo": "0x",
"validator": {
"nodeID": "NodeID-JHD1JDUZYkiMdWPMDA9UJhWXR4wf25t39",
"start": 1711142673,
"end": 1711142913,
"weight": 20,
"subnetID": "CjisYCwF4j7zSyC25MWR21e5xxzJgwdaLEuf7oBXSGpe3oaej"
},
"subnetAuthorization": { "signatureIndices": [0] }
});

let tx: Tx = serde_json::from_value(tx_json).expect("parsing tx");
let packer = tx.pack().expect("packing tx");

let expected_bytes: &[u8] = &[
// codec version
0x00, 0x00, //
//
// platformvm.UnsignedAddSubnetValidatorTx type ID
0x00, 0x00, 0x00, 0x0d, //
//
// network id
0x00, 0x00, 0x05, 0x39, //
// blockchain id
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, //
//
// outputs.len()
0x00, 0x00, 0x00, 0x01, //
//
// outputs[0].assetID
0x94, 0xb4, 0x5a, 0xa6, 0xe4, 0x46, 0x4a, 0x9a, 0xd3, 0xfc, //
0x4e, 0x73, 0xc7, 0x94, 0x7b, 0xa1, 0x63, 0x2d, 0x7c, 0x82, //
0x0b, 0xaf, 0x0e, 0xbb, 0xfc, 0x75, 0x48, 0xc4, 0x82, 0x3f, //
0x26, 0xbd, //
//
// outputs[0] type ID
0x00, 0x00, 0x00, 0x07, //
//
// outputs[0].output.amount
0x00, 0x47, 0x0d, 0xe4, 0xd9, 0x7c, 0xdc, 0xc0, //
//
// outputs[0].output.locktime
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
//
// outputs[0].output.threshold
0x00, 0x00, 0x00, 0x01, //
//
// outputs[0].output.addresses.len()
0x00, 0x00, 0x00, 0x01, //
//
// outputs[0].output.addresses[0]
0x3c, 0xb7, 0xd3, 0x84, 0x2e, 0x8c, 0xee, 0x6a, 0x0e, 0xbd, //
0x09, 0xf1, 0xfe, 0x88, 0x4f, 0x68, 0x61, 0xe1, 0xb2, 0x9c, //
//
// inputs.len()
0x00, 0x00, 0x00, 0x01, //
// inputs[0].txID
0x1a, 0xa6, 0x3b, 0xe8, 0x46, 0x7b, 0x5f, 0x45, 0x76, 0x5f, //
0xb1, 0x72, 0xc6, 0xed, 0x66, 0x44, 0xa6, 0xed, 0xb6, 0x6c, //
0xa0, 0x41, 0x16, 0x7e, 0x60, 0xa0, 0x78, 0x16, 0x62, 0xfa, //
0xde, 0x58, //
//
// inputs[0].outputIndex
0x00, 0x00, 0x00, 0x00, //
//
// inputs[0].assetID
0x94, 0xb4, 0x5a, 0xa6, 0xe4, 0x46, 0x4a, 0x9a, 0xd3, 0xfc, //
0x4e, 0x73, 0xc7, 0x94, 0x7b, 0xa1, 0x63, 0x2d, 0x7c, 0x82, //
0x0b, 0xaf, 0x0e, 0xbb, 0xfc, 0x75, 0x48, 0xc4, 0x82, 0x3f, //
0x26, 0xbd, //
//
// inputs[0] type ID
0x00, 0x00, 0x00, 0x05, //
//
// inputs[0].input.amount
0x00, 0x47, 0x0d, 0xe4, 0xd9, 0x8c, 0x1f, 0x00, //
//
// inputs[0].input.signatureIndices.len()
0x00, 0x00, 0x00, 0x01, //
//
// inputs[0].input.signatureIndices[0]
0x00, 0x00, 0x00, 0x00, //
//
// memo.len()
0x00, 0x00, 0x00, 0x00, //
// validator.nodeID
0xbd, 0x8a, 0xd0, 0xa9, 0x18, 0xe6, 0x79, 0x1a, 0x32, 0x1e, //
0x03, 0x35, 0x5e, 0x70, 0x76, 0x09, 0xec, 0xf8, 0x56, 0xde, //
//
// validator.start
0x00, 0x00, 0x00, 0x00, 0x65, 0xfd, 0xf7, 0x11, //
//
// validator.end
0x00, 0x00, 0x00, 0x00, 0x65, 0xfd, 0xf8, 0x01, //
//
// validator.weight
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, //
//
// validator.subnetID
0x1a, 0xa6, 0x3b, 0xe8, 0x46, 0x7b, 0x5f, 0x45, 0x76, 0x5f, //
0xb1, 0x72, 0xc6, 0xed, 0x66, 0x44, 0xa6, 0xed, 0xb6, 0x6c, //
0xa0, 0x41, 0x16, 0x7e, 0x60, 0xa0, 0x78, 0x16, 0x62, 0xfa, //
0xde, 0x58, //
//
// subnetAuthorization.signatureIndices type ID
0x00, 0x00, 0x00, 0x0a, //
//
// subnetAuthorization.signatureIndices.len()
0x00, 0x00, 0x00, 0x01, //
//
// subnetAuthorization.signatureIndices[0]
0x00, 0x00, 0x00, 0x00, //
];

assert_eq!(packer.take_bytes(), expected_bytes);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct Tx {
pub shares: u32,

/// To be updated after signing.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question: does it matter if we serialize the empty vector?

pub creds: Vec<key::secp256k1::txs::Credential>,
}

Expand Down
1 change: 1 addition & 0 deletions crates/avalanche-types/src/platformvm/txs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ fn test_sort_stakeable_lock_outs() {
/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm/api#Staker>
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
pub struct Validator {
#[serde(rename = "nodeID")]
pub node_id: node::Id,
pub start: u64,
pub end: u64,
Expand Down
2 changes: 1 addition & 1 deletion crates/avalanche-types/src/txs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ fn test_base_tx_serialization() {
// memo
0x00, 0x01, 0x02, 0x03, //
];
// for c in &unsigned_bytes {
// for c in &unsigned_tx_bytes {
// print!("{:#02x},", *c);
// }
assert!(cmp_manager::eq_vectors(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ async fn build_vertex() {

req.vtx_bytes = b.as_ref().to_vec().clone();
info!("built vertex ({} bytes)", req.vtx_bytes.len());
// info!("{:?}", req);

let resp = cli.build_vertex(req).await.expect("failed build_vertex");
assert!(resp.success);
Expand Down
Loading