Skip to main content

dpp/serialization/dashcore/
bls_pubkey.rs

1//! Local serde wrapper for `BlsPublicKey<Bls12381G2Impl>` that tolerates
2//! owned-string sources (`serde_json::Value`, `platform_value::Value`, and
3//! anything routed through serde's `ContentDeserializer` for tagged-enum
4//! buffering).
5//!
6//! ## Why this exists
7//!
8//! Upstream `blstrs_plus` 0.8.18 (`src/serde_impl.rs:119`) implements the
9//! human-readable deserialize path as:
10//!
11//! ```ignore
12//! if d.is_human_readable() {
13//!     let hex_str = <&str>::deserialize(d)?;   // borrowed-only
14//!     ...
15//! }
16//! ```
17//!
18//! `<&str>::deserialize` only succeeds when the deserializer's visitor
19//! receives `visit_borrowed_str` — which `serde_json::from_slice` /
20//! `serde_json::from_str` provide, but `serde_json::from_value`,
21//! `platform_value::from_value`, and `ContentDeserializer` do **not** (they
22//! produce owned `String`). Round-tripping a `BlsPublicKey` through any
23//! `Value` representation therefore fails with
24//! `"invalid type: string ..., expected a borrowed string"`.
25//!
26//! This is technically a serde compatibility quirk rather than a single
27//! crate's bug — but the leaf type is the only place to patch. See plan
28//! §10b "Common pattern: serde's `ContentDeserializer` HR-quirk" for
29//! the broader narrative.
30//!
31//! ## How the workaround works
32//!
33//! A single `deserialize_any` visitor accepts BOTH wire forms upstream emits:
34//! the 96-char hex string (`visit_str`) and the 48-byte compressed-G1 form as
35//! raw bytes or a `u8` sequence (`visit_bytes` / `visit_seq`). Either way it
36//! hex/byte-decodes to the compressed-G1 representation, lifts it to
37//! `G1Projective` via `to_curve`, and wraps into `PublicKey<Bls12381G2Impl>`
38//! directly (the inner field is `pub`).
39//!
40//! Driving it via `deserialize_any` (rather than branching on
41//! `is_human_readable()`) is what makes it robust to serde's internal-tag
42//! `Content` buffer: that buffer's `is_human_readable()` does not reliably
43//! match the original deserializer, so a `Value` (non-HR) pubkey could arrive
44//! as a byte *sequence* while the old HR branch expected a *string* — which is
45//! exactly why the `ValidatorSet` value round-trip test used to be `#[ignore]`d.
46//! Accepting both shapes in one visitor sidesteps that, and also sidesteps the
47//! upstream borrowed-only `<&str>::deserialize` HR path. Both serde_json and
48//! platform_value are self-describing, so `deserialize_any` is safe; bincode
49//! never reaches here (it goes through the separate derived `Decode`).
50//!
51//! Note: `BlsPublicKey<Bls12381G2Impl>` carries a public key on the G1 curve
52//! (the `Bls12381G2Impl` name refers to where signatures live, not keys).
53//! Compressed G1 = 48 bytes = 96 hex chars.
54//!
55//! ## When to remove this
56//!
57//! This wrapper is now self-sufficient (no behavioral dependency on an upstream
58//! fix). Once upstream `blstrs_plus` accepts owned strings AND a byte-sequence
59//! HR form on its own `Deserialize`, this wrapper and the `serde(with = ...)`
60//! annotations on `Validator::public_key` / `ValidatorSetV0::threshold_public_key`
61//! can simply be dropped.
62
63use crate::bls_signatures::inner_types::{G1Affine, GroupEncoding, PrimeCurveAffine};
64use crate::bls_signatures::{Bls12381G2Impl, PublicKey as BlsPublicKey};
65use serde::de::Visitor;
66use serde::{Deserializer, Serialize, Serializer};
67use std::fmt;
68
69/// Compressed-G1 wire size for BLS12-381 (where the public key lives in
70/// `Bls12381G2Impl`).
71const COMPRESSED_G1_LEN: usize = 48;
72
73pub fn serialize<S: Serializer>(
74    pk: &BlsPublicKey<Bls12381G2Impl>,
75    serializer: S,
76) -> Result<S::Ok, S::Error> {
77    // Upstream serialize already produces a hex string in HR and a byte tuple
78    // in non-HR; both are correct on the wire. Nothing to override here.
79    pk.serialize(serializer)
80}
81
82pub fn deserialize<'de, D: Deserializer<'de>>(
83    deserializer: D,
84) -> Result<BlsPublicKey<Bls12381G2Impl>, D::Error> {
85    // One visitor that accepts BOTH wire forms upstream emits: the
86    // human-readable 96-char hex string, and the non-HR 48-byte compressed-G1
87    // form (as `bytes` or a `u8` sequence). Driving it via `deserialize_any`
88    // makes it robust to serde's internal-tag `Content` buffer, whose
89    // `is_human_readable()` does not always match the original deserializer —
90    // the bug that previously forced the ValidatorSet value round-trip test to
91    // be `#[ignore]`d (the non-HR bytes arrived while the old code's HR branch
92    // expected a string → "invalid type: sequence, expected a string"). It also
93    // sidesteps the upstream `<&str>::deserialize` borrowed-only HR path (the
94    // original reason this wrapper exists). Both serde_json and platform_value
95    // are self-describing, so `deserialize_any` is safe; bincode never reaches
96    // here (it goes through the separate derived `Decode`).
97    deserializer.deserialize_any(BlsPublicKeyVisitor)
98}
99
100fn from_compressed_g1_bytes<E: serde::de::Error>(
101    bytes: &[u8],
102) -> Result<BlsPublicKey<Bls12381G2Impl>, E> {
103    if bytes.len() != COMPRESSED_G1_LEN {
104        return Err(E::custom(format!(
105            "expected {COMPRESSED_G1_LEN} compressed-G1 bytes for public key, got {}",
106            bytes.len()
107        )));
108    }
109    let mut compressed = <G1Affine as GroupEncoding>::Repr::default();
110    compressed.as_mut().copy_from_slice(bytes);
111    let affine = Option::<G1Affine>::from(G1Affine::from_bytes(&compressed))
112        .ok_or_else(|| E::custom("not a valid compressed G1 point"))?;
113    Ok(BlsPublicKey::<Bls12381G2Impl>(affine.to_curve()))
114}
115
116struct BlsPublicKeyVisitor;
117
118impl<'de> Visitor<'de> for BlsPublicKeyVisitor {
119    type Value = BlsPublicKey<Bls12381G2Impl>;
120
121    fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
122        write!(
123            f,
124            "a {}-char hex string or {} compressed-G1 bytes",
125            COMPRESSED_G1_LEN * 2,
126            COMPRESSED_G1_LEN
127        )
128    }
129
130    fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
131        if s.len() != COMPRESSED_G1_LEN * 2 {
132            return Err(E::custom(format!(
133                "expected {} hex chars for compressed G1 public key, got {}",
134                COMPRESSED_G1_LEN * 2,
135                s.len()
136            )));
137        }
138        let mut bytes = [0u8; COMPRESSED_G1_LEN];
139        for (i, slot) in bytes.iter_mut().enumerate() {
140            let hi = hex_nibble(s.as_bytes()[i * 2]).map_err(E::custom)?;
141            let lo = hex_nibble(s.as_bytes()[i * 2 + 1]).map_err(E::custom)?;
142            *slot = (hi << 4) | lo;
143        }
144        from_compressed_g1_bytes(&bytes)
145    }
146
147    fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
148        from_compressed_g1_bytes(v)
149    }
150
151    fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
152        let mut bytes = Vec::with_capacity(COMPRESSED_G1_LEN);
153        while let Some(b) = seq.next_element::<u8>()? {
154            // A valid compressed-G1 public key is exactly COMPRESSED_G1_LEN
155            // bytes; reject as soon as a hostile payload exceeds that rather
156            // than allocating/parsing an arbitrarily long sequence first.
157            if bytes.len() == COMPRESSED_G1_LEN {
158                return Err(serde::de::Error::invalid_length(bytes.len() + 1, &self));
159            }
160            bytes.push(b);
161        }
162        from_compressed_g1_bytes(&bytes)
163    }
164}
165
166fn hex_nibble(c: u8) -> Result<u8, &'static str> {
167    match c {
168        b'0'..=b'9' => Ok(c - b'0'),
169        b'a'..=b'f' => Ok(c - b'a' + 10),
170        b'A'..=b'F' => Ok(c - b'A' + 10),
171        _ => Err("invalid hex character in compressed G1 public key"),
172    }
173}
174
175/// `Option<BlsPublicKey<Bls12381G2Impl>>` variant for fields like
176/// `Validator::public_key`.
177pub mod option {
178    use super::*;
179
180    pub fn serialize<S: Serializer>(
181        opt: &Option<BlsPublicKey<Bls12381G2Impl>>,
182        serializer: S,
183    ) -> Result<S::Ok, S::Error> {
184        // Option<T>'s built-in Serialize delegates to T's Serialize, which
185        // is the upstream BlsPublicKey impl — already correct.
186        opt.serialize(serializer)
187    }
188
189    pub fn deserialize<'de, D: Deserializer<'de>>(
190        deserializer: D,
191    ) -> Result<Option<BlsPublicKey<Bls12381G2Impl>>, D::Error> {
192        struct OptionVisitor;
193
194        impl<'de> Visitor<'de> for OptionVisitor {
195            type Value = Option<BlsPublicKey<Bls12381G2Impl>>;
196
197            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
198                f.write_str("Option<BlsPublicKey<Bls12381G2Impl>>")
199            }
200
201            fn visit_none<E: serde::de::Error>(self) -> Result<Self::Value, E> {
202                Ok(None)
203            }
204
205            fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
206                Ok(None)
207            }
208
209            fn visit_some<D2: Deserializer<'de>>(
210                self,
211                inner: D2,
212            ) -> Result<Self::Value, D2::Error> {
213                super::deserialize(inner).map(Some)
214            }
215        }
216
217        deserializer.deserialize_option(OptionVisitor)
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use serde::{Deserialize, Serialize};
225    use serde_json::json;
226
227    // A known-valid compressed-G1 BLS public key (deterministic — the
228    // ValidatorSet fixture's seeded StdRng(42) threshold key).
229    const PK_HEX: &str =
230        "969c5d5873f49aa994c5f6a850924ca1840c4ad1791aaaecd90093d4a5c0c3799f2d98540f5366cfa0a33f143fd69263";
231
232    // Newtypes that drive the `with` module(s) through serde.
233    #[derive(Serialize, Deserialize)]
234    struct Wrap(#[serde(with = "super")] BlsPublicKey<Bls12381G2Impl>);
235
236    #[derive(Serialize, Deserialize)]
237    struct OptWrap(#[serde(with = "super::option")] Option<BlsPublicKey<Bls12381G2Impl>>);
238
239    #[test]
240    fn json_hr_round_trip_is_the_hex_string() {
241        // Human-readable (serde_json::Value): hex in, identical hex out (visit_str).
242        let pk: Wrap = serde_json::from_value(json!(PK_HEX)).expect("from hex");
243        assert_eq!(serde_json::to_value(&pk).expect("to json"), json!(PK_HEX));
244    }
245
246    #[test]
247    fn json_borrowed_str_path_works() {
248        // `serde_json::from_str` yields borrowed strings — the path upstream's
249        // `<&str>::deserialize` handled but `from_value`/Content did not. Our
250        // `visit_str` takes `&str`, so both work.
251        let pk: Wrap = serde_json::from_str(&format!("\"{PK_HEX}\"")).expect("from_str");
252        assert_eq!(serde_json::to_value(&pk).expect("to json"), json!(PK_HEX));
253    }
254
255    #[test]
256    fn value_non_hr_byte_seq_round_trip() {
257        // Non-HR `platform_value` serializes the key as a 48-byte sequence; the
258        // `visit_seq` path (the bug the un-ignored ValidatorSet test exposed)
259        // must reconstruct it. Re-serialize to JSON to confirm the same key.
260        let pk: Wrap = serde_json::from_value(json!(PK_HEX)).expect("from hex");
261        let value = platform_value::to_value(&pk).expect("to value");
262        let pk2: Wrap = platform_value::from_value(value).expect("from value seq");
263        assert_eq!(serde_json::to_value(&pk2).expect("to json"), json!(PK_HEX));
264    }
265
266    #[test]
267    fn option_some_and_none_round_trip() {
268        let some: OptWrap = serde_json::from_value(json!(PK_HEX)).expect("some");
269        assert_eq!(serde_json::to_value(&some).expect("to json"), json!(PK_HEX));
270        let none: OptWrap = serde_json::from_value(json!(null)).expect("none");
271        assert_eq!(serde_json::to_value(&none).expect("to json"), json!(null));
272    }
273}