Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
495 changes: 346 additions & 149 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ version = "1.11.0"
alloy = { version = "=1.0.38", features = ["contract", "json"] }
anyhow = { version = "1.0.95", features = ["backtrace"] }
borsh = "1.5.3"
cait-sith = { git = "https://github.com/sig-net/cait-sith", rev = "9f34e8c", features = ["k256"] }
threshold-signatures = { git = "https://github.com/near/threshold-signatures", rev = "315a9749072fde57cdfc7496000878a3bb97400a" }
ciborium = "0.2.2"
clap = { version = "4.5.4", features = ["derive", "env"] }
deadpool-redis = "0.18.0"
Expand All @@ -35,7 +35,7 @@ rand = "0.8.5"
reqwest = { version = "0.11.16", features = ["blocking", "json"] }
semver = "1.0.23"
serde = { version = "1", features = ["derive"] }
serde_bytes = "0.11.15"
serde_bytes = "0.11.19"
serde_json = "1"
sha3 = "0.10.8"
thiserror = "1"
Expand Down
2 changes: 1 addition & 1 deletion chain-signatures/crypto/src/kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ pub fn check_ec_signature(
) -> anyhow::Result<()> {
let public_key = expected_pk.to_encoded_point(false);
let signature = k256::ecdsa::Signature::from_scalars(x_coordinate(big_r), s)
.context("cannot create signature from cait_sith signature")?;
.context("cannot create signature from threshold-signatures signature")?;
let found_pk = recover(
&msg_hash.to_bytes(),
&signature,
Expand Down
2 changes: 1 addition & 1 deletion chain-signatures/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ alloy.workspace = true
anyhow.workspace = true
borsh.workspace = true
borsh_sol = { package = "borsh", version = "0.10.4" } # solana anchor requires borsh 0.10
cait-sith.workspace = true
threshold-signatures.workspace = true
ciborium.workspace = true
clap.workspace = true
deadpool-redis.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion chain-signatures/node/src/backlog/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use crate::backlog::Checkpoint;
use crate::mesh::MeshState;
use crate::node_client::NodeClient;
use crate::protocol::Chain;
use cait_sith::protocol::Participant;
use std::collections::HashMap;
use threshold_signatures::participants::Participant;
use tokio::task::JoinSet;

/// Queries all participants for their checkpoints and returns a selected checkpoint
Expand Down
33 changes: 3 additions & 30 deletions chain-signatures/node/src/kdf.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,7 @@
use anyhow::Context;
use hkdf::Hkdf;
use k256::{ecdsa::RecoveryId, elliptic_curve::sec1::ToEncodedPoint, AffinePoint, Scalar};
use mpc_crypto::{kdf::recover, x_coordinate, ScalarExt};
use k256::{ecdsa::RecoveryId, elliptic_curve::sec1::ToEncodedPoint, Scalar};
use mpc_crypto::{kdf::recover, x_coordinate};
use mpc_primitives::Signature;
use near_primitives::hash::CryptoHash;
use sha3::Sha3_256;

// In case there are multiple requests in the same block (hence same entropy), we need to ensure
// that we generate different random scalars as delta tweaks.
// Receipt ID should be unique inside of a block, so it serves us as the request identifier.
pub fn derive_delta(
request_id: [u8; 32],
entropy: [u8; 32],
presignature_big_r: AffinePoint,
) -> Scalar {
let hk = Hkdf::<Sha3_256>::new(None, &entropy);
let info = format!("{DELTA_DERIVATION_PREFIX}:{}", CryptoHash(request_id));
let mut okm = [0u8; 32];
hk.expand(info.as_bytes(), &mut okm).unwrap();
hk.expand(
presignature_big_r.to_encoded_point(true).as_bytes(),
&mut okm,
)
.unwrap();
Scalar::from_non_biased(okm)
}

// Constant prefix that ensures delta derivation values are used specifically for
// near-mpc-recovery with key derivation protocol vX.Y.Z.
const DELTA_DERIVATION_PREFIX: &str = "near-mpc-recovery v0.1.0 delta derivation:";

// try to get the correct recovery id for this signature by brute force.
pub fn into_eth_sig(
Expand All @@ -39,7 +12,7 @@ pub fn into_eth_sig(
) -> anyhow::Result<Signature> {
let public_key = public_key.to_encoded_point(false);
let signature = k256::ecdsa::Signature::from_scalars(x_coordinate(big_r), s)
.context("cannot create signature from cait_sith signature")?;
.context("cannot create signature from threshold-signatures signature")?;
let pk0 = recover(
&msg_hash.to_bytes()[..],
&signature,
Expand Down
2 changes: 1 addition & 1 deletion chain-signatures/node/src/mesh/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::time::Duration;

use cait_sith::protocol::Participant;
use near_account_id::AccountId;
use threshold_signatures::participants::Participant;
use tokio::sync::{broadcast, watch};
use tokio::task::JoinHandle;
use tokio_stream::wrappers::WatchStream;
Expand Down
3 changes: 2 additions & 1 deletion chain-signatures/node/src/mesh/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use crate::protocol::contract::primitives::Participants;
use crate::protocol::ParticipantInfo;
use crate::protocol::ProtocolState;
use crate::rpc::ContractStateWatcher;
use cait_sith::protocol::Participant;
use near_account_id::AccountId;
use threshold_signatures::participants::Participant;
use tokio::sync::{mpsc, watch};

pub mod connection;
Expand Down Expand Up @@ -173,6 +173,7 @@ impl Mesh {
ProtocolState::Resharing(resharing) => resharing
.new_participants
.find(&self.my_id)
.or_else(|| resharing.old_participants.find(&self.my_id))
.map(|(p, info)| (*p, info.clone())),
Comment on lines 173 to 177
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

[nitpick] Added fallback to old_participants when looking up participant info during resharing. This change on line 176 ensures that nodes in the old participant set (but not in the new set) can still find their participant info. This is a good fix, but verify that this doesn't cause issues if a participant exists in both old and new sets with different info.

Suggested change
ProtocolState::Resharing(resharing) => resharing
.new_participants
.find(&self.my_id)
.or_else(|| resharing.old_participants.find(&self.my_id))
.map(|(p, info)| (*p, info.clone())),
ProtocolState::Resharing(resharing) => {
let new = resharing.new_participants.find(&self.my_id);
let old = resharing.old_participants.find(&self.my_id);
if let (Some((_, new_info)), Some((_, old_info))) = (new.as_ref(), old.as_ref()) {
if new_info != old_info {
tracing::warn!(
my_id = ?self.my_id,
"Participant exists in both new and old participants with different info during resharing"
);
}
}
new.or_else(|| old).map(|(p, info)| (*p, info.clone()))
}

Copilot uses AI. Check for mistakes.
}
}
Expand Down
2 changes: 1 addition & 1 deletion chain-signatures/node/src/protocol/contract/primitives.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use cait_sith::protocol::Participant;
use mpc_keys::hpke;
use near_primitives::{borsh::BorshDeserialize, types::AccountId};
use serde::{Deserialize, Serialize};
Expand All @@ -7,6 +6,7 @@ use std::{
hash::Hash,
str::FromStr,
};
use threshold_signatures::participants::Participant;

type ParticipantId = u32;

Expand Down
40 changes: 28 additions & 12 deletions chain-signatures/node/src/protocol/cryptography.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ use crate::protocol::state::{PersistentNodeData, WaitingForConsensusState};
use crate::protocol::MeshState;
use crate::types::{ReshareProtocol, SecretKeyShare};

use cait_sith::protocol::{Action, InitializationError, Participant, ProtocolError};
use k256::elliptic_curve::group::GroupEncoding;
use k256::elliptic_curve::sec1::ToEncodedPoint;
use k256::sha2::{Digest, Sha256};
use mpc_crypto::PublicKey;
use threshold_signatures::errors::{InitializationError, ProtocolError};
use threshold_signatures::participants::Participant;
use threshold_signatures::protocol::Action;
use tokio::sync::mpsc;

pub static RESHARING_RUNNING_TIMEOUT_SECS: AtomicU64 = AtomicU64::new(300);
Expand All @@ -31,10 +33,10 @@ pub fn set_resharing_running_timeout(duration: Duration) {

#[derive(thiserror::Error, Debug)]
pub enum CryptographicError {
#[error("cait-sith initialization error: {0}")]
CaitSithInitializationError(#[from] InitializationError),
#[error("cait-sith protocol error: {0}")]
CaitSithProtocolError(#[from] ProtocolError),
#[error("initialization error: {0}")]
Init(#[from] InitializationError),
#[error("protocol error: {0}")]
Protocol(#[from] ProtocolError),
}

pub(crate) trait CryptographicProtocol {
Expand Down Expand Up @@ -93,7 +95,7 @@ impl CryptographicProtocol for GeneratingState {
tracing::debug!("generating: sending a message to many participants");
for p in &participants {
if p == &self.me {
// Skip yourself, cait-sith never sends messages to oneself
// Skip yourself, threshold-signatures never sends messages to oneself
continue;
}

Expand Down Expand Up @@ -124,10 +126,19 @@ impl CryptographicProtocol for GeneratingState {
}
Action::Return(r) => {
tracing::info!(
public_key = hex::encode(r.public_key.to_bytes()),
public_key = hex::encode(
r.public_key
.to_element()
.to_affine()
.to_encoded_point(true)
.as_bytes()
),
"generating: successfully completed key generation"
);
return self.finalize(r.public_key, r.private_share, ctx).await;
// Convert frost_core::VerifyingKey -> AffinePoint for storage
return self
.finalize(r.public_key.to_element().to_affine(), r.private_share, ctx)
.await;
Comment on lines +138 to +141
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

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

[nitpick] The keygen output now returns a frost_core::VerifyingKey instead of directly returning an AffinePoint. The code converts it back to AffinePoint using .to_element().to_affine() on line 140. This round-trip conversion (AffinePoint -> VerifyingKey -> AffinePoint) could be simplified if the internal storage can use VerifyingKey directly, but this may be intentional to maintain compatibility with existing storage.

Copilot uses AI. Check for mistakes.
}
}
}
Expand Down Expand Up @@ -344,11 +355,16 @@ impl CryptographicProtocol for ResharingState {
.await;
}
}
Action::Return(private_share) => {
Action::Return(keygen_output) => {
tracing::info!("resharing: successfully completed key reshare");
resharing.last_activity = Instant::now();
match Self::try_finalize(ctx, &mut resharing, private_share, &self.contract)
.await
match Self::try_finalize(
ctx,
&mut resharing,
keygen_output.private_share,
&self.contract,
)
.await
{
Ok(next_state) => return next_state,
Err(()) => {
Expand Down
5 changes: 3 additions & 2 deletions chain-signatures/node/src/protocol/error.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use cait_sith::protocol::{InitializationError, Participant};
use mpc_primitives::SignId;
use threshold_signatures::errors::InitializationError;
use threshold_signatures::participants::Participant;

use super::{presignature::PresignatureId, triple::TripleId};

#[derive(Debug, thiserror::Error)]
pub enum GenerationError {
#[error("presignature already generated")]
AlreadyGenerated,
#[error("cait-sith initialization error: {0}")]
#[error("threshold-signatures initialization error: {0}")]
CaitSithInitializationError(#[from] InitializationError),
#[error("triple {0} is generating or missing")]
TripleGeneratingOrMissing(TripleId),
Expand Down
6 changes: 3 additions & 3 deletions chain-signatures/node/src/protocol/message/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ use crate::node_client::NodeClient;
use crate::protocol::message::filter::{MessageFilter, MAX_FILTER_SIZE};
use crate::protocol::Config;

use cait_sith::protocol::Participant;
use mpc_contract::config::ProtocolConfig;
use mpc_keys::hpke::{self, Ciphered};
use mpc_primitives::SignId;
use near_account_id::AccountId;
use near_crypto::Signature;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use threshold_signatures::participants::Participant;
use tokio::sync::{mpsc, watch};

use std::collections::{HashMap, VecDeque};
Expand Down Expand Up @@ -1051,10 +1051,10 @@ const fn cbor_name(value: &ciborium::Value) -> &'static str {
mod tests {
use std::time::Duration;

use cait_sith::protocol::Participant;
use mpc_keys::hpke::{self, Ciphered};
use mpc_primitives::SignId;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use threshold_signatures::participants::Participant;

use crate::{
config::{Config, LocalConfig, NetworkConfig, OverrideConfig},
Expand All @@ -1074,7 +1074,7 @@ mod tests {
let associated_data = b"";
let (cipher_sk, cipher_pk) = mpc_keys::hpke::generate();
let starting_message = Message::Generating(GeneratingMessage {
from: cait_sith::protocol::Participant::from(0),
from: Participant::from(0),
data: vec![],
});

Expand Down
2 changes: 1 addition & 1 deletion chain-signatures/node/src/protocol/message/sub.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use cait_sith::protocol::Participant;
use mpc_primitives::SignId;
use threshold_signatures::participants::Participant;
use tokio::sync::{mpsc, oneshot};

use crate::protocol::message::types::Round;
Expand Down
3 changes: 2 additions & 1 deletion chain-signatures/node/src/protocol/message/types.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash as _};

use cait_sith::protocol::{MessageData, Participant};
use serde::{Deserialize, Serialize};
use threshold_signatures::participants::Participant;
use threshold_signatures::protocol::MessageData;

use crate::protocol::posit::PositAction;
use crate::protocol::presignature::{FullPresignatureId, PresignatureId};
Expand Down
4 changes: 2 additions & 2 deletions chain-signatures/node/src/protocol/posit.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use cait_sith::protocol::Participant;
use serde::{Deserialize, Serialize};
use threshold_signatures::participants::Participant;

use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -405,7 +405,7 @@ impl SinglePositCounter {
#[cfg(test)]
mod tests {
use super::*;
use cait_sith::protocol::Participant;
use threshold_signatures::participants::Participant;

type Id = u64;

Expand Down
31 changes: 19 additions & 12 deletions chain-signatures/node/src/protocol/presignature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ use crate::storage::TripleStorage;
use crate::types::{PresignatureProtocol, SecretKeyShare};
use crate::util::{AffinePointExt, JoinMap};

use cait_sith::protocol::{Action, InitializationError, Participant};
use cait_sith::{KeygenOutput, PresignArguments, PresignOutput};
use chrono::Utc;
use k256::{AffinePoint, Scalar, Secp256k1};
use k256::ProjectivePoint;
use k256::{AffinePoint, Scalar};
use mpc_contract::config::ProtocolConfig;
use mpc_crypto::PublicKey;
use serde::ser::SerializeStruct;
Expand All @@ -24,14 +23,20 @@ use sha3::{Digest, Sha3_256};
use std::collections::HashSet;
use std::fmt;
use std::time::{Duration, Instant};
use threshold_signatures::ecdsa::ot_based_ecdsa::{self, PresignArguments, PresignOutput};
use threshold_signatures::ecdsa::{KeygenOutput, Secp256K1Sha256};
use threshold_signatures::errors::InitializationError;
use threshold_signatures::frost_core::{Element, VerifyingKey};
use threshold_signatures::participants::Participant;
use threshold_signatures::protocol::Action;
use tokio::sync::{mpsc, watch};
use tokio::task::JoinHandle;
use tokio::time;

use near_account_id::AccountId;

/// Unique number used to identify a specific ongoing presignature generation protocol.
/// Without `PresignatureId` it would be unclear where to route incoming cait-sith presignature
/// Without `PresignatureId` it would be unclear where to route incoming threshold-signatures presignature
/// generation messages.
pub type PresignatureId = u64;

Expand All @@ -57,7 +62,7 @@ impl FullPresignatureId {
/// A completed presignature.
pub struct Presignature {
pub id: PresignatureId,
pub output: PresignOutput<Secp256k1>,
pub output: PresignOutput,
pub participants: Vec<Participant>,
}

Expand Down Expand Up @@ -564,9 +569,15 @@ impl PresignatureSpawner {
let epoch = self.epoch;
let msg = self.msg.clone();
let my_account_id = self.my_account_id.clone();

// Convert the project AffinePoint public key into the ciphersuite
// verifying key expected by threshold-signatures.
let pk_element: Element<Secp256K1Sha256> = ProjectivePoint::from(self.public_key);
let verifying_key = VerifyingKey::<Secp256K1Sha256>::new(pk_element);

let keygen_out = KeygenOutput {
private_share: self.private_share,
public_key: self.public_key,
public_key: verifying_key,
};

let task = async move {
Expand All @@ -575,11 +586,7 @@ impl PresignatureSpawner {
};

let (pair, dropper) = triples.take();
let protocol = match cait_sith::presign(
&participants,
me,
// These paramaters appear to be to make it easier to use different indexing schemes for triples
// Introduced in this PR https://github.com/LIT-Protocol/cait-sith/pull/7
let protocol = match ot_based_ecdsa::presign::presign(
&participants,
me,
PresignArguments {
Expand Down Expand Up @@ -855,8 +862,8 @@ impl PendingTriples {

#[cfg(test)]
mod tests {
use cait_sith::{protocol::Participant, PresignOutput};
use k256::{elliptic_curve::CurveArithmetic, Secp256k1};
use threshold_signatures::{ecdsa::ot_based_ecdsa::PresignOutput, participants::Participant};

use crate::protocol::presignature::Presignature;

Expand Down
Loading
Loading