diff --git a/Cargo.lock b/Cargo.lock index 2275275a..b592b9cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5418,6 +5418,7 @@ dependencies = [ "pluto-crypto", "pluto-eth2api", "pluto-k1util", + "pluto-ssz", "pluto-tracing", "prost 0.14.3", "prost-types 0.14.3", @@ -5461,6 +5462,7 @@ dependencies = [ "pluto-k1util", "pluto-p2p", "pluto-relay-server", + "pluto-ssz", "pluto-tracing", "rand 0.8.5", "reqwest 0.13.2", @@ -5490,6 +5492,7 @@ dependencies = [ "pluto-eth2util", "pluto-k1util", "pluto-p2p", + "pluto-ssz", "pluto-testutil", "prost 0.14.3", "prost-build", @@ -5523,6 +5526,7 @@ dependencies = [ "pluto-build-proto", "pluto-eth2api", "pluto-eth2util", + "pluto-ssz", "prost 0.14.3", "prost-types 0.14.3", "rand 0.8.5", @@ -5579,6 +5583,7 @@ dependencies = [ "hex", "http", "oas3-gen-support", + "pluto-ssz", "regex", "reqwest 0.13.2", "serde", @@ -5610,6 +5615,7 @@ dependencies = [ "pluto-crypto", "pluto-eth2api", "pluto-k1util", + "pluto-ssz", "pluto-testutil", "rand 0.8.5", "regex", @@ -5734,6 +5740,19 @@ dependencies = [ "vise-exporter", ] +[[package]] +name = "pluto-ssz" +version = "1.7.1" +dependencies = [ + "hex", + "k256", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.18", + "tree_hash", +] + [[package]] name = "pluto-testutil" version = "1.7.1" diff --git a/Cargo.toml b/Cargo.toml index 93eddca0..cf2ec803 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/k1util", "crates/relay-server", "crates/p2p", + "crates/ssz", "crates/testutil", "crates/tracing", "crates/peerinfo", @@ -106,6 +107,7 @@ pluto-eth2util = { path = "crates/eth2util" } pluto-eth1wrap = { path = "crates/eth1wrap" } pluto-k1util = { path = "crates/k1util" } pluto-relay-server = { path = "crates/relay-server" } +pluto-ssz = { path = "crates/ssz" } pluto-testutil = { path = "crates/testutil" } pluto-tracing = { path = "crates/tracing" } pluto-p2p = { path = "crates/p2p" } diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index d799fd7f..9886b484 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -34,6 +34,7 @@ tempfile.workspace = true pluto-cluster.workspace = true pluto-k1util.workspace = true pluto-crypto.workspace = true +pluto-ssz.workspace = true [build-dependencies] pluto-build-proto.workspace = true diff --git a/crates/app/src/obolapi/error.rs b/crates/app/src/obolapi/error.rs index 56227481..16582e92 100644 --- a/crates/app/src/obolapi/error.rs +++ b/crates/app/src/obolapi/error.rs @@ -45,7 +45,7 @@ pub enum Error { /// SSZ hashing error from [`pluto_cluster`]. #[error("SSZ hashing error: {0}")] - Ssz(#[from] pluto_cluster::ssz::SSZError), + Ssz(#[from] pluto_cluster::ssz::SSZError), /// K1 signing error. #[error("K1 signing error: {0}")] @@ -73,5 +73,11 @@ pub enum Error { /// SSZ hasher error. #[error("SSZ hasher error: {0}")] - HasherError(#[from] pluto_cluster::ssz_hasher::HasherError), + HasherError(#[from] pluto_ssz::HasherError), +} + +impl From> for Error { + fn from(error: pluto_ssz::Error) -> Self { + Self::Ssz(pluto_cluster::ssz::SSZError::from(error)) + } } diff --git a/crates/app/src/obolapi/exit.rs b/crates/app/src/obolapi/exit.rs index 362c292c..2b9ec303 100644 --- a/crates/app/src/obolapi/exit.rs +++ b/crates/app/src/obolapi/exit.rs @@ -11,11 +11,11 @@ use serde::{Deserialize, Serialize}; use pluto_cluster::{ helpers::to_0x_hex, ssz::{SSZ_LEN_BLS_SIG, SSZ_LEN_PUB_KEY}, - ssz_hasher::{HashWalker, Hasher}, }; use pluto_eth2api::types::{ GetPoolVoluntaryExitsResponseResponseDatum, Phase0SignedVoluntaryExitMessage, }; +use pluto_ssz::{HashWalker, Hasher, put_bytes_n}; use crate::obolapi::{ client::Client, @@ -26,9 +26,6 @@ use crate::obolapi::{ /// Type alias for signed voluntary exit from eth2api. pub type SignedVoluntaryExit = GetPoolVoluntaryExitsResponseResponseDatum; -// TODO: Unify SSZ hashing across the workspace. `pluto-cluster` already has -// SSZ hashing utilities. Consider extracting a shared SSZ crate (or promoting -// the existing hasher) so all crates share one SSZ interface and error type. /// Trait for types that can be hashed using SSZ hash tree root. pub trait SszHashable { /// Hashes this value into the provided hasher. @@ -48,7 +45,7 @@ impl SszHashable for SignedVoluntaryExit { self.message.hash_with(hh)?; let sig_bytes = from_0x(&self.signature, SSZ_LEN_BLS_SIG)?; - pluto_cluster::helpers::put_bytes_n(hh, &sig_bytes, SSZ_LEN_BLS_SIG)?; + put_bytes_n(hh, &sig_bytes, SSZ_LEN_BLS_SIG)?; hh.merkleize(index)?; Ok(()) @@ -236,7 +233,7 @@ impl SszHashable for FullExitAuthBlob { let index = hh.index(); hh.put_bytes(&self.lock_hash)?; - pluto_cluster::helpers::put_bytes_n(hh, &self.validator_pubkey, SSZ_LEN_PUB_KEY)?; + put_bytes_n(hh, &self.validator_pubkey, SSZ_LEN_PUB_KEY)?; hh.put_uint64(self.share_index)?; hh.merkleize(index)?; diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index d646107e..13c7c012 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -25,6 +25,7 @@ pluto-core.workspace = true pluto-p2p.workspace = true pluto-eth2util.workspace = true pluto-k1util.workspace = true +pluto-ssz.workspace = true libp2p.workspace = true tokio-util.workspace = true tracing.workspace = true diff --git a/crates/cli/src/commands/test/mod.rs b/crates/cli/src/commands/test/mod.rs index 57dc3a8c..774b361d 100644 --- a/crates/cli/src/commands/test/mod.rs +++ b/crates/cli/src/commands/test/mod.rs @@ -32,9 +32,9 @@ use crate::{ use k256::SecretKey; use pluto_app::obolapi::{Client, ClientOptions}; -use pluto_cluster::ssz_hasher::{HashWalker, Hasher}; use pluto_eth2util::enr::Record; use pluto_k1util::{load, sign}; +use pluto_ssz::{HashWalker, Hasher}; use reqwest::{Method, StatusCode, header::CONTENT_TYPE}; use serde_with::{base64::Base64, serde_as}; use std::os::unix::fs::PermissionsExt as _; diff --git a/crates/cli/src/error.rs b/crates/cli/src/error.rs index f16e60cf..e049863e 100644 --- a/crates/cli/src/error.rs +++ b/crates/cli/src/error.rs @@ -73,7 +73,7 @@ pub enum CliError { /// SSZ hasher error. #[error("Hasher error: {0}")] - HasherError(#[from] pluto_cluster::ssz_hasher::HasherError), + HasherError(#[from] pluto_ssz::HasherError), /// HTTP request error. #[error("HTTP request error: {0}")] diff --git a/crates/cluster/Cargo.toml b/crates/cluster/Cargo.toml index a881a110..bc89512b 100644 --- a/crates/cluster/Cargo.toml +++ b/crates/cluster/Cargo.toml @@ -25,6 +25,7 @@ pluto-p2p.workspace = true pluto-eth2util.workspace = true pluto-eth1wrap.workspace = true pluto-k1util.workspace = true +pluto-ssz.workspace = true k256.workspace = true tokio.workspace = true reqwest = { workspace = true, features = ["json"] } diff --git a/crates/cluster/src/definition.rs b/crates/cluster/src/definition.rs index 13d6bba2..f1cfb909 100644 --- a/crates/cluster/src/definition.rs +++ b/crates/cluster/src/definition.rs @@ -1,14 +1,15 @@ use std::collections::HashSet; +use pluto_ssz::{Hasher, serde_utils::Hex0x}; + use crate::{ eip712sigs::{ EIP712Error, digest_eip712, eip712_creator_config_hash, eip712_enr, get_operator_eip712_type, }, - helpers::{EthHex, from_0x_hex_str}, + helpers::from_0x_hex_str, operator::{Operator, OperatorV1X1, OperatorV1X2OrLater}, ssz::{SSZError, hash_definition}, - ssz_hasher::Hasher, version::{CURRENT_VERSION, DKG_ALGO, versions::*}, }; use chrono::{DateTime, Timelike, Utc}; @@ -771,7 +772,7 @@ pub struct Creator { /// The Ethereum address of the creator pub address: String, /// The creator's signature over the config hash - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub config_signature: Vec, } @@ -817,7 +818,7 @@ pub struct DefinitionV1x0or1 { pub dkg_algorithm: String, /// Cluster's 4 byte beacon chain fork version /// (network/chain identifier). - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub fork_version: Vec, /// Config hash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. @@ -928,15 +929,15 @@ pub struct DefinitionV1x2or3 { pub dkg_algorithm: String, /// Cluster's 4 byte beacon chain fork version /// (network/chain identifier). - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub fork_version: Vec, /// Config hash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub config_hash: Vec, /// Definition hash uniquely identifies a cluster definition including /// operator ENRs and signatures. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub definition_hash: Vec, } @@ -1042,15 +1043,15 @@ pub struct DefinitionV1x4 { pub dkg_algorithm: String, /// Cluster's 4 byte beacon chain fork version /// (network/chain identifier). - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub fork_version: Vec, /// Config hash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub config_hash: Vec, /// Definition hash uniquely identifies a cluster definition including /// operator ENRs and signatures. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub definition_hash: Vec, } @@ -1154,15 +1155,15 @@ pub struct DefinitionV1x5to7 { pub dkg_algorithm: String, /// Cluster's 4 byte beacon chain fork version /// (network/chain identifier). - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub fork_version: Vec, /// Config hash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub config_hash: Vec, /// Definition hash uniquely identifies a cluster definition including /// operator ENRs and signatures. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub definition_hash: Vec, } @@ -1251,7 +1252,7 @@ pub struct DefinitionV1x8 { pub dkg_algorithm: String, /// ForkVersion defines the cluster's 4 byte beacon chain fork version /// (network/chain identifier). - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub fork_version: Vec, /// DepositAmounts specifies partial deposit amounts that sum up to at least /// 32ETH. @@ -1259,11 +1260,11 @@ pub struct DefinitionV1x8 { pub deposit_amounts: Vec, /// ConfigHash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub config_hash: Vec, /// DefinitionHash uniquely identifies a cluster definition including /// operator ENRs and signatures. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub definition_hash: Vec, } @@ -1353,7 +1354,7 @@ pub struct DefinitionV1x9 { pub dkg_algorithm: String, /// ForkVersion defines the cluster's 4 byte beacon chain fork version /// (network/chain identifier). - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub fork_version: Vec, /// DepositAmounts specifies partial deposit amounts that sum up to at least /// 32ETH. @@ -1364,11 +1365,11 @@ pub struct DefinitionV1x9 { pub consensus_protocol: String, /// ConfigHash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub config_hash: Vec, /// DefinitionHash uniquely identifies a cluster definition including /// operator ENRs and signatures. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub definition_hash: Vec, } @@ -1460,7 +1461,7 @@ pub struct DefinitionV1x10 { pub dkg_algorithm: String, /// Cluster's 4 byte beacon chain fork version /// (network/chain identifier). - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub fork_version: Vec, /// Partial deposit amounts that sum up to at least /// 32ETH. @@ -1476,11 +1477,11 @@ pub struct DefinitionV1x10 { pub compounding: bool, /// Config hash uniquely identifies a cluster definition excluding operator /// ENRs and signatures. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub config_hash: Vec, /// Definition hash uniquely identifies a cluster definition including /// operator ENRs and signatures. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub definition_hash: Vec, } diff --git a/crates/cluster/src/deposit.rs b/crates/cluster/src/deposit.rs index a3bdc37f..009e65a7 100644 --- a/crates/cluster/src/deposit.rs +++ b/crates/cluster/src/deposit.rs @@ -1,12 +1,12 @@ -use crate::helpers::EthHex; use pluto_eth2api::spec::phase0; +use pluto_ssz::serde_utils::Hex0x; use serde::{Deserialize, Serialize}; use serde_with::{DisplayFromStr, serde_as}; /// DepositData defines the deposit data to activate a validator. /// /// This is a cluster-specific wrapper around the canonical -/// `phase0::DepositData` that uses EthHex serialization (with 0x prefix) to +/// `phase0::DepositData` that uses `Hex0x` serialization (with 0x prefix) to /// maintain lock file JSON compatibility. /// /// Specification: @@ -14,12 +14,12 @@ use serde_with::{DisplayFromStr, serde_as}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DepositData { /// Validator's public key (48 bytes). - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] #[serde(rename = "pubkey")] pub pub_key: phase0::BLSPubKey, /// Withdrawal credentials included in the deposit (32 bytes). - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub withdrawal_credentials: phase0::WithdrawalCredentials, /// Amount in Gwei to be deposited [1ETH..2048ETH]. @@ -28,7 +28,7 @@ pub struct DepositData { pub amount: phase0::Gwei, /// Signature is the BLS signature of the deposit message (96 bytes). - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub signature: phase0::BLSSignature, } diff --git a/crates/cluster/src/distvalidator.rs b/crates/cluster/src/distvalidator.rs index 54cbfe94..a19410a2 100644 --- a/crates/cluster/src/distvalidator.rs +++ b/crates/cluster/src/distvalidator.rs @@ -1,7 +1,8 @@ use pluto_crypto::types::{PUBLIC_KEY_LENGTH, PublicKey}; +use pluto_ssz::serde_utils::Hex0x; use serde::{Deserialize, Serialize}; -use crate::{deposit::DepositData, helpers::EthHex, registration::BuilderRegistration}; +use crate::{deposit::DepositData, registration::BuilderRegistration}; use serde_with::{ base64::{Base64, Standard}, serde_as, @@ -13,14 +14,14 @@ use serde_with::{ pub struct DistValidator { /// Distributed validator group public key. #[serde(rename = "distributed_public_key")] - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub pub_key: Vec, /// 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")] - #[serde_as(as = "Vec")] + #[serde_as(as = "Vec")] pub pub_shares: Vec>, /// Partial deposit data is the list of partial deposit data. @@ -106,7 +107,7 @@ impl DistValidator { pub struct DistValidatorV1x0or1 { /// Distributed validator group public key. #[serde(rename = "distributed_public_key")] - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub pub_key: Vec, /// Public shares are the public keys corresponding to each node's secret @@ -118,7 +119,7 @@ pub struct DistValidatorV1x0or1 { /// Fee recipient address for the validator. #[serde(default)] - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub fee_recipient_address: Vec, } @@ -149,19 +150,19 @@ impl From for DistValidator { pub struct DistValidatorV1x2to5 { /// Distributed validator group public key. #[serde(rename = "distributed_public_key")] - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub pub_key: Vec, /// 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")] - #[serde_as(as = "Vec")] + #[serde_as(as = "Vec")] pub pub_shares: Vec>, /// Fee recipient address for the validator. #[serde(default)] - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub fee_recipient_address: Vec, } @@ -192,14 +193,14 @@ impl From for DistValidator { pub struct DistValidatorV1x6 { /// Distributed validator group public key. #[serde(rename = "distributed_public_key")] - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub pub_key: Vec, /// 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")] - #[serde_as(as = "Vec")] + #[serde_as(as = "Vec")] pub pub_shares: Vec>, /// Deposit data defines the deposit data to activate a validator. @@ -238,14 +239,14 @@ impl From for DistValidator { pub struct DistValidatorV1x7 { /// Distributed validator group public key. #[serde(rename = "distributed_public_key")] - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub pub_key: Vec, /// 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")] - #[serde_as(as = "Vec")] + #[serde_as(as = "Vec")] pub pub_shares: Vec>, /// Deposit data defines the deposit data to activate a validator. @@ -288,14 +289,14 @@ impl From for DistValidator { pub struct DistValidatorV1x8orLater { /// Distributed validator group public key. #[serde(rename = "distributed_public_key")] - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub pub_key: Vec, /// 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")] - #[serde_as(as = "Vec")] + #[serde_as(as = "Vec")] pub pub_shares: Vec>, /// Deposit data defines the deposit data to activate a validator. diff --git a/crates/cluster/src/helpers.rs b/crates/cluster/src/helpers.rs index b5be6f86..4a905411 100644 --- a/crates/cluster/src/helpers.rs +++ b/crates/cluster/src/helpers.rs @@ -2,17 +2,17 @@ use chrono::{DateTime, Utc}; use pluto_crypto::tbls::Tbls; use pluto_eth2util::helpers::{checksum_address, public_key_to_address}; use pluto_k1util::K1UtilError; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serializer}; use serde_with::{DeserializeAs, SerializeAs}; -use std::{borrow::Cow, path::PathBuf}; +use std::path::PathBuf; use crate::{ - definition::{self, ADDRESS_LEN, Definition}, + definition::{self, Definition}, eip712sigs, operator, - ssz::SSZError, - ssz_hasher::HashWalker, }; +pub use pluto_ssz::{from_0x_hex_str, left_pad, to_0x_hex}; + /// Error type returned by `verify_sig`. #[derive(Debug, thiserror::Error)] pub enum VerifySigError { @@ -85,100 +85,6 @@ pub async fn create_validator_keys_dir(parent_dir: &std::path::Path) -> std::io: Ok(vk_dir) } -/// EthHex represents byte slices that are json formatted as 0x prefixed hex. -/// Can be used both as a standalone type and with serde_as. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EthHex(Vec); - -// Standalone Serialize/Deserialize implementations -impl Serialize for EthHex { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("0x{}", hex::encode(&self.0))) - } -} - -impl<'de> Deserialize<'de> for EthHex { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let cow = Cow::::deserialize(deserializer)?; - if cow.is_empty() { - return Ok(EthHex(vec![])); - } - let hex_str = cow.strip_prefix("0x").unwrap_or(&cow); - let bytes = hex::decode(hex_str).map_err(serde::de::Error::custom)?; - Ok(EthHex(bytes)) - } -} - -// SerializeAs/DeserializeAs implementations for use with serde_as -impl SerializeAs for EthHex -where - T: AsRef<[u8]>, -{ - fn serialize_as(value: &T, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&format!("0x{}", hex::encode(value.as_ref()))) - } -} - -impl<'de, T> DeserializeAs<'de, T> for EthHex -where - T: TryFrom>, -{ - fn deserialize_as(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let eth_hex = EthHex::deserialize(deserializer)?; - T::try_from(eth_hex.0).map_err(|_| serde::de::Error::custom("failed to convert bytes")) - } -} - -// Helper methods and conversions -impl EthHex { - /// Create a new EthHex from a byte slice. - pub fn new(bytes: Vec) -> Self { - Self(bytes) - } - - /// Inner bytes. - pub fn inner(&self) -> &Vec { - &self.0 - } -} - -impl From> for EthHex { - fn from(bytes: Vec) -> Self { - Self(bytes) - } -} - -impl From for Vec { - fn from(eth_hex: EthHex) -> Self { - eth_hex.0 - } -} - -impl TryFrom<&str> for EthHex { - type Error = hex::FromHexError; - - fn try_from(value: &str) -> Result { - if value.is_empty() { - return Ok(EthHex(vec![])); - } - let s = value.strip_prefix("0x").unwrap_or(value); - let bytes = hex::decode(s)?; - Ok(EthHex(bytes)) - } -} - /// TimestampSeconds represents a timestamp in seconds since the Unix epoch. pub struct TimestampSeconds; @@ -202,99 +108,6 @@ impl<'de> DeserializeAs<'de, DateTime> for TimestampSeconds { } } -/// Converts a 0x prefixed hex string to a byte slice. -pub fn from_0x_hex_str(s: &str, len: usize) -> Result, hex::FromHexError> { - if s.is_empty() { - return Ok(vec![]); - } - - let s = s.strip_prefix("0x").unwrap_or(s); - let bytes = hex::decode(s)?; - if bytes.len() != len { - return Err(hex::FromHexError::InvalidStringLength); - } - Ok(bytes) -} - -/// `put_byte_list` appends a ssz byte list. -/// See reference: -/// github.com/attestantio/go-eth2-client/spec/bellatrix/ -/// executionpayload_encoding.go:277-284. -pub fn put_byte_list( - hh: &mut H, - bytes: &[u8], - limit: usize, - field: &str, -) -> Result<(), SSZError> { - let elem_indx = hh.index(); - - let byte_len = bytes.len(); - - if byte_len > limit { - return Err(SSZError::::IncorrectListSize { - namespace: "put_byte_list", - field: field.to_string(), - actual: byte_len, - expected: limit, - }); - } - - hh.append_bytes32(bytes) - .map_err(SSZError::::HashWalkerError)?; - - hh.merkleize_with_mixin(elem_indx, byte_len, limit.div_ceil(32)) - .map_err(SSZError::::HashWalkerError)?; - - Ok(()) -} - -/// `put_bytes_n` appends bytes as a ssz fixed size byte array of length n. -pub fn put_bytes_n(hh: &mut H, bytes: &[u8], n: usize) -> Result<(), SSZError> { - if bytes.len() > n { - return Err(SSZError::::IncorrectListSize { - namespace: "put_bytes_n", - field: "".to_string(), - actual: bytes.len(), - expected: n, - }); - } - - hh.put_bytes(&left_pad(bytes, n)) - .map_err(SSZError::::HashWalkerError)?; - - Ok(()) -} - -/// `put_hex_bytes_20` appends a 20 byte fixed size byte ssz array from the -/// 0xhex address. -pub fn put_hex_bytes_20(hh: &mut H, address: &str) -> Result<(), SSZError> { - let bytes = from_0x_hex_str(address, ADDRESS_LEN)?; - hh.put_bytes(&left_pad(&bytes, ADDRESS_LEN)) - .map_err(SSZError::::HashWalkerError)?; - Ok(()) -} - -/// `left_pad` returns the byte slice left padded with zero to ensure a length -/// of at least len. -pub fn left_pad(bytes: &[u8], len: usize) -> Vec { - if bytes.len() >= len { - return bytes.to_vec(); - } - - let pad_count = len.saturating_sub(bytes.len()); - let mut padded = vec![0; pad_count]; - padded.extend_from_slice(bytes); - padded -} -/// `to_0x_hex` converts a byte slice to a 0x prefixed hex string. -pub fn to_0x_hex(bytes: &[u8]) -> String { - if bytes.is_empty() { - return String::new(); - } - - format!("0x{}", hex::encode(bytes)) -} - /// Signs the creator's config hash. pub fn sign_creator( secret: &k256::SecretKey, @@ -357,57 +170,20 @@ pub fn agg_sign( #[cfg(test)] mod tests { use crate::test_cluster; - - use super::*; + use pluto_ssz::serde_utils::Hex0x; + use serde::{Deserialize, Serialize}; use serde_with::serde_as; - #[test] - fn test_left_pad() { - assert_eq!(left_pad(&[0x12, 0x34], 4), vec![0x00, 0x00, 0x12, 0x34]); - assert_eq!(left_pad(&[0xab], 3), vec![0x00, 0x00, 0xab]); - assert_eq!(left_pad(&[1, 2, 3], 3), vec![1, 2, 3]); - assert_eq!(left_pad(&[1, 2, 3], 2), vec![1, 2, 3]); - } - - #[test] - fn test_eth_hex_serialize_deserialize() { - let eth_hex = EthHex(vec![0x01, 0x02, 0x03]); - let serialized = serde_json::to_string(ð_hex).unwrap(); - assert_eq!(serialized, "\"0x010203\""); - let deserialized: EthHex = serde_json::from_str(&serialized).unwrap(); - assert_eq!(deserialized, eth_hex); - } - - #[test] - fn test_empty_eth_hex_serialize_deserialize() { - let eth_hex = EthHex(vec![]); - let serialized = serde_json::to_string(ð_hex).unwrap(); - assert_eq!(serialized, "\"0x\""); - let deserialized: EthHex = serde_json::from_str(&serialized).unwrap(); - assert_eq!(deserialized, eth_hex); - } - - #[test] - fn test_empty_string_deserialize() { - // Empty string should deserialize to empty EthHex - let deserialized: EthHex = serde_json::from_str("\"\"").unwrap(); - assert_eq!(deserialized, EthHex(vec![])); - - // TryFrom should also handle empty string - let from_str = EthHex::try_from("").unwrap(); - assert_eq!(from_str, EthHex(vec![])); - } - #[serde_as] #[derive(Serialize, Deserialize, Debug, PartialEq)] struct TestStruct { - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] data: Vec, - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] hash: [u8; 32], - #[serde_as(as = "Option")] + #[serde_as(as = "Option")] optional_data: Option>, } @@ -427,27 +203,6 @@ mod tests { assert_eq!(decoded, test); } - #[derive(Serialize, Deserialize, Debug, PartialEq)] - struct MixedStruct { - // Using EthHex as a type - eth_hex_field: EthHex, - - // Using regular Vec without hex encoding - regular_bytes: Vec, - } - - #[test] - fn test_mixed_usage() { - let mixed = MixedStruct { - eth_hex_field: EthHex::new(vec![0x01, 0x02, 0x03]), - regular_bytes: vec![0x04, 0x05, 0x06], - }; - - let json = serde_json::to_string(&mixed).unwrap(); - assert!(json.contains("\"0x010203\"")); - assert!(json.contains("[4,5,6]")); - } - #[tokio::test] async fn fetch_definition_valid() { let (lock, ..) = test_cluster::new_for_test(1, 2, 3, 0); diff --git a/crates/cluster/src/lib.rs b/crates/cluster/src/lib.rs index fc85adc6..9edb2cd1 100644 --- a/crates/cluster/src/lib.rs +++ b/crates/cluster/src/lib.rs @@ -26,8 +26,6 @@ pub mod operator; pub mod registration; /// Cluster SSZ management and coordination. pub mod ssz; -/// Cluster SSZ hashing management and coordination. -pub mod ssz_hasher; /// Cluster test cluster management and coordination. #[cfg(test)] pub mod test_cluster; diff --git a/crates/cluster/src/lock.rs b/crates/cluster/src/lock.rs index be5448fb..a370a738 100644 --- a/crates/cluster/src/lock.rs +++ b/crates/cluster/src/lock.rs @@ -4,6 +4,7 @@ use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls, tblsconv}; use pluto_eth1wrap::EthClient; use pluto_eth2api::spec::phase0::{VERSION_LEN, Version}; use pluto_eth2util::registration; +use pluto_ssz::{Hasher, serde_utils::Hex0x}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::{ @@ -12,9 +13,7 @@ use crate::{ DistValidator, DistValidatorV1x0or1, DistValidatorV1x2to5, DistValidatorV1x6, DistValidatorV1x7, DistValidatorV1x8orLater, }, - helpers::EthHex, ssz::{SSZError, hash_lock}, - ssz_hasher::Hasher, version::versions::*, }; use pluto_eth2util::enr::{Record, RecordError}; @@ -484,14 +483,14 @@ pub struct LockV1x2to5 { pub distributed_validators: Vec, /// LockHash uniquely identifies a cluster lock. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub lock_hash: Vec, /// BLS aggregate signature of the lock hash /// signed by all the private key shares of all the distributed /// validators. It acts as an attestation by all the distributed /// validators of the charon cluster they are part of. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub signature_aggregate: Vec, } @@ -539,14 +538,14 @@ pub struct LockV1x6 { pub distributed_validators: Vec, /// Lock hash uniquely identifies a cluster lock. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub lock_hash: Vec, /// BLS aggregate signature of the lock hash /// signed by all the private key shares of all the distributed /// validators. It acts as an attestation by all the distributed /// validators of the charon cluster they are part of. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub signature_aggregate: Vec, } @@ -594,19 +593,19 @@ pub struct LockV1x7 { pub distributed_validators: Vec, /// Lock hash uniquely identifies a cluster lock. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub lock_hash: Vec, /// BLS aggregate signature of the lock hash /// signed by all the private key shares of all the distributed /// validators. It acts as an attestation by all the distributed /// validators of the charon cluster they are part of. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub signature_aggregate: Vec, /// Signatures of the lock hash for each operator /// defined in the Definition. - #[serde_as(as = "Vec")] + #[serde_as(as = "Vec")] pub node_signatures: Vec>, } @@ -655,19 +654,19 @@ pub struct LockV1x8orLater { pub distributed_validators: Vec, /// Lock hash uniquely identifies a cluster lock. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub lock_hash: Vec, /// BLS aggregate signature of the lock hash /// signed by all the private key shares of all the distributed /// validators. It acts as an attestation by all the distributed /// validators of the charon cluster they are part of. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub signature_aggregate: Vec, /// Signatures of the lock hash for each operator /// defined in the Definition. - #[serde_as(as = "Vec")] + #[serde_as(as = "Vec")] pub node_signatures: Vec>, } diff --git a/crates/cluster/src/operator.rs b/crates/cluster/src/operator.rs index dcd873a0..e6ae957e 100644 --- a/crates/cluster/src/operator.rs +++ b/crates/cluster/src/operator.rs @@ -1,4 +1,5 @@ -use crate::{helpers::EthHex, version::ZERO_NONCE}; +use crate::version::ZERO_NONCE; +use pluto_ssz::serde_utils::Hex0x; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -12,10 +13,10 @@ pub struct Operator { /// The ENR of the operator pub enr: String, /// The config signature of the operator - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub config_signature: Vec, /// The ENR signature of the operator - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub enr_signature: Vec, } @@ -32,15 +33,16 @@ pub struct OperatorV1X1 { /// The nonce of the operator (always 0) nonce: u64, /// The config signature of the operator - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub config_signature: Vec, /// The ENR signature of the operator - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub enr_signature: Vec, } /// OperatorV1X2OrLater is the json formatter of Operator for versions v1.2.0 /// and later. +#[serde_as] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub struct OperatorV1X2OrLater { @@ -49,9 +51,11 @@ pub struct OperatorV1X2OrLater { /// The ENR of the operator enr: String, /// The config signature of the operator - config_signature: EthHex, + #[serde_as(as = "Hex0x")] + config_signature: Vec, /// The ENR signature of the operator - enr_signature: EthHex, + #[serde_as(as = "Hex0x")] + enr_signature: Vec, } impl From for Operator { @@ -82,8 +86,8 @@ impl From for Operator { Self { address: operator.address, enr: operator.enr, - config_signature: operator.config_signature.into(), - enr_signature: operator.enr_signature.into(), + config_signature: operator.config_signature, + enr_signature: operator.enr_signature, } } } @@ -93,8 +97,8 @@ impl From for OperatorV1X2OrLater { Self { address: operator.address, enr: operator.enr, - config_signature: operator.config_signature.into(), - enr_signature: operator.enr_signature.into(), + config_signature: operator.config_signature, + enr_signature: operator.enr_signature, } } } diff --git a/crates/cluster/src/registration.rs b/crates/cluster/src/registration.rs index 7fa3fc61..6808819f 100644 --- a/crates/cluster/src/registration.rs +++ b/crates/cluster/src/registration.rs @@ -1,9 +1,10 @@ use chrono::{DateTime, Utc}; use pluto_eth2api::spec::phase0; +use pluto_ssz::serde_utils::Hex0x; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use crate::helpers::{EthHex, TimestampSeconds}; +use crate::helpers::TimestampSeconds; /// BuilderRegistration defines pre-generated signed validator builder /// registration to be sent to builder network. @@ -14,7 +15,7 @@ pub struct BuilderRegistration { pub message: Registration, /// BLS signature of the registration message (96 bytes). - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub signature: phase0::BLSSignature, } @@ -23,7 +24,7 @@ pub struct BuilderRegistration { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Registration { /// Fee recipient address for the registration. - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub fee_recipient: [u8; 20], /// Gas limit for the registration. @@ -35,7 +36,7 @@ pub struct Registration { /// Validator's public key (48 bytes). #[serde(rename = "pubkey")] - #[serde_as(as = "EthHex")] + #[serde_as(as = "Hex0x")] pub pub_key: phase0::BLSPubKey, } diff --git a/crates/cluster/src/ssz.rs b/crates/cluster/src/ssz.rs index 16495237..917d1d34 100644 --- a/crates/cluster/src/ssz.rs +++ b/crates/cluster/src/ssz.rs @@ -1,11 +1,14 @@ +use pluto_ssz::{ + Error as PlutoSszError, HashWalker, Hasher, HasherError, from_0x_hex_str, put_byte_list, + put_bytes_n, put_hex_bytes_n, to_0x_hex, +}; + use crate::{ definition::{ADDRESS_LEN, Definition}, deposit::DepositData, distvalidator::DistValidator, - helpers::{from_0x_hex_str, put_byte_list, put_bytes_n, put_hex_bytes_20, to_0x_hex}, lock::Lock, registration::{BuilderRegistration, Registration}, - ssz_hasher::{HashWalker, Hasher, HasherError}, version::{ZERO_NONCE, versions::*}, }; @@ -94,6 +97,26 @@ impl From for SSZError { } } +impl From::Error>> for SSZError { + fn from(error: PlutoSszError<::Error>) -> Self { + match error { + PlutoSszError::IncorrectListSize { + namespace, + field, + actual, + expected, + } => Self::IncorrectListSize { + namespace, + field, + actual, + expected, + }, + PlutoSszError::HashWalkerError(error) => Self::HashWalkerError(error), + PlutoSszError::FailedToConvertHexString(error) => Self::FailedToConvertHexString(error), + } + } +} + fn get_definition_hash_func( version: &str, ) -> Result, SSZError> { @@ -401,7 +424,7 @@ where let op_sub_idx = hh.index(); // Field (0) 'Address' Bytes20 - put_hex_bytes_20(hh, &operator.address)?; + put_hex_bytes_n(hh, &operator.address, ADDRESS_LEN)?; if !config_only { // Field (1) 'ENR' ByteList[1024] @@ -427,7 +450,7 @@ where let creator_idx = hh.index(); // Field (0) 'Address' Bytes20 - put_hex_bytes_20(hh, &definition.creator.address)?; + put_hex_bytes_n(hh, &definition.creator.address, ADDRESS_LEN)?; if !config_only { // Field (1) 'ConfigSignature' Bytes65 @@ -446,10 +469,10 @@ where let validator_address_sub_idx = hh.index(); // Field (0) 'FeeRecipientAddress' Bytes20 - put_hex_bytes_20(hh, &validator_address.fee_recipient_address)?; + put_hex_bytes_n(hh, &validator_address.fee_recipient_address, ADDRESS_LEN)?; // Field (1) 'WithdrawalAddress' Bytes20 - put_hex_bytes_20(hh, &validator_address.withdrawal_address)?; + put_hex_bytes_n(hh, &validator_address.withdrawal_address, ADDRESS_LEN)?; hh.merkleize(validator_address_sub_idx) .map_err(SSZError::::HashWalkerError)?; @@ -858,7 +881,9 @@ pub(crate) fn hash_deposit_data_v1x6( .map_err(SSZError::::HashWalkerError)?; // Field (3) 'Signature' Bytes96 - put_bytes_n(hh, &deposit_data.signature, SSZ_LEN_BLS_SIG) + put_bytes_n(hh, &deposit_data.signature, SSZ_LEN_BLS_SIG)?; + + Ok(()) } pub(crate) fn hash_deposit_data_v1x7_or_later( diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index ea060e56..f5cac339 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -34,6 +34,7 @@ prost-types.workspace = true hex.workspace = true chrono.workspace = true test-case.workspace = true +pluto-ssz.workspace = true [build-dependencies] pluto-build-proto.workspace = true diff --git a/crates/core/src/signeddata.rs b/crates/core/src/signeddata.rs index 09865daf..1076c8c0 100644 --- a/crates/core/src/signeddata.rs +++ b/crates/core/src/signeddata.rs @@ -1185,10 +1185,8 @@ impl SignedSyncContributionAndProof { mod tests { use super::*; use alloy::primitives::U256; - use pluto_eth2api::spec::{ - altair, bellatrix, capella, deneb, electra, fulu, - ssz_types::{BitList, BitVector}, - }; + use pluto_eth2api::spec::{altair, bellatrix, capella, deneb, electra, fulu}; + use pluto_ssz::{BitList, BitVector}; use serde::{Serialize, de::DeserializeOwned}; use std::{fs, path::PathBuf}; use test_case::test_case; diff --git a/crates/eth2api/Cargo.toml b/crates/eth2api/Cargo.toml index d22c26f4..91624b9c 100644 --- a/crates/eth2api/Cargo.toml +++ b/crates/eth2api/Cargo.toml @@ -23,6 +23,7 @@ validator.workspace = true tree_hash.workspace = true tree_hash_derive.workspace = true alloy.workspace = true +pluto-ssz.workspace = true [dev-dependencies] testcontainers.workspace = true diff --git a/crates/eth2api/src/spec/altair.rs b/crates/eth2api/src/spec/altair.rs index e78382f5..3b8b6f44 100644 --- a/crates/eth2api/src/spec/altair.rs +++ b/crates/eth2api/src/spec/altair.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; use tree_hash_derive::TreeHash; -use crate::spec::ssz_types::BitVector; +use pluto_ssz::BitVector; use crate::spec::phase0; @@ -17,7 +17,7 @@ pub struct SyncAggregate { /// Sync committee participation bits. pub sync_committee_bits: BitVector<512>, /// Aggregate sync committee signature. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub sync_committee_signature: phase0::BLSSignature, } @@ -28,12 +28,12 @@ pub struct SyncAggregate { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockBody { /// RANDAO reveal. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub randao_reveal: phase0::BLSSignature, /// ETH1 data vote. pub eth1_data: phase0::ETH1Data, /// Graffiti bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub graffiti: phase0::Root, /// Proposer slashings included in the block. pub proposer_slashings: @@ -65,10 +65,10 @@ pub struct BeaconBlock { #[serde_as(as = "serde_with::DisplayFromStr")] pub proposer_index: phase0::ValidatorIndex, /// Parent root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_root: phase0::Root, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Block body. pub body: BeaconBlockBody, @@ -83,7 +83,7 @@ pub struct SignedBeaconBlock { /// Unsigned block message. pub message: BeaconBlock, /// Signature of the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } @@ -97,13 +97,13 @@ pub struct SyncCommitteeMessage { #[serde_as(as = "serde_with::DisplayFromStr")] pub slot: phase0::Slot, /// Beacon block root being signed. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub beacon_block_root: phase0::Root, /// Validator index emitting the message. #[serde_as(as = "serde_with::DisplayFromStr")] pub validator_index: phase0::ValidatorIndex, /// Signature over the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } @@ -117,7 +117,7 @@ pub struct SyncCommitteeContribution { #[serde_as(as = "serde_with::DisplayFromStr")] pub slot: phase0::Slot, /// Beacon block root being contributed for. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub beacon_block_root: phase0::Root, /// Subcommittee index. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -125,7 +125,7 @@ pub struct SyncCommitteeContribution { /// Aggregation bits for the contribution. pub aggregation_bits: BitVector<128>, /// Contribution signature. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } @@ -141,7 +141,7 @@ pub struct ContributionAndProof { /// Sync committee contribution. pub contribution: SyncCommitteeContribution, /// Selection proof. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub selection_proof: phase0::BLSSignature, } @@ -154,7 +154,7 @@ pub struct SignedContributionAndProof { /// Unsigned contribution-and-proof message. pub message: ContributionAndProof, /// Signature over the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } diff --git a/crates/eth2api/src/spec/bellatrix.rs b/crates/eth2api/src/spec/bellatrix.rs index cb86773c..8a73eea9 100644 --- a/crates/eth2api/src/spec/bellatrix.rs +++ b/crates/eth2api/src/spec/bellatrix.rs @@ -26,7 +26,7 @@ pub type BaseFeePerGas = U256; #[serde(transparent)] pub struct Transaction { /// Transaction bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub bytes: phase0::SszList, } @@ -47,9 +47,10 @@ impl AsRef<[u8]> for Transaction { /// JSON (de)serialization helpers for execution addresses. pub(crate) mod execution_address_serde { use alloy::primitives::Address; + use pluto_ssz::serde_utils::trim_0x_prefix; use serde::{Deserialize, Deserializer, Serializer, de::Error as DeError}; - use crate::spec::{bellatrix::ExecutionAddress, serde_utils}; + use crate::spec::bellatrix::ExecutionAddress; pub fn serialize( value: &ExecutionAddress, @@ -64,7 +65,7 @@ pub(crate) mod execution_address_serde { deserializer: D, ) -> Result { let value = String::deserialize(deserializer)?; - let trimmed = serde_utils::trim_0x_prefix(value.as_str()); + let trimmed = trim_0x_prefix(value.as_str()); let bytes = hex::decode(trimmed).map_err(D::Error::custom)?; if bytes.len() != 20 { return Err(D::Error::custom(format!( @@ -86,22 +87,22 @@ pub(crate) mod execution_address_serde { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct ExecutionPayload { /// Parent execution block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_hash: phase0::Hash32, /// Fee recipient address. #[serde(with = "execution_address_serde")] pub fee_recipient: ExecutionAddress, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Receipts root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub receipts_root: phase0::Root, /// Logs bloom. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub logs_bloom: [u8; 256], /// Prev randao value. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub prev_randao: [u8; 32], /// Block number. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -116,13 +117,13 @@ pub struct ExecutionPayload { #[serde_as(as = "serde_with::DisplayFromStr")] pub timestamp: u64, /// Extra data bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub extra_data: phase0::SszList, /// Base fee per gas. #[serde(with = "crate::spec::serde_utils::u256_dec_serde")] pub base_fee_per_gas: BaseFeePerGas, /// Execution block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub block_hash: phase0::Hash32, /// Transactions in the payload. pub transactions: phase0::SszList, @@ -135,22 +136,22 @@ pub struct ExecutionPayload { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct ExecutionPayloadHeader { /// Parent execution block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_hash: phase0::Hash32, /// Fee recipient address. #[serde(with = "execution_address_serde")] pub fee_recipient: ExecutionAddress, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Receipts root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub receipts_root: phase0::Root, /// Logs bloom. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub logs_bloom: [u8; 256], /// Prev randao value. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub prev_randao: [u8; 32], /// Block number. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -165,16 +166,16 @@ pub struct ExecutionPayloadHeader { #[serde_as(as = "serde_with::DisplayFromStr")] pub timestamp: u64, /// Extra data bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub extra_data: phase0::SszList, /// Base fee per gas. #[serde(with = "crate::spec::serde_utils::u256_dec_serde")] pub base_fee_per_gas: BaseFeePerGas, /// Execution block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub block_hash: phase0::Hash32, /// Transactions root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub transactions_root: phase0::Root, } @@ -185,12 +186,12 @@ pub struct ExecutionPayloadHeader { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockBody { /// RANDAO reveal. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub randao_reveal: phase0::BLSSignature, /// ETH1 data vote. pub eth1_data: phase0::ETH1Data, /// Graffiti bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub graffiti: phase0::Root, /// Proposer slashings included in the block. pub proposer_slashings: @@ -224,10 +225,10 @@ pub struct BeaconBlock { #[serde_as(as = "serde_with::DisplayFromStr")] pub proposer_index: phase0::ValidatorIndex, /// Parent root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_root: phase0::Root, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Block body. pub body: BeaconBlockBody, @@ -240,12 +241,12 @@ pub struct BeaconBlock { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct BlindedBeaconBlockBody { /// RANDAO reveal. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub randao_reveal: phase0::BLSSignature, /// ETH1 data vote. pub eth1_data: phase0::ETH1Data, /// Graffiti bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub graffiti: phase0::Root, /// Proposer slashings included in the block. pub proposer_slashings: @@ -279,10 +280,10 @@ pub struct BlindedBeaconBlock { #[serde_as(as = "serde_with::DisplayFromStr")] pub proposer_index: phase0::ValidatorIndex, /// Parent root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_root: phase0::Root, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Blinded block body. pub body: BlindedBeaconBlockBody, @@ -297,7 +298,7 @@ pub struct SignedBeaconBlock { /// Unsigned block message. pub message: BeaconBlock, /// Signature of the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } @@ -310,7 +311,7 @@ pub struct SignedBlindedBeaconBlock { /// Unsigned blinded block message. pub message: BlindedBeaconBlock, /// Signature of the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } diff --git a/crates/eth2api/src/spec/capella.rs b/crates/eth2api/src/spec/capella.rs index f00f867b..3d8250e1 100644 --- a/crates/eth2api/src/spec/capella.rs +++ b/crates/eth2api/src/spec/capella.rs @@ -24,7 +24,7 @@ pub struct Withdrawal { #[serde_as(as = "serde_with::DisplayFromStr")] pub validator_index: phase0::ValidatorIndex, /// Destination execution address. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub address: bellatrix::ExecutionAddress, /// Amount in gwei. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -41,7 +41,7 @@ pub struct BLSToExecutionChange { #[serde_as(as = "serde_with::DisplayFromStr")] pub validator_index: phase0::ValidatorIndex, /// BLS public key to change from. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub from_bls_pubkey: phase0::BLSPubKey, /// Execution address to change to. #[serde(with = "bellatrix::execution_address_serde")] @@ -57,7 +57,7 @@ pub struct SignedBLSToExecutionChange { /// Unsigned message. pub message: BLSToExecutionChange, /// Signature over the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } @@ -68,22 +68,22 @@ pub struct SignedBLSToExecutionChange { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct ExecutionPayload { /// Parent execution block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_hash: phase0::Hash32, /// Fee recipient address. #[serde(with = "bellatrix::execution_address_serde")] pub fee_recipient: bellatrix::ExecutionAddress, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Receipts root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub receipts_root: phase0::Root, /// Logs bloom. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub logs_bloom: [u8; 256], /// Prev randao value. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub prev_randao: [u8; 32], /// Block number. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -98,13 +98,13 @@ pub struct ExecutionPayload { #[serde_as(as = "serde_with::DisplayFromStr")] pub timestamp: u64, /// Extra data bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub extra_data: phase0::SszList, /// Base fee per gas. #[serde(with = "crate::spec::serde_utils::u256_dec_serde")] pub base_fee_per_gas: bellatrix::BaseFeePerGas, /// Execution block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub block_hash: phase0::Hash32, /// Transactions in the payload. pub transactions: @@ -120,22 +120,22 @@ pub struct ExecutionPayload { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct ExecutionPayloadHeader { /// Parent execution block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_hash: phase0::Hash32, /// Fee recipient address. #[serde(with = "bellatrix::execution_address_serde")] pub fee_recipient: bellatrix::ExecutionAddress, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Receipts root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub receipts_root: phase0::Root, /// Logs bloom. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub logs_bloom: [u8; 256], /// Prev randao value. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub prev_randao: [u8; 32], /// Block number. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -150,19 +150,19 @@ pub struct ExecutionPayloadHeader { #[serde_as(as = "serde_with::DisplayFromStr")] pub timestamp: u64, /// Extra data bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub extra_data: phase0::SszList, /// Base fee per gas. #[serde(with = "crate::spec::serde_utils::u256_dec_serde")] pub base_fee_per_gas: bellatrix::BaseFeePerGas, /// Execution block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub block_hash: phase0::Hash32, /// Transactions root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub transactions_root: phase0::Root, /// Withdrawals root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub withdrawals_root: phase0::Root, } @@ -173,12 +173,12 @@ pub struct ExecutionPayloadHeader { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockBody { /// RANDAO reveal. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub randao_reveal: phase0::BLSSignature, /// ETH1 data vote. pub eth1_data: phase0::ETH1Data, /// Graffiti bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub graffiti: phase0::Root, /// Proposer slashings included in the block. pub proposer_slashings: @@ -215,10 +215,10 @@ pub struct BeaconBlock { #[serde_as(as = "serde_with::DisplayFromStr")] pub proposer_index: phase0::ValidatorIndex, /// Parent root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_root: phase0::Root, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Block body. pub body: BeaconBlockBody, @@ -231,12 +231,12 @@ pub struct BeaconBlock { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct BlindedBeaconBlockBody { /// RANDAO reveal. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub randao_reveal: phase0::BLSSignature, /// ETH1 data vote. pub eth1_data: phase0::ETH1Data, /// Graffiti bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub graffiti: phase0::Root, /// Proposer slashings included in the block. pub proposer_slashings: @@ -273,10 +273,10 @@ pub struct BlindedBeaconBlock { #[serde_as(as = "serde_with::DisplayFromStr")] pub proposer_index: phase0::ValidatorIndex, /// Parent root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_root: phase0::Root, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Blinded block body. pub body: BlindedBeaconBlockBody, @@ -291,7 +291,7 @@ pub struct SignedBeaconBlock { /// Unsigned block message. pub message: BeaconBlock, /// Signature of the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } @@ -304,7 +304,7 @@ pub struct SignedBlindedBeaconBlock { /// Unsigned blinded block message. pub message: BlindedBeaconBlock, /// Signature of the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } diff --git a/crates/eth2api/src/spec/deneb.rs b/crates/eth2api/src/spec/deneb.rs index c45dffb1..3b467c23 100644 --- a/crates/eth2api/src/spec/deneb.rs +++ b/crates/eth2api/src/spec/deneb.rs @@ -22,7 +22,7 @@ pub type BaseFeePerGas = bellatrix::BaseFeePerGas; #[serde(transparent)] pub struct KZGCommitment { /// Raw commitment bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub bytes: [u8; 48], } @@ -44,7 +44,7 @@ impl AsRef<[u8]> for KZGCommitment { #[serde(transparent)] pub struct KZGProof( /// Raw proof bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub [u8; KZG_PROOF_LEN], ); @@ -66,7 +66,7 @@ impl AsRef<[u8]> for KZGProof { #[serde(transparent)] pub struct Blob( /// Raw blob bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub [u8; BLOB_LEN], ); @@ -89,22 +89,22 @@ impl AsRef<[u8]> for Blob { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct ExecutionPayload { /// Parent execution block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_hash: phase0::Hash32, /// Fee recipient address. #[serde(with = "bellatrix::execution_address_serde")] pub fee_recipient: bellatrix::ExecutionAddress, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Receipts root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub receipts_root: phase0::Root, /// Logs bloom. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub logs_bloom: [u8; 256], /// Prev randao value. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub prev_randao: [u8; 32], /// Block number. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -119,13 +119,13 @@ pub struct ExecutionPayload { #[serde_as(as = "serde_with::DisplayFromStr")] pub timestamp: u64, /// Extra data bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub extra_data: phase0::SszList, /// Base fee per gas. #[serde(with = "crate::spec::serde_utils::u256_dec_serde")] pub base_fee_per_gas: BaseFeePerGas, /// Execution block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub block_hash: phase0::Hash32, /// Transactions in the payload. pub transactions: @@ -147,22 +147,22 @@ pub struct ExecutionPayload { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct ExecutionPayloadHeader { /// Parent execution block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_hash: phase0::Hash32, /// Fee recipient address. #[serde(with = "bellatrix::execution_address_serde")] pub fee_recipient: bellatrix::ExecutionAddress, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Receipts root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub receipts_root: phase0::Root, /// Logs bloom. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub logs_bloom: [u8; 256], /// Prev randao value. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub prev_randao: [u8; 32], /// Block number. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -177,19 +177,19 @@ pub struct ExecutionPayloadHeader { #[serde_as(as = "serde_with::DisplayFromStr")] pub timestamp: u64, /// Extra data bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub extra_data: phase0::SszList, /// Base fee per gas. #[serde(with = "crate::spec::serde_utils::u256_dec_serde")] pub base_fee_per_gas: BaseFeePerGas, /// Execution block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub block_hash: phase0::Hash32, /// Transactions root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub transactions_root: phase0::Root, /// Withdrawals root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub withdrawals_root: phase0::Root, /// Blob gas used. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -206,12 +206,12 @@ pub struct ExecutionPayloadHeader { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockBody { /// RANDAO reveal. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub randao_reveal: phase0::BLSSignature, /// ETH1 data vote. pub eth1_data: phase0::ETH1Data, /// Graffiti bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub graffiti: phase0::Root, /// Proposer slashings included in the block. pub proposer_slashings: @@ -252,10 +252,10 @@ pub struct BeaconBlock { #[serde_as(as = "serde_with::DisplayFromStr")] pub proposer_index: phase0::ValidatorIndex, /// Parent root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_root: phase0::Root, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Block body. pub body: BeaconBlockBody, @@ -268,12 +268,12 @@ pub struct BeaconBlock { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct BlindedBeaconBlockBody { /// RANDAO reveal. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub randao_reveal: phase0::BLSSignature, /// ETH1 data vote. pub eth1_data: phase0::ETH1Data, /// Graffiti bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub graffiti: phase0::Root, /// Proposer slashings included in the block. pub proposer_slashings: @@ -314,10 +314,10 @@ pub struct BlindedBeaconBlock { #[serde_as(as = "serde_with::DisplayFromStr")] pub proposer_index: phase0::ValidatorIndex, /// Parent root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_root: phase0::Root, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Blinded block body. pub body: BlindedBeaconBlockBody, @@ -332,7 +332,7 @@ pub struct SignedBeaconBlock { /// Unsigned block message. pub message: BeaconBlock, /// Signature of the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } @@ -345,7 +345,7 @@ pub struct SignedBlindedBeaconBlock { /// Unsigned blinded block message. pub message: BlindedBeaconBlock, /// Signature of the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } diff --git a/crates/eth2api/src/spec/electra.rs b/crates/eth2api/src/spec/electra.rs index 8572ed37..233caa79 100644 --- a/crates/eth2api/src/spec/electra.rs +++ b/crates/eth2api/src/spec/electra.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; use tree_hash_derive::TreeHash; -use crate::spec::ssz_types::{BitList, BitVector}; +use pluto_ssz::{BitList, BitVector}; use crate::spec::{altair, bellatrix, capella, deneb, phase0}; @@ -26,12 +26,12 @@ pub const MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: usize = 2; #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct IndexedAttestation { /// Indices of attesting validators. - #[serde(with = "crate::spec::serde_utils::ssz_list_u64_string_serde")] + #[serde(with = "pluto_ssz::serde_utils::ssz_list_u64_string_serde")] pub attesting_indices: phase0::SszList, /// Attestation data. pub data: phase0::AttestationData, /// Aggregate signature. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } @@ -57,7 +57,7 @@ pub struct Attestation { /// Attestation data. pub data: phase0::AttestationData, /// Aggregate signature. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, /// Committee bits. pub committee_bits: BitVector<64>, @@ -70,16 +70,16 @@ pub struct Attestation { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct DepositRequest { /// Validator public key. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub pubkey: phase0::BLSPubKey, /// Withdrawal credentials. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub withdrawal_credentials: phase0::WithdrawalCredentials, /// Amount in gwei. #[serde_as(as = "serde_with::DisplayFromStr")] pub amount: phase0::Gwei, /// Signature. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, /// Request index. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -96,7 +96,7 @@ pub struct WithdrawalRequest { #[serde(with = "bellatrix::execution_address_serde")] pub source_address: bellatrix::ExecutionAddress, /// Validator public key. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub validator_pubkey: phase0::BLSPubKey, /// Amount in gwei. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -113,10 +113,10 @@ pub struct ConsolidationRequest { #[serde(with = "bellatrix::execution_address_serde")] pub source_address: bellatrix::ExecutionAddress, /// Source validator public key. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub source_pubkey: phase0::BLSPubKey, /// Target validator public key. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub target_pubkey: phase0::BLSPubKey, } @@ -141,12 +141,12 @@ pub struct ExecutionRequests { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockBody { /// RANDAO reveal. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub randao_reveal: phase0::BLSSignature, /// ETH1 data vote. pub eth1_data: phase0::ETH1Data, /// Graffiti bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub graffiti: phase0::Root, /// Proposer slashings included in the block. pub proposer_slashings: @@ -189,10 +189,10 @@ pub struct BeaconBlock { #[serde_as(as = "serde_with::DisplayFromStr")] pub proposer_index: phase0::ValidatorIndex, /// Parent root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_root: phase0::Root, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Block body. pub body: BeaconBlockBody, @@ -205,12 +205,12 @@ pub struct BeaconBlock { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct BlindedBeaconBlockBody { /// RANDAO reveal. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub randao_reveal: phase0::BLSSignature, /// ETH1 data vote. pub eth1_data: phase0::ETH1Data, /// Graffiti bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub graffiti: phase0::Root, /// Proposer slashings included in the block. pub proposer_slashings: @@ -253,10 +253,10 @@ pub struct BlindedBeaconBlock { #[serde_as(as = "serde_with::DisplayFromStr")] pub proposer_index: phase0::ValidatorIndex, /// Parent root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_root: phase0::Root, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: phase0::Root, /// Blinded block body. pub body: BlindedBeaconBlockBody, @@ -271,7 +271,7 @@ pub struct SignedBeaconBlock { /// Unsigned block message. pub message: BeaconBlock, /// Signature of the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } @@ -284,7 +284,7 @@ pub struct SignedBlindedBeaconBlock { /// Unsigned blinded block message. pub message: BlindedBeaconBlock, /// Signature of the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } @@ -313,7 +313,7 @@ pub struct AggregateAndProof { /// Aggregate attestation. pub aggregate: Attestation, /// Selection proof. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub selection_proof: phase0::BLSSignature, } @@ -326,7 +326,7 @@ pub struct SignedAggregateAndProof { /// Unsigned message. pub message: AggregateAndProof, /// Signature over the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } diff --git a/crates/eth2api/src/spec/mod.rs b/crates/eth2api/src/spec/mod.rs index 7e40de7d..48fc15dd 100644 --- a/crates/eth2api/src/spec/mod.rs +++ b/crates/eth2api/src/spec/mod.rs @@ -8,9 +8,6 @@ pub mod serde_utils; pub mod version; pub use version::{BuilderVersion, DataVersion}; -/// SSZ wrapper container types with TreeHash support. -pub mod ssz_types; - /// Phase 0 consensus types from the Ethereum beacon chain specification. pub mod phase0; diff --git a/crates/eth2api/src/spec/phase0.rs b/crates/eth2api/src/spec/phase0.rs index fd31c2bf..7ea80968 100644 --- a/crates/eth2api/src/spec/phase0.rs +++ b/crates/eth2api/src/spec/phase0.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use serde_with::serde_as; use tree_hash_derive::TreeHash; -pub use crate::spec::ssz_types::{BitList, SszList, SszVector}; +pub use pluto_ssz::{BitList, SszList, SszVector}; /// Fork version length in bytes. pub const VERSION_LEN: usize = 4; @@ -79,10 +79,10 @@ pub type BLSSignature = [u8; BLS_SIGNATURE_LEN]; #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct DepositMessage { /// BLS public key. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub pubkey: BLSPubKey, /// Withdrawal credentials. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub withdrawal_credentials: WithdrawalCredentials, /// Amount in Gwei. #[serde_as(as = "serde_with::DisplayFromStr")] @@ -106,16 +106,16 @@ impl From<&DepositData> for DepositMessage { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct DepositData { /// BLS public key. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub pubkey: BLSPubKey, /// Withdrawal credentials. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub withdrawal_credentials: WithdrawalCredentials, /// Amount in Gwei. #[serde_as(as = "serde_with::DisplayFromStr")] pub amount: Gwei, /// BLS signature. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: BLSSignature, } @@ -126,10 +126,10 @@ pub struct DepositData { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct ForkData { /// Current fork version. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub current_version: Version, /// Genesis validators root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub genesis_validators_root: Root, } @@ -140,10 +140,10 @@ pub struct ForkData { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct SigningData { /// Object root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub object_root: Root, /// Signature domain. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub domain: Domain, } @@ -154,13 +154,13 @@ pub struct SigningData { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct ETH1Data { /// Deposit tree root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub deposit_root: Root, /// Deposit count at the voted ETH1 block. #[serde_as(as = "serde_with::DisplayFromStr")] pub deposit_count: u64, /// ETH1 block hash. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub block_hash: Hash32, } @@ -177,13 +177,13 @@ pub struct BeaconBlockHeader { #[serde_as(as = "serde_with::DisplayFromStr")] pub proposer_index: ValidatorIndex, /// Parent root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_root: Root, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: Root, /// Body root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub body_root: Root, } @@ -196,7 +196,7 @@ pub struct SignedBeaconBlockHeader { /// Unsigned beacon block header. pub message: BeaconBlockHeader, /// Signature over the header. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: BLSSignature, } @@ -219,12 +219,12 @@ pub struct ProposerSlashing { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct IndexedAttestation { /// Indices of attesting validators. - #[serde(with = "crate::spec::serde_utils::ssz_list_u64_string_serde")] + #[serde(with = "pluto_ssz::serde_utils::ssz_list_u64_string_serde")] pub attesting_indices: SszList, /// Attestation data. pub data: AttestationData, /// Aggregate signature. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: BLSSignature, } @@ -257,12 +257,12 @@ pub struct Deposit { #[derive(Debug, Clone, PartialEq, Eq, TreeHash, Serialize, Deserialize)] pub struct BeaconBlockBody { /// RANDAO reveal. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub randao_reveal: BLSSignature, /// ETH1 data vote. pub eth1_data: ETH1Data, /// Graffiti bytes. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub graffiti: Root, /// Proposer slashings included in the block. pub proposer_slashings: SszList, @@ -289,10 +289,10 @@ pub struct BeaconBlock { #[serde_as(as = "serde_with::DisplayFromStr")] pub proposer_index: ValidatorIndex, /// Parent root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub parent_root: Root, /// State root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub state_root: Root, /// Block body. pub body: BeaconBlockBody, @@ -307,7 +307,7 @@ pub struct SignedBeaconBlock { /// Unsigned block message. pub message: BeaconBlock, /// Signature of the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: BLSSignature, } @@ -321,7 +321,7 @@ pub struct Checkpoint { #[serde_as(as = "serde_with::DisplayFromStr")] pub epoch: Epoch, /// Root of the checkpoint. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub root: Root, } @@ -338,7 +338,7 @@ pub struct AttestationData { #[serde_as(as = "serde_with::DisplayFromStr")] pub index: u64, /// Beacon block root. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub beacon_block_root: Root, /// Source checkpoint. pub source: Checkpoint, @@ -357,7 +357,7 @@ pub struct Attestation { /// Attestation data. pub data: AttestationData, /// Aggregate signature. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: BLSSignature, } @@ -373,7 +373,7 @@ pub struct AggregateAndProof { /// Aggregate attestation. pub aggregate: Attestation, /// Selection proof. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub selection_proof: BLSSignature, } @@ -386,7 +386,7 @@ pub struct SignedAggregateAndProof { /// Unsigned message. pub message: AggregateAndProof, /// Signature of the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: BLSSignature, } @@ -413,7 +413,7 @@ pub struct SignedVoluntaryExit { /// Unsigned voluntary exit message. pub message: VoluntaryExit, /// Signature of the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: BLSSignature, } diff --git a/crates/eth2api/src/spec/serde_utils.rs b/crates/eth2api/src/spec/serde_utils.rs index a2128b07..0cf98850 100644 --- a/crates/eth2api/src/spec/serde_utils.rs +++ b/crates/eth2api/src/spec/serde_utils.rs @@ -1,129 +1,10 @@ //! Shared serde helpers for consensus-spec JSON encoding. -use serde::{ - Deserialize, Deserializer, Serializer, - de::{Error as DeError, Unexpected}, -}; -use serde_with::{DeserializeAs, SerializeAs}; - -/// Strips the `0x` or `0X` prefix from a hex string, returning `None` if -/// absent. -pub fn strip_0x_prefix(value: &str) -> Option<&str> { - value - .strip_prefix("0x") - .or_else(|| value.strip_prefix("0X")) -} - -/// Strips the `0x` or `0X` prefix from a hex string, returning the input -/// unchanged if absent. -pub fn trim_0x_prefix(value: &str) -> &str { - strip_0x_prefix(value).unwrap_or(value) -} - -/// Serde adapter for byte-like values encoded as `0x`-prefixed lowercase hex -/// strings. -/// -/// Deserialization accepts both prefixed (`0x...`) and unprefixed (`...`) -/// values. -pub struct Hex0x; - -impl SerializeAs for Hex0x -where - T: AsRef<[u8]>, -{ - fn serialize_as(source: &T, serializer: S) -> Result - where - S: Serializer, - { - let encoded = hex::encode(source.as_ref()); - let out = format!("0x{encoded}"); - serializer.serialize_str(out.as_str()) - } -} - -impl<'de, T> DeserializeAs<'de, T> for Hex0x -where - T: TryFrom>, -{ - fn deserialize_as(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = String::deserialize(deserializer)?; - let trimmed = trim_0x_prefix(value.as_str()); - let decoded = hex::decode(trimmed).map_err(D::Error::custom)?; - decoded.try_into().map_err(|_err: T::Error| { - D::Error::invalid_value( - Unexpected::Str(value.as_str()), - &"hex bytes convertible to target type", - ) - }) - } -} - -/// Serde helpers for SSZ lists of `u64` encoded as JSON strings. -pub(crate) mod ssz_list_u64_string_serde { - use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError}; - - use crate::spec::ssz_types::SszList; - - #[derive(Deserialize)] - #[serde(untagged)] - enum StringOrU64 { - String(String), - U64(u64), - } - - pub fn serialize( - value: &SszList, - serializer: S, - ) -> Result - where - S: Serializer, - { - let strings: Vec = value - .0 - .iter() - .map(std::string::ToString::to_string) - .collect(); - strings.serialize(serializer) - } - - pub fn deserialize<'de, D, const MAX: usize>( - deserializer: D, - ) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let raw = Vec::::deserialize(deserializer)?; - - if MAX > 0 && raw.len() > MAX { - return Err(D::Error::custom(format!( - "list length {} exceeds max {}", - raw.len(), - MAX - ))); - } - - let mut out = Vec::with_capacity(raw.len()); - for value in raw { - let parsed = match value { - StringOrU64::U64(value) => value, - StringOrU64::String(value) => value.parse::().map_err(|err| { - D::Error::custom(format!("invalid integer string '{value}': {err}")) - })?, - }; - out.push(parsed); - } - - Ok(SszList(out)) - } -} - /// JSON helpers for decimal-encoded `U256` values with optional `0x` input /// support. pub(crate) mod u256_dec_serde { use alloy::primitives::U256; + use pluto_ssz::serde_utils::strip_0x_prefix; use serde::{Deserialize, Deserializer, Serializer, de::Error as DeError}; pub fn serialize(value: &U256, serializer: S) -> Result { @@ -132,12 +13,11 @@ pub(crate) mod u256_dec_serde { pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { let value = String::deserialize(deserializer)?; - let (radix, digits) = - if let Some(hex) = crate::spec::serde_utils::strip_0x_prefix(value.as_str()) { - (16, hex) - } else { - (10, value.as_str()) - }; + let (radix, digits) = if let Some(hex) = strip_0x_prefix(value.as_str()) { + (16, hex) + } else { + (10, value.as_str()) + }; U256::from_str_radix(digits, radix) .map_err(|err| D::Error::custom(format!("invalid u256: {err}"))) diff --git a/crates/eth2api/src/test_fixtures.rs b/crates/eth2api/src/test_fixtures.rs index 34023a77..ec23f336 100644 --- a/crates/eth2api/src/test_fixtures.rs +++ b/crates/eth2api/src/test_fixtures.rs @@ -1,9 +1,7 @@ #![allow(missing_docs)] -use crate::spec::{ - altair, bellatrix, capella, deneb, electra, phase0, - ssz_types::{BitList, BitVector}, -}; +use crate::spec::{altair, bellatrix, capella, deneb, electra, phase0}; +use pluto_ssz::{BitList, BitVector}; use serde_json::Value; use tree_hash::TreeHash; diff --git a/crates/eth2api/src/v1.rs b/crates/eth2api/src/v1.rs index b889935f..917719c9 100644 --- a/crates/eth2api/src/v1.rs +++ b/crates/eth2api/src/v1.rs @@ -25,7 +25,7 @@ pub struct ValidatorRegistration { #[serde_as(as = "serde_with::DisplayFromStr")] pub timestamp: u64, /// Validator BLS public key (48 bytes). - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub pubkey: BLSPubKey, } @@ -38,7 +38,7 @@ pub struct SignedValidatorRegistration { /// Unsigned validator registration message. pub message: ValidatorRegistration, /// Signature over the message. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: BLSSignature, } @@ -55,7 +55,7 @@ pub struct BeaconCommitteeSelection { #[serde_as(as = "serde_with::DisplayFromStr")] pub validator_index: ValidatorIndex, /// Selection proof. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub selection_proof: BLSSignature, } @@ -75,7 +75,7 @@ pub struct SyncCommitteeSelection { #[serde_as(as = "serde_with::DisplayFromStr")] pub subcommittee_index: u64, /// Selection proof. - #[serde_as(as = "crate::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub selection_proof: BLSSignature, } diff --git a/crates/eth2util/Cargo.toml b/crates/eth2util/Cargo.toml index f73c518c..52c6747b 100644 --- a/crates/eth2util/Cargo.toml +++ b/crates/eth2util/Cargo.toml @@ -31,6 +31,7 @@ serde_with.workspace = true tree_hash.workspace = true tree_hash_derive.workspace = true pluto-crypto.workspace = true +pluto-ssz.workspace = true tokio.workspace = true pluto-eth2api.workspace = true walkdir.workspace = true diff --git a/crates/eth2util/src/types.rs b/crates/eth2util/src/types.rs index 68c37e1b..20928332 100644 --- a/crates/eth2util/src/types.rs +++ b/crates/eth2util/src/types.rs @@ -229,7 +229,7 @@ pub struct SignedEpoch { pub epoch: phase0::Epoch, /// BLS signature for the epoch. #[tree_hash(skip_hashing)] - #[serde_as(as = "pluto_eth2api::spec::serde_utils::Hex0x")] + #[serde_as(as = "pluto_ssz::serde_utils::Hex0x")] pub signature: phase0::BLSSignature, } diff --git a/crates/ssz/Cargo.toml b/crates/ssz/Cargo.toml new file mode 100644 index 00000000..65c1d01a --- /dev/null +++ b/crates/ssz/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "pluto-ssz" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +hex.workspace = true +k256.workspace = true +serde.workspace = true +serde_with.workspace = true +thiserror.workspace = true +tree_hash.workspace = true + +[dev-dependencies] +serde_json.workspace = true + +[lints] +workspace = true diff --git a/crates/ssz/src/error.rs b/crates/ssz/src/error.rs new file mode 100644 index 00000000..12b4a935 --- /dev/null +++ b/crates/ssz/src/error.rs @@ -0,0 +1,31 @@ +//! Generic SSZ error types. + +/// Error type returned by SSZ helpers and hashing primitives. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Invalid list size or fixed-length byte size. + #[error( + "Invalid list size: function: {namespace}, field: {field}, actual: {actual}, expected: {expected}" + )] + IncorrectListSize { + /// Namespace of the helper reporting the error. + namespace: &'static str, + /// Field name, if relevant. + field: String, + /// Actual length. + actual: usize, + /// Expected or maximum length. + expected: usize, + }, + + /// Error returned by the underlying hash walker. + #[error("Hash walker error: {0}")] + HashWalkerError(E), + + /// Failed to decode or validate a hex string. + #[error("Failed to convert hex string: {0}")] + FailedToConvertHexString(hex::FromHexError), +} + +/// Result type used by SSZ helper functions. +pub type Result = std::result::Result>; diff --git a/crates/cluster/src/ssz_hasher.rs b/crates/ssz/src/hasher.rs similarity index 76% rename from crates/cluster/src/ssz_hasher.rs rename to crates/ssz/src/hasher.rs index deb04834..4a90b8e7 100644 --- a/crates/cluster/src/ssz_hasher.rs +++ b/crates/ssz/src/hasher.rs @@ -1,3 +1,5 @@ +//! SSZ hash walker and merkleization runtime. + use std::sync::LazyLock; use k256::sha2::{Digest, Sha256}; @@ -16,7 +18,7 @@ const ZERO_BYTES: [u8; 32] = [0; 32]; const TRUE_BYTES: [u8; 32] = calculate_bool_bytes(true); const FALSE_BYTES: [u8; 32] = calculate_bool_bytes(false); -/// Precomputed zero hashes for each depth level (0-64) +/// Precomputed zero hashes for each depth level (0-64). static ZERO_HASHES: LazyLock<[[u8; 32]; 65]> = LazyLock::new(|| { let mut hashes = [[0u8; 32]; 65]; @@ -30,54 +32,50 @@ static ZERO_HASHES: LazyLock<[[u8; 32]; 65]> = LazyLock::new(|| { hashes }); -/// Trait for objects that can walk (traverse/append) data for -/// merkleization/hash calculations. +/// Trait for objects that can walk data for SSZ merkleization and hashing. pub trait HashWalker { - /// The error type that can occur during hashing. + /// Error type returned by the walker implementation. type Error: std::error::Error; - /// Finalize and return the hash result. + /// Finalize and return the current hash result. fn hash(&self) -> Result<[u8; 32], Self::Error>; /// Append a single byte. fn append_u8(&mut self, i: u8) -> Result<(), Self::Error>; - /// Append a u32 integer. + /// Append a `u32`. fn append_u32(&mut self, i: u32) -> Result<(), Self::Error>; - /// Append a u64 integer. + /// Append a `u64`. fn append_u64(&mut self, i: u64) -> Result<(), Self::Error>; - /// Append a bytes array, and fill up to `k * 32` bytes if the length is not - /// a multiple of 32. + /// Append bytes and pad to a multiple of 32 bytes. fn append_bytes32(&mut self, b: &[u8]) -> Result<(), Self::Error>; - /// Append an array of 32 u64 values. + /// Append an array of `u64` values. fn put_uint64_array( &mut self, b: &[u64], max_capacity: Option, ) -> Result<(), Self::Error>; - /// Append a u64 value. + /// Append a `u64`. fn put_uint64(&mut self, i: u64) -> Result<(), Self::Error>; - /// Append a u32 value. + /// Append a `u32`. fn put_uint32(&mut self, i: u32) -> Result<(), Self::Error>; - /// Append a u16 value. + /// Append a `u16`. fn put_uint16(&mut self, i: u16) -> Result<(), Self::Error>; - /// Append a u8 value. + /// Append a `u8`. fn put_uint8(&mut self, i: u8) -> Result<(), Self::Error>; - /// Pad data up to 32 bytes. + /// Pad the buffer up to 32 bytes. fn fill_up_to_32(&mut self) -> Result<(), Self::Error>; - /// Append a byte slice. + /// Append raw bytes. fn append(&mut self, b: &[u8]) -> Result<(), Self::Error>; - /// Append a bitlist, with given max size. + /// Append an SSZ bitlist with the provided maximum size. fn put_bitlist(&mut self, bb: &[u8], max_size: usize) -> Result<(), Self::Error>; - /// Append a boolean value. + /// Append a boolean. fn put_bool(&mut self, b: bool) -> Result<(), Self::Error>; - /// Append a byte slice, if the length is less than or equal to 32, it will - /// be appended as is + padding to 32 bytes, otherwise it will be - /// merkleized. + /// Append bytes using SSZ byte-list or byte-vector semantics. fn put_bytes(&mut self, b: &[u8]) -> Result<(), Self::Error>; - /// Current byte index or position in buffer. + /// Return the current buffer index. fn index(&self) -> usize; - /// Perform merkleization at given index. + /// Merkleize the buffer from a starting index. fn merkleize(&mut self, index: usize) -> Result<(), Self::Error>; - /// Perform merkleization with mixin (limit value). + /// Merkleize the buffer from a starting index and mix in the list length. fn merkleize_with_mixin( &mut self, index: usize, @@ -86,41 +84,30 @@ pub trait HashWalker { ) -> Result<(), Self::Error>; } -/// Hash function for hashing SSZ data. +/// Hash function used by the SSZ hasher. pub type HashFn = fn(src: &[u8]) -> Result, HasherError>; -/// Errors that may occur during hashing/merkleization. +/// Errors returned by the SSZ hasher. #[derive(Debug, thiserror::Error)] pub enum HasherError { - /// Invalid buffer length + /// Invalid buffer length. #[error("Invalid buffer length")] InvalidBufferLength, - /// Unsupported version - #[error("Unsupported version: {0}")] - UnsupportedVersion(String), - /// Integer overflow - #[error("Integer overflow")] - IntegerOverflow, - /// Integer underflow - #[error("Integer underflow")] - IntegerUnderflow, - /// Count greater than limit + /// Count exceeded the declared limit. #[error("Count greater than limit: count {count}, limit {limit}")] CountGreaterThanLimit { - /// Count + /// Actual count. count: usize, - /// Limit + /// Declared limit. limit: usize, }, } -/// SSZ hasher for calculating merkle roots. +/// SSZ hasher for calculating Merkle roots. #[derive(Debug)] pub struct Hasher { buf: Vec, - tmp: Vec, - hash: HashFn, } @@ -131,7 +118,7 @@ impl Default for Hasher { } impl Hasher { - /// Create a new hasher. + /// Creates a new hasher with the provided hash function. pub fn new(hash: HashFn) -> Self { Self { buf: Vec::new(), @@ -140,7 +127,7 @@ impl Hasher { } } - /// Default hash function. + /// Default SHA-256 pairwise hash function used during merkleization. pub fn default_hash_fn(src: &[u8]) -> Result, HasherError> { let mut result = Vec::with_capacity(src.len() / 2); @@ -154,6 +141,14 @@ impl Hasher { Ok(result) } + fn pad_to_32(buf: &mut Vec) { + let rest = buf.len() % 32; + if rest != 0 { + #[allow(clippy::arithmetic_side_effects)] + buf.extend_from_slice(&ZERO_BYTES[..32 - rest]); + } + } + #[allow(clippy::arithmetic_side_effects)] fn next_power_of_two(mut v: usize) -> usize { v -= 1; @@ -217,7 +212,7 @@ impl Hasher { Ok(input) } - /// Compute the SSZ hash root. + /// Computes the SSZ hash root of the current buffer. pub fn hash_root(&self) -> Result<[u8; 32], HasherError> { if self.buf.len() != 32 { return Err(HasherError::InvalidBufferLength); @@ -225,7 +220,7 @@ impl Hasher { self.hash() } - /// Reset the hasher. + /// Resets the internal buffer. pub fn reset(&mut self) { self.buf.clear(); } @@ -234,7 +229,6 @@ impl Hasher { impl HashWalker for Hasher { type Error = HasherError; - /// Return the hash of the current buffer. fn hash(&self) -> Result<[u8; 32], Self::Error> { if self.buf.len() < 32 { return Err(HasherError::InvalidBufferLength); @@ -245,35 +239,24 @@ impl HashWalker for Hasher { Ok(result) } - /// Append a single byte. fn append_u8(&mut self, i: u8) -> Result<(), Self::Error> { self.append(&[i]) } - /// Append a u32 integer. fn append_u32(&mut self, i: u32) -> Result<(), Self::Error> { self.append(&i.to_le_bytes()) } - /// Append a u64 integer. fn append_u64(&mut self, i: u64) -> Result<(), Self::Error> { self.append(&i.to_le_bytes()) } - /// Append a bytes array, and fill up to `k * 32` bytes if the length is not - /// a multiple of 32. fn append_bytes32(&mut self, b: &[u8]) -> Result<(), Self::Error> { self.buf.extend_from_slice(b); - let rest = b.len() % 32; - if rest != 0 { - #[allow(clippy::arithmetic_side_effects)] - // rest < 32, ZERO_BYTES is constant with length 32 - self.buf.extend_from_slice(&ZERO_BYTES[..32 - rest]); - } + Self::pad_to_32(&mut self.buf); Ok(()) } - /// Append an array of u64 values. fn put_uint64_array( &mut self, b: &[u64], @@ -296,66 +279,47 @@ impl HashWalker for Hasher { Ok(()) } - /// Append a u64 value. fn put_uint64(&mut self, i: u64) -> Result<(), Self::Error> { self.append_bytes32(&i.to_le_bytes()) } - /// Append a u32 value. fn put_uint32(&mut self, i: u32) -> Result<(), Self::Error> { self.append_bytes32(&i.to_le_bytes()) } - /// Append a u16 value. fn put_uint16(&mut self, i: u16) -> Result<(), Self::Error> { self.append_bytes32(&i.to_le_bytes()) } - /// Append a u8 value. fn put_uint8(&mut self, i: u8) -> Result<(), Self::Error> { self.append_bytes32(&[i]) } - /// Pad data up to 32 bytes. fn fill_up_to_32(&mut self) -> Result<(), Self::Error> { - let rest = self.buf.len() % 32; - if rest != 0 { - #[allow(clippy::arithmetic_side_effects)] - // rest < 32, ZERO_BYTES is constant with length 32 - self.buf.extend_from_slice(&ZERO_BYTES[..32 - rest]); - } + Self::pad_to_32(&mut self.buf); Ok(()) } - /// Append a byte slice. fn append(&mut self, b: &[u8]) -> Result<(), Self::Error> { self.buf.extend_from_slice(b); Ok(()) } - /// Append a bitlist, with given max size. fn put_bitlist(&mut self, bb: &[u8], max_size: usize) -> Result<(), Self::Error> { let size = parse_bitlist(&mut self.tmp, bb)?; - // merkleize the content with mix in length let indx = self.index(); self.append_bytes32(&self.tmp.clone())?; - self.merkleize_with_mixin(indx, size as usize, max_size.div_ceil(256))?; + self.merkleize_with_mixin(indx, size, max_size.div_ceil(256))?; Ok(()) } - /// Append a boolean value. fn put_bool(&mut self, b: bool) -> Result<(), Self::Error> { - if b { - self.buf.extend_from_slice(&TRUE_BYTES) - } else { - self.buf.extend_from_slice(&FALSE_BYTES) - } - + let bytes = if b { &TRUE_BYTES } else { &FALSE_BYTES }; + self.buf.extend_from_slice(bytes); Ok(()) } - /// Append a byte slice (copy). fn put_bytes(&mut self, b: &[u8]) -> Result<(), Self::Error> { if b.len() <= 32 { self.append_bytes32(b) @@ -367,12 +331,10 @@ impl HashWalker for Hasher { } } - /// Get the current index in the buffer. fn index(&self) -> usize { self.buf.len() } - /// Perform merkleization at a given index. fn merkleize(&mut self, index: usize) -> Result<(), Self::Error> { // merkleizeImpl will expand the `input` by 32 bytes if some hashing depth // hits an odd chunk length. But if we're at the end of `h.buf` already, @@ -392,7 +354,6 @@ impl HashWalker for Hasher { Ok(()) } - /// Perform merkleization with a mixin value. fn merkleize_with_mixin( &mut self, index: usize, @@ -401,7 +362,6 @@ impl HashWalker for Hasher { ) -> Result<(), Self::Error> { self.fill_up_to_32()?; - // merkleize the input let mut input: Vec = self.buf[index..].to_vec(); input = self.merkleize_impl(&input, limit)?; @@ -423,7 +383,7 @@ impl HashWalker for Hasher { } } -/// Calculate the limit for the merkleization with a mixin value. +/// Calculates the chunk limit used when merkleizing packed arrays. pub fn calculate_limit(max_capacity: usize, num_items: usize, size: usize) -> usize { let limit = (max_capacity.saturating_mul(size)).div_ceil(32); if limit != 0 { @@ -445,7 +405,6 @@ fn parse_bitlist(tmp: &mut Vec, buf: &[u8]) -> Result { return Err(HasherError::InvalidBufferLength); } - // Find the most significant bit in the last byte let last_byte = buf[buf.len().wrapping_sub(1)]; let msb = 8u8 .wrapping_sub(last_byte.leading_zeros() as u8) diff --git a/crates/ssz/src/helpers.rs b/crates/ssz/src/helpers.rs new file mode 100644 index 00000000..2465e845 --- /dev/null +++ b/crates/ssz/src/helpers.rs @@ -0,0 +1,102 @@ +//! Generic SSZ helper functions. + +use crate::{Error, HashWalker, Result}; + +/// Decodes a `0x`-prefixed hex string and enforces an exact byte length. +pub fn from_0x_hex_str(s: &str, len: usize) -> std::result::Result, hex::FromHexError> { + if s.is_empty() { + return Ok(vec![]); + } + + let s = s.strip_prefix("0x").unwrap_or(s); + let bytes = hex::decode(s)?; + if bytes.len() != len { + return Err(hex::FromHexError::InvalidStringLength); + } + Ok(bytes) +} + +/// Appends an SSZ byte list with the provided maximum size. +pub fn put_byte_list( + hh: &mut H, + bytes: &[u8], + limit: usize, + field: &str, +) -> Result<(), H::Error> { + let elem_indx = hh.index(); + let byte_len = bytes.len(); + + if byte_len > limit { + return Err(Error::IncorrectListSize { + namespace: "put_byte_list", + field: field.to_string(), + actual: byte_len, + expected: limit, + }); + } + + hh.append_bytes32(bytes).map_err(Error::HashWalkerError)?; + hh.merkleize_with_mixin(elem_indx, byte_len, limit.div_ceil(32)) + .map_err(Error::HashWalkerError)?; + + Ok(()) +} + +/// Appends bytes as an SSZ fixed-size byte vector of length `n`. +pub fn put_bytes_n(hh: &mut H, bytes: &[u8], n: usize) -> Result<(), H::Error> { + if bytes.len() > n { + return Err(Error::IncorrectListSize { + namespace: "put_bytes_n", + field: String::new(), + actual: bytes.len(), + expected: n, + }); + } + + hh.put_bytes(&left_pad(bytes, n)) + .map_err(Error::HashWalkerError)?; + + Ok(()) +} + +/// Appends fixed-size bytes decoded from a `0x`-prefixed hex string. +pub fn put_hex_bytes_n(hh: &mut H, hex: &str, n: usize) -> Result<(), H::Error> { + let bytes = from_0x_hex_str(hex, n).map_err(Error::FailedToConvertHexString)?; + hh.put_bytes(&left_pad(&bytes, n)) + .map_err(Error::HashWalkerError)?; + Ok(()) +} + +/// Left-pads the input bytes with zeros up to `len`. +pub fn left_pad(bytes: &[u8], len: usize) -> Vec { + if bytes.len() >= len { + return bytes.to_vec(); + } + + let pad_count = len.saturating_sub(bytes.len()); + let mut padded = vec![0; pad_count]; + padded.extend_from_slice(bytes); + padded +} + +/// Encodes bytes as a `0x`-prefixed lowercase hex string. +pub fn to_0x_hex(bytes: &[u8]) -> String { + if bytes.is_empty() { + return String::new(); + } + + format!("0x{}", hex::encode(bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_left_pad() { + assert_eq!(left_pad(&[0x12, 0x34], 4), vec![0x00, 0x00, 0x12, 0x34]); + assert_eq!(left_pad(&[0xab], 3), vec![0x00, 0x00, 0xab]); + assert_eq!(left_pad(&[1, 2, 3], 3), vec![1, 2, 3]); + assert_eq!(left_pad(&[1, 2, 3], 2), vec![1, 2, 3]); + } +} diff --git a/crates/ssz/src/lib.rs b/crates/ssz/src/lib.rs new file mode 100644 index 00000000..4e1041ad --- /dev/null +++ b/crates/ssz/src/lib.rs @@ -0,0 +1,18 @@ +//! Shared SSZ hashing primitives, helpers, and container wrappers. + +mod error; +mod hasher; +mod helpers; +pub mod serde_utils; +mod types; + +/// Generic SSZ error types. +pub use error::{Error, Result}; +/// SSZ hashing walker and merkleization runtime. +pub use hasher::{HashFn, HashWalker, Hasher, HasherError, calculate_limit}; +/// Generic SSZ helper utilities. +pub use helpers::{ + from_0x_hex_str, left_pad, put_byte_list, put_bytes_n, put_hex_bytes_n, to_0x_hex, +}; +/// Generic SSZ list, vector, and bitfield wrappers. +pub use types::{BitList, BitVector, SszList, SszVector}; diff --git a/crates/ssz/src/serde_utils.rs b/crates/ssz/src/serde_utils.rs new file mode 100644 index 00000000..8fcb795e --- /dev/null +++ b/crates/ssz/src/serde_utils.rs @@ -0,0 +1,131 @@ +//! Generic serde helpers for SSZ-related types and JSON hex encoding. + +use serde::{ + Deserialize, Deserializer, Serializer, + de::{Error as DeError, Unexpected}, +}; +use serde_with::{DeserializeAs, SerializeAs}; + +/// Strips the `0x` or `0X` prefix from a hex string, returning `None` if +/// absent. +pub fn strip_0x_prefix(value: &str) -> Option<&str> { + value + .strip_prefix("0x") + .or_else(|| value.strip_prefix("0X")) +} + +/// Strips the `0x` or `0X` prefix from a hex string, returning the input +/// unchanged if absent. +pub fn trim_0x_prefix(value: &str) -> &str { + strip_0x_prefix(value).unwrap_or(value) +} + +/// Encodes bytes as lowercase `0x`-prefixed hex. +pub fn encode_0x_hex(bytes: &[u8]) -> String { + format!("0x{}", hex::encode(bytes)) +} + +/// Decodes a `0x`-prefixed or unprefixed hex string. +pub fn decode_0x_hex(s: &str) -> Result, E> { + hex::decode(trim_0x_prefix(s)).map_err(E::custom) +} + +/// Serde adapter for byte-like values encoded as `0x`-prefixed lowercase hex +/// strings. +/// +/// Deserialization accepts both prefixed (`0x...`) and unprefixed (`...`) +/// values. +pub struct Hex0x; + +impl SerializeAs for Hex0x +where + T: AsRef<[u8]>, +{ + fn serialize_as(source: &T, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&encode_0x_hex(source.as_ref())) + } +} + +impl<'de, T> DeserializeAs<'de, T> for Hex0x +where + T: TryFrom>, +{ + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + let decoded = decode_0x_hex::(value.as_str())?; + decoded.try_into().map_err(|_err: T::Error| { + D::Error::invalid_value( + Unexpected::Str(value.as_str()), + &"hex bytes convertible to target type", + ) + }) + } +} + +/// Serde helpers for SSZ lists of `u64` encoded as JSON strings. +pub mod ssz_list_u64_string_serde { + use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError}; + + use crate::SszList; + + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrU64 { + String(String), + U64(u64), + } + + /// Serializes an `SszList` as a JSON array of decimal strings. + pub fn serialize( + value: &SszList, + serializer: S, + ) -> Result + where + S: Serializer, + { + let strings: Vec = value + .0 + .iter() + .map(std::string::ToString::to_string) + .collect(); + strings.serialize(serializer) + } + + /// Deserializes a JSON array of decimal strings or integers into an + /// `SszList`. + pub fn deserialize<'de, D, const MAX: usize>( + deserializer: D, + ) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let raw = Vec::::deserialize(deserializer)?; + + if MAX > 0 && raw.len() > MAX { + return Err(D::Error::custom(format!( + "list length {} exceeds max {}", + raw.len(), + MAX + ))); + } + + let mut out = Vec::with_capacity(raw.len()); + for value in raw { + let parsed = match value { + StringOrU64::U64(value) => value, + StringOrU64::String(value) => value.parse::().map_err(|err| { + D::Error::custom(format!("invalid integer string '{value}': {err}")) + })?, + }; + out.push(parsed); + } + + Ok(SszList(out)) + } +} diff --git a/crates/eth2api/src/spec/ssz_types.rs b/crates/ssz/src/types.rs similarity index 76% rename from crates/eth2api/src/spec/ssz_types.rs rename to crates/ssz/src/types.rs index fb1d92ec..47b862f3 100644 --- a/crates/eth2api/src/spec/ssz_types.rs +++ b/crates/ssz/src/types.rs @@ -1,12 +1,10 @@ -//! SSZ container helpers used by spec types. -//! -//! The `tree_hash` crate supports SSZ TreeHash for many primitives, but does -//! not provide `TreeHash` for `Vec` directly. These wrappers encode SSZ -//! list/vector semantics and include optional length enforcement during serde -//! deserialization. +//! Generic SSZ list, vector, and bitfield wrappers. +use crate::serde_utils::{decode_0x_hex, encode_0x_hex}; use serde::{Deserialize, Serialize, de::Error as DeError}; -use tree_hash::{Hash256, PackedEncoding, TreeHash, TreeHashType, merkle_root, mix_in_length}; +use tree_hash::{ + BYTES_PER_CHUNK, Hash256, PackedEncoding, TreeHash, TreeHashType, merkle_root, mix_in_length, +}; fn tree_hash_bytes(values: &[T]) -> Vec { let mut bytes = Vec::with_capacity(values.len().saturating_mul(32)); @@ -24,7 +22,19 @@ fn tree_hash_bytes(values: &[T]) -> Vec { bytes } -/// SSZ variable-length list wrapper with optional max length and TreeHash +fn minimum_leaf_count_for_elements(len: usize) -> usize { + if T::tree_hash_type() == TreeHashType::Basic { + len.div_ceil(T::tree_hash_packing_factor()) + } else { + len + } +} + +fn minimum_leaf_count_for_bits(len: usize) -> usize { + len.div_ceil(BYTES_PER_CHUNK * 8) +} + +/// SSZ variable-length list wrapper with optional max length and `TreeHash` /// support. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct SszList( @@ -85,13 +95,10 @@ impl TreeHash for SszList { fn tree_hash_root(&self) -> Hash256 { let bytes = tree_hash_bytes(&self.0); - let minimum_leaf_count = if MAX == 0 { 0 - } else if T::tree_hash_type() == TreeHashType::Basic { - MAX.div_ceil(T::tree_hash_packing_factor()) } else { - MAX + minimum_leaf_count_for_elements::(MAX) }; let root = merkle_root(bytes.as_slice(), minimum_leaf_count); @@ -99,7 +106,7 @@ impl TreeHash for SszList { } } -/// SSZ fixed-size vector wrapper with TreeHash support. +/// SSZ fixed-size vector wrapper with `TreeHash` support. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct SszVector( /// Elements in the SSZ vector. @@ -153,27 +160,18 @@ impl TreeHash for SszVector { fn tree_hash_root(&self) -> Hash256 { let bytes = tree_hash_bytes(&self.0); - - let minimum_leaf_count = if T::tree_hash_type() == TreeHashType::Basic { - SIZE.div_ceil(T::tree_hash_packing_factor()) - } else { - SIZE - }; + let minimum_leaf_count = minimum_leaf_count_for_elements::(SIZE); merkle_root(bytes.as_slice(), minimum_leaf_count) } } -/// Lookup table for single-bit masks, avoiding shift operators. const BIT_MASK: [u8; 8] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80]; /// SSZ variable-length bitfield with maximum capacity. -/// -/// Stores packed bit data (no sentinel) internally. The SSZ sentinel bit is -/// added during serialization and stripped during deserialization. #[derive(Debug, Clone, PartialEq, Eq)] pub struct BitList { - /// Packed data bits, little-endian bit order (no sentinel). + /// Packed data bits, little-endian bit order without the sentinel bit. bytes: Vec, /// Number of data bits. len: usize, @@ -189,6 +187,15 @@ impl Default for BitList { } impl BitList { + fn append_sentinel(bytes: &mut Vec, len: usize) { + let sentinel_byte = len / 8; + let sentinel_bit = len % 8; + if sentinel_byte >= bytes.len() { + bytes.resize(sentinel_byte.saturating_add(1), 0); + } + bytes[sentinel_byte] |= BIT_MASK[sentinel_bit]; + } + /// Returns the number of data bits. pub fn len(&self) -> usize { self.len @@ -199,7 +206,6 @@ impl BitList { self.len == 0 } - /// Creates a `BitList` from SSZ-encoded bytes (with sentinel bit). fn from_ssz_bytes(ssz: Vec) -> Self { if ssz.is_empty() { return Self::default(); @@ -208,7 +214,7 @@ impl BitList { if last_byte == 0 { return Self::default(); } - // Sentinel is the highest set bit in the last byte. + let sentinel_pos = 7_u32.saturating_sub(last_byte.leading_zeros()) as usize; let len = ssz .len() @@ -218,7 +224,6 @@ impl BitList { let data_byte_len = len.div_ceil(8); let mut bytes = ssz; bytes.truncate(data_byte_len); - // Clear sentinel bit if it shares a byte with data. let rem = len % 8; if rem != 0 && let Some(last) = bytes.last_mut() @@ -228,44 +233,42 @@ impl BitList { Self { bytes, len } } - /// Encodes as SSZ bytes (with sentinel bit appended). fn to_ssz_bytes(&self) -> Vec { - let sentinel_byte = self.len / 8; - let sentinel_bit = self.len % 8; let mut ssz = self.bytes.clone(); - if sentinel_byte >= ssz.len() { - ssz.resize(sentinel_byte.saturating_add(1), 0); - } - ssz[sentinel_byte] |= BIT_MASK[sentinel_bit]; + Self::append_sentinel(&mut ssz, self.len); ssz } - /// Consumes the `BitList` and returns the SSZ-encoded bytes (with - /// sentinel). + /// Consumes the `BitList` and returns the SSZ-encoded bytes with sentinel. pub fn into_bytes(mut self) -> Vec { - let sentinel_byte = self.len / 8; - let sentinel_bit = self.len % 8; - if sentinel_byte >= self.bytes.len() { - self.bytes.resize(sentinel_byte.saturating_add(1), 0); - } - self.bytes[sentinel_byte] |= BIT_MASK[sentinel_bit]; + Self::append_sentinel(&mut self.bytes, self.len); self.bytes } + + /// Creates a `BitList` with the given capacity and specified bits set. + pub fn with_bits(capacity: usize, set_bits: &[usize]) -> Self { + let byte_len = capacity.div_ceil(8); + let mut bytes = vec![0u8; byte_len]; + for &bit in set_bits { + bytes[bit / 8] |= BIT_MASK[bit % 8]; + } + Self { + bytes, + len: capacity, + } + } } impl Serialize for BitList { fn serialize(&self, serializer: S) -> Result { - let ssz = self.to_ssz_bytes(); - let hex_str = format!("0x{}", hex::encode(ssz)); - serializer.serialize_str(hex_str.as_str()) + serializer.serialize_str(&encode_0x_hex(&self.to_ssz_bytes())) } } impl<'de, const MAX: usize> Deserialize<'de> for BitList { fn deserialize>(deserializer: D) -> Result { let s = String::deserialize(deserializer)?; - let trimmed = crate::spec::serde_utils::trim_0x_prefix(s.as_str()); - let ssz = hex::decode(trimmed).map_err(D::Error::custom)?; + let ssz = decode_0x_hex::(s.as_str())?; Ok(Self::from_ssz_bytes(ssz)) } } @@ -284,16 +287,13 @@ impl TreeHash for BitList { } fn tree_hash_root(&self) -> Hash256 { - // 256 bits per 32-byte chunk. - let minimum_leaf_count = MAX.div_ceil(256); + let minimum_leaf_count = minimum_leaf_count_for_bits(MAX); let root = merkle_root(self.bytes.as_slice(), minimum_leaf_count); mix_in_length(&root, self.len) } } /// SSZ fixed-length bitfield. -/// -/// Stores `SIZE` bits packed in little-endian byte order. #[derive(Debug, Clone, PartialEq, Eq)] pub struct BitVector { /// Packed bits, little-endian bit order. @@ -313,20 +313,27 @@ impl BitVector { pub fn new() -> Self { Self::default() } + + /// Creates a `BitVector` with specified bits set. + pub fn with_bits(set_bits: &[usize]) -> Self { + let mut v = Self::new(); + for &bit in set_bits { + v.bytes[bit / 8] |= BIT_MASK[bit % 8]; + } + v + } } impl Serialize for BitVector { fn serialize(&self, serializer: S) -> Result { - let hex_str = format!("0x{}", hex::encode(self.bytes.as_slice())); - serializer.serialize_str(hex_str.as_str()) + serializer.serialize_str(&encode_0x_hex(self.bytes.as_slice())) } } impl<'de, const SIZE: usize> Deserialize<'de> for BitVector { fn deserialize>(deserializer: D) -> Result { let s = String::deserialize(deserializer)?; - let trimmed = crate::spec::serde_utils::trim_0x_prefix(s.as_str()); - let bytes = hex::decode(trimmed).map_err(D::Error::custom)?; + let bytes = decode_0x_hex::(s.as_str())?; let expected = SIZE.div_ceil(8); if bytes.len() != expected { return Err(D::Error::custom(format!( @@ -352,39 +359,11 @@ impl TreeHash for BitVector { } fn tree_hash_root(&self) -> Hash256 { - let minimum_leaf_count = SIZE.div_ceil(256); + let minimum_leaf_count = minimum_leaf_count_for_bits(SIZE); merkle_root(self.bytes.as_slice(), minimum_leaf_count) } } -#[cfg(test)] -impl BitList { - /// Creates a `BitList` with the given capacity and specified bits set. - pub(crate) fn with_bits(capacity: usize, set_bits: &[usize]) -> Self { - let byte_len = capacity.div_ceil(8); - let mut bytes = vec![0u8; byte_len]; - for &bit in set_bits { - bytes[bit / 8] |= BIT_MASK[bit % 8]; - } - Self { - bytes, - len: capacity, - } - } -} - -#[cfg(test)] -impl BitVector { - /// Creates a `BitVector` with specified bits set. - pub(crate) fn with_bits(set_bits: &[usize]) -> Self { - let mut v = Self::new(); - for &bit in set_bits { - v.bytes[bit / 8] |= BIT_MASK[bit % 8]; - } - v - } -} - #[cfg(test)] mod tests { use super::*; @@ -406,9 +385,6 @@ mod tests { #[test] fn ssz_list_tree_hash_depends_on_max_len() { - // For SSZ List[T, MAX], the tree hash uses `minimum_leaf_count` derived from - // MAX. If MAX is wrong/ignored, roots can silently diverge from spec - // implementations. let list_max_4: SszList = vec![42].into(); let list_max_8: SszList = vec![42].into(); assert_ne!(list_max_4.tree_hash_root(), list_max_8.tree_hash_root()); @@ -416,9 +392,6 @@ mod tests { #[test] fn ssz_vector_tree_hash_depends_on_size() { - // For basic types, packing can make different sizes hash to the same single - // chunk (e.g. size 1 vs 2 `u64`s). Use sizes that force a different - // leaf count. let vec_size_4: SszVector = vec![42, 0, 0, 0].into(); let vec_size_5: SszVector = vec![42, 0, 0, 0, 0].into(); assert_ne!(vec_size_4.tree_hash_root(), vec_size_5.tree_hash_root());