Skip to content
Draft
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
6 changes: 6 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions anchor/common/ssv_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ authors = ["Sigma Prime <contact@sigmaprime.io>"]

[features]
arbitrary-fuzz = ["arbitrary"]
serde = ["dep:serde"]

[dependencies]
arbitrary = { workspace = true, optional = true }
Expand All @@ -20,6 +21,7 @@ indexmap = { workspace = true }
openssl = { workspace = true }
operator_key = { workspace = true }
rusqlite = { workspace = true }
serde = { workspace = true, optional = true }
sha2 = { workspace = true }
slashing_protection = { workspace = true }
ssz_types = { workspace = true }
Expand Down
22 changes: 18 additions & 4 deletions anchor/common/ssv_types/src/consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,20 @@ pub struct ProposerConsensusData {
pub data_ssz: VariableList<u8, ProposerConsensusDataLen>,
}

impl ProposerConsensusData {
/// Decode the block data as a blinded beacon block.
pub fn get_blinded_block_data<E: EthSpec>(&self) -> Result<BlindedBeaconBlock<E>, DecodeError> {
let fork = ForkName::from(self.version);
BlindedBeaconBlock::from_ssz_bytes_for_fork(&self.data_ssz, fork)
}

/// Decode the block data as full block contents (block + blobs).
pub fn get_block_data<E: EthSpec>(&self) -> Result<FullBlockContents<E>, DecodeError> {
let fork = ForkName::from(self.version);
FullBlockContents::from_ssz_bytes_for_fork(&self.data_ssz, fork)
}
}

impl QbftData for ProposerConsensusData {
type Hash = Hash256;

Expand Down Expand Up @@ -365,14 +379,14 @@ impl<E: EthSpec> ProposerConsensusDataValidator<E> {
&self,
value: &ProposerConsensusData,
) -> Result<(), DataValidationError> {
let fork = ForkName::from(value.version);

// Always do this check, even if we're not validating slashing. This is to ensure that we
// have a decodable value.
let header = BlindedBeaconBlock::<E>::from_ssz_bytes_for_fork(&value.data_ssz, fork)
let header = value
.get_blinded_block_data::<E>()
.map(|block| block.block_header())
.or_else(|_| {
FullBlockContents::<E>::from_ssz_bytes_for_fork(&value.data_ssz, fork)
value
.get_block_data::<E>()
.map(|block| block.block().block_header())
})
.map_err(DataValidationError::DecodeError)?;
Expand Down
93 changes: 93 additions & 0 deletions anchor/common/ssv_types/src/deserializers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! Custom serde deserializers for Go JSON fixture format.
//!
//! These deserializers bridge Go's JSON serialization format to Anchor's Rust types.
//! Only compiled when the `serde` feature is enabled.
//!
//! Referenced via `#[serde(deserialize_with = "...")]` on struct fields.

use base64::{Engine, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, de::Error};
use ssz_types::VariableList;
use types::{Hash256, Slot};

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

pub fn deserialize_hex_message_id<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<MessageId, D::Error> {
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}")))?;
MessageId::try_from(bytes.as_slice()).map_err(|_| {
Error::custom(format!(
"Invalid MessageId: expected 56 bytes, got {}",
bytes.len()
))
})
}

pub fn deserialize_base64_message_data<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<VariableList<u8, SSVMessageDataLen>, D::Error> {
let b64_str = String::deserialize(deserializer)?;
let bytes = STANDARD
.decode(&b64_str)
.map_err(|e| Error::custom(format!("Failed to decode base64 data: {e}")))?;
VariableList::new(bytes).map_err(|_| Error::custom("SSVMessage data exceeds maximum length"))
}

pub fn deserialize_partial_signature_kind<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<PartialSignatureKind, D::Error> {
let value = u64::deserialize(deserializer)?;
PartialSignatureKind::try_from(value)
.map_err(|_| Error::custom(format!("Invalid PartialSignatureKind value: {value}")))
}

pub fn deserialize_slot<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Slot, D::Error> {
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_signature<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<bls::Signature, D::Error> {
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}")))?;
bls::Signature::deserialize(&bytes)
.map_err(|e| Error::custom(format!("Invalid BLS signature: {e:?}")))
}

pub fn deserialize_hash256<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Hash256, D::Error> {
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() != 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<'de>>(
deserializer: D,
) -> Result<ValidatorIndex, D::Error> {
let index_str = String::deserialize(deserializer)?;
index_str
.parse::<usize>()
.map(ValidatorIndex)
.map_err(|e| Error::custom(format!("Failed to parse validator index: {e}")))
}
3 changes: 3 additions & 0 deletions anchor/common/ssv_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ mod share;
mod sql_conversions;
pub mod test_utils;

#[cfg(feature = "serde")]
pub mod deserializers;

pub use indexmap::IndexSet;
pub use round::Round;
pub use share::ENCRYPTED_KEY_LENGTH;
Expand Down
21 changes: 21 additions & 0 deletions anchor/common/ssv_types/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use tree_hash_derive::TreeHash;
use typenum::{Prod, Sum, U8, U13, U256, U388, U726, U836, U932, U1000, U1000000, Unsigned};
use types::{Hash256, Slot};

#[cfg(feature = "serde")]
use crate::deserializers::*;
use crate::{
MAX_SIGNATURES, OperatorId, RSA_SIGNATURE_SIZE,
consensus::{PrepareJustificationLength, QbftMessage, RoundChangeJustificationLength},
Expand Down Expand Up @@ -117,6 +119,15 @@ impl TryFrom<u64> for MsgType {
}
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for MsgType {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = <u64 as serde::Deserialize>::deserialize(deserializer)?;
MsgType::try_from(value)
.map_err(|_| serde::de::Error::custom(format!("Invalid MsgType value: {value}")))
}
}

const U64_SIZE: usize = 8; // u64 is 8 bytes

impl Encode for MsgType {
Expand Down Expand Up @@ -174,9 +185,19 @@ pub enum SSVMessageError {
/// Represents a bare SSVMessage with a type, ID, and data.
#[derive(Encode, Decode, Clone, PartialEq, Eq, TreeHash)]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
pub struct SSVMessage {
#[cfg_attr(feature = "serde", serde(rename = "MsgType"))]
msg_type: MsgType,
#[cfg_attr(
feature = "serde",
serde(rename = "MsgID", deserialize_with = "deserialize_hex_message_id")
)]
msg_id: MessageId,
#[cfg_attr(
feature = "serde",
serde(rename = "Data", deserialize_with = "deserialize_base64_message_data")
)]
data: VariableList<u8, SSVMessageDataLen>,
}

Expand Down
1 change: 1 addition & 0 deletions anchor/common/ssv_types/src/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use types::Address;
)]
#[ssz(struct_behaviour = "transparent")]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
pub struct OperatorId(pub u64);
impl TreeHash for OperatorId {
fn tree_hash_type() -> TreeHashType {
Expand Down
75 changes: 75 additions & 0 deletions anchor/common/ssv_types/src/partial_sig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ use bls::Signature;
use ssz::{Decode, DecodeError, Encode};
use ssz_derive::{Decode, Encode};
use ssz_types::VariableList;
use thiserror::Error;
use tree_hash::{PackedEncoding, TreeHash, TreeHashType};
use tree_hash_derive::TreeHash;
use typenum::{Prod, Sum, U3, U4, U512, U1000};
use types::{Hash256, Slot};

#[cfg(feature = "serde")]
use crate::deserializers::*;
use crate::{OperatorId, ValidatorIndex};

/// Maximum number of `PartialSignatureMessage`s: 5048
Expand Down Expand Up @@ -118,20 +121,92 @@ impl TreeHash for PartialSignatureKind {

// A partial signature specific message
#[derive(Clone, Debug, PartialEq, Encode, Decode, TreeHash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
pub struct PartialSignatureMessages {
#[cfg_attr(
feature = "serde",
serde(
rename = "Type",
deserialize_with = "deserialize_partial_signature_kind"
)
)]
pub kind: PartialSignatureKind,
#[cfg_attr(
feature = "serde",
serde(rename = "Slot", deserialize_with = "deserialize_slot")
)]
pub slot: Slot,
#[cfg_attr(feature = "serde", serde(rename = "Messages"))]
pub messages: VariableList<PartialSignatureMessage, PartialSignatureMessagesLen>,
}

#[derive(Clone, Debug, PartialEq, Encode, Decode, TreeHash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
pub struct PartialSignatureMessage {
#[cfg_attr(
feature = "serde",
serde(
rename = "PartialSignature",
deserialize_with = "deserialize_signature"
)
)]
pub partial_signature: Signature,
#[cfg_attr(
feature = "serde",
serde(rename = "SigningRoot", deserialize_with = "deserialize_hash256")
)]
pub signing_root: Hash256,
#[cfg_attr(feature = "serde", serde(rename = "Signer"))]
pub signer: OperatorId,
#[cfg_attr(
feature = "serde",
serde(
rename = "ValidatorIndex",
deserialize_with = "deserialize_validator_index"
)
)]
pub validator_index: ValidatorIndex,
}

/// Errors from `PartialSignatureMessages::validate()`.
///
/// Mirrors Go's `PartialSignatureMessages.Validate()` error conditions.
#[derive(Debug, Error)]
pub enum PartialSignatureMessagesError {
#[error("no partial signature messages")]
Empty,
#[error("inconsistent signers")]
InconsistentSigners,
#[error("signer ID 0 not allowed")]
ZeroSigner,
}

impl PartialSignatureMessages {
/// Validate the message structure.
///
/// Mirrors Go's `PartialSignatureMessages.Validate()`:
/// 1. Messages must not be empty
/// 2. All message signers must be the same
/// 3. No signer may have ID 0
pub fn validate(&self) -> Result<(), PartialSignatureMessagesError> {
let first = self
.messages
.first()
.ok_or(PartialSignatureMessagesError::Empty)?;

for m in self.messages.iter() {
if m.signer != first.signer {
return Err(PartialSignatureMessagesError::InconsistentSigners);
}
if m.signer == OperatorId(0) {
return Err(PartialSignatureMessagesError::ZeroSigner);
}
}

Ok(())
}
}

#[cfg(test)]
mod tests {
use tree_hash::TreeHash;
Expand Down
25 changes: 15 additions & 10 deletions anchor/message_validator/src/partial_signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use slot_clock::SlotClock;
use ssv_types::{
OperatorId,
msgid::Role,
partial_sig::{PartialSignatureKind, PartialSignatureMessages},
partial_sig::{PartialSignatureKind, PartialSignatureMessages, PartialSignatureMessagesError},
};
use ssz::Decode;
use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT;
Expand Down Expand Up @@ -105,18 +105,23 @@ fn validate_partial_signature_message_semantics(
return Err(ValidationFailure::PartialSignatureTypeRoleMismatch);
}

// Rule: Partial signature message must have at least one signature
if partial_signature_messages.messages.is_empty() {
return Err(ValidationFailure::NoPartialSignatureMessages);
// Structural validation: empty, internal signer consistency, zero signer.
partial_signature_messages.validate().map_err(|e| match e {
PartialSignatureMessagesError::Empty => ValidationFailure::NoPartialSignatureMessages,
PartialSignatureMessagesError::InconsistentSigners => {
ValidationFailure::InconsistentSigners
}
PartialSignatureMessagesError::ZeroSigner => ValidationFailure::ZeroSigner,
})?;

// Rule: Partial signature signer must match the signed message's signer.
// validate() ensures all inner signers are the same, so check one.
if partial_signature_messages.messages[0].signer != signer {
return Err(ValidationFailure::InconsistentSigners);
}

// Validate each individual message
// Validate validator indices for non-committee duties
for message in &partial_signature_messages.messages {
// Rule: Partial signature signer must be consistent
if message.signer != signer {
return Err(ValidationFailure::InconsistentSigners);
}

// Rule: (only for Validator duties) Validator index must match with validatorPK
// For Committee duties (Committee and AggregatorCommittee), we don't assume that
// operators are synced on the validators set, so we skip this check.
Expand Down
Loading
Loading