Skip to main content

mithril_stm/protocol/key_registration/
closed_registration_entry.rs

1use serde::{Deserialize, Serialize, Serializer, ser::SerializeTuple};
2use std::cmp::Ordering;
3use std::hash::Hash;
4
5use crate::{
6    PhiFValue, RegisterError, RegistrationEntry, Stake, StmResult, VerificationKeyForConcatenation,
7    codec,
8};
9
10#[cfg(feature = "future_snark")]
11use crate::{
12    LotteryTargetValue, VerificationKeyForSnark,
13    proof_system::compute_target_value_for_snark_lottery,
14};
15
16/// CBOR-friendly envelope for `ClosedRegistrationEntry` serialization.
17///
18/// Used as an intermediate representation because `ClosedRegistrationEntry`
19/// has a custom tuple-based `Serialize` implementation that is incompatible
20/// with ciborium's derived `Deserialize` (which expects map format).
21#[derive(Serialize, Deserialize)]
22struct ClosedRegistrationEntryCborEnvelope {
23    verification_key_bytes: Vec<u8>,
24    stake: Stake,
25    #[cfg(feature = "future_snark")]
26    #[serde(skip_serializing_if = "Option::is_none", default)]
27    snark_verification_key_bytes: Option<Vec<u8>>,
28    #[cfg(feature = "future_snark")]
29    #[serde(skip_serializing_if = "Option::is_none", default)]
30    lottery_target_value_bytes: Option<Vec<u8>>,
31}
32
33/// Represents a registration entry of a closed key registration.
34#[derive(PartialEq, Eq, Clone, Debug, Deserialize)]
35pub struct ClosedRegistrationEntry {
36    verification_key_for_concatenation: VerificationKeyForConcatenation,
37    stake: Stake,
38    #[cfg(feature = "future_snark")]
39    #[serde(skip_serializing_if = "Option::is_none", default)]
40    verification_key_for_snark: Option<VerificationKeyForSnark>,
41    #[cfg(feature = "future_snark")]
42    #[serde(skip_serializing_if = "Option::is_none", default)]
43    lottery_target_value: Option<LotteryTargetValue>,
44}
45
46impl ClosedRegistrationEntry {
47    /// Creates a new closed registration entry.
48    pub fn new(
49        verification_key_for_concatenation: VerificationKeyForConcatenation,
50        stake: Stake,
51        #[cfg(feature = "future_snark")] verification_key_for_snark: Option<
52            VerificationKeyForSnark,
53        >,
54        #[cfg(feature = "future_snark")] lottery_target_value: Option<LotteryTargetValue>,
55    ) -> Self {
56        ClosedRegistrationEntry {
57            verification_key_for_concatenation,
58            stake,
59            #[cfg(feature = "future_snark")]
60            verification_key_for_snark,
61            #[cfg(feature = "future_snark")]
62            lottery_target_value,
63        }
64    }
65
66    /// Gets the verification key for concatenation.
67    pub fn get_verification_key_for_concatenation(&self) -> VerificationKeyForConcatenation {
68        self.verification_key_for_concatenation
69    }
70
71    /// Gets the stake.
72    pub fn get_stake(&self) -> Stake {
73        self.stake
74    }
75
76    /// Returns a copy of this entry with SNARK-specific fields removed.
77    ///
78    /// This is used when embedding registration entries in concatenation proofs,
79    /// which do not need SNARK fields and must remain backward-compatible with
80    /// clients that do not support the `future_snark` feature.
81    #[cfg(feature = "future_snark")]
82    pub fn without_snark_fields(&self) -> Self {
83        ClosedRegistrationEntry {
84            verification_key_for_concatenation: self.verification_key_for_concatenation,
85            stake: self.stake,
86            verification_key_for_snark: None,
87            lottery_target_value: None,
88        }
89    }
90
91    #[cfg(feature = "future_snark")]
92    /// Gets the verification key for snark.
93    pub fn get_verification_key_for_snark(&self) -> Option<VerificationKeyForSnark> {
94        self.verification_key_for_snark
95    }
96
97    #[cfg(feature = "future_snark")]
98    /// Gets the lottery target value.
99    pub fn get_lottery_target_value(&self) -> Option<LotteryTargetValue> {
100        self.lottery_target_value
101    }
102
103    /// Converts the registration entry to CBOR bytes with a version prefix.
104    ///
105    /// Uses an intermediate envelope struct to avoid ciborium incompatibility
106    /// with the custom tuple-based `Serialize` implementation.
107    pub(crate) fn to_bytes(&self) -> StmResult<Vec<u8>> {
108        let envelope = ClosedRegistrationEntryCborEnvelope {
109            verification_key_bytes: self.verification_key_for_concatenation.to_bytes().to_vec(),
110            stake: self.stake,
111            #[cfg(feature = "future_snark")]
112            snark_verification_key_bytes: self
113                .verification_key_for_snark
114                .map(|vk| vk.to_bytes().to_vec()),
115            #[cfg(feature = "future_snark")]
116            lottery_target_value_bytes: self
117                .lottery_target_value
118                .map(|ltv| ltv.to_bytes().to_vec()),
119        };
120        codec::to_cbor_bytes(&envelope)
121    }
122
123    /// Creates a registration entry from versioned bytes.
124    ///
125    /// If the bytes start with the CBOR v1 version prefix, decodes the remaining bytes as CBOR.
126    /// Otherwise, falls back to the legacy byte-packing format.
127    pub(crate) fn from_bytes(bytes: &[u8]) -> StmResult<Self> {
128        if codec::has_cbor_v1_prefix(bytes) {
129            let envelope: ClosedRegistrationEntryCborEnvelope =
130                codec::from_cbor_bytes(&bytes[1..])?;
131            let verification_key_for_concatenation =
132                VerificationKeyForConcatenation::from_bytes(&envelope.verification_key_bytes)?;
133
134            #[cfg(feature = "future_snark")]
135            let verification_key_for_snark = envelope
136                .snark_verification_key_bytes
137                .map(|b| VerificationKeyForSnark::from_bytes(&b))
138                .transpose()?;
139
140            #[cfg(feature = "future_snark")]
141            let lottery_target_value = envelope
142                .lottery_target_value_bytes
143                .map(|b| LotteryTargetValue::from_bytes(&b))
144                .transpose()?;
145
146            Ok(ClosedRegistrationEntry {
147                verification_key_for_concatenation,
148                stake: envelope.stake,
149                #[cfg(feature = "future_snark")]
150                verification_key_for_snark,
151                #[cfg(feature = "future_snark")]
152                lottery_target_value,
153            })
154        } else {
155            Self::from_bytes_legacy(bytes)
156        }
157    }
158
159    /// Creates a registration entry from legacy byte-packed format.
160    /// Expects 96 bytes for the verification key for concatenation and 8 bytes for the stake
161    /// (u64 big-endian).
162    /// #[cfg(feature = "future_snark")] Expects 64 bytes for the verification key for snark and 32
163    /// bytes for the lottery target value.
164    /// The order is backward compatible with previous implementations.
165    fn from_bytes_legacy(bytes: &[u8]) -> StmResult<Self> {
166        let verification_key_for_concatenation = VerificationKeyForConcatenation::from_bytes(
167            bytes.get(..96).ok_or(RegisterError::SerializationError)?,
168        )?;
169        let mut u64_bytes = [0u8; 8];
170        u64_bytes.copy_from_slice(bytes.get(96..104).ok_or(RegisterError::SerializationError)?);
171        let stake = Stake::from_be_bytes(u64_bytes);
172
173        #[cfg(feature = "future_snark")]
174        let (verification_key_for_snark, lottery_target_value) = {
175            let schnorr_verification_key = bytes
176                .get(104..168)
177                .map(VerificationKeyForSnark::from_bytes)
178                .transpose()?;
179
180            let lottery_target_value =
181                bytes.get(168..200).map(LotteryTargetValue::from_bytes).transpose()?;
182
183            match (schnorr_verification_key, lottery_target_value) {
184                (Some(_), None) | (None, Some(_)) => {
185                    return Err(RegisterError::SerializationError.into());
186                }
187                _ => {}
188            }
189            (schnorr_verification_key, lottery_target_value)
190        };
191
192        Ok(ClosedRegistrationEntry {
193            verification_key_for_concatenation,
194            stake,
195            #[cfg(feature = "future_snark")]
196            verification_key_for_snark,
197            #[cfg(feature = "future_snark")]
198            lottery_target_value,
199        })
200    }
201}
202
203impl Serialize for ClosedRegistrationEntry {
204    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
205        #[cfg(not(feature = "future_snark"))]
206        {
207            let mut tuple = serializer.serialize_tuple(2)?;
208            tuple.serialize_element(&self.verification_key_for_concatenation)?;
209            tuple.serialize_element(&self.stake)?;
210            tuple.end()
211        }
212        #[cfg(feature = "future_snark")]
213        {
214            let has_snark_fields = self.verification_key_for_snark.is_some()
215                && self.lottery_target_value.is_some()
216                && cfg!(feature = "future_snark");
217            let tuples_number = if has_snark_fields { 4 } else { 2 };
218            let mut tuple = serializer.serialize_tuple(tuples_number)?;
219            tuple.serialize_element(&self.verification_key_for_concatenation)?;
220            tuple.serialize_element(&self.stake)?;
221            if has_snark_fields {
222                tuple.serialize_element(&self.verification_key_for_snark)?;
223                tuple.serialize_element(&self.lottery_target_value)?;
224            }
225            tuple.end()
226        }
227    }
228}
229
230/// Converts a `RegistrationEntry` into a `ClosedRegistrationEntry`.
231///
232/// Extracts the concatenation verification key and stake from the entry. When the `future_snark`
233/// feature is enabled and a SNARK verification key is present, the lottery target value is also
234/// computed from `phi_f`, the entry's stake, and `total_stake` via `compute_target_value_for_snark_lottery`.
235impl TryFrom<(RegistrationEntry, Stake, PhiFValue)> for ClosedRegistrationEntry {
236    type Error = anyhow::Error;
237    fn try_from(entry_total_stake: (RegistrationEntry, Stake, PhiFValue)) -> StmResult<Self> {
238        #[cfg(not(feature = "future_snark"))]
239        let (entry, _total_stake, _phi_f) = entry_total_stake;
240        #[cfg(feature = "future_snark")]
241        let (entry, total_stake, phi_f) = entry_total_stake;
242        #[cfg(feature = "future_snark")]
243        let (schnorr_verification_key, target_value) = {
244            let vk = entry.get_verification_key_for_snark();
245            let target = vk
246                .is_some()
247                .then(|| {
248                    compute_target_value_for_snark_lottery(phi_f, entry.get_stake(), total_stake)
249                })
250                .transpose()?;
251
252            (vk, target)
253        };
254
255        Ok(ClosedRegistrationEntry::new(
256            entry.get_verification_key_for_concatenation(),
257            entry.get_stake(),
258            #[cfg(feature = "future_snark")]
259            schnorr_verification_key,
260            #[cfg(feature = "future_snark")]
261            target_value,
262        ))
263    }
264}
265
266impl Hash for ClosedRegistrationEntry {
267    /// Hashes the registration entry by hashing the stake first, then the verification key.
268    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
269        self.stake.hash(state);
270        self.verification_key_for_concatenation.hash(state);
271        #[cfg(feature = "future_snark")]
272        {
273            self.verification_key_for_snark.hash(state);
274            self.lottery_target_value.hash(state);
275        }
276    }
277
278    fn hash_slice<H: std::hash::Hasher>(data: &[Self], state: &mut H)
279    where
280        Self: Sized,
281    {
282        for piece in data {
283            piece.hash(state)
284        }
285    }
286}
287
288impl PartialOrd for ClosedRegistrationEntry {
289    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
290        Some(std::cmp::Ord::cmp(self, other))
291    }
292}
293
294impl Ord for ClosedRegistrationEntry {
295    /// Orders by stake first, then by Verification key for concatenation.
296    ///
297    /// Note: this ordering intentionally excludes the snark fields
298    /// (`VerificationKeyForSnark`, `LotteryTargetValue`), as we do not need them for ordering
299    /// the Merkle tree leaves.
300    fn cmp(&self, other: &Self) -> Ordering {
301        self.stake.cmp(&other.stake).then(
302            self.verification_key_for_concatenation
303                .cmp(&other.verification_key_for_concatenation),
304        )
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use rand_chacha::ChaCha20Rng;
311    use rand_core::SeedableRng;
312    use std::cmp::Ordering;
313
314    use crate::{
315        VerificationKeyProofOfPossessionForConcatenation, signature_scheme::BlsSigningKey,
316    };
317
318    #[cfg(feature = "future_snark")]
319    use crate::{VerificationKeyForSnark, signature_scheme::SchnorrSigningKey};
320
321    use super::*;
322
323    fn create_closed_registration_entry(
324        rng: &mut ChaCha20Rng,
325        stake: Stake,
326    ) -> ClosedRegistrationEntry {
327        let bls_sk = BlsSigningKey::generate(rng);
328        let bls_pk = VerificationKeyProofOfPossessionForConcatenation::from(&bls_sk);
329
330        #[cfg(feature = "future_snark")]
331        let schnorr_verification_key = {
332            let sk = SchnorrSigningKey::generate(rng);
333            VerificationKeyForSnark::new_from_signing_key(sk.clone())
334        };
335        ClosedRegistrationEntry::new(
336            bls_pk.vk,
337            stake,
338            #[cfg(feature = "future_snark")]
339            Some(schnorr_verification_key),
340            #[cfg(feature = "future_snark")]
341            Some(LotteryTargetValue::get_one()),
342        )
343    }
344
345    #[test]
346    fn test_ord_different_stakes() {
347        let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
348
349        let entry_low_stake = create_closed_registration_entry(&mut rng, 100);
350        let entry_high_stake = create_closed_registration_entry(&mut rng, 200);
351
352        assert_eq!(entry_low_stake.cmp(&entry_high_stake), Ordering::Less);
353        assert_eq!(entry_high_stake.cmp(&entry_low_stake), Ordering::Greater);
354    }
355
356    #[test]
357    fn test_ord_same_stake_different_keys() {
358        let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
359
360        let entry1 = create_closed_registration_entry(&mut rng, 100);
361        let entry2 = create_closed_registration_entry(&mut rng, 100);
362
363        let cmp_result = entry1.cmp(&entry2);
364        assert!(cmp_result == Ordering::Less || cmp_result == Ordering::Greater);
365
366        assert_eq!(entry2.cmp(&entry1), cmp_result.reverse());
367    }
368
369    mod golden {
370        use super::*;
371
372        #[cfg(not(feature = "future_snark"))]
373        const GOLDEN_BYTES: &[u8; 104] = &[
374            143, 161, 255, 48, 78, 57, 204, 220, 25, 221, 164, 252, 248, 14, 56, 126, 186, 135,
375            228, 188, 145, 181, 52, 200, 97, 99, 213, 46, 0, 199, 193, 89, 187, 88, 29, 135, 173,
376            244, 86, 36, 83, 54, 67, 164, 6, 137, 94, 72, 6, 105, 128, 128, 93, 48, 176, 11, 4,
377            246, 138, 48, 180, 133, 90, 142, 192, 24, 193, 111, 142, 31, 76, 111, 110, 234, 153,
378            90, 208, 192, 31, 124, 95, 102, 49, 158, 99, 52, 220, 165, 94, 251, 68, 69, 121, 16,
379            224, 194, 0, 0, 0, 0, 0, 0, 0, 1,
380        ];
381
382        #[cfg(feature = "future_snark")]
383        const GOLDEN_BYTES: &[u8; 200] = &[
384            143, 161, 255, 48, 78, 57, 204, 220, 25, 221, 164, 252, 248, 14, 56, 126, 186, 135,
385            228, 188, 145, 181, 52, 200, 97, 99, 213, 46, 0, 199, 193, 89, 187, 88, 29, 135, 173,
386            244, 86, 36, 83, 54, 67, 164, 6, 137, 94, 72, 6, 105, 128, 128, 93, 48, 176, 11, 4,
387            246, 138, 48, 180, 133, 90, 142, 192, 24, 193, 111, 142, 31, 76, 111, 110, 234, 153,
388            90, 208, 192, 31, 124, 95, 102, 49, 158, 99, 52, 220, 165, 94, 251, 68, 69, 121, 16,
389            224, 194, 0, 0, 0, 0, 0, 0, 0, 1, 200, 194, 6, 212, 77, 254, 23, 111, 33, 34, 139, 71,
390            131, 196, 108, 13, 217, 75, 187, 131, 158, 77, 197, 163, 30, 123, 151, 237, 157, 232,
391            167, 10, 45, 121, 194, 155, 110, 46, 240, 74, 141, 138, 78, 228, 92, 179, 58, 63, 233,
392            239, 84, 114, 149, 77, 188, 93, 8, 22, 11, 12, 45, 186, 211, 56, 1, 0, 0, 0, 0, 0, 0,
393            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
394        ];
395
396        fn golden_value() -> ClosedRegistrationEntry {
397            let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
398            let bls_sk = BlsSigningKey::generate(&mut rng);
399            let bls_pk = VerificationKeyProofOfPossessionForConcatenation::from(&bls_sk);
400
401            #[cfg(feature = "future_snark")]
402            let schnorr_verification_key = {
403                let sk = SchnorrSigningKey::generate(&mut rng);
404                VerificationKeyForSnark::new_from_signing_key(sk.clone())
405            };
406            ClosedRegistrationEntry::new(
407                bls_pk.vk,
408                1,
409                #[cfg(feature = "future_snark")]
410                Some(schnorr_verification_key),
411                #[cfg(feature = "future_snark")]
412                Some(LotteryTargetValue::get_one()),
413            )
414        }
415
416        #[test]
417        fn golden_conversions() {
418            let value = ClosedRegistrationEntry::from_bytes(GOLDEN_BYTES)
419                .expect("This from bytes should not fail");
420            assert_eq!(golden_value(), value);
421
422            let serialized = ClosedRegistrationEntry::to_bytes(&value)
423                .expect("ClosedRegistrationEntry serialization should not fail");
424            let golden_serialized = ClosedRegistrationEntry::to_bytes(&golden_value())
425                .expect("ClosedRegistrationEntry serialization should not fail");
426            assert_eq!(golden_serialized, serialized);
427        }
428
429        #[cfg(not(feature = "future_snark"))]
430        const GOLDEN_CBOR_BYTES: &[u8; 219] = &[
431            1, 162, 118, 118, 101, 114, 105, 102, 105, 99, 97, 116, 105, 111, 110, 95, 107, 101,
432            121, 95, 98, 121, 116, 101, 115, 152, 96, 24, 143, 24, 161, 24, 255, 24, 48, 24, 78,
433            24, 57, 24, 204, 24, 220, 24, 25, 24, 221, 24, 164, 24, 252, 24, 248, 14, 24, 56, 24,
434            126, 24, 186, 24, 135, 24, 228, 24, 188, 24, 145, 24, 181, 24, 52, 24, 200, 24, 97, 24,
435            99, 24, 213, 24, 46, 0, 24, 199, 24, 193, 24, 89, 24, 187, 24, 88, 24, 29, 24, 135, 24,
436            173, 24, 244, 24, 86, 24, 36, 24, 83, 24, 54, 24, 67, 24, 164, 6, 24, 137, 24, 94, 24,
437            72, 6, 24, 105, 24, 128, 24, 128, 24, 93, 24, 48, 24, 176, 11, 4, 24, 246, 24, 138, 24,
438            48, 24, 180, 24, 133, 24, 90, 24, 142, 24, 192, 24, 24, 24, 193, 24, 111, 24, 142, 24,
439            31, 24, 76, 24, 111, 24, 110, 24, 234, 24, 153, 24, 90, 24, 208, 24, 192, 24, 31, 24,
440            124, 24, 95, 24, 102, 24, 49, 24, 158, 24, 99, 24, 52, 24, 220, 24, 165, 24, 94, 24,
441            251, 24, 68, 24, 69, 24, 121, 16, 24, 224, 24, 194, 101, 115, 116, 97, 107, 101, 1,
442        ];
443
444        #[cfg(feature = "future_snark")]
445        const GOLDEN_CBOR_BYTES: &[u8; 433] = &[
446            1, 164, 118, 118, 101, 114, 105, 102, 105, 99, 97, 116, 105, 111, 110, 95, 107, 101,
447            121, 95, 98, 121, 116, 101, 115, 152, 96, 24, 143, 24, 161, 24, 255, 24, 48, 24, 78,
448            24, 57, 24, 204, 24, 220, 24, 25, 24, 221, 24, 164, 24, 252, 24, 248, 14, 24, 56, 24,
449            126, 24, 186, 24, 135, 24, 228, 24, 188, 24, 145, 24, 181, 24, 52, 24, 200, 24, 97, 24,
450            99, 24, 213, 24, 46, 0, 24, 199, 24, 193, 24, 89, 24, 187, 24, 88, 24, 29, 24, 135, 24,
451            173, 24, 244, 24, 86, 24, 36, 24, 83, 24, 54, 24, 67, 24, 164, 6, 24, 137, 24, 94, 24,
452            72, 6, 24, 105, 24, 128, 24, 128, 24, 93, 24, 48, 24, 176, 11, 4, 24, 246, 24, 138, 24,
453            48, 24, 180, 24, 133, 24, 90, 24, 142, 24, 192, 24, 24, 24, 193, 24, 111, 24, 142, 24,
454            31, 24, 76, 24, 111, 24, 110, 24, 234, 24, 153, 24, 90, 24, 208, 24, 192, 24, 31, 24,
455            124, 24, 95, 24, 102, 24, 49, 24, 158, 24, 99, 24, 52, 24, 220, 24, 165, 24, 94, 24,
456            251, 24, 68, 24, 69, 24, 121, 16, 24, 224, 24, 194, 101, 115, 116, 97, 107, 101, 1,
457            120, 28, 115, 110, 97, 114, 107, 95, 118, 101, 114, 105, 102, 105, 99, 97, 116, 105,
458            111, 110, 95, 107, 101, 121, 95, 98, 121, 116, 101, 115, 152, 64, 24, 200, 24, 194, 6,
459            24, 212, 24, 77, 24, 254, 23, 24, 111, 24, 33, 24, 34, 24, 139, 24, 71, 24, 131, 24,
460            196, 24, 108, 13, 24, 217, 24, 75, 24, 187, 24, 131, 24, 158, 24, 77, 24, 197, 24, 163,
461            24, 30, 24, 123, 24, 151, 24, 237, 24, 157, 24, 232, 24, 167, 10, 24, 45, 24, 121, 24,
462            194, 24, 155, 24, 110, 24, 46, 24, 240, 24, 74, 24, 141, 24, 138, 24, 78, 24, 228, 24,
463            92, 24, 179, 24, 58, 24, 63, 24, 233, 24, 239, 24, 84, 24, 114, 24, 149, 24, 77, 24,
464            188, 24, 93, 8, 22, 11, 12, 24, 45, 24, 186, 24, 211, 24, 56, 120, 26, 108, 111, 116,
465            116, 101, 114, 121, 95, 116, 97, 114, 103, 101, 116, 95, 118, 97, 108, 117, 101, 95,
466            98, 121, 116, 101, 115, 152, 32, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
467            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
468        ];
469
470        #[test]
471        fn cbor_golden_bytes_can_be_decoded() {
472            let decoded = ClosedRegistrationEntry::from_bytes(GOLDEN_CBOR_BYTES)
473                .expect("CBOR golden bytes deserialization should not fail");
474            assert_eq!(golden_value(), decoded);
475        }
476
477        #[test]
478        fn cbor_encoding_is_stable() {
479            let bytes = ClosedRegistrationEntry::to_bytes(&golden_value())
480                .expect("ClosedRegistrationEntry serialization should not fail");
481            assert_eq!(GOLDEN_CBOR_BYTES.as_slice(), bytes.as_slice());
482        }
483    }
484
485    mod envelope_compatibility {
486        use super::*;
487        use crate::codec;
488
489        #[test]
490        fn forward_compatible_with_extra_fields() {
491            let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
492            let entry = create_closed_registration_entry(&mut rng, 42);
493
494            #[derive(serde::Serialize)]
495            struct EvolvedEnvelope {
496                verification_key_bytes: Vec<u8>,
497                stake: Stake,
498                new_field: String,
499            }
500
501            let evolved = EvolvedEnvelope {
502                verification_key_bytes: entry
503                    .get_verification_key_for_concatenation()
504                    .to_bytes()
505                    .to_vec(),
506                stake: 42,
507                new_field: "extra".to_string(),
508            };
509            let evolved_bytes =
510                codec::to_cbor_bytes(&evolved).expect("evolved serialization should not fail");
511
512            let decoded = ClosedRegistrationEntry::from_bytes(&evolved_bytes)
513                .expect("decoding with extra field should succeed");
514            assert_eq!(entry.get_stake(), decoded.get_stake());
515            assert_eq!(
516                entry.get_verification_key_for_concatenation(),
517                decoded.get_verification_key_for_concatenation()
518            );
519        }
520
521        #[test]
522        fn backward_compatible_with_missing_optional_fields() {
523            #[derive(serde::Serialize)]
524            struct MinimalEnvelope {
525                verification_key_bytes: Vec<u8>,
526                stake: Stake,
527            }
528
529            let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
530            let entry = create_closed_registration_entry(&mut rng, 42);
531
532            let minimal = MinimalEnvelope {
533                verification_key_bytes: entry
534                    .get_verification_key_for_concatenation()
535                    .to_bytes()
536                    .to_vec(),
537                stake: 42,
538            };
539            let minimal_bytes =
540                codec::to_cbor_bytes(&minimal).expect("minimal serialization should not fail");
541
542            let decoded = ClosedRegistrationEntry::from_bytes(&minimal_bytes)
543                .expect("decoding with missing optional fields should succeed");
544            assert_eq!(42, decoded.get_stake());
545            assert_eq!(
546                entry.get_verification_key_for_concatenation(),
547                decoded.get_verification_key_for_concatenation()
548            );
549        }
550    }
551
552    #[cfg(feature = "future_snark")]
553    mod without_snark_fields {
554        use super::*;
555
556        #[test]
557        fn preserves_concatenation_key_and_stake() {
558            let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
559            let entry = create_closed_registration_entry(&mut rng, 42);
560
561            let stripped = entry.without_snark_fields();
562
563            assert_eq!(
564                entry.get_verification_key_for_concatenation(),
565                stripped.get_verification_key_for_concatenation()
566            );
567            assert_eq!(entry.get_stake(), stripped.get_stake());
568        }
569
570        #[test]
571        fn clears_snark_fields() {
572            let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
573            let entry = create_closed_registration_entry(&mut rng, 42);
574            assert!(entry.get_verification_key_for_snark().is_some());
575            assert!(entry.get_lottery_target_value().is_some());
576
577            let stripped = entry.without_snark_fields();
578
579            assert!(stripped.get_verification_key_for_snark().is_none());
580            assert!(stripped.get_lottery_target_value().is_none());
581        }
582
583        #[test]
584        fn serializes_as_two_element_json_tuple() {
585            let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
586            let entry = create_closed_registration_entry(&mut rng, 42);
587
588            let stripped = entry.without_snark_fields();
589            let json: serde_json::Value =
590                serde_json::to_value(stripped).expect("JSON serialization should not fail");
591
592            let array = json.as_array().expect("should serialize as a JSON array");
593            assert_eq!(
594                2,
595                array.len(),
596                "stripped entry should serialize as a 2-element tuple"
597            );
598        }
599
600        #[test]
601        fn entry_with_snark_fields_serializes_as_four_element_json_tuple() {
602            let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
603            let entry = create_closed_registration_entry(&mut rng, 42);
604
605            let json: serde_json::Value =
606                serde_json::to_value(entry).expect("JSON serialization should not fail");
607
608            let array = json.as_array().expect("should serialize as a JSON array");
609            assert_eq!(
610                4,
611                array.len(),
612                "full entry should serialize as a 4-element tuple"
613            );
614        }
615    }
616}