Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
72 changes: 69 additions & 3 deletions crates/cluster/src/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1301,12 +1301,18 @@ impl From<DefinitionV1x5to7> for Definition {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DefinitionV1x8 {
/// Name is a human-readable cosmetic identifier. Max 256 chars.
// charon marshals `name` with `omitempty`, so it is absent when empty.
#[serde(default)]
pub name: String,
/// Creator identifies the creator of a cluster definition. They may also be
/// an operator.
pub creator: Creator,
/// Operators define the charon nodes in the cluster and their operators.
/// Max 256 operators.
// charon marshals a nil `operators` slice as JSON `null`, and older tools
// may omit the key entirely, so accept both.
#[serde(default)]
#[serde_as(as = "DefaultOnNull")]
pub operators: Vec<OperatorV1X2OrLater>,
/// UUID is a human-readable random unique identifier. Max 64 chars.
pub uuid: String,
Expand All @@ -1315,6 +1321,8 @@ pub struct DefinitionV1x8 {
/// Timestamp is the human-readable timestamp of this definition. Max 32
/// chars. Note that this was added in v1.1.0, so may be empty for older
/// versions.
// charon marshals `timestamp` with `omitempty`, so it is absent when empty.
#[serde(default)]
pub timestamp: String,
/// NumValidators is the number of DVs to be created in the cluster lock
/// file.
Expand All @@ -1323,7 +1331,10 @@ pub struct DefinitionV1x8 {
/// for number of nodes/peers.
pub threshold: u64,
/// ValidatorAddresses define addresses of each validator.
#[serde(rename = "validators")]
// charon marshals a nil `validators` slice as JSON `null`, and older tools
// may omit the key entirely, so accept both.
#[serde(default, rename = "validators")]
#[serde_as(as = "DefaultOnNull")]
pub validator_addresses: Vec<ValidatorAddresses>,
/// DKGAlgorithm to use for key generation. Max 32 chars.
pub dkg_algorithm: String,
Expand Down Expand Up @@ -1405,12 +1416,18 @@ impl From<DefinitionV1x8> for Definition {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DefinitionV1x9 {
/// Name is a human-readable cosmetic identifier. Max 256 chars.
// charon marshals `name` with `omitempty`, so it is absent when empty.
#[serde(default)]
pub name: String,
/// Creator identifies the creator of a cluster definition. They may also be
/// an operator.
pub creator: Creator,
/// Operators define the charon nodes in the cluster and their operators.
/// Max 256 operators.
// charon marshals a nil `operators` slice as JSON `null`, and older tools
// may omit the key entirely, so accept both.
#[serde(default)]
#[serde_as(as = "DefaultOnNull")]
pub operators: Vec<OperatorV1X2OrLater>,
/// UUID is a human-readable random unique identifier. Max 64 chars.
pub uuid: String,
Expand All @@ -1419,6 +1436,8 @@ pub struct DefinitionV1x9 {
/// Timestamp is the human-readable timestamp of this definition. Max 32
/// chars. Note that this was added in v1.1.0, so may be empty for older
/// versions.
// charon marshals `timestamp` with `omitempty`, so it is absent when empty.
#[serde(default)]
pub timestamp: String,
/// NumValidators is the number of DVs to be created in the cluster lock
/// file.
Expand All @@ -1427,7 +1446,10 @@ pub struct DefinitionV1x9 {
/// for number of nodes/peers.
pub threshold: u64,
/// ValidatorAddresses define addresses of each validator.
#[serde(rename = "validators")]
// charon marshals a nil `validators` slice as JSON `null`, and older tools
// may omit the key entirely, so accept both.
#[serde(default, rename = "validators")]
#[serde_as(as = "DefaultOnNull")]
pub validator_addresses: Vec<ValidatorAddresses>,
/// DKGAlgorithm to use for key generation. Max 32 chars.
pub dkg_algorithm: String,
Expand Down Expand Up @@ -1520,6 +1542,10 @@ pub struct DefinitionV1x10 {
pub creator: Creator,
/// Charon nodes in the cluster and their operators.
/// Max 256 operators.
// charon marshals a nil `operators` slice as JSON `null`, and older tools
// may omit the key entirely, so accept both.
#[serde(default)]
#[serde_as(as = "DefaultOnNull")]
pub operators: Vec<OperatorV1X2OrLater>,
/// Human-readable random unique identifier. Max 64 chars.
pub uuid: String,
Expand All @@ -1528,6 +1554,8 @@ pub struct DefinitionV1x10 {
/// Human-readable timestamp of this definition. Max 32
/// chars. Note that this was added in v1.1.0, so may be empty for older
/// versions.
// charon marshals `timestamp` with `omitempty`, so it is absent when empty.
#[serde(default)]
pub timestamp: String,
/// Number of DVs to be created in the cluster lock
/// file.
Expand All @@ -1536,7 +1564,10 @@ pub struct DefinitionV1x10 {
/// for number of nodes/peers.
pub threshold: u64,
/// Addresses of each validator.
#[serde(rename = "validators")]
// charon marshals a nil `validators` slice as JSON `null`, and older tools
// may omit the key entirely, so accept both.
#[serde(default, rename = "validators")]
#[serde_as(as = "DefaultOnNull")]
pub validator_addresses: Vec<ValidatorAddresses>,
/// DKG algorithm to use for key generation. Max 32 chars.
pub dkg_algorithm: String,
Expand Down Expand Up @@ -1985,6 +2016,41 @@ mod tests {
assert!(definition.verify_hashes().is_ok());
}

/// charon marshals nil `operators`/`validators` slices as JSON `null` and
/// omits `name`/`timestamp` (both `omitempty`). Every supported definition
/// version must accept those shapes rather than fail deserialization.
#[test]
fn definition_accepts_null_slices_and_absent_omitempty_fields() {
for fixture in [
include_str!("testdata/cluster_definition_v1_8_0.json"),
include_str!("testdata/cluster_definition_v1_9_0.json"),
include_str!("testdata/cluster_definition_v1_10_0.json"),
] {
let mut value: serde_json::Value = serde_json::from_str(fixture).unwrap();
let obj = value.as_object_mut().unwrap();
obj.insert("operators".to_owned(), serde_json::Value::Null);
// charon only marshals `validators: null` when there are no
// validators, so keep the count consistent with the deserializer's
// num_validators/validators cross-check.
obj.insert("validators".to_owned(), serde_json::Value::Null);
obj.insert("num_validators".to_owned(), serde_json::json!(0));
obj.remove("name");
obj.remove("timestamp");

let version = value["version"].as_str().unwrap().to_owned();
let definition = serde_json::from_value::<Definition>(value)
.unwrap_or_else(|e| panic!("version {version} must parse: {e}"));

assert!(definition.operators.is_empty(), "version {version}");
assert!(
definition.validator_addresses.is_empty(),
"version {version}"
);
assert_eq!(definition.name, "", "version {version}");
assert_eq!(definition.timestamp, "", "version {version}");
}
}

#[test]
fn definition_empty_deposit_amounts_serialize_as_null() {
let mut definition = Definition {
Expand Down
32 changes: 31 additions & 1 deletion crates/cluster/src/distvalidator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,11 +327,16 @@ pub struct DistValidatorV1x8orLater {
/// Public shares are the public keys corresponding to each node's secret
/// key share. It can be used to verify a partial signature created by
/// any node in the cluster.
#[serde(rename = "public_shares")]
// charon marshals `public_shares` with `omitempty`, so it is absent when
// empty.
#[serde(default, rename = "public_shares")]
#[serde_as(as = "Vec<HexBytes>")]
pub pub_shares: Vec<Vec<u8>>,

/// Deposit data defines the deposit data to activate a validator.
// charon marshals `partial_deposit_data` with `omitempty`, so it is absent
// when empty.
#[serde(default)]
pub partial_deposit_data: Vec<DepositData>,

/// Builder registration is the pre-generated signed validator builder
Expand Down Expand Up @@ -360,3 +365,28 @@ impl From<DistValidatorV1x8orLater> for DistValidator {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

/// charon marshals `public_shares` and `partial_deposit_data` with
/// `omitempty`, so both keys are absent when empty. Deserialization must
/// tolerate that rather than require the keys.
#[test]
fn dist_validator_v1x8_accepts_absent_omitempty_fields() {
let lock: serde_json::Value =
serde_json::from_str(include_str!("testdata/cluster_lock_v1_10_0.json")).unwrap();

let mut validator = lock["distributed_validators"][0].clone();
let obj = validator.as_object_mut().unwrap();
obj.remove("public_shares");
obj.remove("partial_deposit_data");

let dist_validator: DistValidatorV1x8orLater =
serde_json::from_value(validator).expect("must parse without omitempty fields");

assert!(dist_validator.pub_shares.is_empty());
assert!(dist_validator.partial_deposit_data.is_empty());
}
}
Loading