1use alloy::{
2 primitives::{Address, Bytes},
3 sol_types::SolValue,
4};
5use ark_bn254::G2Affine;
6use ark_ec::{AffineRepr, CurveGroup as _};
7use ark_ed_on_bn254::EdwardsConfig;
8use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
9use hotshot_types::{
10 light_client::{StateKeyPair, StateSignature, StateVerKey, hash_bytes_to_field},
11 signature_key::{BLSKeyPair, BLSPubKey, BLSSignature},
12 traits::signature_key::SignatureKey,
13};
14use jf_signature::{
15 bls_over_bn254,
16 constants::{CS_ID_BLS_BN254, CS_ID_SCHNORR},
17 schnorr,
18};
19
20use crate::{
21 field_to_u256,
22 sol_types::{StakeTableV3, *},
23 u256_to_field,
24};
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct StateSignatureSol(pub Bytes);
29
30#[derive(Debug, Clone, Copy, Default)]
31pub enum StakeTableContractVersion {
32 V1,
33 V2,
34 #[default]
35 V3,
36}
37
38fn version_from_major(major: u8) -> anyhow::Result<StakeTableContractVersion> {
39 match major {
40 1 => Ok(StakeTableContractVersion::V1),
41 2 => Ok(StakeTableContractVersion::V2),
42 3 => Ok(StakeTableContractVersion::V3),
43 _ => anyhow::bail!("Unsupported stake table contract version: {major}"),
44 }
45}
46
47impl TryFrom<StakeTableV3::getVersionReturn> for StakeTableContractVersion {
48 type Error = anyhow::Error;
49
50 fn try_from(value: StakeTableV3::getVersionReturn) -> anyhow::Result<Self> {
51 version_from_major(value.majorVersion)
52 }
53}
54
55impl TryFrom<G2PointSol> for BLSPubKey {
61 type Error = StakeTableSolError;
62
63 fn try_from(value: G2PointSol) -> Result<Self, Self::Error> {
64 let point: G2Affine = value.into();
65 let mut bytes = vec![];
66 point
67 .into_group()
68 .serialize_uncompressed(&mut bytes)
69 .unwrap();
70 Self::deserialize_uncompressed(&bytes[..]).map_err(|_| StakeTableSolError::InvalidBlsKey)
71 }
72}
73
74impl From<BLSPubKey> for G2PointSol {
75 fn from(value: BLSPubKey) -> Self {
76 value.to_affine().into()
77 }
78}
79
80impl TryFrom<EdOnBN254PointSol> for StateVerKey {
81 type Error = StakeTableSolError;
82
83 fn try_from(value: EdOnBN254PointSol) -> Result<Self, Self::Error> {
84 let x: ark_ed_on_bn254::Fq = u256_to_field(value.x);
87 let y: ark_ed_on_bn254::Fq = u256_to_field(value.y);
88 if field_to_u256(x) != value.x || field_to_u256(y) != value.y {
89 return Err(StakeTableSolError::InvalidSchnorrKey);
90 }
91 let point = ark_ed_on_bn254::EdwardsAffine::new_unchecked(x, y);
93 if point.is_zero()
94 || !point.is_on_curve()
95 || !point.is_in_correct_subgroup_assuming_on_curve()
96 {
97 return Err(StakeTableSolError::InvalidSchnorrKey);
98 }
99 Ok(Self::from(point))
100 }
101}
102
103impl From<bls_over_bn254::Signature> for G1PointSol {
104 fn from(sig: bls_over_bn254::Signature) -> Self {
105 sig.sigma.into_affine().into()
106 }
107}
108
109impl From<StateVerKey> for EdOnBN254PointSol {
110 fn from(ver_key: StateVerKey) -> Self {
111 ver_key.to_affine().into()
112 }
113}
114
115impl From<StateSignature> for StateSignatureSol {
116 fn from(sig: StateSignature) -> Self {
117 let mut buf = vec![];
118 sig.serialize_compressed(&mut buf).expect("serialize works");
119 Self(buf.into())
120 }
121}
122
123impl From<StateSignatureSol> for Bytes {
124 fn from(sig_sol: StateSignatureSol) -> Self {
125 sig_sol.0
126 }
127}
128
129pub fn sign_address_bls(bls_key_pair: &BLSKeyPair, address: Address) -> bls_over_bn254::Signature {
130 bls_key_pair.sign(&address.abi_encode(), CS_ID_BLS_BN254)
131}
132
133pub fn sign_address_schnorr(schnorr_key_pair: &StateKeyPair, address: Address) -> StateSignature {
134 let msg = [hash_bytes_to_field(&address.abi_encode()).expect("hash to field works")];
135 schnorr_key_pair.sign(&msg, CS_ID_SCHNORR)
136}
137
138pub fn authenticate_schnorr_sig(
140 schnorr_vk: &StateVerKey,
141 address: Address,
142 schnorr_sig: &StateSignature,
143) -> Result<(), StakeTableSolError> {
144 let msg = [hash_bytes_to_field(&address.abi_encode()).expect("hash to field works")];
145 schnorr_vk.verify(&msg, schnorr_sig, CS_ID_SCHNORR)?;
146 Ok(())
147}
148
149pub fn authenticate_bls_sig(
151 bls_vk: &BLSPubKey,
152 address: Address,
153 bls_sig: &BLSSignature,
154) -> Result<(), StakeTableSolError> {
155 let msg = address.abi_encode();
156 if !bls_vk.validate(bls_sig, &msg) {
157 return Err(StakeTableSolError::InvalidBlsSignature);
158 }
159 Ok(())
160}
161
162fn authenticate_stake_table_validator_event(
163 account: Address,
164 bls_vk: G2PointSol,
165 schnorr_vk: EdOnBN254PointSol,
166 bls_sig: G1PointSol,
167 schnorr_sig: &[u8],
168) -> Result<(BLSPubKey, StateVerKey), StakeTableSolError> {
169 let bls_vk = BLSPubKey::try_from(bls_vk)?;
170 let bls_sig_jellyfish = {
171 let sigma_affine: ark_bn254::G1Affine = bls_sig.into();
172 BLSSignature {
173 sigma: sigma_affine.into_group(),
174 }
175 };
176 authenticate_bls_sig(&bls_vk, account, &bls_sig_jellyfish)?;
177
178 let schnorr_vk = StateVerKey::try_from(schnorr_vk)?;
179 let schnorr_sig_jellyfish =
180 schnorr::Signature::<EdwardsConfig>::deserialize_compressed(schnorr_sig)?;
181 authenticate_schnorr_sig(&schnorr_vk, account, &schnorr_sig_jellyfish)?;
182 Ok((bls_vk, schnorr_vk))
183}
184
185#[derive(Debug, thiserror::Error)]
187pub enum StakeTableSolError {
188 #[error("Failed to deserialize Schnorr signature")]
189 SchnorrSigDeserializationError(#[from] ark_serialize::SerializationError),
190 #[error("BLS signature invalid")]
191 InvalidBlsSignature,
192 #[error("Schnorr signature invalid")]
193 InvalidSchnorrSignature(#[from] jf_signature::SignatureError),
194 #[error("Invalid BLS key")]
195 InvalidBlsKey,
196 #[error("Invalid Schnorr key")]
197 InvalidSchnorrKey,
198}
199
200impl StakeTableV3::ValidatorRegisteredV3 {
201 pub fn authenticate(&self) -> Result<(BLSPubKey, StateVerKey), StakeTableSolError> {
203 authenticate_stake_table_validator_event(
204 self.account,
205 self.blsVK,
206 self.schnorrVK,
207 self.blsSig.into(),
208 &self.schnorrSig,
209 )
210 }
211}
212
213impl StakeTableV3::ValidatorRegisteredV2 {
214 pub fn authenticate(&self) -> Result<(BLSPubKey, StateVerKey), StakeTableSolError> {
216 authenticate_stake_table_validator_event(
217 self.account,
218 self.blsVK,
219 self.schnorrVK,
220 self.blsSig.into(),
221 &self.schnorrSig,
222 )
223 }
224}
225
226impl StakeTableV3::ConsensusKeysUpdatedV2 {
227 pub fn authenticate(&self) -> Result<(BLSPubKey, StateVerKey), StakeTableSolError> {
229 authenticate_stake_table_validator_event(
230 self.account,
231 self.blsVK,
232 self.schnorrVK,
233 self.blsSig.into(),
234 &self.schnorrSig,
235 )
236 }
237}
238
239#[cfg(test)]
240mod test {
241 use alloy::primitives::{Address, U256};
242 use hotshot_types::{
243 light_client::{StateKeyPair, StateVerKey},
244 signature_key::{BLSKeyPair, BLSPrivKey, BLSPubKey},
245 };
246
247 use super::{StateSignatureSol, sign_address_bls, sign_address_schnorr};
248 use crate::sol_types::{
249 EdOnBN254PointSol, G1PointSol, G2PointSol,
250 StakeTableV3::{ConsensusKeysUpdatedV2, ValidatorRegisteredV2},
251 };
252
253 fn check_round_trip(pk: BLSPubKey) {
254 let g2 = G2PointSol::from(pk);
255 let pk2 = BLSPubKey::try_from(g2).expect("valid BLS key roundtrip");
256 assert_eq!(pk2, pk, "Failed to roundtrip G2PointSol to BLSPubKey: {pk}");
257 }
258
259 #[test]
260 fn test_bls_g2_point_roundtrip() {
261 let mut rng = rand::thread_rng();
262 for _ in 0..100 {
263 let pk = (&BLSPrivKey::generate(&mut rng)).into();
264 check_round_trip(pk);
265 }
266 }
267
268 #[test]
269 fn test_bls_g2_point_alloy_migration_regression() {
270 let s = "BLS_VER_KEY~JlRLUrn0T_MltAJXaaojwk_CnCgd0tyPny_IGdseMBLBPv9nWabIPAaS-aHmn0ARu5YZHJ7mfmGQ-alW42tkJM663Lse-Is80fyA1jnRxPsHcJDnO05oW1M1SC5LeE8sXITbuhmtG2JdTAgmLqWOxbMRmVIqS1AQXqvGGXdo5qpd";
272 let pk: BLSPubKey = s.parse().unwrap();
273 check_round_trip(pk);
274 }
275
276 #[test]
277 fn test_validator_registered_event_authentication() {
278 for _ in 0..10 {
279 let bls_key_pair = BLSKeyPair::generate(&mut rand::thread_rng());
280 let schnorr_key_pair = StateKeyPair::generate();
281 let address = Address::random();
282
283 let bls_sig = sign_address_bls(&bls_key_pair, address);
284 let schnorr_sig = sign_address_schnorr(&schnorr_key_pair, address);
285
286 let valid_event = ValidatorRegisteredV2 {
287 account: address,
288 blsVK: bls_key_pair.ver_key().into(),
289 schnorrVK: schnorr_key_pair.ver_key().into(),
290 commission: 1000, blsSig: G1PointSol::from(bls_sig.clone()).into(),
292 schnorrSig: StateSignatureSol::from(schnorr_sig.clone()).into(),
293 metadataUri: "dummy-meta".to_string(),
294 };
295 assert!(valid_event.authenticate().is_ok());
296
297 let wrong_bls_sig =
298 sign_address_bls(&BLSKeyPair::generate(&mut rand::thread_rng()), address);
299 let mut bad_bls_event = valid_event.clone();
300 bad_bls_event.blsSig = G1PointSol::from(wrong_bls_sig).into();
301 assert!(bad_bls_event.authenticate().is_err());
302
303 let wrong_schnorr_sig = sign_address_schnorr(&StateKeyPair::generate(), address);
304 let mut bad_schnorr_event = valid_event.clone();
305 bad_schnorr_event.schnorrSig = StateSignatureSol::from(wrong_schnorr_sig).into();
306 assert!(bad_schnorr_event.authenticate().is_err());
307 }
308 }
309
310 #[test]
311 fn test_zero_g2_bls_pubkey_conversion_is_err() {
312 let zero_g2 = G2PointSol {
313 x0: U256::ZERO,
314 x1: U256::ZERO,
315 y0: U256::ZERO,
316 y1: U256::ZERO,
317 };
318 assert!(BLSPubKey::try_from(zero_g2).is_err());
319 }
320
321 #[test]
322 fn test_zero_g2_authenticate_returns_err_not_panic() {
323 use alloy::primitives::Bytes;
324 let event = ConsensusKeysUpdatedV2 {
325 account: Address::random(),
326 blsVK: G2PointSol {
327 x0: U256::ZERO,
328 x1: U256::ZERO,
329 y0: U256::ZERO,
330 y1: U256::ZERO,
331 },
332 schnorrVK: StateKeyPair::generate().ver_key().into(),
333 blsSig: G1PointSol {
334 x: U256::ZERO,
335 y: U256::ZERO,
336 }
337 .into(),
338 schnorrSig: Bytes::from(vec![0u8; 64]),
339 };
340 assert!(event.authenticate().is_err());
341 }
342
343 #[test]
344 fn test_consensus_keys_updated_event_authentication() {
345 for _ in 0..10 {
346 let bls_key_pair = BLSKeyPair::generate(&mut rand::thread_rng());
347 let schnorr_key_pair = StateKeyPair::generate();
348 let address = Address::random();
349
350 let bls_sig = sign_address_bls(&bls_key_pair, address);
351 let schnorr_sig = sign_address_schnorr(&schnorr_key_pair, address);
352
353 let valid_event = ConsensusKeysUpdatedV2 {
354 account: address,
355 blsVK: bls_key_pair.ver_key().into(),
356 schnorrVK: schnorr_key_pair.ver_key().into(),
357 blsSig: G1PointSol::from(bls_sig.clone()).into(),
358 schnorrSig: StateSignatureSol::from(schnorr_sig.clone()).into(),
359 };
360 let (bls, schnorr) = valid_event.authenticate().expect("valid keys parse");
361 assert_eq!(bls, bls_key_pair.ver_key());
362 assert_eq!(schnorr, schnorr_key_pair.ver_key());
363
364 let wrong_bls_sig =
365 sign_address_bls(&BLSKeyPair::generate(&mut rand::thread_rng()), address);
366 let mut bad_bls_event = valid_event.clone();
367 bad_bls_event.blsSig = G1PointSol::from(wrong_bls_sig).into();
368 assert!(bad_bls_event.authenticate().is_err());
369
370 let wrong_schnorr_sig = sign_address_schnorr(&StateKeyPair::generate(), address);
371 let mut bad_schnorr_event = valid_event.clone();
372 bad_schnorr_event.schnorrSig = StateSignatureSol::from(wrong_schnorr_sig).into();
373 assert!(bad_schnorr_event.authenticate().is_err());
374 }
375 }
376
377 #[test]
378 fn test_zero_schnorr_verkey_conversion_is_err() {
379 let zero_ed = EdOnBN254PointSol {
380 x: U256::ZERO,
381 y: U256::ZERO,
382 };
383 assert!(StateVerKey::try_from(zero_ed).is_err());
384 }
385
386 #[test]
387 fn test_zero_schnorr_authenticate_returns_err_not_panic() {
388 use alloy::primitives::Bytes;
389 let bls_key_pair = BLSKeyPair::generate(&mut rand::thread_rng());
390 let address = Address::random();
391 let bls_sig = sign_address_bls(&bls_key_pair, address);
392 let event = ValidatorRegisteredV2 {
393 account: address,
394 blsVK: bls_key_pair.ver_key().into(),
395 schnorrVK: EdOnBN254PointSol {
396 x: U256::ZERO,
397 y: U256::ZERO,
398 },
399 commission: 0,
400 blsSig: G1PointSol::from(bls_sig).into(),
401 schnorrSig: Bytes::from(vec![0u8; 64]),
402 metadataUri: String::new(),
403 };
404 assert!(event.authenticate().is_err());
405 }
406
407 #[test]
410 fn test_schnorr_verkey_out_of_range_is_err() {
411 let out_of_range = EdOnBN254PointSol {
412 x: U256::MAX,
413 y: U256::from(1),
414 };
415 assert!(StateVerKey::try_from(out_of_range).is_err());
416 }
417
418 #[test]
421 fn test_schnorr_identity_point_is_err() {
422 let identity = EdOnBN254PointSol {
423 x: U256::ZERO,
424 y: U256::from(1),
425 };
426 let point: ark_ed_on_bn254::EdwardsAffine = identity.into();
427 assert!(point.is_on_curve());
428 assert!(point.is_in_correct_subgroup_assuming_on_curve());
429 assert!(StateVerKey::try_from(identity).is_err());
430 }
431
432 #[test]
434 fn test_valid_schnorr_verkey_roundtrips() {
435 let vk = StateKeyPair::generate().ver_key();
436 let sol: EdOnBN254PointSol = vk.clone().into();
437 assert_eq!(StateVerKey::try_from(sol).expect("valid key"), vk);
438 }
439}
440
441#[cfg(test)]
446mod proptest_p2p_addr {
447 use alloy::providers::ProviderBuilder;
448 use hotshot_types::addr::NetAddr;
449 use proptest::{
450 prelude::*,
451 test_runner::{Config as ProptestConfig, TestRunner},
452 };
453
454 use crate::sol_types::StakeTableV3;
455
456 fn p2p_addr_strategy() -> impl Strategy<Value = String> {
457 prop_oneof![
458 (1..255u8, 1..255u8, 1..255u8, 1..255u8, 1..65535u16)
460 .prop_map(|(a, b, c, d, p)| format!("{a}.{b}.{c}.{d}:{p}")),
461 ("[a-z][a-z0-9.-]{0,20}", 1..65535u16).prop_map(|(h, p)| format!("{h}:{p}")),
463 "[a-z][a-z0-9.-]{0,20}".prop_map(|h| h.to_string()),
465 Just("".to_string()),
467 "[a-z]{1,10}".prop_map(|h| format!("{h}:0")),
469 "[a-z]{1,10}".prop_map(|h| format!("{h}:08080")),
471 "[a-z]{1,10}".prop_map(|h| format!("{h}:99999")),
473 ("[a-z]{1,10}", "[a-z]{1,5}").prop_map(|(h, p)| format!("{h}:{p}")),
475 Just(":".to_string()),
477 (1..65535u16).prop_map(|p| format!(":{p}")),
479 (1..65535u16).prop_map(|p| format!("::1:{p}")),
481 (1..65535u16).prop_map(|p| format!("[::1]:{p}")),
483 (1..255u8, 1..255u8, 1..65535u16, 1..65535u16)
485 .prop_map(|(a, b, p1, p2)| format!("{a}.{b}.0.1:{p1},{a}.{b}.0.2:{p2}")),
486 ("[a-z]{1,5}", 1..65535u16).prop_map(|(h, p)| format!("{h}_name:{p}")),
488 ("[a-z]{1,5}", 1..65535u16).prop_map(|(h, p)| format!(" {h}:{p} ")),
490 "[a-z]{1,5}".prop_map(|h| format!("{h}::8080")),
492 (400..600usize, 1..65535u16).prop_map(|(len, p)| format!("{}:{p}", "a".repeat(len))),
494 prop::collection::vec(any::<u8>(), 0..512)
496 .prop_map(|v| String::from_utf8_lossy(&v).to_string()),
497 ]
498 }
499
500 #[test]
501 fn solidity_rust_p2p_validation_equivalence() {
502 let rt = tokio::runtime::Runtime::new().unwrap();
503
504 let provider = ProviderBuilder::new().connect_anvil_with_wallet();
505 let contract_addr = rt.block_on(async {
506 let contract = StakeTableV3::deploy(&provider).await.unwrap();
507 *contract.address()
508 });
509 let contract = StakeTableV3::new(contract_addr, &provider);
510
511 let cases = std::env::var("PROPTEST_CASES")
512 .ok()
513 .and_then(|v| v.parse().ok())
514 .unwrap_or(512);
515 let mut runner = TestRunner::new(ProptestConfig {
516 cases,
517 ..ProptestConfig::default()
518 });
519
520 runner
521 .run(&p2p_addr_strategy(), |addr_str| {
522 let (sol_valid, rust_valid) = rt.block_on(async {
523 let sol_valid = contract
524 .validateP2pAddr(addr_str.clone())
525 .call()
526 .await
527 .is_ok();
528 let rust_valid = addr_str.parse::<NetAddr>().is_ok();
529 (sol_valid, rust_valid)
530 });
531
532 if sol_valid {
533 prop_assert!(
534 rust_valid,
535 "Solidity accepted '{}' but Rust rejected it",
536 addr_str
537 );
538 }
539 Ok(())
540 })
541 .unwrap();
542 }
543}