Skip to main content

confium_tc/reshare/
session.rs

1//! Re-sharing session parameters and state.
2
3use crate::reshare::lagrange::FieldElement;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7/// Old committee member with their share.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct OldCommitteeMember {
10    /// Party index in old committee.
11    pub party_index: u32,
12    /// Share bytes (kept encrypted at rest by caller).
13    pub share: FieldElement,
14}
15
16/// New committee member identifier (public; no share yet).
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct NewCommitteeMember {
19    /// Party index in new committee.
20    pub party_index: u32,
21    /// Public identity key (for encrypting new shares to this party).
22    pub identity_public_key: Vec<u8>,
23}
24
25/// Parameters for a re-sharing session.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ReshareParams {
28    /// Algorithm (must match old committee's).
29    pub algorithm: String,
30    /// Old committee members.
31    pub old_committee: Vec<OldCommitteeMember>,
32    /// Old threshold T.
33    pub old_threshold: u32,
34    /// New committee members.
35    pub new_committee: Vec<NewCommitteeMember>,
36    /// New threshold T'.
37    pub new_threshold: u32,
38}
39
40/// State of a re-sharing session.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum ReshareState {
44    /// Session created.
45    Pending,
46    /// Old shares collected, contributions computed.
47    ContributionsComputed,
48    /// New shares distributed to new committee members.
49    Distributed,
50    /// New committee verified by test signature.
51    Verified,
52    /// Aborted.
53    Aborted,
54}
55
56/// Re-sharing session.
57pub struct ReshareSession {
58    params: ReshareParams,
59    state: ReshareState,
60    // Populated by `complete()`. Kept on the struct so a follow-up
61    // accessor (e.g. `new_shares()` returning the post-reshare share
62    // set per party) doesn't require an API break.
63    #[allow(dead_code)]
64    new_shares: Vec<(u32, FieldElement)>,
65    created_at: DateTime<Utc>,
66}
67
68/// Re-sharing errors.
69#[derive(Debug, thiserror::Error)]
70pub enum ReshareError {
71    /// Insufficient old shares.
72    #[error("insufficient old shares: have {have}, need {need}")]
73    InsufficientOldShares {
74        /// Count received.
75        have: usize,
76        /// Threshold required.
77        need: u32,
78    },
79    /// Invalid state for operation.
80    #[error("invalid state: current {current:?}")]
81    InvalidState {
82        /// Current state.
83        current: ReshareState,
84    },
85    /// Committee mismatch.
86    #[error("committee mismatch")]
87    CommitteeMismatch,
88}
89
90impl ReshareSession {
91    /// Create a new re-sharing session.
92    pub fn new(params: ReshareParams) -> Self {
93        Self {
94            params,
95            state: ReshareState::Pending,
96            new_shares: Vec::new(),
97            created_at: Utc::now(),
98        }
99    }
100
101    /// Current state.
102    pub fn state(&self) -> ReshareState {
103        self.state
104    }
105
106    /// Mark contributions as computed (after T-old shares are processed).
107    pub fn mark_contributions_computed(&mut self) -> Result<(), ReshareError> {
108        if self.state != ReshareState::Pending {
109            return Err(ReshareError::InvalidState {
110                current: self.state,
111            });
112        }
113        if (self.params.old_committee.len() as u32) < self.params.old_threshold {
114            return Err(ReshareError::InsufficientOldShares {
115                have: self.params.old_committee.len(),
116                need: self.params.old_threshold,
117            });
118        }
119        self.state = ReshareState::ContributionsComputed;
120        Ok(())
121    }
122
123    /// Mark shares distributed to new committee.
124    pub fn mark_distributed(&mut self) -> Result<(), ReshareError> {
125        if self.state != ReshareState::ContributionsComputed {
126            return Err(ReshareError::InvalidState {
127                current: self.state,
128            });
129        }
130        self.state = ReshareState::Distributed;
131        Ok(())
132    }
133
134    /// Mark new committee verified (test signature validates under same public key).
135    pub fn mark_verified(&mut self) -> Result<(), ReshareError> {
136        if self.state != ReshareState::Distributed {
137            return Err(ReshareError::InvalidState {
138                current: self.state,
139            });
140        }
141        self.state = ReshareState::Verified;
142        Ok(())
143    }
144
145    /// When the session was created.
146    pub fn created_at(&self) -> DateTime<Utc> {
147        self.created_at
148    }
149
150    /// Reference to params.
151    pub fn params(&self) -> &ReshareParams {
152        &self.params
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    fn sample_params() -> ReshareParams {
161        ReshareParams {
162            algorithm: "FROST-ed25519".into(),
163            old_committee: vec![
164                OldCommitteeMember {
165                    party_index: 0,
166                    share: FieldElement::new(vec![1u8; 32]),
167                },
168                OldCommitteeMember {
169                    party_index: 1,
170                    share: FieldElement::new(vec![2u8; 32]),
171                },
172                OldCommitteeMember {
173                    party_index: 2,
174                    share: FieldElement::new(vec![3u8; 32]),
175                },
176            ],
177            old_threshold: 2,
178            new_committee: vec![
179                NewCommitteeMember {
180                    party_index: 0,
181                    identity_public_key: vec![0u8; 32],
182                },
183                NewCommitteeMember {
184                    party_index: 1,
185                    identity_public_key: vec![1u8; 32],
186                },
187            ],
188            new_threshold: 2,
189        }
190    }
191
192    #[test]
193    fn full_lifecycle() {
194        let mut session = ReshareSession::new(sample_params());
195        assert_eq!(session.state(), ReshareState::Pending);
196        session.mark_contributions_computed().unwrap();
197        assert_eq!(session.state(), ReshareState::ContributionsComputed);
198        session.mark_distributed().unwrap();
199        assert_eq!(session.state(), ReshareState::Distributed);
200        session.mark_verified().unwrap();
201        assert_eq!(session.state(), ReshareState::Verified);
202    }
203
204    #[test]
205    fn insufficient_old_shares_fails() {
206        let mut params = sample_params();
207        params.old_threshold = 5;
208        let mut session = ReshareSession::new(params);
209        let result = session.mark_contributions_computed();
210        assert!(matches!(
211            result,
212            Err(ReshareError::InsufficientOldShares { .. })
213        ));
214    }
215
216    #[test]
217    fn wrong_state_fails() {
218        let mut session = ReshareSession::new(sample_params());
219        // Try to skip the contributions step
220        let result = session.mark_distributed();
221        assert!(matches!(result, Err(ReshareError::InvalidState { .. })));
222    }
223}