Skip to main content

miden_validator/
storage_key.rs

1use std::fmt;
2use std::io::Cursor;
3
4use golden_core::{GoldenGroup, ParticipantIndex};
5use golden_ehtdh1::wire::{from_wire_bytes, to_wire_bytes};
6use golden_ehtdh1::{
7    Ciphertext,
8    PublicKeySet,
9    SealingKey,
10    SecretShare,
11    SetupContext,
12    UnsealingShare,
13    derive_context_session_id,
14};
15use golden_halo2curves::golden_group::Secp256k1GoldenGroup;
16use miden_protocol::utils::serde::{
17    ByteReader,
18    ByteWriter,
19    Deserializable,
20    DeserializationError,
21    Serializable,
22};
23use rand_core_06::{CryptoRng, RngCore};
24use zeroize::Zeroizing;
25
26use crate::private_record::CONTENT_KEY_BYTES;
27use crate::{PrivateRecordError, PrivateRecordShareRequest, StoredPrivateRecord};
28
29/// Golden group used for validator storage keys.
30type StorageGroup = Secp256k1GoldenGroup;
31
32/// Identifier for one version of the validator storage key.
33#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
34pub struct StorageKeyEpoch([u8; 32]);
35
36impl StorageKeyEpoch {
37    /// Creates a storage key epoch from its canonical bytes.
38    pub const fn new(bytes: [u8; 32]) -> Self {
39        Self(bytes)
40    }
41
42    /// Parses exactly 32 epoch bytes from hexadecimal text.
43    pub fn from_hex(encoded: impl AsRef<[u8]>) -> Result<Self, hex::FromHexError> {
44        let mut bytes = [0; 32];
45        hex::decode_to_slice(encoded, &mut bytes)?;
46        Ok(Self(bytes))
47    }
48
49    /// Returns the canonical epoch bytes.
50    pub const fn as_bytes(&self) -> &[u8; 32] {
51        &self.0
52    }
53}
54
55impl Serializable for StorageKeyEpoch {
56    fn write_into<W: ByteWriter>(&self, target: &mut W) {
57        target.write_bytes(&self.0);
58    }
59}
60
61impl Deserializable for StorageKeyEpoch {
62    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
63        Ok(Self(source.read_array()?))
64    }
65}
66
67/// Encoded epoch, public setup, public key set, and private share for one validator.
68///
69/// Encoded values can contain inconsistent key material. [`Self::decode`] checks their consistency
70/// before constructing an operator key.
71pub struct EncodedGoldenOperatorKey {
72    key_epoch: StorageKeyEpoch,
73    setup_context: Vec<u8>,
74    public_key_set: Vec<u8>,
75    secret_share: Zeroizing<Vec<u8>>,
76}
77
78impl fmt::Debug for EncodedGoldenOperatorKey {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        formatter
81            .debug_struct("EncodedGoldenOperatorKey")
82            .field("key_epoch", &self.key_epoch)
83            .field("setup_context_bytes", &self.setup_context.len())
84            .field("public_key_set_bytes", &self.public_key_set.len())
85            .field("secret_share", &"<redacted>")
86            .finish()
87    }
88}
89
90impl EncodedGoldenOperatorKey {
91    const BUNDLE_HEADER: &[u8] = b"miden-validator-storage-key\x01";
92
93    /// Encodes the version header and epoch, followed by the setup context, public key set, and
94    /// secret share. Each Golden wire value has a little-endian `u32` byte-length prefix.
95    ///
96    /// The output contains the private share, so its buffer is cleared on drop.
97    pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
98        let mut bytes = Zeroizing::new(Vec::new());
99        bytes.extend_from_slice(Self::BUNDLE_HEADER);
100        bytes.extend_from_slice(self.key_epoch.as_bytes());
101        for value in [&self.setup_context, &self.public_key_set, &*self.secret_share] {
102            let length = u32::try_from(value.len()).expect("storage key field must fit in u32");
103            bytes.write_u32(length);
104            bytes.extend_from_slice(value);
105        }
106        bytes
107    }
108
109    /// Parses one complete versioned bundle and rejects unknown formats, truncated values, and
110    /// trailing bytes.
111    ///
112    /// Valid framing does not guarantee consistent key material. Call [`Self::decode`] before
113    /// using the bundle as an operator key.
114    pub fn from_bytes(bytes: &[u8]) -> Result<Self, DeserializationError> {
115        let mut source = Cursor::new(bytes);
116        if source.read_slice(Self::BUNDLE_HEADER.len())? != Self::BUNDLE_HEADER {
117            return Err(DeserializationError::InvalidValue(
118                "unsupported storage key bundle format".into(),
119            ));
120        }
121        let key_epoch = StorageKeyEpoch::read_from(&mut source)?;
122        let mut values = [Vec::new(), Vec::new(), Vec::new()];
123        for value in &mut values {
124            let length = source.read_u32()? as usize;
125            *value = source.read_slice(length)?.to_vec();
126        }
127        let [setup_context, public_key_set, secret_share] = values;
128        let bundle = Self::new(key_epoch, setup_context, public_key_set, secret_share);
129        if source.has_more_bytes() {
130            return Err(DeserializationError::InvalidValue(
131                "storage key bundle contains trailing bytes".into(),
132            ));
133        }
134        Ok(bundle)
135    }
136
137    /// Collects the epoch and encoded Golden values into a storage-key bundle.
138    pub fn new(
139        key_epoch: StorageKeyEpoch,
140        setup_context: Vec<u8>,
141        public_key_set: Vec<u8>,
142        secret_share: Vec<u8>,
143    ) -> Self {
144        Self {
145            key_epoch,
146            setup_context,
147            public_key_set,
148            secret_share: Zeroizing::new(secret_share),
149        }
150    }
151
152    /// Splits the bundle into its epoch, setup, public key set, and protected secret share.
153    pub fn into_parts(self) -> (StorageKeyEpoch, Vec<u8>, Vec<u8>, Zeroizing<Vec<u8>>) {
154        (self.key_epoch, self.setup_context, self.public_key_set, self.secret_share)
155    }
156
157    /// Decodes and validates the operator key.
158    pub fn decode(self) -> Result<GoldenOperatorKey, GoldenOperatorKeyError> {
159        let setup_context = from_wire_bytes(&self.setup_context).map_err(|source| {
160            GoldenOperatorKeyError::InvalidWireValue { field: "setup context", source }
161        })?;
162        let public_key_set = from_wire_bytes(&self.public_key_set).map_err(|source| {
163            GoldenOperatorKeyError::InvalidWireValue { field: "public key set", source }
164        })?;
165        let secret_share = from_wire_bytes(&self.secret_share).map_err(|source| {
166            GoldenOperatorKeyError::InvalidWireValue { field: "secret share", source }
167        })?;
168
169        GoldenOperatorKey::new(self.key_epoch, setup_context, public_key_set, secret_share)
170    }
171}
172
173/// Validated Golden material held by one validator operator.
174pub struct GoldenOperatorKey {
175    key_epoch: StorageKeyEpoch,
176    setup_context: SetupContext,
177    public_key_set: PublicKeySet<StorageGroup>,
178    secret_share: SecretShare<StorageGroup>,
179    sealing_key: SealingKey<StorageGroup>,
180}
181
182impl fmt::Debug for GoldenOperatorKey {
183    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
184        formatter
185            .debug_struct("GoldenOperatorKey")
186            .field("key_epoch", &self.key_epoch)
187            .field("setup_context", &self.setup_context)
188            .field("public_key_set", &self.public_key_set)
189            .field("secret_share", &"<redacted>")
190            .field("sealing_key", &self.sealing_key)
191            .finish()
192    }
193}
194
195impl GoldenOperatorKey {
196    /// Creates an operator key after checking its public and private material.
197    pub fn new(
198        key_epoch: StorageKeyEpoch,
199        setup_context: SetupContext,
200        public_key_set: PublicKeySet<StorageGroup>,
201        secret_share: SecretShare<StorageGroup>,
202    ) -> Result<Self, GoldenOperatorKeyError> {
203        if setup_context.backend_id != StorageGroup::BACKEND_ID {
204            return Err(GoldenOperatorKeyError::BackendMismatch {
205                expected: StorageGroup::BACKEND_ID,
206                actual: setup_context.backend_id,
207            });
208        }
209        if setup_context.context_session_id
210            != derive_context_session_id(setup_context.decryption_session_id)
211        {
212            return Err(GoldenOperatorKeyError::ContextSessionMismatch);
213        }
214
215        let public_key_set = PublicKeySet::new(
216            public_key_set.threshold,
217            public_key_set.joint_public_key,
218            public_key_set.public_shares,
219        )
220        .map_err(GoldenOperatorKeyError::InvalidPublicKeySet)?;
221
222        if setup_context.threshold != public_key_set.threshold {
223            return Err(GoldenOperatorKeyError::ThresholdMismatch {
224                setup: setup_context.threshold,
225                public_key_set: public_key_set.threshold,
226            });
227        }
228
229        let participants =
230            public_key_set.public_shares.keys().copied().collect::<Vec<ParticipantIndex>>();
231        if setup_context.participants != participants {
232            return Err(GoldenOperatorKeyError::ParticipantSetMismatch);
233        }
234
235        let public_share = public_key_set.public_share(secret_share.participant).ok_or(
236            GoldenOperatorKeyError::UnknownLocalParticipant(secret_share.participant.get()),
237        )?;
238        if public_share.decryption != StorageGroup::mul_generator(&secret_share.decryption)
239            || public_share.context != StorageGroup::mul_generator(&secret_share.context)
240        {
241            return Err(GoldenOperatorKeyError::SecretShareMismatch);
242        }
243        if setup_context.epoch != *key_epoch.as_bytes() {
244            return Err(GoldenOperatorKeyError::EpochMismatch);
245        }
246
247        let sealing_key = SealingKey::new(public_key_set.joint_public_key)
248            .map_err(GoldenOperatorKeyError::InvalidSealingKey)?;
249
250        Ok(Self {
251            key_epoch,
252            setup_context,
253            public_key_set,
254            secret_share,
255            sealing_key,
256        })
257    }
258
259    /// Returns canonical values that can restore this operator key.
260    pub fn encode(&self) -> EncodedGoldenOperatorKey {
261        EncodedGoldenOperatorKey::new(
262            self.key_epoch,
263            to_wire_bytes(&self.setup_context),
264            to_wire_bytes(&self.public_key_set),
265            to_wire_bytes(&self.secret_share),
266        )
267    }
268
269    /// Returns the storage key epoch.
270    pub const fn key_epoch(&self) -> StorageKeyEpoch {
271        self.key_epoch
272    }
273
274    /// Returns the setup context identifier.
275    pub fn setup_context_id(&self) -> [u8; 32] {
276        self.setup_context.root()
277    }
278
279    /// Returns the public sealing key for this epoch.
280    pub const fn sealing_key(&self) -> &SealingKey<StorageGroup> {
281        &self.sealing_key
282    }
283
284    /// Returns the public key set used to verify decryption shares.
285    pub const fn public_key_set(&self) -> &PublicKeySet<StorageGroup> {
286        &self.public_key_set
287    }
288
289    /// Returns the Golden setup context.
290    pub const fn setup_context(&self) -> &SetupContext {
291        &self.setup_context
292    }
293
294    /// Returns the participant that owns this operator key.
295    pub const fn participant(&self) -> ParticipantIndex {
296        self.secret_share.participant
297    }
298
299    /// Issues a canonical decryption share for one encrypted content key and exact context.
300    pub(crate) fn issue_decryption_share<R>(
301        &self,
302        rng: &mut R,
303        ciphertext_bytes: &[u8],
304        context: &[u8],
305    ) -> Result<Vec<u8>, PrivateRecordError>
306    where
307        R: RngCore + CryptoRng,
308    {
309        let ciphertext: Ciphertext<StorageGroup> =
310            from_wire_bytes(ciphertext_bytes).map_err(PrivateRecordError::InvalidGoldenEncoding)?;
311        if ciphertext.encrypted_payload.len() != CONTENT_KEY_BYTES {
312            return Err(PrivateRecordError::InvalidEncryptedRecordKey);
313        }
314        ciphertext
315            .verify_with_associated_data(context)
316            .map_err(PrivateRecordError::InvalidGoldenEncoding)?;
317
318        let share = UnsealingShare::new(self.secret_share.clone())
319            .decrypt_share_with_associated_data(
320                rng,
321                &self.setup_context,
322                &ciphertext,
323                context,
324                context,
325            )
326            .map_err(PrivateRecordError::ShareGeneration)?;
327        Ok(to_wire_bytes(&share))
328    }
329
330    /// Checks one private-record request and returns a canonical decryption share.
331    pub fn issue_private_record_share<R>(
332        &self,
333        rng: &mut R,
334        request: &PrivateRecordShareRequest,
335        record: &StoredPrivateRecord,
336    ) -> Result<Vec<u8>, PrivateRecordError>
337    where
338        R: RngCore + CryptoRng,
339    {
340        record.validate_share_request(request, self.key_epoch, self.setup_context_id())?;
341        self.issue_decryption_share(rng, record.encrypted_record_key(), request.context())
342    }
343}
344
345/// Error raised while loading a Golden operator key.
346#[derive(Debug, thiserror::Error)]
347pub enum GoldenOperatorKeyError {
348    /// A canonical Golden value could not be decoded.
349    #[error("invalid Golden {field}")]
350    InvalidWireValue {
351        field: &'static str,
352        #[source]
353        source: golden_ehtdh1::Error,
354    },
355    /// The setup names a different group backend.
356    #[error("Golden backend mismatch: expected {expected}, got {actual}")]
357    BackendMismatch { expected: &'static str, actual: String },
358    /// The context session was not derived from the decryption session.
359    #[error("Golden context session does not match the decryption session")]
360    ContextSessionMismatch,
361    /// The public key set is not internally valid.
362    #[error("invalid Golden public key set")]
363    InvalidPublicKeySet(#[source] golden_ehtdh1::Error),
364    /// The threshold differs between public setup values.
365    #[error(
366        "Golden threshold mismatch: setup context uses {setup}, public key set uses {public_key_set}"
367    )]
368    ThresholdMismatch { setup: usize, public_key_set: usize },
369    /// The participant list differs between public setup values.
370    #[error("Golden participant set mismatch")]
371    ParticipantSetMismatch,
372    /// The local secret share has no matching public share.
373    #[error("Golden participant {0} is not in the public key set")]
374    UnknownLocalParticipant(u32),
375    /// The local secret share does not open its public points.
376    #[error("Golden secret share does not match its public share")]
377    SecretShareMismatch,
378    /// The node epoch differs from the Golden setup epoch.
379    #[error("Golden setup epoch does not match the node storage key epoch")]
380    EpochMismatch,
381    /// The joint public key cannot be used for sealing.
382    #[error("invalid Golden sealing key")]
383    InvalidSealingKey(#[source] golden_ehtdh1::Error),
384}
385
386#[cfg(test)]
387pub(crate) mod tests {
388    use std::collections::BTreeMap;
389
390    use golden_core::{GoldenScalar, SessionId};
391    use golden_ehtdh1::{PublicShare, derive_context_session_id};
392    use golden_halo2curves::golden_group::Secp256k1Scalar;
393
394    use super::*;
395
396    const EPOCH: StorageKeyEpoch = StorageKeyEpoch::new([9; 32]);
397
398    fn participant(value: u32) -> ParticipantIndex {
399        ParticipantIndex::new(value).unwrap()
400    }
401
402    fn scalar(value: u64) -> Secp256k1Scalar {
403        Secp256k1Scalar::from_u64(value).unwrap()
404    }
405
406    fn evaluate(
407        secret: Secp256k1Scalar,
408        coefficient: Secp256k1Scalar,
409        participant: ParticipantIndex,
410    ) -> Secp256k1Scalar {
411        secret.add(&coefficient.mul(&participant.to_scalar().unwrap()))
412    }
413
414    fn values_for(
415        local_participant: ParticipantIndex,
416    ) -> (SetupContext, PublicKeySet<StorageGroup>, SecretShare<StorageGroup>) {
417        let participants = [participant(1), participant(2), participant(3)];
418        let decryption_secret = scalar(11);
419        let decryption_coefficient = scalar(7);
420        let context_coefficient = scalar(13);
421        let mut public_shares = BTreeMap::new();
422        let mut local_secret_share = None;
423
424        for participant in participants {
425            let decryption = evaluate(decryption_secret, decryption_coefficient, participant);
426            let context = evaluate(scalar(0), context_coefficient, participant);
427            public_shares.insert(
428                participant,
429                PublicShare {
430                    decryption: StorageGroup::mul_generator(&decryption),
431                    context: StorageGroup::mul_generator(&context),
432                },
433            );
434            if participant == local_participant {
435                local_secret_share = Some(SecretShare { participant, decryption, context });
436            }
437        }
438
439        let decryption_session_id = SessionId([2; 32]);
440        let setup_context = SetupContext {
441            backend_id: StorageGroup::BACKEND_ID.to_owned(),
442            threshold: 2,
443            registry_root: [1; 32],
444            participants: participants.to_vec(),
445            decryption_session_id,
446            context_session_id: derive_context_session_id(decryption_session_id),
447            decryption_transcript_root: [3; 32],
448            context_transcript_root: [4; 32],
449            epoch: *EPOCH.as_bytes(),
450        };
451        let public_key_set =
452            PublicKeySet::new(2, StorageGroup::mul_generator(&decryption_secret), public_shares)
453                .unwrap();
454
455        (setup_context, public_key_set, local_secret_share.unwrap())
456    }
457
458    fn values() -> (SetupContext, PublicKeySet<StorageGroup>, SecretShare<StorageGroup>) {
459        values_for(participant(1))
460    }
461
462    pub(crate) fn operator_keys() -> Vec<GoldenOperatorKey> {
463        [participant(1), participant(2), participant(3)]
464            .into_iter()
465            .map(|participant| {
466                let (setup_context, public_key_set, secret_share) = values_for(participant);
467                GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share).unwrap()
468            })
469            .collect()
470    }
471
472    fn operator_key() -> GoldenOperatorKey {
473        operator_keys().remove(0)
474    }
475
476    /// Writes the deterministic, insecure two-of-three fixture to
477    /// `scripts/testdata/insecure-storage-key/`. The output contains one complete bundle per
478    /// participant.
479    ///
480    /// Threshold recovery requires distinct participant shares. This test stays ignored to avoid
481    /// rewriting committed files during normal test runs.
482    #[test]
483    #[ignore = "writes fixture files; run explicitly to regenerate"]
484    fn write_insecure_storage_key_fixture() {
485        use std::path::Path;
486
487        let dir = Path::new(env!("CARGO_MANIFEST_DIR"))
488            .join("../../scripts/testdata/insecure-storage-key");
489        fs_err::create_dir_all(&dir).unwrap();
490
491        for index in [1u32, 2, 3] {
492            let (setup_context, public_key_set, secret_share) = values_for(participant(index));
493            let validator_dir = dir.join(format!("validator-{index}"));
494            fs_err::create_dir_all(&validator_dir).unwrap();
495            let operator_key =
496                GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share).unwrap();
497            fs_err::write(
498                validator_dir.join("storage-key.bundle"),
499                operator_key.encode().to_bytes(),
500            )
501            .unwrap();
502        }
503    }
504
505    #[test]
506    fn restart_bundle_round_trips() {
507        let expected = operator_key();
508        let bytes = expected.encode().to_bytes();
509        let decoded = EncodedGoldenOperatorKey::from_bytes(&bytes).unwrap().decode().unwrap();
510
511        assert_eq!(decoded.key_epoch(), EPOCH);
512        assert_eq!(decoded.setup_context(), expected.setup_context());
513        assert_eq!(decoded.public_key_set(), expected.public_key_set());
514        assert_eq!(decoded.participant(), expected.participant());
515        assert_eq!(decoded.sealing_key(), expected.sealing_key());
516        assert_eq!(decoded.setup_context_id(), expected.setup_context_id());
517        assert_eq!(*decoded.encode().to_bytes(), *bytes);
518    }
519
520    #[test]
521    fn bundle_rejects_unknown_format() {
522        let mut bytes = operator_key().encode().to_bytes();
523        bytes[EncodedGoldenOperatorKey::BUNDLE_HEADER.len() - 1] = 2;
524        assert!(matches!(
525            EncodedGoldenOperatorKey::from_bytes(&bytes),
526            Err(DeserializationError::InvalidValue(_)),
527        ));
528    }
529
530    #[test]
531    fn bundle_rejects_truncation_and_trailing_bytes() {
532        let mut bytes = operator_key().encode().to_bytes();
533        for length in 0..bytes.len() {
534            assert!(EncodedGoldenOperatorKey::from_bytes(&bytes[..length]).is_err());
535        }
536        bytes.push(0);
537        assert!(EncodedGoldenOperatorKey::from_bytes(&bytes).is_err());
538    }
539
540    #[test]
541    fn bundle_rejects_field_length_exceeding_input() {
542        let mut bytes = operator_key().encode().to_bytes();
543        let offset = EncodedGoldenOperatorKey::BUNDLE_HEADER.len() + 32;
544        bytes[offset..offset + 4].copy_from_slice(&u32::MAX.to_le_bytes());
545        assert!(matches!(
546            EncodedGoldenOperatorKey::from_bytes(&bytes),
547            Err(DeserializationError::UnexpectedEOF),
548        ));
549    }
550
551    #[test]
552    fn restart_bundle_exposes_persisted_parts() {
553        let expected = operator_key();
554        let (key_epoch, setup_context, public_key_set, secret_share) =
555            expected.encode().into_parts();
556        let decoded_setup = from_wire_bytes::<SetupContext>(&setup_context).unwrap();
557        let decoded_public_key_set =
558            from_wire_bytes::<PublicKeySet<StorageGroup>>(&public_key_set).unwrap();
559
560        assert_eq!(key_epoch, EPOCH);
561        assert_eq!(&decoded_setup, expected.setup_context());
562        assert_eq!(&decoded_public_key_set, expected.public_key_set());
563        assert!(from_wire_bytes::<SecretShare<StorageGroup>>(&secret_share).is_ok());
564    }
565
566    #[test]
567    fn rejects_inconsistent_public_setup() {
568        let (mut setup_context, public_key_set, secret_share) = values();
569        setup_context.backend_id = "wrong-backend".to_owned();
570        assert!(matches!(
571            GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
572            Err(GoldenOperatorKeyError::BackendMismatch { .. })
573        ));
574
575        let (mut setup_context, public_key_set, secret_share) = values();
576        setup_context.context_session_id = SessionId([8; 32]);
577        assert!(matches!(
578            GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
579            Err(GoldenOperatorKeyError::ContextSessionMismatch)
580        ));
581
582        let (mut setup_context, public_key_set, secret_share) = values();
583        setup_context.threshold = 3;
584        assert!(matches!(
585            GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
586            Err(GoldenOperatorKeyError::ThresholdMismatch { .. })
587        ));
588
589        let (mut setup_context, public_key_set, secret_share) = values();
590        setup_context.participants.pop();
591        assert!(matches!(
592            GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
593            Err(GoldenOperatorKeyError::ParticipantSetMismatch)
594        ));
595    }
596
597    #[test]
598    fn rejects_invalid_local_secret() {
599        let (setup_context, public_key_set, mut secret_share) = values();
600        secret_share.participant = participant(4);
601        assert!(matches!(
602            GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
603            Err(GoldenOperatorKeyError::UnknownLocalParticipant(4))
604        ));
605
606        let (setup_context, public_key_set, mut secret_share) = values();
607        secret_share.decryption = secret_share.decryption.add(&scalar(1));
608        assert!(matches!(
609            GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
610            Err(GoldenOperatorKeyError::SecretShareMismatch)
611        ));
612    }
613
614    #[test]
615    fn rejects_epoch_mismatch() {
616        let (setup_context, public_key_set, secret_share) = values();
617        assert!(matches!(
618            GoldenOperatorKey::new(
619                StorageKeyEpoch::new([10; 32]),
620                setup_context,
621                public_key_set,
622                secret_share,
623            ),
624            Err(GoldenOperatorKeyError::EpochMismatch)
625        ));
626    }
627
628    #[test]
629    fn rejects_malformed_restart_value() {
630        let mut encoded = operator_key().encode();
631        encoded.setup_context.pop();
632
633        assert!(matches!(
634            encoded.decode(),
635            Err(GoldenOperatorKeyError::InvalidWireValue { field: "setup context", .. })
636        ));
637    }
638}