hotshot_contract_adapter/
light_client.rs1use 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 pub fn dummy_genesis() -> Self {
41 Self {
42 viewNum: 0,
43 blockHeight: 0,
44 blockCommRoot: U256::from(42),
45 }
46 }
47
48 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 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 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
140pub 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
162pub 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 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}