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 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
21 changes: 11 additions & 10 deletions crates/avalanche-types/src/key/secp256k1/txs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,13 +341,14 @@ 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)]
#[serde(rename_all = "camelCase")]
pub struct Input {
pub sig_indices: Vec<u32>,
pub signature_indices: Vec<u32>,
}

impl Input {
pub fn new(sig_indices: Vec<u32>) -> Self {
Self { sig_indices }
pub fn new(signature_indices: Vec<u32>) -> Self {
Self { signature_indices }
}

pub fn type_name() -> String {
Expand All @@ -361,7 +362,7 @@ impl Input {

impl Ord for Input {
fn cmp(&self, other: &Input) -> Ordering {
SigIndices::new(&self.sig_indices).cmp(&SigIndices::new(&other.sig_indices))
SigIndices::new(&self.signature_indices).cmp(&SigIndices::new(&other.signature_indices))
}
}

Expand All @@ -383,13 +384,13 @@ fn test_sort_inputs() {
let mut inputs: Vec<Input> = Vec::new();
for i in (0..10).rev() {
inputs.push(Input {
sig_indices: vec![i as u32, 2, 2, 3, 4, 5, 6, 7, 8, 9],
signature_indices: vec![i as u32, 2, 2, 3, 4, 5, 6, 7, 8, 9],
});
inputs.push(Input {
sig_indices: vec![i as u32, 1, 2, 3, 4, 5, 6, 7, 8, 9],
signature_indices: vec![i as u32, 1, 2, 3, 4, 5, 6, 7, 8, 9],
});
inputs.push(Input {
sig_indices: vec![i as u32, 1, 2, 3, 4, 5],
signature_indices: vec![i as u32, 1, 2, 3, 4, 5],
});
}
assert!(!cmp_manager::is_sorted_and_unique(&inputs));
Expand All @@ -398,15 +399,15 @@ fn test_sort_inputs() {
let mut sorted_inputs: Vec<Input> = Vec::new();
for i in 0..10 {
sorted_inputs.push(Input {
sig_indices: vec![i as u32, 1, 2, 3, 4, 5],
signature_indices: vec![i as u32, 1, 2, 3, 4, 5],
});
}
for i in 0..10 {
sorted_inputs.push(Input {
sig_indices: vec![i as u32, 1, 2, 3, 4, 5, 6, 7, 8, 9],
signature_indices: vec![i as u32, 1, 2, 3, 4, 5, 6, 7, 8, 9],
});
sorted_inputs.push(Input {
sig_indices: vec![i as u32, 2, 2, 3, 4, 5, 6, 7, 8, 9],
signature_indices: vec![i as u32, 2, 2, 3, 4, 5, 6, 7, 8, 9],
});
}
assert!(cmp_manager::is_sorted_and_unique(&sorted_inputs));
Expand Down
184 changes: 175 additions & 9 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 @@ -20,16 +27,19 @@ impl Default for Validator {
/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm/txs#Tx>
/// ref. <https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/platformvm/txs#UnsignedTx>
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct Tx {
/// The transaction ID is empty for unsigned 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,
pub subnet_auth: key::secp256k1::txs::Input,
pub subnet_authorization: key::secp256k1::txs::Input,

/// To be updated after signing.
#[serde(default)]
pub creds: Vec<key::secp256k1::txs::Credential>,
}

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<()> {
fn pack(&self) -> Result<packer::Packer> {
// marshal "unsigned tx" with the codec version
let type_id = Self::type_id();
let packer = self.base_tx.pack(codec::VERSION, type_id)?;
Expand All @@ -88,11 +96,19 @@ impl Tx {
// pack the third field "subnet_auth" in the struct
let subnet_auth_type_id = key::secp256k1::txs::Input::type_id();
packer.pack_u32(subnet_auth_type_id)?;
packer.pack_u32(self.subnet_auth.sig_indices.len() as u32)?;
for sig_idx in self.subnet_auth.sig_indices.iter() {
packer.pack_u32(self.subnet_authorization.signature_indices.len() as u32)?;
for sig_idx in self.subnet_authorization.signature_indices.iter() {
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 @@ -230,8 +246,8 @@ fn test_add_subnet_validator_tx_serialization_with_one_signer() {
0x59, 0xb9,
])),
},
subnet_auth: key::secp256k1::txs::Input {
sig_indices: vec![0_u32],
subnet_authorization: key::secp256k1::txs::Input {
signature_indices: vec![0_u32],
},
..Tx::default()
};
Expand Down Expand Up @@ -405,3 +421,153 @@ fn test_add_subnet_validator_tx_serialization_with_one_signer() {
&tx_bytes_with_signatures
));
}

// TODO: split up serialization tests
#[test]
4tXJ7f marked this conversation as resolved.
Show resolved Hide resolved
fn test_json_deserialize() {
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": 19999999899000000u64,
"locktime": 0,
"threshold": 1
}
}
],
"inputs": [
{
"txID": "CjisYCwF4j7zSyC25MWR21e5xxzJgwdaLEuf7oBXSGpe3oaej",
"outputIndex": 0,
"assetID": "28VTWcsTZ55draGkmjdcS9CFFv4zC3PbvVjkyoqxzNC7Y5msRP",
"fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ",
"input": { "amount": 19999999900000000u64, "signatureIndices": [0] }
}
],
"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)]
pub creds: Vec<key::secp256k1::txs::Credential>,
}

Expand Down
6 changes: 3 additions & 3 deletions crates/avalanche-types/src/platformvm/txs/create_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ impl Tx {
// pack the seventh field "subnet_auth" in the struct
let subnet_auth_type_id = key::secp256k1::txs::Input::type_id();
packer.pack_u32(subnet_auth_type_id)?;
packer.pack_u32(self.subnet_auth.sig_indices.len() as u32)?;
for sig_idx in self.subnet_auth.sig_indices.iter() {
packer.pack_u32(self.subnet_auth.signature_indices.len() as u32)?;
for sig_idx in self.subnet_auth.signature_indices.iter() {
packer.pack_u32(*sig_idx)?;
}

Expand Down Expand Up @@ -364,7 +364,7 @@ fn test_create_chain_tx_serialization_with_one_signer() {
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x22, 0x7d,
]),
subnet_auth: key::secp256k1::txs::Input {
sig_indices: vec![0],
signature_indices: vec![0],
},
..Tx::default()
};
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
4 changes: 2 additions & 2 deletions crates/avalanche-types/src/wallet/p/add_subnet_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ where
);

let (ins, unstaked_outs, _, signers) = self.inner.spend(0, self.inner.inner.tx_fee).await?;
let (subnet_auth, subnet_signers) = self.inner.authorize(self.subnet_id).await?;
let (subnet_authorization, subnet_signers) = self.inner.authorize(self.subnet_id).await?;

let mut tx = platformvm::txs::add_subnet_validator::Tx {
base_tx: txs::Tx {
Expand All @@ -227,7 +227,7 @@ where
subnet_id: self.subnet_id,
},
// if "sig_indices" empty, it errors with "unauthorized subnet modification: input has less signers than expected"
subnet_auth,
subnet_authorization,
..Default::default()
};
tx.sign([signers, subnet_signers].concat()).await?;
Expand Down
4 changes: 2 additions & 2 deletions crates/avalanche-types/src/wallet/p/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,12 +393,12 @@ where
retryable: false,
});
}
let (sig_indices, keys) = res.unwrap();
let (signature_indices, keys) = res.unwrap();

return Ok((
key::secp256k1::txs::Input {
// if empty, it errors with "unauthorized subnet modification: input has less signers than expected"
sig_indices,
signature_indices,
},
vec![keys],
));
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