nym_credential_proxy_lib/
quorum_checker.rs

1// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: GPL-3.0-only
3
4use crate::error::CredentialProxyError;
5use crate::shared_state::nyxd_client::ChainClient;
6use nym_ecash_signer_check::{check_known_dealers, dkg_details_with_client};
7use std::ops::Deref;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::time::Duration;
11use tokio_util::sync::CancellationToken;
12use tracing::{error, info, warn};
13
14#[derive(Clone)]
15pub struct QuorumState {
16    available: Arc<AtomicBool>,
17}
18
19impl QuorumState {
20    pub fn available(&self) -> bool {
21        self.available.load(Ordering::Acquire)
22    }
23}
24
25pub struct QuorumStateChecker {
26    client: ChainClient,
27    cancellation_token: CancellationToken,
28    check_interval: Duration,
29    quorum_state: QuorumState,
30}
31
32impl QuorumStateChecker {
33    pub async fn new(
34        client: ChainClient,
35        check_interval: Duration,
36        cancellation_token: CancellationToken,
37    ) -> Result<Self, CredentialProxyError> {
38        let this = QuorumStateChecker {
39            client,
40            cancellation_token,
41            check_interval,
42            quorum_state: QuorumState {
43                available: Arc::new(Default::default()),
44            },
45        };
46
47        // first check MUST succeed, otherwise we shouldn't start
48        let quorum_available = this.check_quorum_state().await?;
49        this.quorum_state
50            .available
51            .store(quorum_available, Ordering::Relaxed);
52        Ok(this)
53    }
54
55    pub fn quorum_state_ref(&self) -> QuorumState {
56        self.quorum_state.clone()
57    }
58
59    async fn check_quorum_state(&self) -> Result<bool, CredentialProxyError> {
60        let client_guard = self.client.query_chain().await;
61
62        // split the operation as we only need to hold the reference to chain client for the first part
63        // and the second half doesn't rely on it (and takes way longer)
64        let dkg_details = dkg_details_with_client(client_guard.deref()).await?;
65        drop(client_guard);
66
67        let res = check_known_dealers(dkg_details).await?;
68
69        let Some(signing_threshold) = res.threshold else {
70            warn!(
71                "signing threshold is currently unavailable and we have not yet implemented credential issuance during DKG transition"
72            );
73            return Ok(false);
74        };
75
76        let mut working_issuer = 0;
77
78        for result in res.results {
79            if result.chain_available() && result.signing_available() {
80                working_issuer += 1;
81            }
82        }
83
84        Ok((working_issuer as u64) >= signing_threshold)
85    }
86
87    pub async fn run_forever(self) {
88        info!("starting quorum state checker");
89        loop {
90            tokio::select! {
91                biased;
92                _ = self.cancellation_token.cancelled() => {
93                    break
94                }
95                _ = tokio::time::sleep(self.check_interval) => {
96                    match self.check_quorum_state().await {
97                        Ok(available) => self.quorum_state.available.store(available, Ordering::SeqCst),
98                        Err(err) => error!("failed to check current quorum state: {err}"),
99                    }
100                }
101            }
102        }
103    }
104}