hotshot_contract_adapter/
stake_table.rs

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::{hash_bytes_to_field, StateKeyPair, StateSignature, StateVerKey},
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::{
21    StakeTableV2::{getVersionReturn, ConsensusKeysUpdatedV2, ValidatorRegisteredV2},
22    *,
23};
24
25// Allows us to implement From on existing Bytes type
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct StateSignatureSol(pub Bytes);
28
29#[derive(Debug, Clone, Copy, Default)]
30pub enum StakeTableContractVersion {
31    V1,
32    #[default]
33    V2,
34}
35
36impl TryFrom<getVersionReturn> for StakeTableContractVersion {
37    type Error = anyhow::Error;
38
39    fn try_from(value: getVersionReturn) -> anyhow::Result<Self> {
40        match value.majorVersion {
41            1 => Ok(StakeTableContractVersion::V1),
42            2 => Ok(StakeTableContractVersion::V2),
43            _ => anyhow::bail!("Unsupported stake table contract version: {:?}", value),
44        }
45    }
46}
47
48impl From<G2PointSol> for BLSPubKey {
49    fn from(value: G2PointSol) -> Self {
50        let point: G2Affine = value.into();
51        let mut bytes = vec![];
52        point
53            .into_group()
54            .serialize_uncompressed(&mut bytes)
55            .unwrap();
56        Self::deserialize_uncompressed(&bytes[..]).unwrap()
57    }
58}
59
60impl From<BLSPubKey> for G2PointSol {
61    fn from(value: BLSPubKey) -> Self {
62        value.to_affine().into()
63    }
64}
65
66impl From<EdOnBN254PointSol> for StateVerKey {
67    fn from(value: EdOnBN254PointSol) -> Self {
68        let point: ark_ed_on_bn254::EdwardsAffine = value.into();
69        Self::from(point)
70    }
71}
72
73impl From<bls_over_bn254::Signature> for G1PointSol {
74    fn from(sig: bls_over_bn254::Signature) -> Self {
75        sig.sigma.into_affine().into()
76    }
77}
78
79impl From<StateVerKey> for EdOnBN254PointSol {
80    fn from(ver_key: StateVerKey) -> Self {
81        ver_key.to_affine().into()
82    }
83}
84
85impl From<StateSignature> for StateSignatureSol {
86    fn from(sig: StateSignature) -> Self {
87        let mut buf = vec![];
88        sig.serialize_compressed(&mut buf).expect("serialize works");
89        Self(buf.into())
90    }
91}
92
93impl From<StateSignatureSol> for Bytes {
94    fn from(sig_sol: StateSignatureSol) -> Self {
95        sig_sol.0
96    }
97}
98
99pub fn sign_address_bls(bls_key_pair: &BLSKeyPair, address: Address) -> bls_over_bn254::Signature {
100    bls_key_pair.sign(&address.abi_encode(), CS_ID_BLS_BN254)
101}
102
103pub fn sign_address_schnorr(schnorr_key_pair: &StateKeyPair, address: Address) -> StateSignature {
104    let msg = [hash_bytes_to_field(&address.abi_encode()).expect("hash to field works")];
105    schnorr_key_pair.sign(&msg, CS_ID_SCHNORR)
106}
107
108/// Authenticate a Schnorr signature over an Ethereum address
109pub fn authenticate_schnorr_sig(
110    schnorr_vk: &StateVerKey,
111    address: Address,
112    schnorr_sig: &StateSignature,
113) -> Result<(), StakeTableSolError> {
114    let msg = [hash_bytes_to_field(&address.abi_encode()).expect("hash to field works")];
115    schnorr_vk.verify(&msg, schnorr_sig, CS_ID_SCHNORR)?;
116    Ok(())
117}
118
119/// Authenticate a BLS signature over an Ethereum address
120pub fn authenticate_bls_sig(
121    bls_vk: &BLSPubKey,
122    address: Address,
123    bls_sig: &BLSSignature,
124) -> Result<(), StakeTableSolError> {
125    let msg = address.abi_encode();
126    if !bls_vk.validate(bls_sig, &msg) {
127        return Err(StakeTableSolError::InvalidBlsSignature);
128    }
129    Ok(())
130}
131
132fn authenticate_stake_table_validator_event(
133    account: Address,
134    bls_vk: G2PointSol,
135    schnorr_vk: EdOnBN254PointSol,
136    bls_sig: G1PointSol,
137    schnorr_sig: &[u8],
138) -> Result<(), StakeTableSolError> {
139    // TODO(alex): simplify this once jellyfish has `VerKey::from_affine()`
140    let bls_vk = {
141        let bls_vk_inner: ark_bn254::G2Affine = bls_vk.into();
142        let bls_vk_inner = bls_vk_inner.into_group();
143
144        // the two unwrap are safe since it's BLSPubKey is just a wrapper around G2Projective
145        let mut ser_bytes: Vec<u8> = Vec::new();
146        bls_vk_inner.serialize_uncompressed(&mut ser_bytes).unwrap();
147        BLSPubKey::deserialize_uncompressed(&ser_bytes[..]).unwrap()
148    };
149    let bls_sig_jellyfish = {
150        let sigma_affine: ark_bn254::G1Affine = bls_sig.into();
151        BLSSignature {
152            sigma: sigma_affine.into_group(),
153        }
154    };
155    authenticate_bls_sig(&bls_vk, account, &bls_sig_jellyfish)?;
156
157    let schnorr_vk: StateVerKey = schnorr_vk.into();
158    let schnorr_sig_jellyfish =
159        schnorr::Signature::<EdwardsConfig>::deserialize_compressed(schnorr_sig)?;
160    authenticate_schnorr_sig(&schnorr_vk, account, &schnorr_sig_jellyfish)?;
161    Ok(())
162}
163
164/// Errors encountered when processing stake table events
165#[derive(Debug, thiserror::Error)]
166pub enum StakeTableSolError {
167    #[error("Failed to deserialize Schnorr signature")]
168    SchnorrSigDeserializationError(#[from] ark_serialize::SerializationError),
169    #[error("BLS signature invalid")]
170    InvalidBlsSignature,
171    #[error("Schnorr signature invalid")]
172    InvalidSchnorrSignature(#[from] jf_signature::SignatureError),
173}
174
175impl ValidatorRegisteredV2 {
176    /// verified the BLS and Schnorr signatures in the event
177    pub fn authenticate(&self) -> Result<(), StakeTableSolError> {
178        authenticate_stake_table_validator_event(
179            self.account,
180            self.blsVK,
181            self.schnorrVK,
182            self.blsSig.into(),
183            &self.schnorrSig,
184        )?;
185        Ok(())
186    }
187}
188
189impl ConsensusKeysUpdatedV2 {
190    /// verified the BLS and Schnorr signatures in the event
191    pub fn authenticate(&self) -> Result<(), StakeTableSolError> {
192        authenticate_stake_table_validator_event(
193            self.account,
194            self.blsVK,
195            self.schnorrVK,
196            self.blsSig.into(),
197            &self.schnorrSig,
198        )?;
199        Ok(())
200    }
201}
202
203impl From<StakeTable::ValidatorRegistered> for StakeTableV2::InitialCommission {
204    fn from(value: StakeTable::ValidatorRegistered) -> Self {
205        Self {
206            validator: value.account,
207            commission: value.commission,
208        }
209    }
210}
211
212#[cfg(test)]
213mod test {
214    use alloy::primitives::Address;
215    use hotshot_types::{
216        light_client::StateKeyPair,
217        signature_key::{BLSKeyPair, BLSPrivKey, BLSPubKey},
218    };
219
220    use super::{sign_address_bls, sign_address_schnorr, StateSignatureSol};
221    use crate::sol_types::{
222        G1PointSol, G2PointSol,
223        StakeTableV2::{ConsensusKeysUpdatedV2, ValidatorRegisteredV2},
224    };
225
226    fn check_round_trip(pk: BLSPubKey) {
227        let g2 = G2PointSol::from(pk);
228        let pk2 = BLSPubKey::from(g2);
229        assert_eq!(pk2, pk, "Failed to roundtrip G2PointSol to BLSPubKey: {pk}");
230    }
231
232    #[test]
233    fn test_bls_g2_point_roundtrip() {
234        let mut rng = rand::thread_rng();
235        for _ in 0..100 {
236            let pk = (&BLSPrivKey::generate(&mut rng)).into();
237            check_round_trip(pk);
238        }
239    }
240
241    #[test]
242    fn test_bls_g2_point_alloy_migration_regression() {
243        // This pubkey fails the roundtrip if "serialize_{un,}compressed" are mixed
244        let s = "BLS_VER_KEY~JlRLUrn0T_MltAJXaaojwk_CnCgd0tyPny_IGdseMBLBPv9nWabIPAaS-aHmn0ARu5YZHJ7mfmGQ-alW42tkJM663Lse-Is80fyA1jnRxPsHcJDnO05oW1M1SC5LeE8sXITbuhmtG2JdTAgmLqWOxbMRmVIqS1AQXqvGGXdo5qpd";
245        let pk: BLSPubKey = s.parse().unwrap();
246        check_round_trip(pk);
247    }
248
249    #[test]
250    fn test_validator_registered_event_authentication() {
251        for _ in 0..10 {
252            let bls_key_pair = BLSKeyPair::generate(&mut rand::thread_rng());
253            let schnorr_key_pair = StateKeyPair::generate();
254            let address = Address::random();
255
256            let bls_sig = sign_address_bls(&bls_key_pair, address);
257            let schnorr_sig = sign_address_schnorr(&schnorr_key_pair, address);
258
259            let valid_event = ValidatorRegisteredV2 {
260                account: address,
261                blsVK: bls_key_pair.ver_key().into(),
262                schnorrVK: schnorr_key_pair.ver_key().into(),
263                commission: 1000, // 10%
264                blsSig: G1PointSol::from(bls_sig.clone()).into(),
265                schnorrSig: StateSignatureSol::from(schnorr_sig.clone()).into(),
266            };
267            assert!(valid_event.authenticate().is_ok());
268
269            let wrong_bls_sig =
270                sign_address_bls(&BLSKeyPair::generate(&mut rand::thread_rng()), address);
271            let mut bad_bls_event = valid_event.clone();
272            bad_bls_event.blsSig = G1PointSol::from(wrong_bls_sig).into();
273            assert!(bad_bls_event.authenticate().is_err());
274
275            let wrong_schnorr_sig = sign_address_schnorr(&StateKeyPair::generate(), address);
276            let mut bad_schnorr_event = valid_event.clone();
277            bad_schnorr_event.schnorrSig = StateSignatureSol::from(wrong_schnorr_sig).into();
278            assert!(bad_schnorr_event.authenticate().is_err());
279        }
280    }
281
282    #[test]
283    fn test_consensus_keys_updated_event_authentication() {
284        for _ in 0..10 {
285            let bls_key_pair = BLSKeyPair::generate(&mut rand::thread_rng());
286            let schnorr_key_pair = StateKeyPair::generate();
287            let address = Address::random();
288
289            let bls_sig = sign_address_bls(&bls_key_pair, address);
290            let schnorr_sig = sign_address_schnorr(&schnorr_key_pair, address);
291
292            let valid_event = ConsensusKeysUpdatedV2 {
293                account: address,
294                blsVK: bls_key_pair.ver_key().into(),
295                schnorrVK: schnorr_key_pair.ver_key().into(),
296                blsSig: G1PointSol::from(bls_sig.clone()).into(),
297                schnorrSig: StateSignatureSol::from(schnorr_sig.clone()).into(),
298            };
299            assert!(valid_event.authenticate().is_ok());
300
301            let wrong_bls_sig =
302                sign_address_bls(&BLSKeyPair::generate(&mut rand::thread_rng()), address);
303            let mut bad_bls_event = valid_event.clone();
304            bad_bls_event.blsSig = G1PointSol::from(wrong_bls_sig).into();
305            assert!(bad_bls_event.authenticate().is_err());
306
307            let wrong_schnorr_sig = sign_address_schnorr(&StateKeyPair::generate(), address);
308            let mut bad_schnorr_event = valid_event.clone();
309            bad_schnorr_event.schnorrSig = StateSignatureSol::from(wrong_schnorr_sig).into();
310            assert!(bad_schnorr_event.authenticate().is_err());
311        }
312    }
313}