use crate::providers::EcdsaTaskId; use crate::providers::eddsa::EddsaTaskId; use crate::providers::robust_ecdsa::RobustEcdsaTaskId; use crate::providers::{ckd::CKDTaskId, verify_foreign_tx::VerifyForeignTxTaskId}; use anyhow::Context; use borsh::{BorshDeserialize, BorshSerialize}; use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display}; use threshold_signatures::participants::Participant; /// A unique ID representing a resource (e.g., a triple/presignature/signature, or a channel). /// The ID shall be globally unique across all participants and across time. /// /// The ID does not need to be globally unique across different *types* of assets, /// as in, it is OK for a triple to have the same unique ID as a presignature. /// /// The uniqueness of the unique ID is based on some assumptions: /// - Participants follow the correct unique ID generation algorithm; /// specifically, they each only pick unique IDs they are allowed to pick from. /// - At least one second passes during a restart of the binary. /// /// The unique ID contains three parts: the participant ID, the timestamp, and a /// counter. The counter is used to distinguish between multiple assets generated /// by the same participant during the same second. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct UniqueId(u128); impl UniqueId { /// Only for testing. Use `generate` or `pick_new_after` instead. pub fn new(participant_id: ParticipantId, timestamp: u64, counter: u32) -> Self { let id = ((participant_id.raw() as u128) << 96) | ((timestamp as u128) << 32) | counter as u128; Self(id) } /// Generates a unique ID using the current wall time. pub fn generate(participant_id: ParticipantId) -> Self { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); Self::new(participant_id, now, 0) } pub fn participant_id(&self) -> ParticipantId { ParticipantId::from_raw((self.0 >> 96) as u32) } pub fn timestamp(&self) -> u64 { ((self.0 >> 32) & ((1u128 << 64) - 1)) as u64 } pub fn counter(&self) -> u32 { (self.0 & ((1u128 << 32) - 1)) as u32 } /// Returns the key prefix for the given participant ID. It can be used to /// perform a range query in the database for all keys for this participant. pub fn prefix_for_participant_id(participant_id: ParticipantId) -> Vec { participant_id.raw().to_be_bytes().to_vec() } /// Pick a new unique ID based on the current time, but ensuring that it is /// after the current unique ID. All unique IDs should be picked this way, /// except the very first one, which should be generated with `generate`. pub fn pick_new_after(&self) -> Self { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); if now > self.timestamp() { Self::new(self.participant_id(), now, 0) } else { Self::new(self.participant_id(), self.timestamp(), self.counter() + 1) } } /// Validates that this unique ID belongs to the given participant. /// Returns an error if the embedded participant ID does not match. pub fn validate_owned_by(&self, expected: ParticipantId) -> anyhow::Result<()> { anyhow::ensure!( self.participant_id() == expected, "UniqueId {:?} belongs to participant {}, expected {}", self, self.participant_id(), expected ); Ok(()) } /// Add the given delta to the counter, returning a new unique ID. /// This is useful for generating multiple unique IDs in a row, for batched /// generation of multiple assets at once. pub fn add_to_counter(&self, delta: u32) -> anyhow::Result { let new_counter = self .counter() .checked_add(delta) .context("Counter overflow")?; Ok(Self::new( self.participant_id(), self.timestamp(), new_counter, )) } } impl Debug for UniqueId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("UniqueId") .field(&self.participant_id()) .field(&self.timestamp()) .field(&self.counter()) .finish() } } impl BorshSerialize for UniqueId { fn serialize(&self, writer: &mut W) -> std::io::Result<()> { // We must serialize in big-endian order to ensure that the // lexicalgraphical order of the keys is the same as the numerical // order. writer.write_all(&self.0.to_be_bytes()) } } impl BorshDeserialize for UniqueId { fn deserialize_reader(reader: &mut R) -> std::io::Result { let mut bytes = [0u8; 16]; reader.read_exact(&mut bytes)?; Ok(Self(u128::from_be_bytes(bytes))) } } #[derive( Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, BorshSerialize, BorshDeserialize, Serialize, Deserialize, )] pub struct ParticipantId(u32); impl From for ParticipantId { fn from(participant: Participant) -> Self { ParticipantId(participant.into()) } } impl From for Participant { fn from(participant_id: ParticipantId) -> Self { Participant::from(participant_id.0) } } impl ParticipantId { pub fn raw(self) -> u32 { self.0 } pub fn from_raw(raw: u32) -> Self { ParticipantId(raw) } } impl From for ParticipantId { fn from(p: near_mpc_contract_interface::types::AuthenticatedParticipantId) -> Self { ParticipantId(p.0.0) } } impl From for ParticipantId { fn from(p: near_mpc_contract_interface::types::ParticipantId) -> Self { ParticipantId(p.0) } } impl Display for ParticipantId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl Debug for ParticipantId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } /// A batched list of multiple cait-sith protocol messages. pub type BatchedMessages = Vec>; #[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, BorshSerialize, BorshDeserialize, )] pub struct ChannelId(pub UniqueId); #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct MpcMessage { pub channel_id: ChannelId, pub kind: MpcMessageKind, } #[derive(Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub enum MpcMessageKind { Start(MpcStartMessage), Computation(Vec>), Abort(String), Success, } /// Redacts the raw bytes in Computation messages. /// These bytes contain serialized protocol round data (commitments, encrypted shares, proofs) /// which must not be leaked to logs. impl Debug for MpcMessageKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MpcMessageKind::Start(msg) => f.debug_tuple("Start").field(msg).finish(), MpcMessageKind::Computation(chunks) => f .debug_tuple("Computation") .field(&format_args!( "[{} chunks, {} bytes]", chunks.len(), chunks.iter().map(|c| c.len()).sum::() )) .finish(), MpcMessageKind::Abort(err) => f.debug_tuple("Abort").field(err).finish(), MpcMessageKind::Success => write!(f, "Success"), } } } #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct MpcStartMessage { pub task_id: MpcTaskId, pub participants: Vec, } #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct MpcPeerMessage { pub from: ParticipantId, pub message: MpcMessage, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize)] pub enum MpcTaskId { EcdsaTaskId(EcdsaTaskId), EddsaTaskId(EddsaTaskId), CKDTaskId(CKDTaskId), RobustEcdsaTaskId(RobustEcdsaTaskId), VerifyForeignTxTaskId(VerifyForeignTxTaskId), } #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] pub struct IndexerHeightMessage { pub height: u64, } pub struct PeerIndexerHeightMessage { pub from: ParticipantId, pub message: IndexerHeightMessage, } pub enum PeerMessage { Mpc(MpcPeerMessage), IndexerHeight(PeerIndexerHeightMessage), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Version { pub version: String, pub build: String, pub commit: String, #[serde(default)] pub rustc_version: String, } #[cfg(test)] mod tests { use super::*; #[test] fn test_validate_owned_by_accepts_matching_participant() { // given let participant = ParticipantId::from_raw(5); let id = UniqueId::new(participant, 1000, 0); let id_from_batch = id.add_to_counter(42).unwrap(); // when/then id.validate_owned_by(participant).unwrap(); id_from_batch.validate_owned_by(participant).unwrap(); } #[test] fn test_validate_owned_by_rejects_mismatched_participant() { // given let owner = ParticipantId::from_raw(5); let other = ParticipantId::from_raw(99); let id = UniqueId::new(owner, 1000, 0); // when let err = id .validate_owned_by(other) .expect_err("Should reject mismatched participant"); // then assert!( err.to_string().contains("expected 99"), "Unexpected error message: {}", err ); } #[test] #[expect(non_snake_case)] fn mpc_message_kind_debug__should_redact_computation_payload() { // given let secret_data = b"SECRET_SHARE_DATA_THAT_MUST_NOT_LEAK".to_vec(); let kind = MpcMessageKind::Computation(vec![secret_data]); // when let debug_output = format!("{:?}", kind); // then assert!( !debug_output.contains("SECRET_SHARE_DATA"), "Debug output must not contain raw computation bytes, got: {}", debug_output ); assert!( debug_output.contains("Computation"), "Debug output should identify the message kind, got: {}", debug_output ); assert!( debug_output.contains("1 chunks"), "Debug output should show chunk count, got: {}", debug_output ); } #[test] #[expect(non_snake_case)] fn mpc_message_kind_debug__should_show_chunk_count_and_total_bytes() { // given let kind = MpcMessageKind::Computation(vec![vec![0u8; 100], vec![0u8; 200], vec![0u8; 50]]); // when let debug_output = format!("{:?}", kind); // then assert!( debug_output.contains("3 chunks"), "Debug output should show 3 chunks, got: {}", debug_output ); assert!( debug_output.contains("350 bytes"), "Debug output should show 350 total bytes, got: {}", debug_output ); } #[test] #[expect(non_snake_case)] fn mpc_message_kind_debug__should_show_non_sensitive_variants_normally() { // given let start = MpcMessageKind::Start(MpcStartMessage { task_id: MpcTaskId::EcdsaTaskId(EcdsaTaskId::ManyTriples { start: UniqueId::new(ParticipantId::from_raw(0), 42, 0), count: 1, }), participants: vec![ParticipantId::from_raw(0)], }); let abort = MpcMessageKind::Abort("some error".into()); let success = MpcMessageKind::Success; // when let start_debug = format!("{:?}", start); let abort_debug = format!("{:?}", abort); let success_debug = format!("{:?}", success); // then assert!(start_debug.contains("Start"), "got: {}", start_debug); assert!( abort_debug.contains("some error"), "Abort debug should show the error string, got: {}", abort_debug ); assert_eq!(success_debug, "Success"); } #[test] #[expect(non_snake_case)] fn mpc_peer_message_debug__should_redact_computation_payload() { // given let secret_data = b"PRIVATE_KEY_SHARE_MATERIAL".to_vec(); let message = MpcPeerMessage { from: ParticipantId::from_raw(1), message: MpcMessage { channel_id: ChannelId(UniqueId::new(ParticipantId::from_raw(0), 99, 0)), kind: MpcMessageKind::Computation(vec![secret_data]), }, }; // when let debug_output = format!("{:?}", message); // then assert!( !debug_output.contains("PRIVATE_KEY_SHARE"), "MpcPeerMessage debug must not leak computation bytes, got: {}", debug_output ); assert!( debug_output.contains("1 chunks"), "Should show chunk metadata, got: {}", debug_output ); } }