Skip to main content

hotshot_contract_adapter/
light_client.rs

1//! Helpers and test mocks for Light Client logic
2
3use std::collections::{HashMap, HashSet};
4
5use alloy::{
6    primitives::{FixedBytes, U256},
7    sol_types::SolValue,
8};
9use ark_ff::PrimeField;
10use hotshot_types::{
11    data::ViewNumber,
12    epoch_membership::EpochMembershipCoordinator,
13    light_client::{
14        CircuitField, GenericLightClientState, GenericStakeTableState, LightClientState,
15        StakeTableState,
16    },
17    message::UpgradeLock,
18    simple_certificate::LightClientStateUpdateCertificateV2,
19    simple_vote::HasEpoch,
20    traits::{
21        node_implementation::NodeType,
22        signature_key::{LCV2StateSignatureKey, LCV3StateSignatureKey, StakeTableEntryType},
23    },
24};
25use hotshot_utils::anytrace::*;
26use rand::Rng;
27
28use crate::{
29    field_to_u256,
30    sol_types::{LightClient, LightClientStateSol, StakeTableStateSol},
31    u256_to_field,
32};
33
34impl LightClientStateSol {
35    /// Return a dummy new genesis that will pass constructor/initializer sanity checks
36    /// in the contract.
37    ///
38    /// # Warning
39    /// NEVER use this for production, this is test only.
40    pub fn dummy_genesis() -> Self {
41        Self {
42            viewNum: 0,
43            blockHeight: 0,
44            blockCommRoot: U256::from(42),
45        }
46    }
47
48    /// Return a random value
49    pub fn rand<R: Rng>(rng: &mut R) -> Self {
50        Self {
51            viewNum: rng.r#gen::<u64>(),
52            blockHeight: rng.r#gen::<u64>(),
53            blockCommRoot: U256::from_limbs(rng.r#gen::<[u64; 4]>()),
54        }
55    }
56}
57
58impl From<LightClient::finalizedStateReturn> for LightClientStateSol {
59    fn from(v: LightClient::finalizedStateReturn) -> Self {
60        let tuple: (u64, u64, U256) = v.into();
61        tuple.into()
62    }
63}
64
65impl<F: PrimeField> From<LightClientStateSol> for GenericLightClientState<F> {
66    fn from(v: LightClientStateSol) -> Self {
67        Self {
68            view_number: v.viewNum,
69            block_height: v.blockHeight,
70            block_comm_root: u256_to_field(v.blockCommRoot),
71        }
72    }
73}
74
75impl<F: PrimeField> From<GenericLightClientState<F>> for LightClientStateSol {
76    fn from(v: GenericLightClientState<F>) -> Self {
77        Self {
78            viewNum: v.view_number,
79            blockHeight: v.block_height,
80            blockCommRoot: field_to_u256(v.block_comm_root),
81        }
82    }
83}
84
85impl StakeTableStateSol {
86    /// Return a dummy new genesis stake state that will pass constructor/initializer sanity checks
87    /// in the contract.
88    ///
89    /// # Warning
90    /// NEVER use this for production, this is test only.
91    pub fn dummy_genesis() -> Self {
92        Self {
93            threshold: U256::from(1),
94            blsKeyComm: U256::from(123),
95            schnorrKeyComm: U256::from(123),
96            amountComm: U256::from(20),
97        }
98    }
99
100    /// Returns a random value
101    pub fn rand<R: Rng>(rng: &mut R) -> Self {
102        Self {
103            threshold: U256::from_limbs(rng.r#gen::<[u64; 4]>()),
104            blsKeyComm: U256::from_limbs(rng.r#gen::<[u64; 4]>()),
105            schnorrKeyComm: U256::from_limbs(rng.r#gen::<[u64; 4]>()),
106            amountComm: U256::from_limbs(rng.r#gen::<[u64; 4]>()),
107        }
108    }
109}
110
111impl From<LightClient::genesisStakeTableStateReturn> for StakeTableStateSol {
112    fn from(v: LightClient::genesisStakeTableStateReturn) -> Self {
113        let tuple: (U256, U256, U256, U256) = v.into();
114        tuple.into()
115    }
116}
117
118impl<F: PrimeField> From<StakeTableStateSol> for GenericStakeTableState<F> {
119    fn from(s: StakeTableStateSol) -> Self {
120        Self {
121            threshold: u256_to_field(s.threshold),
122            bls_key_comm: u256_to_field(s.blsKeyComm),
123            schnorr_key_comm: u256_to_field(s.schnorrKeyComm),
124            amount_comm: u256_to_field(s.amountComm),
125        }
126    }
127}
128
129impl<F: PrimeField> From<GenericStakeTableState<F>> for StakeTableStateSol {
130    fn from(v: GenericStakeTableState<F>) -> Self {
131        Self {
132            blsKeyComm: field_to_u256(v.bls_key_comm),
133            schnorrKeyComm: field_to_u256(v.schnorr_key_comm),
134            amountComm: field_to_u256(v.amount_comm),
135            threshold: field_to_u256(v.threshold),
136        }
137    }
138}
139
140/// Derive the signed state digest used for LCV3 light-client signatures:
141/// `keccak256(abi.encodePacked(abi.encode(state) || abi.encode(stake) || abi.encode(auth_root)))`,
142/// converted to a `CircuitField`.
143pub fn derive_signed_state_digest(
144    lc_state: &LightClientState,
145    next_stake_state: &StakeTableState,
146    auth_root: &FixedBytes<32>,
147) -> CircuitField {
148    let lc_state_sol: LightClientStateSol = (*lc_state).into();
149    let stake_st_sol: StakeTableStateSol = (*next_stake_state).into();
150
151    let res = alloy::primitives::keccak256(
152        (
153            lc_state_sol.abi_encode(),
154            stake_st_sol.abi_encode(),
155            auth_root.abi_encode(),
156        )
157            .abi_encode_packed(),
158    );
159    CircuitField::from_be_bytes_mod_order(res.as_ref())
160}
161
162/// Validates a light client state update certificate:
163/// - every signer is in the voting stake table for the cert's epoch
164/// - each signature is valid (LCV2 always; LCV3 once post-DrbAndHeaderUpgrade)
165/// - the accumulated stake of signers meets the success threshold
166pub async fn validate_light_client_state_update_certificate<TYPES: NodeType>(
167    state_cert: &LightClientStateUpdateCertificateV2<TYPES>,
168    membership_coordinator: &EpochMembershipCoordinator<TYPES>,
169    upgrade_lock: &UpgradeLock<TYPES>,
170) -> Result<()> {
171    tracing::debug!("Validating light client state update certificate");
172
173    let epoch_membership = membership_coordinator.membership_for_epoch(state_cert.epoch())?;
174
175    let membership_stake_table = epoch_membership.stake_table();
176    let membership_success_threshold = epoch_membership.success_threshold();
177
178    let mut state_key_map = HashMap::new();
179    membership_stake_table.into_iter().for_each(|config| {
180        state_key_map.insert(
181            config.state_ver_key.clone(),
182            config.stake_table_entry.stake(),
183        );
184    });
185
186    let mut accumulated_stake = U256::from(0);
187    let mut seen_keys = HashSet::new();
188    let signed_state_digest = derive_signed_state_digest(
189        &state_cert.light_client_state,
190        &state_cert.next_stake_table_state,
191        &state_cert.auth_root,
192    );
193    for (key, sig, sig_v2) in state_cert.signatures.iter() {
194        if !seen_keys.insert(key.clone()) {
195            bail!("Duplicate signature for key: {key:?}");
196        }
197        if let Some(stake) = state_key_map.get(key) {
198            accumulated_stake += *stake;
199            #[allow(clippy::collapsible_else_if)]
200            // We only perform the second signature check prior to the DrbAndHeaderUpgrade
201            if !upgrade_lock
202                .proposal2_version(ViewNumber::new(state_cert.light_client_state.view_number))
203            {
204                if !<TYPES::StateSignatureKey as LCV2StateSignatureKey>::verify_state_sig(
205                    key,
206                    sig_v2,
207                    &state_cert.light_client_state,
208                    &state_cert.next_stake_table_state,
209                ) {
210                    bail!("Invalid light client state update certificate signature");
211                }
212            } else {
213                if !<TYPES::StateSignatureKey as LCV3StateSignatureKey>::verify_state_sig(
214                    key,
215                    sig,
216                    signed_state_digest,
217                ) || !<TYPES::StateSignatureKey as LCV2StateSignatureKey>::verify_state_sig(
218                    key,
219                    sig_v2,
220                    &state_cert.light_client_state,
221                    &state_cert.next_stake_table_state,
222                ) {
223                    bail!("Invalid light client state update certificate signature");
224                }
225            }
226        } else {
227            bail!("Invalid light client state update certificate signature");
228        }
229    }
230    if accumulated_stake < membership_success_threshold {
231        bail!("Light client state update certificate does not meet the success threshold");
232    }
233
234    Ok(())
235}