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}