Skip to main content

miden_validator/
storage_key.rs

1use std::fmt;
2
3use golden_core::{GoldenGroup, ParticipantIndex};
4use golden_ehtdh1::wire::{from_wire_bytes, to_wire_bytes};
5use golden_ehtdh1::{
6    Ciphertext,
7    PublicKeySet,
8    SealingKey,
9    SecretShare,
10    SetupContext,
11    UnsealingShare,
12    derive_context_session_id,
13};
14use golden_halo2curves::golden_group::Secp256k1GoldenGroup;
15use rand_core_06::{CryptoRng, RngCore};
16use zeroize::Zeroizing;
17
18use crate::private_record::CONTENT_KEY_BYTES;
19use crate::{PrivateRecordError, PrivateRecordShareRequest, StoredPrivateRecord};
20
21/// Golden group used for validator storage keys.
22type StorageGroup = Secp256k1GoldenGroup;
23
24/// Identifier for one version of the validator storage key.
25#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
26pub struct StorageKeyEpoch([u8; 32]);
27
28impl StorageKeyEpoch {
29    /// Creates a storage key epoch from its canonical bytes.
30    pub const fn new(bytes: [u8; 32]) -> Self {
31        Self(bytes)
32    }
33
34    /// Returns the canonical epoch bytes.
35    pub const fn as_bytes(&self) -> &[u8; 32] {
36        &self.0
37    }
38}
39
40/// Canonical Golden values needed to restore one validator operator key.
41pub struct EncodedGoldenOperatorKey {
42    key_epoch: StorageKeyEpoch,
43    setup_context: Vec<u8>,
44    public_key_set: Vec<u8>,
45    secret_share: Zeroizing<Vec<u8>>,
46}
47
48impl fmt::Debug for EncodedGoldenOperatorKey {
49    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50        formatter
51            .debug_struct("EncodedGoldenOperatorKey")
52            .field("key_epoch", &self.key_epoch)
53            .field("setup_context_bytes", &self.setup_context.len())
54            .field("public_key_set_bytes", &self.public_key_set.len())
55            .field("secret_share", &"<redacted>")
56            .finish()
57    }
58}
59
60impl EncodedGoldenOperatorKey {
61    /// Creates a restart bundle from canonical Golden wire values.
62    pub fn new(
63        key_epoch: StorageKeyEpoch,
64        setup_context: Vec<u8>,
65        public_key_set: Vec<u8>,
66        secret_share: Vec<u8>,
67    ) -> Self {
68        Self {
69            key_epoch,
70            setup_context,
71            public_key_set,
72            secret_share: Zeroizing::new(secret_share),
73        }
74    }
75
76    /// Splits the bundle into its epoch, setup, public key set, and protected secret share.
77    pub fn into_parts(self) -> (StorageKeyEpoch, Vec<u8>, Vec<u8>, Zeroizing<Vec<u8>>) {
78        (self.key_epoch, self.setup_context, self.public_key_set, self.secret_share)
79    }
80
81    /// Decodes and validates the operator key.
82    pub fn decode(self) -> Result<GoldenOperatorKey, GoldenOperatorKeyError> {
83        let setup_context = from_wire_bytes(&self.setup_context).map_err(|source| {
84            GoldenOperatorKeyError::InvalidWireValue { field: "setup context", source }
85        })?;
86        let public_key_set = from_wire_bytes(&self.public_key_set).map_err(|source| {
87            GoldenOperatorKeyError::InvalidWireValue { field: "public key set", source }
88        })?;
89        let secret_share = from_wire_bytes(&self.secret_share).map_err(|source| {
90            GoldenOperatorKeyError::InvalidWireValue { field: "secret share", source }
91        })?;
92
93        GoldenOperatorKey::new(self.key_epoch, setup_context, public_key_set, secret_share)
94    }
95}
96
97/// Validated Golden material held by one validator operator.
98pub struct GoldenOperatorKey {
99    key_epoch: StorageKeyEpoch,
100    setup_context: SetupContext,
101    public_key_set: PublicKeySet<StorageGroup>,
102    secret_share: SecretShare<StorageGroup>,
103    sealing_key: SealingKey<StorageGroup>,
104}
105
106impl fmt::Debug for GoldenOperatorKey {
107    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108        formatter
109            .debug_struct("GoldenOperatorKey")
110            .field("key_epoch", &self.key_epoch)
111            .field("setup_context", &self.setup_context)
112            .field("public_key_set", &self.public_key_set)
113            .field("secret_share", &"<redacted>")
114            .field("sealing_key", &self.sealing_key)
115            .finish()
116    }
117}
118
119impl GoldenOperatorKey {
120    /// Creates an operator key after checking its public and private material.
121    pub fn new(
122        key_epoch: StorageKeyEpoch,
123        setup_context: SetupContext,
124        public_key_set: PublicKeySet<StorageGroup>,
125        secret_share: SecretShare<StorageGroup>,
126    ) -> Result<Self, GoldenOperatorKeyError> {
127        if setup_context.backend_id != StorageGroup::BACKEND_ID {
128            return Err(GoldenOperatorKeyError::BackendMismatch {
129                expected: StorageGroup::BACKEND_ID,
130                actual: setup_context.backend_id,
131            });
132        }
133        if setup_context.context_session_id
134            != derive_context_session_id(setup_context.decryption_session_id)
135        {
136            return Err(GoldenOperatorKeyError::ContextSessionMismatch);
137        }
138
139        let public_key_set = PublicKeySet::new(
140            public_key_set.threshold,
141            public_key_set.joint_public_key,
142            public_key_set.public_shares,
143        )
144        .map_err(GoldenOperatorKeyError::InvalidPublicKeySet)?;
145
146        if setup_context.threshold != public_key_set.threshold {
147            return Err(GoldenOperatorKeyError::ThresholdMismatch {
148                setup: setup_context.threshold,
149                public_key_set: public_key_set.threshold,
150            });
151        }
152
153        let participants =
154            public_key_set.public_shares.keys().copied().collect::<Vec<ParticipantIndex>>();
155        if setup_context.participants != participants {
156            return Err(GoldenOperatorKeyError::ParticipantSetMismatch);
157        }
158
159        let public_share = public_key_set.public_share(secret_share.participant).ok_or(
160            GoldenOperatorKeyError::UnknownLocalParticipant(secret_share.participant.get()),
161        )?;
162        if public_share.decryption != StorageGroup::mul_generator(&secret_share.decryption)
163            || public_share.context != StorageGroup::mul_generator(&secret_share.context)
164        {
165            return Err(GoldenOperatorKeyError::SecretShareMismatch);
166        }
167        if setup_context.epoch != *key_epoch.as_bytes() {
168            return Err(GoldenOperatorKeyError::EpochMismatch);
169        }
170
171        let sealing_key = SealingKey::new(public_key_set.joint_public_key)
172            .map_err(GoldenOperatorKeyError::InvalidSealingKey)?;
173
174        Ok(Self {
175            key_epoch,
176            setup_context,
177            public_key_set,
178            secret_share,
179            sealing_key,
180        })
181    }
182
183    /// Returns canonical values that can restore this operator key.
184    pub fn encode(&self) -> EncodedGoldenOperatorKey {
185        EncodedGoldenOperatorKey::new(
186            self.key_epoch,
187            to_wire_bytes(&self.setup_context),
188            to_wire_bytes(&self.public_key_set),
189            to_wire_bytes(&self.secret_share),
190        )
191    }
192
193    /// Returns the storage key epoch.
194    pub const fn key_epoch(&self) -> StorageKeyEpoch {
195        self.key_epoch
196    }
197
198    /// Returns the setup context identifier.
199    pub fn setup_context_id(&self) -> [u8; 32] {
200        self.setup_context.root()
201    }
202
203    /// Returns the public sealing key for this epoch.
204    pub const fn sealing_key(&self) -> &SealingKey<StorageGroup> {
205        &self.sealing_key
206    }
207
208    /// Returns the public key set used to verify decryption shares.
209    pub const fn public_key_set(&self) -> &PublicKeySet<StorageGroup> {
210        &self.public_key_set
211    }
212
213    /// Returns the Golden setup context.
214    pub const fn setup_context(&self) -> &SetupContext {
215        &self.setup_context
216    }
217
218    /// Returns the participant that owns this operator key.
219    pub const fn participant(&self) -> ParticipantIndex {
220        self.secret_share.participant
221    }
222
223    /// Issues a canonical decryption share for one encrypted content key and exact context.
224    pub(crate) fn issue_decryption_share<R>(
225        &self,
226        rng: &mut R,
227        ciphertext_bytes: &[u8],
228        context: &[u8],
229    ) -> Result<Vec<u8>, PrivateRecordError>
230    where
231        R: RngCore + CryptoRng,
232    {
233        let ciphertext: Ciphertext<StorageGroup> =
234            from_wire_bytes(ciphertext_bytes).map_err(PrivateRecordError::InvalidGoldenEncoding)?;
235        if ciphertext.encrypted_payload.len() != CONTENT_KEY_BYTES {
236            return Err(PrivateRecordError::InvalidEncryptedRecordKey);
237        }
238        ciphertext
239            .verify_with_associated_data(context)
240            .map_err(PrivateRecordError::InvalidGoldenEncoding)?;
241
242        let share = UnsealingShare::new(self.secret_share.clone())
243            .decrypt_share_with_associated_data(
244                rng,
245                &self.setup_context,
246                &ciphertext,
247                context,
248                context,
249            )
250            .map_err(PrivateRecordError::ShareGeneration)?;
251        Ok(to_wire_bytes(&share))
252    }
253
254    /// Checks one private-record request and returns a canonical decryption share.
255    pub fn issue_private_record_share<R>(
256        &self,
257        rng: &mut R,
258        request: &PrivateRecordShareRequest,
259        record: &StoredPrivateRecord,
260    ) -> Result<Vec<u8>, PrivateRecordError>
261    where
262        R: RngCore + CryptoRng,
263    {
264        record.validate_share_request(request, self.key_epoch, self.setup_context_id())?;
265        self.issue_decryption_share(rng, record.encrypted_record_key(), request.context())
266    }
267}
268
269/// Error raised while loading a Golden operator key.
270#[derive(Debug, thiserror::Error)]
271pub enum GoldenOperatorKeyError {
272    /// A canonical Golden value could not be decoded.
273    #[error("invalid Golden {field}")]
274    InvalidWireValue {
275        field: &'static str,
276        #[source]
277        source: golden_ehtdh1::Error,
278    },
279    /// The setup names a different group backend.
280    #[error("Golden backend mismatch: expected {expected}, got {actual}")]
281    BackendMismatch { expected: &'static str, actual: String },
282    /// The context session was not derived from the decryption session.
283    #[error("Golden context session does not match the decryption session")]
284    ContextSessionMismatch,
285    /// The public key set is not internally valid.
286    #[error("invalid Golden public key set")]
287    InvalidPublicKeySet(#[source] golden_ehtdh1::Error),
288    /// The threshold differs between public setup values.
289    #[error(
290        "Golden threshold mismatch: setup context uses {setup}, public key set uses {public_key_set}"
291    )]
292    ThresholdMismatch { setup: usize, public_key_set: usize },
293    /// The participant list differs between public setup values.
294    #[error("Golden participant set mismatch")]
295    ParticipantSetMismatch,
296    /// The local secret share has no matching public share.
297    #[error("Golden participant {0} is not in the public key set")]
298    UnknownLocalParticipant(u32),
299    /// The local secret share does not open its public points.
300    #[error("Golden secret share does not match its public share")]
301    SecretShareMismatch,
302    /// The node epoch differs from the Golden setup epoch.
303    #[error("Golden setup epoch does not match the node storage key epoch")]
304    EpochMismatch,
305    /// The joint public key cannot be used for sealing.
306    #[error("invalid Golden sealing key")]
307    InvalidSealingKey(#[source] golden_ehtdh1::Error),
308}
309
310#[cfg(test)]
311pub(crate) mod tests {
312    use std::collections::BTreeMap;
313
314    use golden_core::{GoldenScalar, SessionId};
315    use golden_ehtdh1::{PublicShare, derive_context_session_id};
316    use golden_halo2curves::golden_group::Secp256k1Scalar;
317
318    use super::*;
319
320    const EPOCH: StorageKeyEpoch = StorageKeyEpoch::new([9; 32]);
321
322    fn participant(value: u32) -> ParticipantIndex {
323        ParticipantIndex::new(value).unwrap()
324    }
325
326    fn scalar(value: u64) -> Secp256k1Scalar {
327        Secp256k1Scalar::from_u64(value).unwrap()
328    }
329
330    fn evaluate(
331        secret: Secp256k1Scalar,
332        coefficient: Secp256k1Scalar,
333        participant: ParticipantIndex,
334    ) -> Secp256k1Scalar {
335        secret.add(&coefficient.mul(&participant.to_scalar().unwrap()))
336    }
337
338    fn values_for(
339        local_participant: ParticipantIndex,
340    ) -> (SetupContext, PublicKeySet<StorageGroup>, SecretShare<StorageGroup>) {
341        let participants = [participant(1), participant(2), participant(3)];
342        let decryption_secret = scalar(11);
343        let decryption_coefficient = scalar(7);
344        let context_coefficient = scalar(13);
345        let mut public_shares = BTreeMap::new();
346        let mut local_secret_share = None;
347
348        for participant in participants {
349            let decryption = evaluate(decryption_secret, decryption_coefficient, participant);
350            let context = evaluate(scalar(0), context_coefficient, participant);
351            public_shares.insert(
352                participant,
353                PublicShare {
354                    decryption: StorageGroup::mul_generator(&decryption),
355                    context: StorageGroup::mul_generator(&context),
356                },
357            );
358            if participant == local_participant {
359                local_secret_share = Some(SecretShare { participant, decryption, context });
360            }
361        }
362
363        let decryption_session_id = SessionId([2; 32]);
364        let setup_context = SetupContext {
365            backend_id: StorageGroup::BACKEND_ID.to_owned(),
366            threshold: 2,
367            registry_root: [1; 32],
368            participants: participants.to_vec(),
369            decryption_session_id,
370            context_session_id: derive_context_session_id(decryption_session_id),
371            decryption_transcript_root: [3; 32],
372            context_transcript_root: [4; 32],
373            epoch: *EPOCH.as_bytes(),
374        };
375        let public_key_set =
376            PublicKeySet::new(2, StorageGroup::mul_generator(&decryption_secret), public_shares)
377                .unwrap();
378
379        (setup_context, public_key_set, local_secret_share.unwrap())
380    }
381
382    fn values() -> (SetupContext, PublicKeySet<StorageGroup>, SecretShare<StorageGroup>) {
383        values_for(participant(1))
384    }
385
386    pub(crate) fn operator_keys() -> Vec<GoldenOperatorKey> {
387        [participant(1), participant(2), participant(3)]
388            .into_iter()
389            .map(|participant| {
390                let (setup_context, public_key_set, secret_share) = values_for(participant);
391                GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share).unwrap()
392            })
393            .collect()
394    }
395
396    fn operator_key() -> GoldenOperatorKey {
397        operator_keys().remove(0)
398    }
399
400    /// Regenerates the committed insecure storage-key fixture under
401    /// `scripts/testdata/insecure-storage-key/`.
402    ///
403    /// The fixture holds a full two-of-three setup: one shared
404    /// `setup-context.wire` and `public-key-set.wire`, plus a *distinct*
405    /// `validator-<n>/secret-share.wire` for each participant. This lets the
406    /// docker-compose network give every validator its own share, which is
407    /// required for a real threshold recovery — mounting the same share into
408    /// all three validators makes any 2-of-3 combine collapse to a single
409    /// participant and fail.
410    ///
411    /// Ignored by default so it never runs in CI; regenerate the fixture with:
412    ///
413    /// ```text
414    /// cargo test -p miden-validator --lib storage_key::tests::write_insecure_storage_key_fixture -- --ignored
415    /// ```
416    #[test]
417    #[ignore = "writes fixture files; run explicitly to regenerate"]
418    fn write_insecure_storage_key_fixture() {
419        use std::path::Path;
420
421        let dir = Path::new(env!("CARGO_MANIFEST_DIR"))
422            .join("../../scripts/testdata/insecure-storage-key");
423        fs_err::create_dir_all(&dir).unwrap();
424
425        let (setup_context, public_key_set, _) = values_for(participant(1));
426        fs_err::write(dir.join("setup-context.wire"), to_wire_bytes(&setup_context)).unwrap();
427        fs_err::write(dir.join("public-key-set.wire"), to_wire_bytes(&public_key_set)).unwrap();
428
429        for index in [1u32, 2, 3] {
430            let (.., secret_share) = values_for(participant(index));
431            let validator_dir = dir.join(format!("validator-{index}"));
432            fs_err::create_dir_all(&validator_dir).unwrap();
433            fs_err::write(validator_dir.join("secret-share.wire"), to_wire_bytes(&secret_share))
434                .unwrap();
435        }
436    }
437
438    #[test]
439    fn restart_bundle_round_trips() {
440        let expected = operator_key();
441        let decoded = expected.encode().decode().unwrap();
442
443        assert_eq!(decoded.key_epoch(), EPOCH);
444        assert_eq!(decoded.setup_context(), expected.setup_context());
445        assert_eq!(decoded.public_key_set(), expected.public_key_set());
446        assert_eq!(decoded.participant(), expected.participant());
447        assert_eq!(decoded.sealing_key(), expected.sealing_key());
448        assert_eq!(decoded.setup_context_id(), expected.setup_context_id());
449    }
450
451    #[test]
452    fn restart_bundle_exposes_persisted_parts() {
453        let expected = operator_key();
454        let (key_epoch, setup_context, public_key_set, secret_share) =
455            expected.encode().into_parts();
456        let decoded_setup = from_wire_bytes::<SetupContext>(&setup_context).unwrap();
457        let decoded_public_key_set =
458            from_wire_bytes::<PublicKeySet<StorageGroup>>(&public_key_set).unwrap();
459
460        assert_eq!(key_epoch, EPOCH);
461        assert_eq!(&decoded_setup, expected.setup_context());
462        assert_eq!(&decoded_public_key_set, expected.public_key_set());
463        assert!(from_wire_bytes::<SecretShare<StorageGroup>>(&secret_share).is_ok());
464    }
465
466    #[test]
467    fn rejects_inconsistent_public_setup() {
468        let (mut setup_context, public_key_set, secret_share) = values();
469        setup_context.backend_id = "wrong-backend".to_owned();
470        assert!(matches!(
471            GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
472            Err(GoldenOperatorKeyError::BackendMismatch { .. })
473        ));
474
475        let (mut setup_context, public_key_set, secret_share) = values();
476        setup_context.context_session_id = SessionId([8; 32]);
477        assert!(matches!(
478            GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
479            Err(GoldenOperatorKeyError::ContextSessionMismatch)
480        ));
481
482        let (mut setup_context, public_key_set, secret_share) = values();
483        setup_context.threshold = 3;
484        assert!(matches!(
485            GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
486            Err(GoldenOperatorKeyError::ThresholdMismatch { .. })
487        ));
488
489        let (mut setup_context, public_key_set, secret_share) = values();
490        setup_context.participants.pop();
491        assert!(matches!(
492            GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
493            Err(GoldenOperatorKeyError::ParticipantSetMismatch)
494        ));
495    }
496
497    #[test]
498    fn rejects_invalid_local_secret() {
499        let (setup_context, public_key_set, mut secret_share) = values();
500        secret_share.participant = participant(4);
501        assert!(matches!(
502            GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
503            Err(GoldenOperatorKeyError::UnknownLocalParticipant(4))
504        ));
505
506        let (setup_context, public_key_set, mut secret_share) = values();
507        secret_share.decryption = secret_share.decryption.add(&scalar(1));
508        assert!(matches!(
509            GoldenOperatorKey::new(EPOCH, setup_context, public_key_set, secret_share),
510            Err(GoldenOperatorKeyError::SecretShareMismatch)
511        ));
512    }
513
514    #[test]
515    fn rejects_epoch_mismatch() {
516        let (setup_context, public_key_set, secret_share) = values();
517        assert!(matches!(
518            GoldenOperatorKey::new(
519                StorageKeyEpoch::new([10; 32]),
520                setup_context,
521                public_key_set,
522                secret_share,
523            ),
524            Err(GoldenOperatorKeyError::EpochMismatch)
525        ));
526    }
527
528    #[test]
529    fn rejects_malformed_restart_value() {
530        let mut encoded = operator_key().encode();
531        encoded.setup_context.pop();
532
533        assert!(matches!(
534            encoded.decode(),
535            Err(GoldenOperatorKeyError::InvalidWireValue { field: "setup context", .. })
536        ));
537    }
538}