Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1430acc
feat: add deadline.rs
varex83 Mar 11, 2026
364977e
feat: deadline tests
varex83 Mar 11, 2026
368177c
fix: remove comments
varex83 Mar 11, 2026
f4b6893
Merge remote-tracking branch 'origin/main' into bohdan/deadline
varex83 Mar 11, 2026
de6dba4
wip: add parasig db and some tests
varex83 Mar 12, 2026
d71c885
refactor: remove old app/deadline
varex83 Mar 12, 2026
13de57a
feat: add rust docs
varex83 Mar 17, 2026
fc85e63
feat: add clone box and clone eq
varex83 Mar 17, 2026
5767cc1
Merge branch 'bohdan/update-signeddata-trait' into bohdan/parasigdb
varex83 Mar 18, 2026
e214d6f
Merge remote-tracking branch 'origin/main' into bohdan/deadline
varex83 Mar 18, 2026
7aced94
Merge branch 'bohdan/deadline' into bohdan/parasigdb
varex83 Mar 18, 2026
288f63c
feat: finish tests
varex83 Mar 18, 2026
8a3780e
feat: add parsigex [wip]
varex83 Mar 19, 2026
a4c4bb2
fix: typo
varex83 Mar 19, 2026
1b0711f
Merge branch 'bohdan/parasigdb' into bohdan/dkg-parsigex
varex83 Mar 19, 2026
6e9d224
fix: parsigex
varex83 Mar 20, 2026
52cbe10
Merge remote-tracking branch 'origin/main' into bohdan/dkg-parsigex
varex83 Apr 6, 2026
0247655
refactor: parsigex
varex83 Apr 9, 2026
eb5615e
fix: linter
varex83 Apr 9, 2026
6d3e20b
feat: add subscribe
varex83agent Apr 13, 2026
e643bd1
feat: add documentation for parsigex
varex83 Apr 13, 2026
e5f1750
fix: review comments
varex83 Apr 20, 2026
b0721f8
fix: review comments
varex83 Apr 20, 2026
ac01508
fix: review comments
varex83 Apr 20, 2026
b07a460
fix: cargo deny
varex83 Apr 20, 2026
0de6cdc
fix: linter
varex83 Apr 20, 2026
4a95b0b
Merge remote-tracking branch 'origin/main' into bohdan/dkg-parsigex
varex83 Apr 21, 2026
d248c45
fix: remove unused deps
varex83 Apr 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 145 additions & 104 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"crates/app",
"crates/parsigex",
"crates/build-proto",
"crates/cli",
"crates/cluster",
Expand Down Expand Up @@ -99,6 +100,7 @@ wiremock = "0.6"

# Crates in the workspace
pluto-app = { path = "crates/app" }
pluto-parsigex = { path = "crates/parsigex" }
pluto-build-proto = { path = "crates/build-proto" }
pluto-cli = { path = "crates/cli" }
pluto-cluster = { path = "crates/cluster" }
Expand Down
8 changes: 8 additions & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ cancellation.workspace = true
chrono.workspace = true
crossbeam.workspace = true
futures.workspace = true
futures-timer.workspace = true
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused dependencies:

  • unsigned-varint
  • futures-timer

dyn-clone.workspace = true
dyn-eq.workspace = true
hex.workspace = true
Expand All @@ -31,18 +32,25 @@ tokio-util.workspace = true
tracing.workspace = true
pluto-eth2util.workspace = true
tree_hash.workspace = true
unsigned-varint.workspace = true

[dev-dependencies]
anyhow.workspace = true
alloy.workspace = true
clap.workspace = true
rand.workspace = true
libp2p.workspace = true
k256.workspace = true
prost.workspace = true
prost-types.workspace = true
hex.workspace = true
chrono.workspace = true
test-case.workspace = true
pluto-eth2util.workspace = true
pluto-cluster.workspace = true
pluto-p2p.workspace = true
pluto-testutil.workspace = true
pluto-tracing.workspace = true
Comment thread
varex83 marked this conversation as resolved.
Outdated
tokio = { workspace = true, features = ["test-util"] }
wiremock.workspace = true
pluto-ssz.workspace = true
Expand Down
4 changes: 4 additions & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub mod deadline;
/// parsigdb
pub mod parsigdb;

mod parsigex_codec;

pub use parsigex_codec::ParSigExCodecError;

/// Test utilities.
#[cfg(test)]
pub mod testutils;
118 changes: 118 additions & 0 deletions crates/core/src/parsigex_codec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! Partial signature exchange codec helpers used by core types.

use std::any::Any;

use crate::{
signeddata::{
Attestation, BeaconCommitteeSelection, SignedAggregateAndProof, SignedRandao,
SignedSyncContributionAndProof, SignedSyncMessage, SignedVoluntaryExit,
SyncCommitteeSelection, VersionedAttestation, VersionedSignedAggregateAndProof,
VersionedSignedProposal, VersionedSignedValidatorRegistration,
},
types::{DutyType, Signature, SignedData},
};

/// Error type for partial signature exchange codec operations.
#[derive(Debug, thiserror::Error)]
pub enum ParSigExCodecError {
/// Missing duty or data set fields.
#[error("invalid parsigex msg fields")]
InvalidMessageFields,

/// Invalid partial signed data set proto.
#[error("invalid partial signed data set proto fields")]
InvalidParSignedDataSetFields,

/// Invalid partial signed proto.
#[error("invalid partial signed proto")]
InvalidParSignedProto,
Comment thread
varex83 marked this conversation as resolved.
Outdated

/// Invalid duty type.
#[error("invalid duty")]
InvalidDuty,

/// Unsupported duty type.
#[error("unsupported duty type")]
UnsupportedDutyType,

/// Deprecated builder proposer duty.
#[error("deprecated duty builder proposer")]
DeprecatedBuilderProposer,

/// Failed to parse a public key.
#[error("invalid public key: {0}")]
InvalidPubKey(String),

/// Invalid share index.
#[error("invalid share index")]
InvalidShareIndex,

/// Serialization failed.
#[error("marshal signed data: {0}")]
Serialize(#[from] serde_json::Error),
}

pub(crate) fn serialize_signed_data(data: &dyn SignedData) -> Result<Vec<u8>, ParSigExCodecError> {
Comment thread
varex83agent marked this conversation as resolved.
Comment thread
varex83 marked this conversation as resolved.
let any = data as &dyn Any;

macro_rules! serialize_as {
($ty:ty) => {
if let Some(value) = any.downcast_ref::<$ty>() {
return Ok(serde_json::to_vec(value)?);
}
};
}

serialize_as!(Attestation);
serialize_as!(VersionedAttestation);
serialize_as!(VersionedSignedProposal);
serialize_as!(VersionedSignedValidatorRegistration);
serialize_as!(SignedVoluntaryExit);
serialize_as!(SignedRandao);
serialize_as!(Signature);
serialize_as!(BeaconCommitteeSelection);
serialize_as!(SignedAggregateAndProof);
serialize_as!(VersionedSignedAggregateAndProof);
serialize_as!(SignedSyncMessage);
serialize_as!(SyncCommitteeSelection);
serialize_as!(SignedSyncContributionAndProof);

Err(ParSigExCodecError::UnsupportedDutyType)
}

pub(crate) fn deserialize_signed_data(
duty_type: &DutyType,
bytes: &[u8],
) -> Result<Box<dyn SignedData>, ParSigExCodecError> {
macro_rules! deserialize_json {
($ty:ty) => {
serde_json::from_slice::<$ty>(bytes)
.map(|value| Box::new(value) as Box<dyn SignedData>)
.map_err(ParSigExCodecError::from)
};
}

match duty_type {
// Match Go order: old Attestation format first, then VersionedAttestation.
DutyType::Attester => deserialize_json!(Attestation)
.or_else(|_| deserialize_json!(VersionedAttestation))
.map_err(|_| ParSigExCodecError::UnsupportedDutyType),
DutyType::Proposer => deserialize_json!(VersionedSignedProposal),
DutyType::BuilderProposer => Err(ParSigExCodecError::DeprecatedBuilderProposer),
DutyType::BuilderRegistration => deserialize_json!(VersionedSignedValidatorRegistration),
DutyType::Exit => deserialize_json!(SignedVoluntaryExit),
DutyType::Randao => deserialize_json!(SignedRandao),
DutyType::Signature => deserialize_json!(Signature),
DutyType::PrepareAggregator => deserialize_json!(BeaconCommitteeSelection),
// Match Go order: old SignedAggregateAndProof format first, then versioned.
DutyType::Aggregator => deserialize_json!(SignedAggregateAndProof)
.or_else(|_| deserialize_json!(VersionedSignedAggregateAndProof))
.map_err(|_| ParSigExCodecError::UnsupportedDutyType),
DutyType::SyncMessage => deserialize_json!(SignedSyncMessage),
DutyType::PrepareSyncContribution => deserialize_json!(SyncCommitteeSelection),
DutyType::SyncContribution => deserialize_json!(SignedSyncContributionAndProof),
DutyType::Unknown | DutyType::InfoSync | DutyType::DutySentinel(_) => {
Comment thread
varex83 marked this conversation as resolved.
Outdated
Err(ParSigExCodecError::UnsupportedDutyType)
}
}
}
3 changes: 3 additions & 0 deletions crates/core/src/signeddata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ pub enum SignedDataError {
/// Invalid attestation wrapper JSON.
#[error("unmarshal attestation")]
AttestationJson,
/// Custom error.
#[error("{0}")]
Custom(Box<dyn std::error::Error>),
Comment thread
varex83 marked this conversation as resolved.
Outdated
}

fn hash_root<T: TreeHash>(value: &T) -> [u8; 32] {
Expand Down
145 changes: 142 additions & 3 deletions crates/core/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
//! Types for the Charon core.

use std::{collections::HashMap, fmt::Display, iter};
use std::{any::Any, collections::HashMap, fmt::Display, iter};

use chrono::{DateTime, Duration, Utc};
use dyn_clone::DynClone;
use dyn_eq::DynEq;
use serde::{Deserialize, Serialize};
use std::fmt::Debug as StdDebug;

use crate::signeddata::SignedDataError;
use crate::{
ParSigExCodecError,
corepb::v1::core as pbcore,
parsigex_codec::{deserialize_signed_data, serialize_signed_data},
signeddata::SignedDataError,
};

/// The type of duty.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
Expand Down Expand Up @@ -66,6 +71,52 @@ impl DutyType {
}
}

impl From<&DutyType> for i32 {
fn from(duty_type: &DutyType) -> Self {
match duty_type {
DutyType::Unknown => 0,
DutyType::Proposer => 1,
DutyType::Attester => 2,
DutyType::Signature => 3,
DutyType::Exit => 4,
DutyType::BuilderProposer => 5,
DutyType::BuilderRegistration => 6,
DutyType::Randao => 7,
DutyType::PrepareAggregator => 8,
DutyType::Aggregator => 9,
DutyType::SyncMessage => 10,
DutyType::PrepareSyncContribution => 11,
DutyType::SyncContribution => 12,
DutyType::InfoSync => 13,
DutyType::DutySentinel(_) => 14,
Comment thread
varex83 marked this conversation as resolved.
Outdated
}
}
}

impl TryFrom<i32> for DutyType {
type Error = ParSigExCodecError;

fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(DutyType::Unknown),
1 => Ok(DutyType::Proposer),
2 => Ok(DutyType::Attester),
3 => Ok(DutyType::Signature),
4 => Ok(DutyType::Exit),
5 => Ok(DutyType::BuilderProposer),
6 => Ok(DutyType::BuilderRegistration),
7 => Ok(DutyType::Randao),
8 => Ok(DutyType::PrepareAggregator),
9 => Ok(DutyType::Aggregator),
10 => Ok(DutyType::SyncMessage),
11 => Ok(DutyType::PrepareSyncContribution),
12 => Ok(DutyType::SyncContribution),
13 => Ok(DutyType::InfoSync),
_ => Err(ParSigExCodecError::InvalidDuty),
}
}
}

/// SlotNumber struct
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SlotNumber(u64);
Expand Down Expand Up @@ -192,6 +243,28 @@ impl Duty {
}
}

impl From<&Duty> for pbcore::Duty {
fn from(duty: &Duty) -> Self {
Self {
slot: duty.slot.inner(),
r#type: i32::from(&duty.duty_type),
}
}
}

impl TryFrom<&pbcore::Duty> for Duty {
type Error = ParSigExCodecError;

fn try_from(duty: &pbcore::Duty) -> Result<Self, Self::Error> {
let duty_type = DutyType::try_from(duty.r#type)?;
if !duty_type.is_valid() {
return Err(ParSigExCodecError::InvalidDuty);
}

Ok(Self::new(duty.slot.into(), duty_type))
}
}

/// The type of proposal.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
Expand Down Expand Up @@ -452,7 +525,7 @@ impl AsRef<[u8; SIG_LEN]> for Signature {
}

/// Signed data type
pub trait SignedData: DynClone + DynEq + StdDebug + Send + Sync {
pub trait SignedData: Any + DynClone + DynEq + StdDebug + Send + Sync {
/// signature returns the signed duty data's signature.
fn signature(&self) -> Result<Signature, SignedDataError>;

Expand Down Expand Up @@ -517,6 +590,39 @@ impl ParSignedData {
}
}

impl TryFrom<&ParSignedData> for pbcore::ParSignedData {
type Error = ParSigExCodecError;

fn try_from(data: &ParSignedData) -> Result<Self, Self::Error> {
let encoded = serialize_signed_data(data.signed_data.as_ref())?;
let share_idx =
i32::try_from(data.share_idx).map_err(|_| ParSigExCodecError::InvalidShareIndex)?;
let signature = data.signed_data.signature().map_err(|err| {
ParSigExCodecError::Serialize(serde_json::Error::io(std::io::Error::other(
Comment thread
varex83 marked this conversation as resolved.
Outdated
err.to_string(),
)))
})?;

Ok(Self {
data: encoded.into(),
signature: signature.as_ref().to_vec().into(),
share_idx,
})
}
}

impl TryFrom<(&DutyType, &pbcore::ParSignedData)> for ParSignedData {
type Error = ParSigExCodecError;

fn try_from(value: (&DutyType, &pbcore::ParSignedData)) -> Result<Self, Self::Error> {
let (duty_type, data) = value;
let share_idx =
u64::try_from(data.share_idx).map_err(|_| ParSigExCodecError::InvalidShareIndex)?;
let signed_data = deserialize_signed_data(duty_type, &data.data)?;
Ok(Self::new_boxed(signed_data, share_idx))
}
}

/// ParSignedDataSet is a set of partially signed duty data only signed by a
/// single threshold BLS share.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
Expand Down Expand Up @@ -554,6 +660,39 @@ impl ParSignedDataSet {
}
}

impl TryFrom<&ParSignedDataSet> for pbcore::ParSignedDataSet {
type Error = ParSigExCodecError;

fn try_from(set: &ParSignedDataSet) -> Result<Self, Self::Error> {
let mut out = std::collections::BTreeMap::new();
for (pub_key, value) in set.inner() {
out.insert(pub_key.to_string(), pbcore::ParSignedData::try_from(value)?);
}

Ok(Self { set: out })
}
}

impl TryFrom<(&DutyType, &pbcore::ParSignedDataSet)> for ParSignedDataSet {
type Error = ParSigExCodecError;

fn try_from(value: (&DutyType, &pbcore::ParSignedDataSet)) -> Result<Self, Self::Error> {
let (duty_type, set) = value;
if set.set.is_empty() {
return Err(ParSigExCodecError::InvalidParSignedDataSetFields);
}

let mut out = Self::new();
for (pub_key, value) in &set.set {
let pub_key = PubKey::try_from(pub_key.as_str())
.map_err(|_| ParSigExCodecError::InvalidPubKey(pub_key.clone()))?;
out.insert(pub_key, ParSignedData::try_from((duty_type, value))?);
}

Ok(out)
}
}

/// SignedDataSet is a set of signed duty data.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedDataSet<T: SignedData>(HashMap<PubKey, T>);
Expand Down
Loading
Loading