nym_validator_client/coconut/
mod.rs1use crate::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
5use crate::nyxd::error::NyxdError;
6use nym_coconut_dkg_common::types::{EpochId, NodeIndex};
7use nym_coconut_dkg_common::verification_key::ContractVKShare;
8use nym_compact_ecash::error::CompactEcashError;
9use nym_compact_ecash::{Base58, VerificationKeyAuth};
10use std::fmt::{Display, Formatter};
11use thiserror::Error;
12use tracing::warn;
13use url::Url;
14
15#[derive(Clone)]
17pub struct EcashApiClient {
18 pub api_client: nym_http_api_client::Client,
19 pub verification_key: VerificationKeyAuth,
20 pub node_id: NodeIndex,
21 pub cosmos_address: cosmrs::AccountId,
22}
23
24impl EcashApiClient {
25 pub fn try_construct_from_share(share: ContractVKShare) -> Result<Self, EcashApiError> {
26 if !share.verified {
27 return Err(EcashApiError::UnverifiedShare);
28 }
29
30 let url_address = Url::parse(&share.announce_address)?;
31
32 let api_client = nym_http_api_client::Client::builder(url_address)
39 .map_err(|e| EcashApiError::ClientError(e.to_string()))?
40 .build()
41 .map_err(|e| EcashApiError::ClientError(e.to_string()))?;
42
43 Ok(EcashApiClient {
44 api_client,
45 verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?,
46 node_id: share.node_index,
47 cosmos_address: share.owner.as_str().parse()?,
48 })
49 }
50}
51
52impl Display for EcashApiClient {
53 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
54 write!(
55 f,
56 "[id: {}] {} @ ({})",
57 self.node_id,
58 self.cosmos_address,
59 self.api_client
60 .base_urls()
61 .iter()
62 .map(|url| url.to_string())
63 .collect::<Vec<String>>()
64 .join(", ")
65 )
66 }
67}
68
69#[derive(Debug, Error)]
73pub enum EcashApiError {
74 #[error("the provided key share hasn't been verified")]
76 UnverifiedShare,
77
78 #[error("failed to query the contract: {source}")]
79 ContractQueryFailure {
80 #[from]
81 source: NyxdError,
82 },
83
84 #[error("the provided announce address is malformed: {source}")]
85 MalformedAnnounceAddress {
86 #[from]
87 source: url::ParseError,
88 },
89
90 #[error("the provided verification key is malformed: {source}")]
91 MalformedVerificationKey {
92 #[from]
93 source: CompactEcashError,
94 },
95
96 #[error("failed to create API client: {0}")]
97 ClientError(String),
98
99 #[error("the provided account address is malformed: {source}")]
100 MalformedAccountAddress {
101 #[from]
102 source: cosmrs::ErrorReport,
103 },
104
105 #[error("nym api error")]
106 NymApi {
107 #[from]
108 source: crate::ValidatorClientError,
109 },
110}
111
112impl TryFrom<ContractVKShare> for EcashApiClient {
113 type Error = EcashApiError;
114
115 fn try_from(share: ContractVKShare) -> Result<Self, Self::Error> {
116 if !share.verified {
117 return Err(EcashApiError::UnverifiedShare);
118 }
119
120 let url_address = Url::parse(&share.announce_address)?;
121
122 let api_client = nym_http_api_client::Client::builder(url_address)
129 .map_err(|e| EcashApiError::ClientError(e.to_string()))?
130 .build()
131 .map_err(|e| EcashApiError::ClientError(e.to_string()))?;
132
133 Ok(EcashApiClient {
134 api_client,
135 verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?,
136 node_id: share.node_index,
137 cosmos_address: share.owner.as_str().parse()?,
138 })
139 }
140}
141
142pub fn usable_hickory_ecash_api_clients(shares: Vec<ContractVKShare>) -> Vec<EcashApiClient> {
155 let mut clients = Vec::with_capacity(shares.len());
156
157 for share in shares {
158 let owner = share.owner.clone();
159 let epoch_id = share.epoch_id;
160
161 match EcashApiClient::try_construct_from_share(share) {
162 Ok(client) => clients.push(client),
163 Err(err) => {
164 warn!("ignoring the key share of {owner} for epoch {epoch_id}: {err}")
165 }
166 }
167 }
168
169 clients
170}
171
172pub async fn all_ecash_api_clients<C>(
173 client: &C,
174 epoch_id: EpochId,
175) -> Result<Vec<EcashApiClient>, EcashApiError>
176where
177 C: DkgQueryClient,
178{
179 Ok(usable_hickory_ecash_api_clients(
180 client.get_all_verification_key_shares(epoch_id).await?,
181 ))
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use crate::nyxd::contract_traits::dkg_query_client::{DkgQueryMsg, PagedVKSharesResponse};
188 use async_trait::async_trait;
189 use cosmrs::AccountId;
190 use cosmwasm_std::Addr;
191 use nym_compact_ecash::ttp_keygen;
192 use serde::{Deserialize, Serialize};
193
194 struct StubDkgQueryClient {
196 shares: Vec<ContractVKShare>,
197 }
198
199 fn respond<S, T>(value: S) -> Result<T, NyxdError>
200 where
201 S: Serialize,
202 for<'a> T: Deserialize<'a>,
203 {
204 let raw = serde_json::to_vec(&value).expect("failed to serialise the stub response");
205 Ok(serde_json::from_slice(&raw).expect("the stub returned the wrong response type"))
206 }
207
208 #[async_trait]
209 impl DkgQueryClient for StubDkgQueryClient {
210 async fn query_dkg_contract<T>(&self, query: DkgQueryMsg) -> Result<T, NyxdError>
211 where
212 for<'a> T: Deserialize<'a>,
213 {
214 match query {
215 DkgQueryMsg::GetVerificationKeys { epoch_id, .. } => {
216 respond(PagedVKSharesResponse {
217 shares: self
218 .shares
219 .iter()
220 .filter(|share| share.epoch_id == epoch_id)
221 .cloned()
222 .collect(),
223 per_page: self.shares.len(),
224 start_next_after: None,
225 })
226 }
227 other => panic!("the stub does not serve {other:?}"),
228 }
229 }
230 }
231
232 fn share(index: NodeIndex, key: &VerificationKeyAuth, verified: bool) -> ContractVKShare {
233 let owner = AccountId::new("n", &[index as u8; 32]).unwrap();
234
235 ContractVKShare {
236 share: key.to_bs58(),
237 announce_address: format!("http://localhost:{}", 8080 + index),
238 node_index: index,
239 owner: Addr::unchecked(owner.to_string()),
240 epoch_id: 0,
241 verified,
242 }
243 }
244
245 #[tokio::test]
246 async fn unverified_shares_are_skipped_rather_than_failing_the_whole_epoch() {
247 let keys = ttp_keygen(2, 3).unwrap();
248
249 let shares = vec![
252 share(1, &keys[0].verification_key(), true),
253 share(2, &keys[1].verification_key(), false),
254 share(3, &keys[2].verification_key(), true),
255 ];
256
257 let client = StubDkgQueryClient { shares };
258 let clients = all_ecash_api_clients(&client, 0)
259 .await
260 .expect("a single unverified share must not brick signer discovery for the epoch");
261
262 assert_eq!(clients.len(), 2);
266 assert_eq!(
267 clients.iter().map(|c| c.node_id).collect::<Vec<_>>(),
268 vec![1, 3]
269 );
270 }
271}