Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b03626ffb2 | |||
| c0f8d98b63 | |||
| 91995da4f1 | |||
| 01fa1df66c | |||
| baddaaac22 | |||
| 2c4b5f168b |
@@ -14,7 +14,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-15]
|
||||
platform: [macos-14]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
outputs:
|
||||
|
||||
Generated
+168
@@ -847,6 +847,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "binstring"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef"
|
||||
|
||||
[[package]]
|
||||
name = "bip32"
|
||||
version = "0.5.3"
|
||||
@@ -942,6 +948,17 @@ dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2b_simd"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"constant_time_eq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.2"
|
||||
@@ -1332,6 +1349,17 @@ version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "coarsetime"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasix",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
@@ -1855,6 +1883,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ct-codecs"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
@@ -2411,6 +2445,16 @@ dependencies = [
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-compact"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190"
|
||||
dependencies = [
|
||||
"ct-codecs",
|
||||
"getrandom 0.2.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-consensus"
|
||||
version = "2.1.0"
|
||||
@@ -2475,6 +2519,8 @@ dependencies = [
|
||||
"ff",
|
||||
"generic-array 0.14.7",
|
||||
"group",
|
||||
"hkdf",
|
||||
"pem-rfc7468",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sec1",
|
||||
@@ -3336,6 +3382,30 @@ dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac-sha1-compact"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18492c9f6f9a560e0d346369b665ad2bdbc89fa9bceca75796584e79042694c3"
|
||||
|
||||
[[package]]
|
||||
name = "hmac-sha256"
|
||||
version = "1.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac-sha512"
|
||||
version = "1.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89e8d20b3799fa526152a5301a771eaaad80857f83e01b23216ceaafb2d9280"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.11"
|
||||
@@ -4062,6 +4132,32 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jwt-simple"
|
||||
version = "0.12.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "731011e9647a71ff4f8474176ff6ce6e0d2de87a0173f15613af3a84c3e3401a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"binstring",
|
||||
"blake2b_simd",
|
||||
"coarsetime",
|
||||
"ct-codecs",
|
||||
"ed25519-compact",
|
||||
"hmac-sha1-compact",
|
||||
"hmac-sha256",
|
||||
"hmac-sha512",
|
||||
"k256",
|
||||
"p256",
|
||||
"p384",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"superboring",
|
||||
"thiserror 2.0.12",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "k256"
|
||||
version = "0.13.4"
|
||||
@@ -5525,6 +5621,7 @@ dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"jwt-simple",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-types",
|
||||
"rand 0.8.5",
|
||||
@@ -6639,6 +6736,26 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-signers-monitor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"humantime",
|
||||
"itertools 0.14.0",
|
||||
"nym-bin-common",
|
||||
"nym-ecash-signer-check",
|
||||
"nym-network-defaults",
|
||||
"nym-task",
|
||||
"nym-validator-client",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"zulip-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.61"
|
||||
@@ -7077,6 +7194,22 @@ dependencies = [
|
||||
"x25519-dalek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-upgrade-mode-check"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"jwt-simple",
|
||||
"nym-crypto",
|
||||
"nym-http-api-client",
|
||||
"reqwest 0.12.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-validator-client"
|
||||
version = "0.1.0"
|
||||
@@ -7645,6 +7778,18 @@ dependencies = [
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p384"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
|
||||
dependencies = [
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"primeorder",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pairing"
|
||||
version = "0.23.0"
|
||||
@@ -8625,6 +8770,7 @@ dependencies = [
|
||||
"pkcs1",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sha2 0.10.9",
|
||||
"signature",
|
||||
"spki",
|
||||
"subtle 2.6.1",
|
||||
@@ -9914,6 +10060,19 @@ version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142"
|
||||
|
||||
[[package]]
|
||||
name = "superboring"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"hmac-sha256",
|
||||
"hmac-sha512",
|
||||
"rand 0.8.5",
|
||||
"rsa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@@ -11411,6 +11570,15 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
|
||||
|
||||
[[package]]
|
||||
name = "wasix"
|
||||
version = "0.12.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d"
|
||||
dependencies = [
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
|
||||
+3
-2
@@ -95,7 +95,7 @@ members = [
|
||||
"common/ticketbooks-merkle",
|
||||
"common/topology",
|
||||
"common/tun",
|
||||
"common/types",
|
||||
"common/types", "common/upgrade-mode-check",
|
||||
"common/verloc",
|
||||
"common/wasm/client-core",
|
||||
"common/wasm/storage",
|
||||
@@ -122,7 +122,7 @@ members = [
|
||||
"nym-node-status-api/nym-node-status-client",
|
||||
"nym-node/nym-node-metrics",
|
||||
"nym-node/nym-node-requests",
|
||||
"nym-outfox",
|
||||
"nym-outfox", "nym-signers-monitor",
|
||||
"nym-statistics-api",
|
||||
"nym-validator-rewarder",
|
||||
"nyx-chain-watcher",
|
||||
@@ -274,6 +274,7 @@ inquire = "0.6.2"
|
||||
ip_network = "0.4.1"
|
||||
ipnetwork = "0.20"
|
||||
itertools = "0.14.0"
|
||||
jwt-simple = { version = "0.12.12", default-features = false, features = ["pure-rust"] }
|
||||
k256 = "0.13"
|
||||
lazy_static = "1.5.0"
|
||||
ledger-transport = "0.10.0"
|
||||
|
||||
@@ -175,6 +175,7 @@ impl MixTrafficController {
|
||||
},
|
||||
None => {
|
||||
tracing::trace!("MixTrafficController, client request channel closed");
|
||||
break
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::helpers::get_time_now;
|
||||
use crate::client::replies::{
|
||||
reply_controller::ReplyControllerSender, reply_storage::SentReplyKeys,
|
||||
};
|
||||
@@ -22,7 +23,7 @@ use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, C
|
||||
use nym_task::TaskClient;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Duration;
|
||||
use tracing::*;
|
||||
|
||||
// The interval at which we check for stale buffers
|
||||
@@ -54,7 +55,7 @@ struct ReceivedMessagesBufferInner<R: MessageReceiver> {
|
||||
stats_tx: ClientStatsSender,
|
||||
|
||||
// Periodically check for stale buffers to clean up
|
||||
last_stale_check: Instant,
|
||||
last_stale_check: crate::client::helpers::Instant,
|
||||
}
|
||||
|
||||
impl<R: MessageReceiver> ReceivedMessagesBufferInner<R> {
|
||||
@@ -154,7 +155,7 @@ impl<R: MessageReceiver> ReceivedMessagesBufferInner<R> {
|
||||
}
|
||||
|
||||
fn cleanup_stale_buffers(&mut self) {
|
||||
let now = Instant::now();
|
||||
let now = get_time_now();
|
||||
if now - self.last_stale_check > STALE_BUFFER_CHECK_INTERVAL {
|
||||
self.last_stale_check = now;
|
||||
self.message_receiver
|
||||
@@ -190,7 +191,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
message_sender: None,
|
||||
recently_reconstructed: HashSet::new(),
|
||||
stats_tx,
|
||||
last_stale_check: Instant::now(),
|
||||
last_stale_check: get_time_now(),
|
||||
})),
|
||||
reply_key_storage,
|
||||
reply_controller_sender,
|
||||
|
||||
+3
-14
@@ -28,7 +28,7 @@ use cosmrs::proto::cosmwasm::wasm::v1::{
|
||||
QueryRawContractStateResponse, QuerySmartContractStateRequest, QuerySmartContractStateResponse,
|
||||
};
|
||||
use cosmrs::tendermint::{block, chain, Hash};
|
||||
use cosmrs::{AccountId, Coin as CosmosCoin, Tx};
|
||||
use cosmrs::{AccountId, Coin as CosmosCoin};
|
||||
use prost::Message;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -556,23 +556,12 @@ pub trait CosmWasmClient: TendermintRpcClient {
|
||||
Ok(serde_json::from_slice(&res.data)?)
|
||||
}
|
||||
|
||||
// deprecation warning is due to the fact the protobuf files built were based on cosmos-sdk 0.44,
|
||||
// where they prefer using tx_bytes directly. However, in 0.42, which we are using at the time
|
||||
// of writing this, the option does not work
|
||||
// TODO: we should really stop using the `tx` argument here and use `tx_bytes` exlusively,
|
||||
// however, at the time of writing this update, while our QA and mainnet networks do support it,
|
||||
// sandbox is still running old version of wasmd that lacks support for `tx_bytes`
|
||||
#[allow(deprecated)]
|
||||
async fn query_simulate(
|
||||
&self,
|
||||
tx: Option<Tx>,
|
||||
tx_bytes: Vec<u8>,
|
||||
) -> Result<SimulateResponse, NyxdError> {
|
||||
async fn query_simulate(&self, tx_bytes: Vec<u8>) -> Result<SimulateResponse, NyxdError> {
|
||||
let path = Some("/cosmos.tx.v1beta1.Service/Simulate".to_owned());
|
||||
|
||||
let req = SimulateRequest {
|
||||
tx: tx.map(Into::into),
|
||||
tx_bytes,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let res = self
|
||||
|
||||
+7
-10
@@ -81,17 +81,14 @@ where
|
||||
auth_info: single_unspecified_signer_auth(public_key, sequence_response.sequence),
|
||||
signatures: vec![Vec::new()],
|
||||
};
|
||||
self.query_simulate(Some(partial_tx), Vec::new()).await
|
||||
|
||||
// for completion sake, once we're able to transition into using `tx_bytes`,
|
||||
// we might want to use something like this instead:
|
||||
// let tx_raw: tx::Raw = cosmrs::proto::cosmos::tx::v1beta1::TxRaw {
|
||||
// body_bytes: partial_tx.body.into_bytes().unwrap(),
|
||||
// auth_info_bytes: partial_tx.auth_info.into_bytes().unwrap(),
|
||||
// signatures: partial_tx.signatures,
|
||||
// }
|
||||
// .into();
|
||||
// self.query_simulate(None, tx_raw.to_bytes().unwrap()).await
|
||||
let tx_raw: tx::Raw = cosmrs::proto::cosmos::tx::v1beta1::TxRaw {
|
||||
body_bytes: partial_tx.body.into_bytes()?,
|
||||
auth_info_bytes: partial_tx.auth_info.into_bytes()?,
|
||||
signatures: partial_tx.signatures,
|
||||
}
|
||||
.into();
|
||||
self.query_simulate(tx_raw.to_bytes()?).await
|
||||
}
|
||||
|
||||
async fn upload(
|
||||
|
||||
@@ -18,6 +18,7 @@ digest = { workspace = true, optional = true }
|
||||
generic-array = { workspace = true, optional = true }
|
||||
hkdf = { workspace = true, optional = true }
|
||||
hmac = { workspace = true, optional = true }
|
||||
jwt-simple = { workspace = true, optional = true }
|
||||
cipher = { workspace = true, optional = true }
|
||||
x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true }
|
||||
ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true }
|
||||
@@ -39,6 +40,7 @@ rand_chacha = { workspace = true }
|
||||
[features]
|
||||
default = []
|
||||
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
|
||||
naive_jwt = ["asymmetric", "jwt-simple"]
|
||||
serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"]
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub use ed25519_dalek::SignatureError;
|
||||
use ed25519_dalek::{SecretKey, Signer, SigningKey};
|
||||
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
|
||||
|
||||
use ed25519_dalek::Signer;
|
||||
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
@@ -13,6 +14,9 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
#[cfg(feature = "serde")]
|
||||
pub mod serde_helpers;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
pub use serde_helpers::*;
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
use nym_sphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH};
|
||||
|
||||
@@ -81,8 +85,8 @@ impl KeyPair {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_secret(secret: SecretKey, index: u32) -> Self {
|
||||
let ed25519_signing_key = SigningKey::from(secret);
|
||||
pub fn from_secret(secret: ed25519_dalek::SecretKey, index: u32) -> Self {
|
||||
let ed25519_signing_key = ed25519_dalek::SigningKey::from(secret);
|
||||
|
||||
KeyPair {
|
||||
private_key: PrivateKey(ed25519_signing_key.to_bytes()),
|
||||
@@ -276,7 +280,7 @@ impl Display for PrivateKey {
|
||||
|
||||
impl<'a> From<&'a PrivateKey> for PublicKey {
|
||||
fn from(pk: &'a PrivateKey) -> Self {
|
||||
PublicKey(SigningKey::from_bytes(&pk.0).verifying_key())
|
||||
PublicKey(ed25519_dalek::SigningKey::from_bytes(&pk.0).verifying_key())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +324,7 @@ impl PrivateKey {
|
||||
}
|
||||
|
||||
pub fn sign<M: AsRef<[u8]>>(&self, message: M) -> Signature {
|
||||
let signing_key: SigningKey = self.0.into();
|
||||
let signing_key: ed25519_dalek::SigningKey = self.0.into();
|
||||
let sig = signing_key.sign(message.as_ref());
|
||||
Signature(sig)
|
||||
}
|
||||
@@ -425,9 +429,57 @@ impl<'d> Deserialize<'d> for Signature {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "naive_jwt")]
|
||||
impl PublicKey {
|
||||
pub fn to_jwt_compatible_key(&self) -> jwt_simple::algorithms::Ed25519PublicKey {
|
||||
(*self).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "naive_jwt")]
|
||||
impl From<PublicKey> for jwt_simple::algorithms::Ed25519PublicKey {
|
||||
fn from(value: PublicKey) -> Self {
|
||||
// SAFETY: we have a valid ed25519 pubkey, we're just changing to a different library wrapper
|
||||
#[allow(clippy::unwrap_used)]
|
||||
jwt_simple::algorithms::Ed25519PublicKey::from_bytes(&value.to_bytes()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "naive_jwt")]
|
||||
impl PrivateKey {
|
||||
pub fn to_jwt_compatible_keys(&self) -> jwt_simple::algorithms::Ed25519KeyPair {
|
||||
let pub_key = self.public_key();
|
||||
let mut bytes = zeroize::Zeroizing::new([0u8; 64]);
|
||||
|
||||
bytes[..SECRET_KEY_LENGTH]
|
||||
.copy_from_slice(zeroize::Zeroizing::new(self.to_bytes()).as_ref());
|
||||
bytes[SECRET_KEY_LENGTH..].copy_from_slice(&pub_key.to_bytes());
|
||||
|
||||
// SAFETY: we have a valid ed25519 keys, we're just changing to a different library wrapper
|
||||
#[allow(clippy::unwrap_used)]
|
||||
jwt_simple::algorithms::Ed25519KeyPair::from_bytes(bytes.as_ref()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "naive_jwt")]
|
||||
impl KeyPair {
|
||||
pub fn to_jwt_compatible_keys(&self) -> jwt_simple::algorithms::Ed25519KeyPair {
|
||||
let mut bytes = zeroize::Zeroizing::new([0u8; 64]);
|
||||
|
||||
bytes[..SECRET_KEY_LENGTH]
|
||||
.copy_from_slice(zeroize::Zeroizing::new(self.private_key.to_bytes()).as_ref());
|
||||
bytes[SECRET_KEY_LENGTH..].copy_from_slice(&self.public_key.to_bytes());
|
||||
|
||||
// SAFETY: we have a valid ed25519 keys, we're just changing to a different library wrapper
|
||||
#[allow(clippy::unwrap_used)]
|
||||
jwt_simple::algorithms::Ed25519KeyPair::from_bytes(bytes.as_ref()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::thread_rng;
|
||||
|
||||
fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
|
||||
|
||||
@@ -438,4 +490,29 @@ mod tests {
|
||||
assert_zeroize::<PrivateKey>();
|
||||
assert_zeroize_on_drop::<PrivateKey>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(all(feature = "naive_jwt", feature = "rand"))]
|
||||
fn check_jwt_key_compat_conversion() {
|
||||
let mut rng = thread_rng();
|
||||
let keys = KeyPair::new(&mut rng);
|
||||
let jwt_keys = keys.to_jwt_compatible_keys();
|
||||
|
||||
// internally they're represented by hidden `Edwards25519KeyPair` (plus key_id)
|
||||
// which has way nicer API for assertions
|
||||
let jwt_keys_inner =
|
||||
jwt_simple::algorithms::Edwards25519KeyPair::from_bytes(&jwt_keys.to_bytes()).unwrap();
|
||||
|
||||
let compact_ed25519 = jwt_keys_inner.as_ref();
|
||||
assert!(compact_ed25519
|
||||
.sk
|
||||
.validate_public_key(&compact_ed25519.pk)
|
||||
.is_ok());
|
||||
|
||||
let dummy_message = "hello world";
|
||||
let sig1 = keys.private_key.sign(dummy_message).to_bytes();
|
||||
let sig2 = compact_ed25519.sk.sign(dummy_message, None).to_vec();
|
||||
|
||||
assert_eq!(sig1.to_vec(), sig2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,14 @@ impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC> {
|
||||
pub fn malformed_details(&self) -> bool {
|
||||
self.information.parse().is_err()
|
||||
}
|
||||
|
||||
pub fn try_get_test_result(&self) -> Option<&SignerTestResult<LS, TS, LC, TC>> {
|
||||
if let SignerStatus::Tested { result } = &self.status {
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "nym-upgrade-mode-check"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
jwt-simple = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
time = { workspace = true, features = ["serde"] }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
nym-http-api-client = { path = "../http-api-client", default-features = false }
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde", "naive_jwt"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
time = { workspace = true, features = ["macros"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::UpgradeModeCheckError;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_http_api_client::generate_user_agent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||
pub struct UpgradeModeAttestation {
|
||||
#[serde(flatten)]
|
||||
pub content: UpgradeModeAttestationContent,
|
||||
|
||||
#[serde(with = "ed25519::bs58_ed25519_signature")]
|
||||
pub signature: ed25519::Signature,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename = "upgrade_mode")]
|
||||
pub struct UpgradeModeAttestationContent {
|
||||
#[serde(with = "time::serde::timestamp")]
|
||||
pub starting_time: OffsetDateTime,
|
||||
|
||||
#[serde(with = "ed25519::bs58_ed25519_pubkey")]
|
||||
pub attester_public_key: ed25519::PublicKey,
|
||||
}
|
||||
|
||||
impl UpgradeModeAttestation {
|
||||
pub fn verify(&self) -> bool {
|
||||
self.content
|
||||
.attester_public_key
|
||||
.verify(self.content.as_json(), &self.signature)
|
||||
.is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl UpgradeModeAttestationContent {
|
||||
pub fn as_json(&self) -> String {
|
||||
// SAFETY: Serialize impl is valid and we have no non-string map keys
|
||||
#[allow(clippy::unwrap_used)]
|
||||
serde_json::to_string(&self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_new_attestation(key: &ed25519::PrivateKey) -> UpgradeModeAttestation {
|
||||
generate_new_attestation_with_starting_time(key, OffsetDateTime::now_utc())
|
||||
}
|
||||
|
||||
pub fn generate_new_attestation_with_starting_time(
|
||||
key: &ed25519::PrivateKey,
|
||||
starting_time: OffsetDateTime,
|
||||
) -> UpgradeModeAttestation {
|
||||
let content = UpgradeModeAttestationContent {
|
||||
starting_time,
|
||||
attester_public_key: key.into(),
|
||||
};
|
||||
UpgradeModeAttestation {
|
||||
signature: key.sign(content.as_json()),
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn attempt_retrieve(
|
||||
url: &str,
|
||||
) -> Result<Option<UpgradeModeAttestation>, UpgradeModeCheckError> {
|
||||
let retrieval_failure = |source| UpgradeModeCheckError::AttestationRetrievalFailure {
|
||||
url: url.to_string(),
|
||||
source,
|
||||
};
|
||||
|
||||
let attestation = reqwest::ClientBuilder::new()
|
||||
.user_agent(generate_user_agent!())
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
.map_err(retrieval_failure)?
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(retrieval_failure)?
|
||||
.json::<Option<UpgradeModeAttestation>>()
|
||||
.await
|
||||
.map_err(retrieval_failure)?;
|
||||
|
||||
Ok(attestation)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn upgrade_mode_attestation_serde_json() -> anyhow::Result<()> {
|
||||
// unix timestamp: 1629720000
|
||||
let starting_time = time::macros::datetime!(2021-08-23 12:00 UTC);
|
||||
|
||||
let key = ed25519::PrivateKey::from_bytes(&[
|
||||
108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248,
|
||||
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
|
||||
])?;
|
||||
|
||||
let attestation = generate_new_attestation_with_starting_time(&key, starting_time);
|
||||
|
||||
let attestation_json = serde_json::to_string(&attestation)?;
|
||||
let attestation_content_json = attestation.content.as_json();
|
||||
|
||||
let expected_attestation = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","signature":"5rWUr2ypaDTtrMKegMP3tQkkZGFAuhNTnEVCVe5Azv6QqvLzoGdQiMkFmeyhDd1XSfoXpL9fFM58rsdA1kf4GYMM"}"#;
|
||||
let expected_content = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt"}"#;
|
||||
|
||||
assert_eq!(attestation_content_json, expected_content);
|
||||
assert_eq!(attestation_json, expected_attestation);
|
||||
|
||||
let recovered_attestation = serde_json::from_str(&attestation_json)?;
|
||||
assert_eq!(attestation, recovered_attestation);
|
||||
|
||||
let recovered_content = serde_json::from_str(&attestation_content_json)?;
|
||||
assert_eq!(attestation.content, recovered_content);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UpgradeModeCheckError {
|
||||
#[error("failed to decode jwt metadata")]
|
||||
TokenMetadataDecodeFailure { source: jwt_simple::Error },
|
||||
|
||||
#[error("the jwt metadata didn't contain explicit public key")]
|
||||
MissingTokenPublicKey,
|
||||
|
||||
#[error("the attached public key was not valid ed25519 public key")]
|
||||
MalformedEd25519PublicKey { source: Ed25519RecoveryError },
|
||||
|
||||
#[error("failed to verify the jwt: {source}")]
|
||||
JwtVerificationFailure { source: jwt_simple::Error },
|
||||
|
||||
#[error("failed to retrieve attestation from {url}:{source}")]
|
||||
AttestationRetrievalFailure { url: String, source: reqwest::Error },
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{UpgradeModeAttestation, UpgradeModeCheckError};
|
||||
use jwt_simple::claims::Claims;
|
||||
use jwt_simple::common::{KeyMetadata, VerificationOptions};
|
||||
use jwt_simple::prelude::{EdDSAKeyPairLike, EdDSAPublicKeyLike};
|
||||
use jwt_simple::token::Token;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
// for now use static issuer such as "nym-credential-proxy"
|
||||
pub fn generate_jwt_for_upgrade_mode_attestation(
|
||||
attestation: UpgradeModeAttestation,
|
||||
validity: Duration,
|
||||
keys: &ed25519::KeyPair,
|
||||
issuer: Option<&'static str>,
|
||||
) -> String {
|
||||
let claim = Claims::with_custom_claims(attestation, validity.into());
|
||||
let mut claim = if let Some(issuer) = issuer {
|
||||
claim.with_issuer(issuer)
|
||||
} else {
|
||||
claim
|
||||
};
|
||||
claim.create_nonce();
|
||||
|
||||
let md = KeyMetadata::default().with_public_key(keys.public_key().to_base58_string());
|
||||
|
||||
let mut jwt_keys = keys.to_jwt_compatible_keys();
|
||||
// SAFETY: trait impl for EdDSA is infallible
|
||||
#[allow(clippy::unwrap_used)]
|
||||
jwt_keys.attach_metadata(md).unwrap();
|
||||
|
||||
// SAFETY: our construction of the jwt is valid
|
||||
#[allow(clippy::unwrap_used)]
|
||||
jwt_keys.sign(claim).unwrap()
|
||||
}
|
||||
|
||||
pub fn validate_upgrade_mode_jwt(
|
||||
token: &str,
|
||||
expected_issuer: Option<&'static str>,
|
||||
) -> Result<UpgradeModeAttestation, UpgradeModeCheckError> {
|
||||
// for now, we completely ignore the validity of the pubkey (I know, I know).
|
||||
// that will be changed later on
|
||||
// so as a bypass we have to extract the claimed issuer from the jwt to verify against it
|
||||
let metadata = Token::decode_metadata(token)
|
||||
.map_err(|source| UpgradeModeCheckError::TokenMetadataDecodeFailure { source })?;
|
||||
|
||||
let pub_key = metadata
|
||||
.public_key()
|
||||
.ok_or(UpgradeModeCheckError::MissingTokenPublicKey)?;
|
||||
|
||||
let ed25519_pub_key = ed25519::PublicKey::from_base58_string(pub_key)
|
||||
.map_err(|source| UpgradeModeCheckError::MalformedEd25519PublicKey { source })?;
|
||||
|
||||
let mut opts = VerificationOptions::default();
|
||||
if let Some(issuer) = expected_issuer {
|
||||
opts.allowed_issuers = Some(HashSet::from_iter(vec![issuer.to_string()]));
|
||||
}
|
||||
|
||||
let attestation = ed25519_pub_key
|
||||
.to_jwt_compatible_key()
|
||||
.verify_token::<UpgradeModeAttestation>(token, Some(opts))
|
||||
.map_err(|source| UpgradeModeCheckError::JwtVerificationFailure { source })?
|
||||
.custom;
|
||||
|
||||
Ok(attestation)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate_new_attestation;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
|
||||
#[test]
|
||||
fn generate_and_validate_jwt() {
|
||||
let attestation_key = ed25519::PrivateKey::from_bytes(&[
|
||||
108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248,
|
||||
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
|
||||
])
|
||||
.unwrap();
|
||||
let jwt_key = ed25519::PrivateKey::from_bytes(&[
|
||||
152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167,
|
||||
2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32,
|
||||
])
|
||||
.unwrap();
|
||||
let keys = ed25519::KeyPair::from(jwt_key);
|
||||
|
||||
let attestation = generate_new_attestation(&attestation_key);
|
||||
let jwt_issuer = generate_jwt_for_upgrade_mode_attestation(
|
||||
attestation,
|
||||
Duration::from_secs(60 * 60),
|
||||
&keys,
|
||||
Some("nym-credential-proxy"),
|
||||
);
|
||||
// we expect 'nym-credential-proxy' issuer
|
||||
assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("nym-credential-proxy")).is_ok());
|
||||
|
||||
// we don't care about issuer
|
||||
assert!(validate_upgrade_mode_jwt(&jwt_issuer, None).is_ok());
|
||||
|
||||
// we expect another-issuer
|
||||
assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("another-issuer")).is_err());
|
||||
|
||||
let jwt_no_issuer = generate_jwt_for_upgrade_mode_attestation(
|
||||
attestation,
|
||||
Duration::from_secs(60 * 60),
|
||||
&keys,
|
||||
None,
|
||||
);
|
||||
// we expect 'nym-credential-proxy' issuer
|
||||
assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, Some("nym-credential-proxy")).is_err());
|
||||
|
||||
// we don't care about issuer
|
||||
assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, None).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub(crate) mod attestation;
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod jwt;
|
||||
|
||||
pub use attestation::{
|
||||
attempt_retrieve, generate_new_attestation, generate_new_attestation_with_starting_time,
|
||||
UpgradeModeAttestation,
|
||||
};
|
||||
pub use error::UpgradeModeCheckError;
|
||||
pub use jwt::{generate_jwt_for_upgrade_mode_attestation, validate_upgrade_mode_jwt};
|
||||
@@ -25,7 +25,7 @@
|
||||
//! ```
|
||||
|
||||
use crate::error::ZulipClientError;
|
||||
use crate::message::{SendMessageResponse, SendableMessage};
|
||||
use crate::message::{DirectMessage, SendMessageResponse, SendableMessage, StreamMessage};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_http_api_client::UserAgent;
|
||||
use reqwest::{header, Method, RequestBuilder};
|
||||
@@ -92,6 +92,20 @@ impl Client {
|
||||
.map_err(|source| ZulipClientError::RequestDecodeFailure { source })
|
||||
}
|
||||
|
||||
pub async fn send_direct_message(
|
||||
&self,
|
||||
msg: impl Into<DirectMessage>,
|
||||
) -> Result<SendMessageResponse, ZulipClientError> {
|
||||
self.send_message(msg.into()).await
|
||||
}
|
||||
|
||||
pub async fn send_channel_message(
|
||||
&self,
|
||||
msg: impl Into<StreamMessage>,
|
||||
) -> Result<SendMessageResponse, ZulipClientError> {
|
||||
self.send_message(msg.into()).await
|
||||
}
|
||||
|
||||
fn build_request(&self, method: Method, endpoint: &'static str) -> RequestBuilder {
|
||||
let url = format!("{}{endpoint}", self.server_url);
|
||||
trace!("posting to {url}");
|
||||
|
||||
@@ -22,7 +22,7 @@ pub enum SendMessageResponse {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum SendableMessageContent {
|
||||
@@ -40,7 +40,7 @@ pub enum SendableMessageContent {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct SendableMessage {
|
||||
#[serde(flatten)]
|
||||
@@ -117,17 +117,17 @@ impl StreamMessage {
|
||||
pub fn new(
|
||||
to: impl Into<ToChannel>,
|
||||
content: impl Into<String>,
|
||||
topic: Option<String>,
|
||||
topic: impl IntoMaybeTopic,
|
||||
) -> Self {
|
||||
StreamMessage {
|
||||
to: to.into().to_string(),
|
||||
topic,
|
||||
topic: topic.into_maybe_topic(),
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_topic(to: impl Into<ToChannel>, content: impl Into<String>) -> Self {
|
||||
Self::new(to, content, None)
|
||||
Self::new(to, content, None::<String>)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -194,22 +194,74 @@ impl From<StreamMessage> for SendableMessageContent {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<(T, S, Option<S>)> for StreamMessage
|
||||
impl<T, S, U> From<(T, S, U)> for StreamMessage
|
||||
where
|
||||
T: Into<ToChannel>,
|
||||
S: Into<String>,
|
||||
U: IntoMaybeTopic,
|
||||
{
|
||||
fn from((to, content, topic): (T, S, Option<S>)) -> Self {
|
||||
StreamMessage::new(to, content, topic.map(Into::into))
|
||||
fn from((to, content, topic): (T, S, U)) -> Self {
|
||||
StreamMessage::new(to, content, topic)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<(T, S, Option<S>)> for SendableMessage
|
||||
impl<T, S> From<(T, S)> for StreamMessage
|
||||
where
|
||||
T: Into<ToChannel>,
|
||||
S: Into<String>,
|
||||
{
|
||||
fn from(inner: (T, S, Option<S>)) -> Self {
|
||||
fn from((to, content): (T, S)) -> Self {
|
||||
StreamMessage::no_topic(to, content)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, U> From<(T, S, U)> for SendableMessage
|
||||
where
|
||||
T: Into<ToChannel>,
|
||||
S: Into<String>,
|
||||
U: IntoMaybeTopic,
|
||||
{
|
||||
fn from(inner: (T, S, U)) -> Self {
|
||||
StreamMessage::from(inner).into()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoMaybeTopic {
|
||||
fn into_maybe_topic(self) -> Option<String>;
|
||||
}
|
||||
|
||||
impl<S> IntoMaybeTopic for &Option<S>
|
||||
where
|
||||
S: Into<String> + Clone,
|
||||
{
|
||||
fn into_maybe_topic(self) -> Option<String> {
|
||||
self.clone().map(|s| s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> IntoMaybeTopic for Option<S>
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
fn into_maybe_topic(self) -> Option<String> {
|
||||
self.map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoMaybeTopic for String {
|
||||
fn into_maybe_topic(self) -> Option<String> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoMaybeTopic for &String {
|
||||
fn into_maybe_topic(self) -> Option<String> {
|
||||
Some(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoMaybeTopic for &str {
|
||||
fn into_maybe_topic(self) -> Option<String> {
|
||||
Some(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "nym-signers-monitor"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["cargo", "derive", "env", "string"] }
|
||||
humantime = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
time = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
nym-bin-common = { path = "../common/bin-common", features = ["output_format", "basic_tracing"] }
|
||||
nym-ecash-signer-check = { path = "../common/ecash-signer-check" }
|
||||
nym-network-defaults = { path = "../common/network-defaults" }
|
||||
nym-task = { path = "../common/task" }
|
||||
nym-validator-client = { path = "../common/client-libs/validator-client" }
|
||||
zulip-client = { path = "../common/zulip-client" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use nym_bin_common::bin_info_owned;
|
||||
use nym_bin_common::output_format::OutputFormat;
|
||||
|
||||
#[derive(clap::Args, Debug)]
|
||||
pub(crate) struct Args {
|
||||
#[arg(short, long, default_value_t = OutputFormat::default())]
|
||||
output: OutputFormat,
|
||||
}
|
||||
|
||||
pub(crate) fn execute(args: Args) {
|
||||
println!("{}", args.output.format(&bin_info_owned!()))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub(crate) mod vars {
|
||||
pub(crate) const ZULIP_BOT_EMAIL_ARG: &str = "ZULIP_BOT_EMAIL";
|
||||
pub(crate) const ZULIP_BOT_API_KEY_ARG: &str = "ZULIP_BOT_API_KEY";
|
||||
pub(crate) const ZULIP_SERVER_URL_ARG: &str = "ZULIP_SERVER_URL";
|
||||
pub(crate) const ZULIP_NOTIFICATION_CHANNEL_ID_ARG: &str = "ZULIP_NOTIFICATION_CHANNEL_ID";
|
||||
pub(crate) const ZULIP_NOTIFICATION_CHANNEL_TOPIC_ARG: &str =
|
||||
"ZULIP_NOTIFICATION_CHANNEL_TOPIC";
|
||||
|
||||
pub(crate) const SIGNERS_MONITOR_CHECK_INTERVAL_ARG: &str = "SIGNERS_MONITOR_CHECK_INTERVAL";
|
||||
pub(crate) const SIGNERS_MONITOR_MIN_NOTIFICATION_DELAY_ARG: &str =
|
||||
"SIGNERS_MONITOR_MIN_NOTIFICATION_DELAY";
|
||||
|
||||
pub(crate) const KNOWN_NETWORK_NAME_ARG: &str = "KNOWN_NETWORK_NAME";
|
||||
pub(crate) const NYXD_CLIENT_CONFIG_ENV_FILE_ARG: &str = "NYXD_CLIENT_CONFIG_ENV_FILE";
|
||||
pub(crate) const NYXD_RPC_ENDPOINT_ARG: &str = "NYXD_RPC_ENDPOINT";
|
||||
pub(crate) const NYXD_DKG_CONTRACT_ADDRESS_ARG: &str = "NYXD_DKG_CONTRACT_ADDRESS";
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use nym_bin_common::bin_info;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub(crate) mod build_info;
|
||||
pub(crate) mod env;
|
||||
pub(crate) mod run;
|
||||
|
||||
// Helper for passing LONG_VERSION to clap
|
||||
fn pretty_build_info_static() -> &'static str {
|
||||
static PRETTY_BUILD_INFORMATION: OnceLock<String> = OnceLock::new();
|
||||
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)]
|
||||
pub(crate) struct Cli {
|
||||
#[clap(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
pub async fn execute(self) -> anyhow::Result<()> {
|
||||
match self.command {
|
||||
Commands::BuildInfo(args) => build_info::execute(args),
|
||||
Commands::Run(args) => run::execute(*args).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub(crate) enum Commands {
|
||||
/// Show build information of this binary
|
||||
BuildInfo(build_info::Args),
|
||||
|
||||
/// Start signers monitor and send notifications on any failures
|
||||
Run(Box<run::Args>),
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::cli::env::vars::*;
|
||||
use crate::monitor::SignersMonitor;
|
||||
use anyhow::{bail, Context};
|
||||
use clap::ArgGroup;
|
||||
use nym_network_defaults::{setup_env, NymNetworkDetails};
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, clap::Args)]
|
||||
pub(crate) struct NyxdConnectionArgs {
|
||||
// for well-known networks, such mainnet, we can use hardcoded values
|
||||
/// Name of a well known network (such as 'mainnet') that has well-known
|
||||
/// pre-configured setup values
|
||||
#[clap(long, env = KNOWN_NETWORK_NAME_ARG)]
|
||||
pub(crate) known_network_name: Option<String>,
|
||||
|
||||
/// Path pointing to an env file that configures the nyxd client.
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
env = NYXD_CLIENT_CONFIG_ENV_FILE_ARG
|
||||
)]
|
||||
pub(crate) config_env_file: Option<std::path::PathBuf>,
|
||||
|
||||
/// For unknown networks (or if one wishes to override defaults),
|
||||
/// specify the RPC endpoint of a node from which signer information should be retrieved
|
||||
#[clap(long, env = NYXD_RPC_ENDPOINT_ARG)]
|
||||
pub(crate) nyxd_rpc_endpoint: Option<Url>,
|
||||
|
||||
/// For unknown networks, specify address of the DKG contract to pull signer information from.
|
||||
#[clap(
|
||||
long,
|
||||
requires("nyxd_rpc_endpoint"),
|
||||
env = NYXD_DKG_CONTRACT_ADDRESS_ARG
|
||||
)]
|
||||
pub(crate) dkg_contract_address: Option<AccountId>,
|
||||
// if needed down the line (not sure why), we could define additional args
|
||||
// for specifying denoms, etc.
|
||||
// #[clap(long, requires("dkg_contract_address"))]
|
||||
// pub(crate) mix_denom: Option<String>,
|
||||
}
|
||||
|
||||
impl NyxdConnectionArgs {
|
||||
fn get_minimal_nym_network_details(&self) -> anyhow::Result<NymNetworkDetails> {
|
||||
if let Some(known_network_name) = &self.known_network_name {
|
||||
match known_network_name.as_str() {
|
||||
"mainnet" => return Ok(NymNetworkDetails::new_mainnet()),
|
||||
other => bail!("{other} is not a known network name - please use another method of setting up chain connection"),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(config_env_file) = &self.config_env_file {
|
||||
setup_env(Some(config_env_file));
|
||||
return Ok(NymNetworkDetails::new_from_env());
|
||||
}
|
||||
|
||||
// SAFETY: clap ensures at least one of the fields is set
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let dkg_contract = self.dkg_contract_address.as_ref().unwrap();
|
||||
|
||||
// use mainnet's chain details (i.e. prefixes, denoms, etc)
|
||||
let mainnet_chain_details = NymNetworkDetails::new_mainnet().chain_details;
|
||||
Ok(NymNetworkDetails::new_empty()
|
||||
.with_chain_details(mainnet_chain_details)
|
||||
.with_coconut_dkg_contract(Some(dkg_contract.to_string())))
|
||||
}
|
||||
|
||||
pub(crate) fn try_create_nyxd_client(&self) -> anyhow::Result<QueryHttpRpcNyxdClient> {
|
||||
let network_details = self.get_minimal_nym_network_details()?;
|
||||
|
||||
let nyxd_endpoint = match &self.nyxd_rpc_endpoint {
|
||||
Some(nyxd_rpc_endpoint) => nyxd_rpc_endpoint.clone(),
|
||||
None => network_details
|
||||
.endpoints
|
||||
.first()
|
||||
.context("no nyxd endpoints provided")?
|
||||
.nyxd_url
|
||||
.parse()?,
|
||||
};
|
||||
|
||||
Ok(QueryHttpRpcNyxdClient::connect_with_network_details(
|
||||
nyxd_endpoint.as_str(),
|
||||
network_details,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::Args, Debug)]
|
||||
#[command(group(
|
||||
ArgGroup::new("nyxd_connection")
|
||||
.multiple(true)
|
||||
.required(true)
|
||||
.args([
|
||||
"known_network_name",
|
||||
"config_env_file",
|
||||
"nyxd_rpc_endpoint"
|
||||
])
|
||||
))]
|
||||
pub(crate) struct Args {
|
||||
/// Specify email address for the bot responsible for sending notifications to the zulip server
|
||||
/// in case 'upgrade' mode is detected
|
||||
#[clap(
|
||||
long,
|
||||
env = ZULIP_BOT_EMAIL_ARG
|
||||
)]
|
||||
pub(crate) zulip_bot_email: String,
|
||||
|
||||
/// Specify the API key for the bot responsible for sending notifications to the zulip server
|
||||
/// in case 'upgrade' mode is detected
|
||||
#[clap(
|
||||
long,
|
||||
env = ZULIP_BOT_API_KEY_ARG
|
||||
)]
|
||||
pub(crate) zulip_bot_api_key: String,
|
||||
|
||||
/// Specify the sever endpoint for the bot responsible for sending notifications
|
||||
/// in case 'upgrade' mode is detected
|
||||
#[clap(
|
||||
long,
|
||||
env = ZULIP_SERVER_URL_ARG
|
||||
)]
|
||||
pub(crate) zulip_server_url: Url,
|
||||
|
||||
/// Specify the channel id for where the notification is going to be sent
|
||||
#[clap(
|
||||
long,
|
||||
env = ZULIP_NOTIFICATION_CHANNEL_ID_ARG
|
||||
)]
|
||||
pub(crate) zulip_notification_channel_id: u32,
|
||||
|
||||
/// Optionally specify the channel topic for where the notification is going to be sent
|
||||
#[clap(
|
||||
long,
|
||||
env = ZULIP_NOTIFICATION_CHANNEL_TOPIC_ARG
|
||||
)]
|
||||
pub(crate) zulip_notification_topic: Option<String>,
|
||||
|
||||
/// Specify the delay between subsequent signers checks
|
||||
#[clap(
|
||||
long,
|
||||
env = SIGNERS_MONITOR_CHECK_INTERVAL_ARG,
|
||||
value_parser = humantime::parse_duration,
|
||||
default_value = "15m"
|
||||
)]
|
||||
pub(crate) signers_check_interval: Duration,
|
||||
|
||||
/// Specify the minimum delay between two subsequent notifications
|
||||
#[clap(
|
||||
long,
|
||||
env = SIGNERS_MONITOR_MIN_NOTIFICATION_DELAY_ARG,
|
||||
value_parser = humantime::parse_duration,
|
||||
default_value = "1h"
|
||||
)]
|
||||
pub(crate) minimum_notification_delay: Duration,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub(crate) nyxd_connection: NyxdConnectionArgs,
|
||||
}
|
||||
|
||||
pub(crate) async fn execute(args: Args) -> anyhow::Result<()> {
|
||||
SignersMonitor::new(args)?.run().await
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::cli::Cli;
|
||||
use clap::Parser;
|
||||
use nym_bin_common::bin_info_owned;
|
||||
use nym_bin_common::logging::setup_tracing_logger;
|
||||
use tracing::{info, trace};
|
||||
|
||||
mod cli;
|
||||
mod monitor;
|
||||
pub(crate) mod test_result;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
setup_tracing_logger();
|
||||
let cli = Cli::parse();
|
||||
trace!("args: {cli:#?}");
|
||||
|
||||
let bin_info = bin_info_owned!();
|
||||
info!("using the following version: {bin_info}");
|
||||
|
||||
cli.execute().await
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::cli::run;
|
||||
use crate::test_result::{DisplayableSignerResult, Summary, TestResult};
|
||||
use nym_ecash_signer_check::check_signers_with_client;
|
||||
use nym_task::ShutdownManager;
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::time::interval;
|
||||
use tracing::{error, info};
|
||||
|
||||
const LOST_QUORUM_MSG: &str = r#"
|
||||
# 🔥🔥🔥 LOST SIGNING QUORUM 🔥🔥🔥
|
||||
We seem to have lost the signing quorum - check if we should enable the 'upgrade' mode!
|
||||
"#;
|
||||
|
||||
const UNKNOWN_QUORUM_MSG: &str = r#"
|
||||
# ❓❓❓ UNKNOWN SIGNING QUORUM ❓❓❓
|
||||
We can't determine the signing quroum - if we're not undergoing DKG exchange check if we should enable the 'upgrade' mode!
|
||||
"#;
|
||||
|
||||
pub(crate) struct SignersMonitor {
|
||||
zulip_client: zulip_client::Client,
|
||||
nyxd_client: QueryHttpRpcNyxdClient,
|
||||
|
||||
notification_channel_id: u32,
|
||||
notification_topic: Option<String>,
|
||||
check_interval: Duration,
|
||||
min_notification_delay: Duration,
|
||||
last_notification_sent: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
impl SignersMonitor {
|
||||
pub(crate) fn new(args: run::Args) -> anyhow::Result<Self> {
|
||||
let zulip_client = zulip_client::Client::builder(
|
||||
args.zulip_bot_email,
|
||||
args.zulip_bot_api_key,
|
||||
args.zulip_server_url,
|
||||
)?
|
||||
.build()?;
|
||||
let nyxd_client = args.nyxd_connection.try_create_nyxd_client()?;
|
||||
|
||||
Ok(SignersMonitor {
|
||||
zulip_client,
|
||||
nyxd_client,
|
||||
notification_channel_id: args.zulip_notification_channel_id,
|
||||
notification_topic: args.zulip_notification_topic,
|
||||
check_interval: args.signers_check_interval,
|
||||
min_notification_delay: args.minimum_notification_delay,
|
||||
last_notification_sent: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn check_signers(&mut self) -> anyhow::Result<TestResult> {
|
||||
info!("starting signer check...");
|
||||
let check_result = check_signers_with_client(&self.nyxd_client).await?;
|
||||
|
||||
let mut unreachable_signers = 0;
|
||||
let mut unknown_local_chain_status = 0;
|
||||
let mut stalled_local_chain = 0;
|
||||
let mut working_local_chain = 0;
|
||||
let mut unknown_credential_issuance_status = 0;
|
||||
let mut working_credential_issuance = 0;
|
||||
let mut unavailable_credential_issuance = 0;
|
||||
|
||||
let mut fully_working = 0;
|
||||
|
||||
let mut signers = Vec::new();
|
||||
for result in &check_result.results {
|
||||
if result.signer_unreachable() {
|
||||
unreachable_signers += 1;
|
||||
}
|
||||
|
||||
if result.unknown_chain_status() {
|
||||
unknown_local_chain_status += 1;
|
||||
}
|
||||
if result.chain_available() {
|
||||
working_local_chain += 1;
|
||||
}
|
||||
if result.chain_provably_stalled() || result.chain_unprovably_stalled() {
|
||||
stalled_local_chain += 1;
|
||||
}
|
||||
|
||||
if result.unknown_signing_status() {
|
||||
unknown_credential_issuance_status += 1;
|
||||
}
|
||||
if result.signing_available() {
|
||||
working_credential_issuance += 1;
|
||||
}
|
||||
if result.signing_provably_unavailable() || result.signing_unprovably_unavailable() {
|
||||
unavailable_credential_issuance += 1;
|
||||
}
|
||||
|
||||
let signing_available = if result.unknown_signing_status() {
|
||||
None
|
||||
} else {
|
||||
Some(result.signing_available())
|
||||
};
|
||||
|
||||
let chain_not_stalled = if result.unknown_chain_status() {
|
||||
None
|
||||
} else {
|
||||
Some(result.chain_available())
|
||||
};
|
||||
|
||||
if (result.chain_available()) && (result.signing_available()) {
|
||||
fully_working += 1;
|
||||
}
|
||||
|
||||
signers.push(DisplayableSignerResult {
|
||||
version: result
|
||||
.try_get_test_result()
|
||||
.map(|r| r.reported_version.clone()),
|
||||
url: result.information.announce_address.clone(),
|
||||
signing_available,
|
||||
chain_not_stalled,
|
||||
})
|
||||
}
|
||||
|
||||
let signing_quorum_available = check_result.threshold.map(|threshold| {
|
||||
(working_local_chain as u64) >= threshold
|
||||
&& (working_credential_issuance as u64) >= threshold
|
||||
});
|
||||
signers.sort_by_key(|s| s.version.clone());
|
||||
|
||||
let summary = Summary {
|
||||
signing_quorum_available,
|
||||
fully_working,
|
||||
unreachable_signers,
|
||||
registered_signers: check_result.results.len(),
|
||||
unknown_local_chain_status,
|
||||
stalled_local_chain,
|
||||
working_local_chain,
|
||||
unknown_credential_issuance_status,
|
||||
working_credential_issuance,
|
||||
unavailable_credential_issuance,
|
||||
threshold: check_result.threshold,
|
||||
};
|
||||
|
||||
Ok(TestResult { summary, signers })
|
||||
}
|
||||
|
||||
async fn perform_signer_check(&mut self) -> anyhow::Result<()> {
|
||||
let result = self.check_signers().await?;
|
||||
let result_md = result.results_to_markdown_message();
|
||||
|
||||
let msg = if result.quorum_unavailable() {
|
||||
Some(format!("{LOST_QUORUM_MSG}\n\n{result_md}",))
|
||||
} else if result.quorum_unknown() {
|
||||
Some(format!("{UNKNOWN_QUORUM_MSG}\n\n{result_md}",))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(msg) = msg {
|
||||
self.maybe_notify_about_failure(&msg).await?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn maybe_notify_about_failure(&mut self, message: &String) -> anyhow::Result<()> {
|
||||
if let Some(last_notification_sent) = self.last_notification_sent {
|
||||
if last_notification_sent + self.min_notification_delay > OffsetDateTime::now_utc() {
|
||||
info!("too soon to send another notification");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
self.send_zulip_notification(message).await?;
|
||||
self.last_notification_sent = Some(OffsetDateTime::now_utc());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_zulip_notification(&self, message: &String) -> anyhow::Result<()> {
|
||||
self.zulip_client
|
||||
.send_channel_message((
|
||||
self.notification_channel_id,
|
||||
message,
|
||||
&self.notification_topic,
|
||||
))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_shutdown_notification(&self) -> anyhow::Result<()> {
|
||||
println!("here be sending shutdown notification");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn run(&mut self) -> anyhow::Result<()> {
|
||||
let shutdown_manager =
|
||||
ShutdownManager::new("nym-signers-monitor").with_default_shutdown_signals()?;
|
||||
|
||||
let mut check_interval = interval(self.check_interval);
|
||||
|
||||
while !shutdown_manager.root_token.is_cancelled() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown_manager.root_token.cancelled() => {
|
||||
info!("received shutdown");
|
||||
break;
|
||||
}
|
||||
_ = check_interval.tick() => {
|
||||
if let Err(err) = self.perform_signer_check().await {
|
||||
error!("failed to check signers: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
shutdown_manager.close();
|
||||
shutdown_manager.run_until_shutdown().await;
|
||||
|
||||
if let Err(err) = self.send_shutdown_notification().await {
|
||||
error!("failed to send shutdown notification: {err}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
fn maybe_bool_to_emoji_string(maybe_bool: Option<bool>) -> String {
|
||||
match maybe_bool {
|
||||
None => "⚠️ unknown".into(),
|
||||
Some(true) => "✅ yes".into(),
|
||||
Some(false) => "❌ no".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DisplayableSignerResult {
|
||||
pub(crate) url: String,
|
||||
pub(crate) version: Option<String>,
|
||||
pub(crate) signing_available: Option<bool>,
|
||||
pub(crate) chain_not_stalled: Option<bool>,
|
||||
}
|
||||
|
||||
impl DisplayableSignerResult {
|
||||
fn to_markdown_table_row(&self) -> String {
|
||||
format!(
|
||||
"| {} | {} | {} | {} |",
|
||||
self.url,
|
||||
self.version.as_deref().unwrap_or("unknown"),
|
||||
maybe_bool_to_emoji_string(self.signing_available),
|
||||
maybe_bool_to_emoji_string(self.chain_not_stalled)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TestResult {
|
||||
pub(crate) summary: Summary,
|
||||
pub(crate) signers: Vec<DisplayableSignerResult>,
|
||||
}
|
||||
|
||||
impl TestResult {
|
||||
pub(crate) fn quorum_unavailable(&self) -> bool {
|
||||
self.summary.signing_quorum_available.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub(crate) fn quorum_unknown(&self) -> bool {
|
||||
self.summary.signing_quorum_available.is_none()
|
||||
}
|
||||
|
||||
pub(crate) fn results_to_markdown_message(&self) -> String {
|
||||
let p_available = format!(
|
||||
"{:.2}",
|
||||
(self.summary.fully_working as f32 / self.summary.registered_signers as f32) * 100.
|
||||
);
|
||||
|
||||
format!(
|
||||
r#"
|
||||
## Summary
|
||||
- quorum available: {} ({p_available}% of signers fully available)
|
||||
- signers fully working: {}
|
||||
- signing threshold: {}
|
||||
- registered signers: {}
|
||||
- unreachable signers: {}
|
||||
|
||||
### Chain Status
|
||||
- unknown status: {}
|
||||
- working chain: {}
|
||||
- stalled chain: {}
|
||||
|
||||
### Credential Issuance Status
|
||||
(note: signers below 1.1.64 do not return fully reliable results)
|
||||
- unknown status: {}
|
||||
- working issuance: {}
|
||||
- unavailable issuance: {}
|
||||
|
||||
## Detailed Results
|
||||
| address | version | chain working | issuance (maybe) available |
|
||||
| - | - | - | - |
|
||||
{}
|
||||
"#,
|
||||
maybe_bool_to_emoji_string(self.summary.signing_quorum_available),
|
||||
self.summary.fully_working,
|
||||
self.summary
|
||||
.threshold
|
||||
.map(|threshold| threshold.to_string())
|
||||
.unwrap_or("???".to_string()),
|
||||
self.summary.registered_signers,
|
||||
self.summary.unreachable_signers,
|
||||
self.summary.unknown_local_chain_status,
|
||||
self.summary.working_local_chain,
|
||||
self.summary.stalled_local_chain,
|
||||
self.summary.unknown_credential_issuance_status,
|
||||
self.summary.working_credential_issuance,
|
||||
self.summary.unavailable_credential_issuance,
|
||||
self.signers
|
||||
.iter()
|
||||
.map(|r| r.to_markdown_table_row())
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Summary {
|
||||
pub(crate) signing_quorum_available: Option<bool>,
|
||||
pub(crate) fully_working: usize,
|
||||
pub(crate) threshold: Option<u64>,
|
||||
|
||||
pub(crate) registered_signers: usize,
|
||||
pub(crate) unreachable_signers: usize,
|
||||
|
||||
pub(crate) unknown_local_chain_status: usize,
|
||||
pub(crate) stalled_local_chain: usize,
|
||||
pub(crate) working_local_chain: usize,
|
||||
|
||||
pub(crate) unknown_credential_issuance_status: usize,
|
||||
pub(crate) working_credential_issuance: usize,
|
||||
pub(crate) unavailable_credential_issuance: usize,
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "15.3.2",
|
||||
"minimumSystemVersion": "14.4.1",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": "Developer ID Application: Nym Technologies SA (VW5DZLFHM5)",
|
||||
"entitlements": null
|
||||
|
||||
Reference in New Issue
Block a user