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::sol_types::{StakeTableV3, *};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct StateSignatureSol(pub Bytes);
25
26#[derive(Debug, Clone, Copy, Default)]
27pub enum StakeTableContractVersion {
28 V1,
29 V2,
30 #[default]
31 V3,
32}
33
34fn version_from_major(major: u8) -> anyhow::Result<StakeTableContractVersion> {
35 match major {
36 1 => Ok(StakeTableContractVersion::V1),
37 2 => Ok(StakeTableContractVersion::V2),
38 3 => Ok(StakeTableContractVersion::V3),
39 _ => anyhow::bail!("Unsupported stake table contract version: {major}"),
40 }
41}
42
43impl TryFrom<StakeTableV3::getVersionReturn> for StakeTableContractVersion {
44 type Error = anyhow::Error;
45
46 fn try_from(value: StakeTableV3::getVersionReturn) -> anyhow::Result<Self> {
47 version_from_major(value.majorVersion)
48 }
49}
50
51impl From<G2PointSol> for BLSPubKey {
52 fn from(value: G2PointSol) -> Self {
53 let point: G2Affine = value.into();
54 let mut bytes = vec![];
55 point
56 .into_group()
57 .serialize_uncompressed(&mut bytes)
58 .unwrap();
59 Self::deserialize_uncompressed(&bytes[..]).unwrap()
60 }
61}
62
63impl From<BLSPubKey> for G2PointSol {
64 fn from(value: BLSPubKey) -> Self {
65 value.to_affine().into()
66 }
67}
68
69impl From<EdOnBN254PointSol> for StateVerKey {
70 fn from(value: EdOnBN254PointSol) -> Self {
71 let point: ark_ed_on_bn254::EdwardsAffine = value.into();
72 Self::from(point)
73 }
74}
75
76impl From<bls_over_bn254::Signature> for G1PointSol {
77 fn from(sig: bls_over_bn254::Signature) -> Self {
78 sig.sigma.into_affine().into()
79 }
80}
81
82impl From<StateVerKey> for EdOnBN254PointSol {
83 fn from(ver_key: StateVerKey) -> Self {
84 ver_key.to_affine().into()
85 }
86}
87
88impl From<StateSignature> for StateSignatureSol {
89 fn from(sig: StateSignature) -> Self {
90 let mut buf = vec![];
91 sig.serialize_compressed(&mut buf).expect("serialize works");
92 Self(buf.into())
93 }
94}
95
96impl From<StateSignatureSol> for Bytes {
97 fn from(sig_sol: StateSignatureSol) -> Self {
98 sig_sol.0
99 }
100}
101
102pub fn sign_address_bls(bls_key_pair: &BLSKeyPair, address: Address) -> bls_over_bn254::Signature {
103 bls_key_pair.sign(&address.abi_encode(), CS_ID_BLS_BN254)
104}
105
106pub fn sign_address_schnorr(schnorr_key_pair: &StateKeyPair, address: Address) -> StateSignature {
107 let msg = [hash_bytes_to_field(&address.abi_encode()).expect("hash to field works")];
108 schnorr_key_pair.sign(&msg, CS_ID_SCHNORR)
109}
110
111pub fn authenticate_schnorr_sig(
113 schnorr_vk: &StateVerKey,
114 address: Address,
115 schnorr_sig: &StateSignature,
116) -> Result<(), StakeTableSolError> {
117 let msg = [hash_bytes_to_field(&address.abi_encode()).expect("hash to field works")];
118 schnorr_vk.verify(&msg, schnorr_sig, CS_ID_SCHNORR)?;
119 Ok(())
120}
121
122pub fn authenticate_bls_sig(
124 bls_vk: &BLSPubKey,
125 address: Address,
126 bls_sig: &BLSSignature,
127) -> Result<(), StakeTableSolError> {
128 let msg = address.abi_encode();
129 if !bls_vk.validate(bls_sig, &msg) {
130 return Err(StakeTableSolError::InvalidBlsSignature);
131 }
132 Ok(())
133}
134
135fn authenticate_stake_table_validator_event(
136 account: Address,
137 bls_vk: G2PointSol,
138 schnorr_vk: EdOnBN254PointSol,
139 bls_sig: G1PointSol,
140 schnorr_sig: &[u8],
141) -> Result<(), StakeTableSolError> {
142 let bls_vk = {
144 let bls_vk_inner: ark_bn254::G2Affine = bls_vk.into();
145 let bls_vk_inner = bls_vk_inner.into_group();
146
147 let mut ser_bytes: Vec<u8> = Vec::new();
149 bls_vk_inner.serialize_uncompressed(&mut ser_bytes).unwrap();
150 BLSPubKey::deserialize_uncompressed(&ser_bytes[..]).unwrap()
151 };
152 let bls_sig_jellyfish = {
153 let sigma_affine: ark_bn254::G1Affine = bls_sig.into();
154 BLSSignature {
155 sigma: sigma_affine.into_group(),
156 }
157 };
158 authenticate_bls_sig(&bls_vk, account, &bls_sig_jellyfish)?;
159
160 let schnorr_vk: StateVerKey = schnorr_vk.into();
161 let schnorr_sig_jellyfish =
162 schnorr::Signature::<EdwardsConfig>::deserialize_compressed(schnorr_sig)?;
163 authenticate_schnorr_sig(&schnorr_vk, account, &schnorr_sig_jellyfish)?;
164 Ok(())
165}
166
167#[derive(Debug, thiserror::Error)]
169pub enum StakeTableSolError {
170 #[error("Failed to deserialize Schnorr signature")]
171 SchnorrSigDeserializationError(#[from] ark_serialize::SerializationError),
172 #[error("BLS signature invalid")]
173 InvalidBlsSignature,
174 #[error("Schnorr signature invalid")]
175 InvalidSchnorrSignature(#[from] jf_signature::SignatureError),
176}
177
178impl StakeTableV3::ValidatorRegisteredV3 {
179 pub fn authenticate(&self) -> Result<(), StakeTableSolError> {
181 authenticate_stake_table_validator_event(
182 self.account,
183 self.blsVK,
184 self.schnorrVK,
185 self.blsSig.into(),
186 &self.schnorrSig,
187 )
188 }
189}
190
191impl StakeTableV3::ValidatorRegisteredV2 {
192 pub fn authenticate(&self) -> Result<(), StakeTableSolError> {
193 authenticate_stake_table_validator_event(
194 self.account,
195 self.blsVK,
196 self.schnorrVK,
197 self.blsSig.into(),
198 &self.schnorrSig,
199 )
200 }
201}
202
203impl StakeTableV3::ConsensusKeysUpdatedV2 {
204 pub fn authenticate(&self) -> Result<(), StakeTableSolError> {
205 authenticate_stake_table_validator_event(
206 self.account,
207 self.blsVK,
208 self.schnorrVK,
209 self.blsSig.into(),
210 &self.schnorrSig,
211 )
212 }
213}
214
215#[cfg(test)]
216mod test {
217 use alloy::primitives::Address;
218 use hotshot_types::{
219 light_client::StateKeyPair,
220 signature_key::{BLSKeyPair, BLSPrivKey, BLSPubKey},
221 };
222
223 use super::{StateSignatureSol, sign_address_bls, sign_address_schnorr};
224 use crate::sol_types::{
225 G1PointSol, G2PointSol,
226 StakeTableV3::{ConsensusKeysUpdatedV2, ValidatorRegisteredV2},
227 };
228
229 fn check_round_trip(pk: BLSPubKey) {
230 let g2 = G2PointSol::from(pk);
231 let pk2 = BLSPubKey::from(g2);
232 assert_eq!(pk2, pk, "Failed to roundtrip G2PointSol to BLSPubKey: {pk}");
233 }
234
235 #[test]
236 fn test_bls_g2_point_roundtrip() {
237 let mut rng = rand::thread_rng();
238 for _ in 0..100 {
239 let pk = (&BLSPrivKey::generate(&mut rng)).into();
240 check_round_trip(pk);
241 }
242 }
243
244 #[test]
245 fn test_bls_g2_point_alloy_migration_regression() {
246 let s = "BLS_VER_KEY~JlRLUrn0T_MltAJXaaojwk_CnCgd0tyPny_IGdseMBLBPv9nWabIPAaS-aHmn0ARu5YZHJ7mfmGQ-alW42tkJM663Lse-Is80fyA1jnRxPsHcJDnO05oW1M1SC5LeE8sXITbuhmtG2JdTAgmLqWOxbMRmVIqS1AQXqvGGXdo5qpd";
248 let pk: BLSPubKey = s.parse().unwrap();
249 check_round_trip(pk);
250 }
251
252 #[test]
253 fn test_validator_registered_event_authentication() {
254 for _ in 0..10 {
255 let bls_key_pair = BLSKeyPair::generate(&mut rand::thread_rng());
256 let schnorr_key_pair = StateKeyPair::generate();
257 let address = Address::random();
258
259 let bls_sig = sign_address_bls(&bls_key_pair, address);
260 let schnorr_sig = sign_address_schnorr(&schnorr_key_pair, address);
261
262 let valid_event = ValidatorRegisteredV2 {
263 account: address,
264 blsVK: bls_key_pair.ver_key().into(),
265 schnorrVK: schnorr_key_pair.ver_key().into(),
266 commission: 1000, blsSig: G1PointSol::from(bls_sig.clone()).into(),
268 schnorrSig: StateSignatureSol::from(schnorr_sig.clone()).into(),
269 metadataUri: "dummy-meta".to_string(),
270 };
271 assert!(valid_event.authenticate().is_ok());
272
273 let wrong_bls_sig =
274 sign_address_bls(&BLSKeyPair::generate(&mut rand::thread_rng()), address);
275 let mut bad_bls_event = valid_event.clone();
276 bad_bls_event.blsSig = G1PointSol::from(wrong_bls_sig).into();
277 assert!(bad_bls_event.authenticate().is_err());
278
279 let wrong_schnorr_sig = sign_address_schnorr(&StateKeyPair::generate(), address);
280 let mut bad_schnorr_event = valid_event.clone();
281 bad_schnorr_event.schnorrSig = StateSignatureSol::from(wrong_schnorr_sig).into();
282 assert!(bad_schnorr_event.authenticate().is_err());
283 }
284 }
285
286 #[test]
287 fn test_consensus_keys_updated_event_authentication() {
288 for _ in 0..10 {
289 let bls_key_pair = BLSKeyPair::generate(&mut rand::thread_rng());
290 let schnorr_key_pair = StateKeyPair::generate();
291 let address = Address::random();
292
293 let bls_sig = sign_address_bls(&bls_key_pair, address);
294 let schnorr_sig = sign_address_schnorr(&schnorr_key_pair, address);
295
296 let valid_event = ConsensusKeysUpdatedV2 {
297 account: address,
298 blsVK: bls_key_pair.ver_key().into(),
299 schnorrVK: schnorr_key_pair.ver_key().into(),
300 blsSig: G1PointSol::from(bls_sig.clone()).into(),
301 schnorrSig: StateSignatureSol::from(schnorr_sig.clone()).into(),
302 };
303 assert!(valid_event.authenticate().is_ok());
304
305 let wrong_bls_sig =
306 sign_address_bls(&BLSKeyPair::generate(&mut rand::thread_rng()), address);
307 let mut bad_bls_event = valid_event.clone();
308 bad_bls_event.blsSig = G1PointSol::from(wrong_bls_sig).into();
309 assert!(bad_bls_event.authenticate().is_err());
310
311 let wrong_schnorr_sig = sign_address_schnorr(&StateKeyPair::generate(), address);
312 let mut bad_schnorr_event = valid_event.clone();
313 bad_schnorr_event.schnorrSig = StateSignatureSol::from(wrong_schnorr_sig).into();
314 assert!(bad_schnorr_event.authenticate().is_err());
315 }
316 }
317}
318
319#[cfg(test)]
324mod proptest_p2p_addr {
325 use alloy::providers::ProviderBuilder;
326 use hotshot_types::addr::NetAddr;
327 use proptest::{
328 prelude::*,
329 test_runner::{Config as ProptestConfig, TestRunner},
330 };
331
332 use crate::sol_types::StakeTableV3;
333
334 fn p2p_addr_strategy() -> impl Strategy<Value = String> {
335 prop_oneof![
336 (1..255u8, 1..255u8, 1..255u8, 1..255u8, 1..65535u16)
338 .prop_map(|(a, b, c, d, p)| format!("{a}.{b}.{c}.{d}:{p}")),
339 ("[a-z][a-z0-9.-]{0,20}", 1..65535u16).prop_map(|(h, p)| format!("{h}:{p}")),
341 "[a-z][a-z0-9.-]{0,20}".prop_map(|h| h.to_string()),
343 Just("".to_string()),
345 "[a-z]{1,10}".prop_map(|h| format!("{h}:0")),
347 "[a-z]{1,10}".prop_map(|h| format!("{h}:08080")),
349 "[a-z]{1,10}".prop_map(|h| format!("{h}:99999")),
351 ("[a-z]{1,10}", "[a-z]{1,5}").prop_map(|(h, p)| format!("{h}:{p}")),
353 Just(":".to_string()),
355 (1..65535u16).prop_map(|p| format!(":{p}")),
357 (1..65535u16).prop_map(|p| format!("::1:{p}")),
359 (1..65535u16).prop_map(|p| format!("[::1]:{p}")),
361 (1..255u8, 1..255u8, 1..65535u16, 1..65535u16)
363 .prop_map(|(a, b, p1, p2)| format!("{a}.{b}.0.1:{p1},{a}.{b}.0.2:{p2}")),
364 ("[a-z]{1,5}", 1..65535u16).prop_map(|(h, p)| format!("{h}_name:{p}")),
366 ("[a-z]{1,5}", 1..65535u16).prop_map(|(h, p)| format!(" {h}:{p} ")),
368 "[a-z]{1,5}".prop_map(|h| format!("{h}::8080")),
370 (400..600usize, 1..65535u16).prop_map(|(len, p)| format!("{}:{p}", "a".repeat(len))),
372 prop::collection::vec(any::<u8>(), 0..512)
374 .prop_map(|v| String::from_utf8_lossy(&v).to_string()),
375 ]
376 }
377
378 #[test]
379 fn solidity_rust_p2p_validation_equivalence() {
380 let rt = tokio::runtime::Runtime::new().unwrap();
381
382 let provider = ProviderBuilder::new().connect_anvil_with_wallet();
383 let contract_addr = rt.block_on(async {
384 let contract = StakeTableV3::deploy(&provider).await.unwrap();
385 *contract.address()
386 });
387 let contract = StakeTableV3::new(contract_addr, &provider);
388
389 let cases = std::env::var("PROPTEST_CASES")
390 .ok()
391 .and_then(|v| v.parse().ok())
392 .unwrap_or(512);
393 let mut runner = TestRunner::new(ProptestConfig {
394 cases,
395 ..ProptestConfig::default()
396 });
397
398 runner
399 .run(&p2p_addr_strategy(), |addr_str| {
400 let (sol_valid, rust_valid) = rt.block_on(async {
401 let sol_valid = contract
402 .validateP2pAddr(addr_str.clone())
403 .call()
404 .await
405 .is_ok();
406 let rust_valid = addr_str.parse::<NetAddr>().is_ok();
407 (sol_valid, rust_valid)
408 });
409
410 if sol_valid {
411 prop_assert!(
412 rust_valid,
413 "Solidity accepted '{}' but Rust rejected it",
414 addr_str
415 );
416 }
417 Ok(())
418 })
419 .unwrap();
420 }
421}