Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7c3c6e0
ssz infra
Zacholme7 Sep 2, 2025
2639197
fix validation
Zacholme7 Sep 2, 2025
80809ca
lints
Zacholme7 Sep 2, 2025
7cb61e8
lints
Zacholme7 Sep 2, 2025
7380282
Apply suggestion from @Copilot
Zacholme7 Sep 2, 2025
f61ab9a
Merge branch 'release-v0.3.0' into ssz-infra
Zacholme7 Sep 2, 2025
eb591c8
Merge branch 'ssz-infra' of github.com:Zacholme7/anchor into ssz-infra
Zacholme7 Sep 2, 2025
30c436f
Merge branch 'release-v0.3.0' into ssz-infra
Zacholme7 Sep 2, 2025
7151416
merge and move ssz to try_from
Zacholme7 Sep 3, 2025
327d8ad
size enforcement test
Zacholme7 Sep 3, 2025
cf088f8
fix: update msg_container with latest changes
Zacholme7 Sep 3, 2025
efbc1bf
fix wacky branch
Zacholme7 Sep 3, 2025
e3be77d
helper function for bound checking
Zacholme7 Sep 3, 2025
8aa0b88
Merge branch 'ssz-infra' into ssz-infra-unstable
Zacholme7 Sep 3, 2025
49e5868
import type spec tests
Zacholme7 Sep 3, 2025
d7fe320
cleanup
Zacholme7 Sep 3, 2025
003d110
fix clippy lints on the ssz infra branch
Zacholme7 Sep 3, 2025
c50ab00
Merge branch 'ssz-infra-unstable' into type-spec-tests-unstable
Zacholme7 Sep 3, 2025
04534bd
merge unstable
Zacholme7 Sep 4, 2025
fc4d210
Merge branch 'ssz-infra-unstable' into type-spec-tests-unstable
Zacholme7 Sep 4, 2025
afdb6fa
fix ci issues
Zacholme7 Sep 4, 2025
01867a5
fix ssv-spec submodule
Zacholme7 Sep 4, 2025
ef20da1
merge unstable
Zacholme7 Sep 22, 2025
1df5ee4
clean up spec tests
Zacholme7 Sep 23, 2025
d4610cc
sort
Zacholme7 Sep 23, 2025
b5c41eb
remove a ton of un-used code
Zacholme7 Sep 23, 2025
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "anchor/spec_tests/ssv-spec"]
path = anchor/spec_tests/ssv-spec
url = https://github.com/ssvlabs/ssv-spec
56 changes: 56 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ members = [
"anchor/processor",
"anchor/qbft_manager",
"anchor/signature_collector",
"anchor/spec_tests",
"anchor/subnet_service",
"anchor/validator_store",
]
Expand Down
17 changes: 15 additions & 2 deletions anchor/common/ssv_types/src/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ use std::fmt::Debug;

use derive_more::{Deref, Display, From};
use indexmap::IndexSet;
use serde::Deserialize;
use ssz_derive::{Decode, Encode};
use types::{Address, Graffiti, PublicKeyBytes};

use crate::{OperatorId, committee::CommitteeId};

/// Unique identifier for a cluster
#[derive(Clone, Copy, Default, Eq, PartialEq, Hash, From, Deref)]
#[derive(Clone, Copy, Default, Eq, PartialEq, Hash, From, Deref, Deserialize)]
pub struct ClusterId(pub [u8; 32]);

impl Debug for ClusterId {
Expand Down Expand Up @@ -67,7 +68,19 @@ pub struct ClusterMember {

/// Index of the validator in the validator registry.
#[derive(
Clone, Copy, Display, Debug, Default, Eq, PartialEq, Hash, From, Deref, Encode, Decode,
Clone,
Copy,
Display,
Debug,
Default,
Eq,
PartialEq,
Hash,
From,
Deref,
Encode,
Decode,
Deserialize,
)]
#[ssz(struct_behaviour = "transparent")]
pub struct ValidatorIndex(pub usize);
Expand Down
3 changes: 2 additions & 1 deletion anchor/common/ssv_types/src/committee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::fmt::{Debug, Formatter};

use derive_more::{Deref, From};
use indexmap::IndexSet;
use serde::Deserialize;
use sha2::{Digest, Sha256};

use crate::{OperatorId, ValidatorIndex};
Expand All @@ -16,7 +17,7 @@ pub struct CommitteeInfo {
}

/// Unique identifier for a committee
#[derive(Clone, Copy, Default, Eq, PartialEq, Hash, From, Deref)]
#[derive(Clone, Copy, Default, Eq, PartialEq, Hash, From, Deref, Deserialize)]
pub struct CommitteeId(pub [u8; COMMITTEE_ID_LEN]);

impl Debug for CommitteeId {
Expand Down
5 changes: 3 additions & 2 deletions anchor/common/ssv_types/src/consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::{

use derive_more::{From, Into};
use eth2::types::FullBlockContents;
use serde::Deserialize;
use sha2::{Digest, Sha256};
use slashing_protection::{NotSafe, SlashingDatabase};
use ssz::{Decode, DecodeError, Encode};
Expand Down Expand Up @@ -406,7 +407,7 @@ impl From<DecodeError> for DataValidationError {
}
}

#[derive(Clone, Debug, TreeHash, PartialEq, Encode, Decode)]
#[derive(Clone, Debug, TreeHash, PartialEq, Encode, Decode, Deserialize)]
pub struct ValidatorDuty {
pub r#type: BeaconRole,
pub pub_key: PublicKeyBytes,
Expand All @@ -419,7 +420,7 @@ pub struct ValidatorDuty {
pub validator_sync_committee_indices: VariableList<u64, U13>,
}

#[derive(Clone, Copy, Debug, PartialEq, Encode, Decode)]
#[derive(Clone, Copy, Debug, PartialEq, Encode, Decode, Deserialize)]
#[ssz(struct_behaviour = "transparent")]
pub struct BeaconRole(u64);

Expand Down
213 changes: 213 additions & 0 deletions anchor/common/ssv_types/src/deserializers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
use base64::prelude::*;
use serde::{Deserialize, Deserializer, de::Error};
use serde_json::Value;
use ssz_types::VariableList;
use types::{Hash256, Slot};

use crate::{
ValidatorIndex,
message::{SSVMessageDataLen, SignatureList},
msgid::MessageId,
partial_sig::PartialSignatureKind,
try_to_variable_list,
};

pub fn deserialize_base64_or_empty<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: serde::Deserializer<'de>,
T: TryFrom<Vec<u8>>,
{
let value = Value::deserialize(deserializer)?;

match value {
Value::Null => Ok(Vec::new()), // Return empty Vec for null values
Value::String(s) => BASE64_STANDARD
.decode(s.as_bytes())
.map_err(D::Error::custom),
_ => Err(D::Error::custom("Expected null or a base64 string")),
}
.and_then(|vec| {
vec.try_into()
.map_err(|_| D::Error::custom("Failed to convert from Vec<u8> to actual type"))
})
}

pub fn deserialize_base64_signatures<'de, D>(deserializer: D) -> Result<SignatureList, D::Error>
where
D: serde::Deserializer<'de>,
{
let string_vec: Vec<String> = serde::Deserialize::deserialize(deserializer)?;

let mut signatures = VariableList::empty();

for string in string_vec {
let decoded_bytes = BASE64_STANDARD
.decode(&string)
.map_err(serde::de::Error::custom)?;

let signature_variable_list = VariableList::new(decoded_bytes)
.map_err(|e| D::Error::custom(format!("Signature too long: {e:?}")))?;

if let Err(err) = signatures.push(signature_variable_list) {
return Err(D::Error::custom(format!("Too many signatures: {err:?}")));
}
}

Ok(signatures)
}

pub fn deserialize_base64_message_data<'de, D>(
deserializer: D,
) -> Result<VariableList<u8, SSVMessageDataLen>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;

match value {
Value::Null => {
Ok(
try_to_variable_list(vec![], |_, _| D::Error::custom("Empty vec too large"))
.unwrap(),
)
} // Empty vec always fits
Value::String(s) => {
let decoded = BASE64_STANDARD
.decode(s.as_bytes())
.map_err(D::Error::custom)?;
try_to_variable_list(decoded, |actual, max| {
D::Error::custom(format!(
"Data too large for VariableList: {} > {}",
actual, max
))
})
}
_ => Err(D::Error::custom("Expected null or a base64 string")),
}
}

pub fn deserialize_hex_message_id<'de, D>(deserializer: D) -> Result<MessageId, D::Error>
where
D: Deserializer<'de>,
{
let hex_str = String::deserialize(deserializer)?;
let hex_str = hex_str.strip_prefix("0x").unwrap_or(&hex_str);
let bytes =
hex::decode(hex_str).map_err(|e| Error::custom(format!("Failed to decode hex: {e}")))?;

if bytes.len() != 56 {
return Err(Error::custom(format!(
"Expected 56 bytes for MessageId, got {}",
bytes.len()
)));
}

let array: [u8; 56] = bytes
.try_into()
.map_err(|_| Error::custom("Failed to convert to array"))?;
Ok(MessageId::from(array))
}

pub fn deserialize_slot<'de, D>(deserializer: D) -> Result<Slot, D::Error>
where
D: Deserializer<'de>,
{
let slot_str = String::deserialize(deserializer)?;
slot_str
.parse::<u64>()
.map(Slot::new)
.map_err(|e| Error::custom(format!("Failed to parse slot: {e}")))
}

pub fn deserialize_partial_signature_kind<'de, D>(
deserializer: D,
) -> Result<PartialSignatureKind, D::Error>
where
D: Deserializer<'de>,
{
let value = u64::deserialize(deserializer)?;
match value {
0 => Ok(PartialSignatureKind::PostConsensus),
1 => Ok(PartialSignatureKind::RandaoPartialSig),
2 => Ok(PartialSignatureKind::SelectionProofPartialSig),
3 => Ok(PartialSignatureKind::ContributionProofs),
4 => Ok(PartialSignatureKind::ValidatorRegistration),
5 => Ok(PartialSignatureKind::VoluntaryExit),
_ => Err(Error::custom(format!(
"Invalid PartialSignatureKind value: {}",
value
))),
}
}

pub fn deserialize_signature<'de, D>(deserializer: D) -> Result<types::Signature, D::Error>
where
D: Deserializer<'de>,
{
let sig_opt: Option<String> = Option::deserialize(deserializer)?;
match sig_opt {
Some(sig_str) => {
// Handle empty string as empty signature (for invalid test cases)
if sig_str.is_empty() {
return Ok(types::Signature::empty());
}

let sig_bytes = if let Some(stripped) = sig_str.strip_prefix("0x") {
// Handle hex string with 0x prefix
hex::decode(stripped)
.map_err(|e| Error::custom(format!("Failed to decode hex signature: {e}")))?
} else if sig_str.chars().all(|c| c.is_ascii_hexdigit()) && sig_str.len() % 2 == 0 {
// Try hex without prefix if all characters are hex digits and even length
hex::decode(&sig_str)
.map_err(|e| Error::custom(format!("Failed to decode hex signature: {e}")))?
} else {
// Fall back to base64 for backward compatibility
BASE64_STANDARD
.decode(&sig_str)
.map_err(|e| Error::custom(format!("Failed to decode base64 signature: {e}")))?
};

if sig_bytes.len() != 96 {
return Err(Error::custom(format!(
"Signature must be 96 bytes, got {}",
sig_bytes.len()
)));
}

Ok(types::Signature::deserialize(&sig_bytes)
.map_err(|e| Error::custom(format!("Failed to parse signature: {e:?}")))?)
}
None => {
// Return empty signature for null values
Ok(types::Signature::empty())
}
}
}

pub fn deserialize_hash256<'de, D>(deserializer: D) -> Result<Hash256, D::Error>
where
D: Deserializer<'de>,
{
let hash_str = String::deserialize(deserializer)?;
let hash_str = hash_str.strip_prefix("0x").unwrap_or(&hash_str);
let bytes =
hex::decode(hash_str).map_err(|e| Error::custom(format!("Failed to decode hex: {e}")))?;
if bytes.len() != 32 {
return Err(Error::custom(format!(
"Expected 32 bytes for Hash256, got {}",
bytes.len()
)));
}
Ok(Hash256::from_slice(&bytes))
}

pub fn deserialize_validator_index<'de, D>(deserializer: D) -> Result<ValidatorIndex, D::Error>
where
D: Deserializer<'de>,
{
let index_str = String::deserialize(deserializer)?;
index_str
.parse::<usize>()
.map(ValidatorIndex)
.map_err(|e| Error::custom(format!("Failed to parse validator index: {e}")))
}
1 change: 1 addition & 0 deletions anchor/common/ssv_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub use share::Share;
mod cluster;
mod committee;
pub mod consensus;
pub mod deserializers;
pub mod domain_type;
pub mod message;
pub mod msgid;
Expand Down
Loading
Loading