Compare commits

..

20 Commits

Author SHA1 Message Date
Jędrzej Stuczyński f77e350211 [squashed] Created a DKG-manager tui interface 2023-11-30 17:38:19 +00:00
Jon Häggblad 4a8c09f476 Handle dynamic ip allocation in ip packet router (#4186)
* Add dynamic connect support to ip packet router

* Disconnect inactive clients

* Don't generate ip same as tun device

* clippy

* Extract a few functions to separate mod

* clippy
2023-11-27 15:31:35 +01:00
Jon Häggblad ca2ad13579 Handle connect request in IPR (#4183)
* wip: first attempt at connected clients

* redo connected clients

* fix bug in if statement
2023-11-27 13:18:31 +01:00
Jon Häggblad 44395f8466 Implement ip-packet-router connect request response outline (#4177)
* Try revised request response connect types

* wip: try to get data requests work

* Also serialize response

* tweak log

* Add code path for connect response fail

* Convenience functions on ip packet request

* Remove commented out code

* Restore log to debug
2023-11-27 12:17:58 +01:00
Jędrzej Stuczyński 39d714f2c0 Merge pull request #4171 from nymtech/research/partial_blind_signature_verification
Research/partial blind signature verification
2023-11-27 09:57:44 +00:00
Jon Häggblad 336f220c83 Fix some clippy for beta toolchain (#4182) 2023-11-27 10:52:18 +01:00
Tommy Verrall 6b2c13b3fd Merge pull request #4126 from nymtech/chore/adjust-selection-weights
using performance^20 when calculating active set selection weight
2023-11-27 07:46:57 +00:00
Tommy Verrall 17357da97e Merge pull request #4167 from nymtech/bugfix/gateway-ws-listener-before-NR
start gateway websocket listener before embedded NR
2023-11-24 17:59:06 +00:00
Jędrzej Stuczyński d92c8c4149 deal with the pow error during stake adjustment 2023-11-24 09:40:17 +00:00
Jędrzej Stuczyński 4c8fa74dfe remove the panic 2023-11-24 09:31:46 +00:00
Jędrzej Stuczyński b1ac5b4e86 using multimiller loop for partial blinded signature verification 2023-11-23 11:21:47 +00:00
aniampio ae22a132a3 Update benchmarks 2023-11-23 11:21:47 +00:00
aniampio d0b9583b79 Add benchmark for verification of a single partial blind signature 2023-11-23 11:21:46 +00:00
aniampio 1f5568b4a7 Extend the function to support also public attributes 2023-11-23 11:21:46 +00:00
aniampio 16f51a0014 Add documentation to the verification function 2023-11-23 11:21:46 +00:00
aniampio 5e8767c5d6 Update tests 2023-11-23 11:21:46 +00:00
aniampio 15abfb390f Make the function more generic 2023-11-23 11:21:46 +00:00
aniampio 45e5f12198 Add function to verify partial blind signature 2023-11-23 11:21:46 +00:00
Jędrzej Stuczyński 24746dc47b start gateway websocket listener before embedded NR 2023-11-21 14:32:23 +00:00
Jędrzej Stuczyński ef8e452f30 using performance^20 when calculating active set selection weight 2023-11-10 09:54:03 +00:00
51 changed files with 3638 additions and 258 deletions
+1
View File
@@ -9,6 +9,7 @@
target
.env
.env.dev
envs/devnet.env
/.vscode/settings.json
validator/.vscode
sample-configs/validator-config.toml
Generated
+6 -75
View File
@@ -2337,24 +2337,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "defguard_wireguard_rs"
version = "0.3.0"
dependencies = [
"base64 0.21.4",
"libc",
"log",
"netlink-packet-core 0.7.0",
"netlink-packet-generic",
"netlink-packet-route 0.17.1",
"netlink-packet-utils",
"netlink-packet-wireguard",
"netlink-sys",
"nix 0.27.1",
"serde",
"thiserror",
]
[[package]]
name = "der"
version = "0.6.1"
@@ -5712,29 +5694,6 @@ dependencies = [
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-core"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4"
dependencies = [
"anyhow",
"byteorder",
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-generic"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd7eb8ad331c84c6b8cb7f685b448133e5ad82e1ffd5acafac374af4a5a308b"
dependencies = [
"anyhow",
"byteorder",
"netlink-packet-core 0.7.0",
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-route"
version = "0.12.0"
@@ -5745,21 +5704,7 @@ dependencies = [
"bitflags 1.3.2",
"byteorder",
"libc",
"netlink-packet-core 0.4.2",
"netlink-packet-utils",
]
[[package]]
name = "netlink-packet-route"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66"
dependencies = [
"anyhow",
"bitflags 1.3.2",
"byteorder",
"libc",
"netlink-packet-core 0.7.0",
"netlink-packet-core",
"netlink-packet-utils",
]
@@ -5775,20 +5720,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "netlink-packet-wireguard"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b25b050ff1f6a1e23c6777b72db22790fe5b6b5ccfd3858672587a79876c8f"
dependencies = [
"anyhow",
"byteorder",
"libc",
"log",
"netlink-packet-generic",
"netlink-packet-utils",
]
[[package]]
name = "netlink-proto"
version = "0.10.0"
@@ -5798,7 +5729,7 @@ dependencies = [
"bytes",
"futures",
"log",
"netlink-packet-core 0.4.2",
"netlink-packet-core",
"netlink-sys",
"thiserror",
"tokio",
@@ -5850,7 +5781,6 @@ dependencies = [
"bitflags 2.4.1",
"cfg-if",
"libc",
"memoffset 0.9.0",
]
[[package]]
@@ -6552,7 +6482,6 @@ dependencies = [
"clap 4.4.7",
"colored",
"dashmap",
"defguard_wireguard_rs",
"dirs 4.0.0",
"dotenvy",
"futures",
@@ -6679,7 +6608,9 @@ dependencies = [
"bincode",
"bytes",
"nym-sphinx",
"rand 0.8.5",
"serde",
"thiserror",
]
[[package]]
@@ -6704,6 +6635,7 @@ dependencies = [
"nym-tun",
"nym-wireguard",
"nym-wireguard-types",
"rand 0.8.5",
"reqwest",
"serde",
"serde_json",
@@ -7628,7 +7560,6 @@ dependencies = [
"boringtun",
"bytes",
"dashmap",
"defguard_wireguard_rs",
"etherparse",
"futures",
"ip_network",
@@ -9292,7 +9223,7 @@ checksum = "322c53fd76a18698f1c27381d58091de3a043d356aa5bd0d510608b565f469a0"
dependencies = [
"futures",
"log",
"netlink-packet-route 0.12.0",
"netlink-packet-route",
"netlink-proto",
"nix 0.24.3",
"thiserror",
+3 -1
View File
@@ -103,6 +103,7 @@ members = [
"nym-outfox",
"tools/internal/ssl-inject",
"tools/internal/sdk-version-bump",
"tools/internal/dkg-manager",
"tools/nym-cli",
"tools/nym-nr-query",
"tools/ts-rs-cli",
@@ -160,7 +161,8 @@ serde = "1.0.152"
serde_json = "1.0.91"
tap = "1.0.1"
thiserror = "1.0.48"
tokio = "1.24.1"
tokio = "1.33.0"
tokio-util = "0.7.10"
tokio-tungstenite = "0.20.1"
tracing = "0.1.37"
tungstenite = { version = "0.20.1", default-features = false }
+2 -2
View File
@@ -9,8 +9,8 @@ edition = "2021"
[dependencies]
futures = { workspace = true }
log = { workspace = true }
tokio = { version = "1.24.1", features = ["time", "net", "rt"] }
tokio-util = { version = "0.7.4", features = ["codec"] }
tokio = { workspace = true, features = ["time", "net", "rt"] }
tokio-util = { workspace = true, features = ["codec"] }
# internal
nym-sphinx = { path = "../../nymsphinx" }
@@ -32,7 +32,7 @@ pub trait CoconutBandwidthSigningClient {
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = CoconutBandwidthExecuteMsg::DepositFunds {
data: DepositData::new(info.to_string(), verification_key, encryption_key),
data: DepositData::new(info, verification_key, encryption_key),
};
self.execute_coconut_bandwidth_contract(
fee,
@@ -6,8 +6,8 @@ use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cw3::{
ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterListResponse,
VoterResponse,
ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterDetail,
VoterListResponse, VoterResponse,
};
use cw_utils::ThresholdResponse;
use nym_multisig_contract_common::msg::QueryMsg as MultisigQueryMsg;
@@ -114,6 +114,26 @@ pub trait PagedMultisigQueryClient: MultisigQueryClient {
Ok(proposals)
}
async fn get_all_voters(&self) -> Result<Vec<VoterDetail>, NyxdError> {
let mut voters = Vec::new();
let mut start_after = None;
loop {
let mut paged_response = self.list_voters(start_after.take(), None).await?;
let last_voter = paged_response.voters.last().map(|prop| prop.addr.clone());
voters.append(&mut paged_response.voters);
if let Some(start_after_res) = last_voter {
start_after = Some(start_after_res)
} else {
break;
}
}
Ok(voters)
}
}
#[async_trait]
@@ -47,6 +47,10 @@ pub use cosmrs::Coin as CosmosCoin;
pub use cosmrs::Gas;
pub use cosmrs::{bip32, AccountId, Denom};
pub use cosmwasm_std::Coin as CosmWasmCoin;
pub use cw2;
pub use cw3;
pub use cw4;
pub use cw_controllers;
pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
pub use tendermint_rpc::{
endpoint::{tx::Response as TxResponse, validators::Response as ValidatorResponse},
@@ -174,17 +174,19 @@ impl Display for EpochState {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
EpochState::PublicKeySubmission { resharing } => {
write!(f, "PublicKeySubmission with resharing {resharing}")
write!(f, "PublicKeySubmission (resharing: {resharing})")
}
EpochState::DealingExchange { resharing } => {
write!(f, "DealingExchange (resharing: {resharing})")
}
EpochState::DealingExchange { resharing } => write!(f, "DealingExchange {resharing}"),
EpochState::VerificationKeySubmission { resharing } => {
write!(f, "VerificationKeySubmission with resharing {resharing}")
write!(f, "VerificationKeySubmission (resharing: {resharing})")
}
EpochState::VerificationKeyValidation { resharing } => {
write!(f, "VerificationKeyValidation with resharing {resharing}")
write!(f, "VerificationKeyValidation (resharing: {resharing})")
}
EpochState::VerificationKeyFinalization { resharing } => {
write!(f, "VerificationKeyFinalization with resharing {resharing}")
write!(f, "VerificationKeyFinalization (resharing: {resharing})")
}
EpochState::InProgress => write!(f, "InProgress"),
}
@@ -3,6 +3,7 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Decimal;
use cosmwasm_std::OverflowError;
use cosmwasm_std::Uint128;
use serde::de::Error;
use serde::{Deserialize, Deserializer};
@@ -71,6 +72,10 @@ impl Percent {
// we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range
truncate_decimal(hundred * self.0).u128() as u8
}
pub fn checked_pow(&self, exp: u32) -> Result<Self, OverflowError> {
self.0.checked_pow(exp).map(Percent)
}
}
impl Display for Percent {
+5 -25
View File
@@ -366,11 +366,7 @@ pub fn creating_proof_of_chunking_for_100_parties(c: &mut Criterion) {
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let remote_share_key_pairs = shares.iter().zip(receivers.values()).collect::<Vec<_>>();
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, &params, &mut rng);
@@ -400,11 +396,7 @@ pub fn verifying_proof_of_chunking_for_100_parties(c: &mut Criterion) {
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let remote_share_key_pairs = shares.iter().zip(receivers.values()).collect::<Vec<_>>();
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, &params, &mut rng);
@@ -436,11 +428,7 @@ pub fn creating_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) {
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let remote_share_key_pairs = shares.iter().zip(receivers.values()).collect::<Vec<_>>();
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, &params, &mut rng);
@@ -478,11 +466,7 @@ pub fn verifying_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) {
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let remote_share_key_pairs = shares.iter().zip(receivers.values()).collect::<Vec<_>>();
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, &params, &mut rng);
@@ -545,11 +529,7 @@ pub fn share_encryption_100(c: &mut Criterion) {
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
.collect::<Vec<_>>();
let remote_share_key_pairs = shares
.iter()
.zip(receivers.values())
.map(|(share, key)| (share, key))
.collect::<Vec<_>>();
let remote_share_key_pairs = shares.iter().zip(receivers.values()).collect::<Vec<_>>();
c.bench_function("100 shares encryption", |b| {
b.iter(|| black_box(encrypt_shares(&remote_share_key_pairs, &params, &mut rng)))
+2
View File
@@ -14,4 +14,6 @@ license.workspace = true
bincode = "1.3.3"
bytes = "1.5.0"
nym-sphinx = { path = "../nymsphinx" }
rand = "0.8.5"
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
+349 -16
View File
@@ -1,35 +1,308 @@
#[derive(serde::Serialize, serde::Deserialize)]
pub struct TaggedIpPacket {
pub packet: bytes::Bytes,
pub return_address: nym_sphinx::addressing::clients::Recipient,
pub return_mix_hops: Option<u8>,
// pub return_mix_delays: Option<f64>,
use std::net::IpAddr;
use nym_sphinx::addressing::clients::Recipient;
use serde::{Deserialize, Serialize};
pub const CURRENT_VERSION: u8 = 1;
fn generate_random() -> u64 {
use rand::RngCore;
let mut rng = rand::rngs::OsRng;
rng.next_u64()
}
impl TaggedIpPacket {
pub fn new(
packet: bytes::Bytes,
return_address: nym_sphinx::addressing::clients::Recipient,
return_mix_hops: Option<u8>,
) -> Self {
TaggedIpPacket {
packet,
return_address,
return_mix_hops,
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IpPacketRequest {
pub version: u8,
pub data: IpPacketRequestData,
}
impl IpPacketRequest {
pub fn new_static_connect_request(
ip: IpAddr,
reply_to: Recipient,
reply_to_hops: Option<u8>,
reply_to_avg_mix_delays: Option<f64>,
) -> (Self, u64) {
let request_id = generate_random();
(
Self {
version: CURRENT_VERSION,
data: IpPacketRequestData::StaticConnect(StaticConnectRequest {
request_id,
ip,
reply_to,
reply_to_hops,
reply_to_avg_mix_delays,
}),
},
request_id,
)
}
pub fn new_dynamic_connect_request(
reply_to: Recipient,
reply_to_hops: Option<u8>,
reply_to_avg_mix_delays: Option<f64>,
) -> (Self, u64) {
let request_id = generate_random();
(
Self {
version: CURRENT_VERSION,
data: IpPacketRequestData::DynamicConnect(DynamicConnectRequest {
request_id,
reply_to,
reply_to_hops,
reply_to_avg_mix_delays,
}),
},
request_id,
)
}
pub fn new_ip_packet(ip_packet: bytes::Bytes) -> Self {
Self {
version: CURRENT_VERSION,
data: IpPacketRequestData::Data(DataRequest { ip_packet }),
}
}
pub fn id(&self) -> Option<u64> {
match &self.data {
IpPacketRequestData::StaticConnect(request) => Some(request.request_id),
IpPacketRequestData::DynamicConnect(request) => Some(request.request_id),
IpPacketRequestData::Data(_) => None,
}
}
pub fn recipient(&self) -> Option<&Recipient> {
match &self.data {
IpPacketRequestData::StaticConnect(request) => Some(&request.reply_to),
IpPacketRequestData::DynamicConnect(request) => Some(&request.reply_to),
IpPacketRequestData::Data(_) => None,
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum IpPacketRequestData {
StaticConnect(StaticConnectRequest),
DynamicConnect(DynamicConnectRequest),
Data(DataRequest),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct StaticConnectRequest {
pub request_id: u64,
pub ip: IpAddr,
pub reply_to: Recipient,
pub reply_to_hops: Option<u8>,
pub reply_to_avg_mix_delays: Option<f64>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct DynamicConnectRequest {
pub request_id: u64,
pub reply_to: Recipient,
pub reply_to_hops: Option<u8>,
pub reply_to_avg_mix_delays: Option<f64>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct DataRequest {
pub ip_packet: bytes::Bytes,
}
// ---
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct IpPacketResponse {
pub version: u8,
pub data: IpPacketResponseData,
}
impl IpPacketResponse {
pub fn new_static_connect_success(request_id: u64, reply_to: Recipient) -> Self {
Self {
version: CURRENT_VERSION,
data: IpPacketResponseData::StaticConnect(StaticConnectResponse {
request_id,
reply_to,
reply: StaticConnectResponseReply::Success,
}),
}
}
pub fn new_static_connect_failure(
request_id: u64,
reply_to: Recipient,
reason: StaticConnectFailureReason,
) -> Self {
Self {
version: CURRENT_VERSION,
data: IpPacketResponseData::StaticConnect(StaticConnectResponse {
request_id,
reply_to,
reply: StaticConnectResponseReply::Failure(reason),
}),
}
}
pub fn new_dynamic_connect_success(request_id: u64, reply_to: Recipient, ip: IpAddr) -> Self {
Self {
version: CURRENT_VERSION,
data: IpPacketResponseData::DynamicConnect(DynamicConnectResponse {
request_id,
reply_to,
reply: DynamicConnectResponseReply::Success(DynamicConnectSuccess { ip }),
}),
}
}
pub fn new_dynamic_connect_failure(
request_id: u64,
reply_to: Recipient,
reason: DynamicConnectFailureReason,
) -> Self {
Self {
version: CURRENT_VERSION,
data: IpPacketResponseData::DynamicConnect(DynamicConnectResponse {
request_id,
reply_to,
reply: DynamicConnectResponseReply::Failure(reason),
}),
}
}
pub fn new_ip_packet(ip_packet: bytes::Bytes) -> Self {
Self {
version: CURRENT_VERSION,
data: IpPacketResponseData::Data(DataResponse { ip_packet }),
}
}
pub fn id(&self) -> Option<u64> {
match &self.data {
IpPacketResponseData::StaticConnect(response) => Some(response.request_id),
IpPacketResponseData::DynamicConnect(response) => Some(response.request_id),
IpPacketResponseData::Data(_) => None,
}
}
pub fn recipient(&self) -> Option<&Recipient> {
match &self.data {
IpPacketResponseData::StaticConnect(response) => Some(&response.reply_to),
IpPacketResponseData::DynamicConnect(response) => Some(&response.reply_to),
IpPacketResponseData::Data(_) => None,
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum IpPacketResponseData {
StaticConnect(StaticConnectResponse),
DynamicConnect(DynamicConnectResponse),
Data(DataResponse),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StaticConnectResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: StaticConnectResponseReply,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum StaticConnectResponseReply {
Success,
Failure(StaticConnectFailureReason),
}
impl StaticConnectResponseReply {
pub fn is_success(&self) -> bool {
match self {
StaticConnectResponseReply::Success => true,
StaticConnectResponseReply::Failure(_) => false,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
pub enum StaticConnectFailureReason {
#[error("requested ip address is already in use")]
RequestedIpAlreadyInUse,
#[error("requested nym-address is already in use")]
RequestedNymAddressAlreadyInUse,
#[error("{0}")]
Other(String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DynamicConnectResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: DynamicConnectResponseReply,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum DynamicConnectResponseReply {
Success(DynamicConnectSuccess),
Failure(DynamicConnectFailureReason),
}
impl DynamicConnectResponseReply {
pub fn is_success(&self) -> bool {
match self {
DynamicConnectResponseReply::Success(_) => true,
DynamicConnectResponseReply::Failure(_) => false,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DynamicConnectSuccess {
pub ip: IpAddr,
}
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
pub enum DynamicConnectFailureReason {
#[error("requested nym-address is already in use")]
RequestedNymAddressAlreadyInUse,
#[error("no available ip address")]
NoAvailableIp,
#[error("{0}")]
Other(String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DataResponse {
pub ip_packet: bytes::Bytes,
}
fn make_bincode_serializer() -> impl bincode::Options {
@@ -38,3 +311,63 @@ fn make_bincode_serializer() -> impl bincode::Options {
.with_big_endian()
.with_varint_encoding()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_size_of_request() {
let connect = IpPacketRequest {
version: 4,
data: IpPacketRequestData::StaticConnect(
StaticConnectRequest {
request_id: 123,
ip: IpAddr::from([10, 0, 0, 1]),
reply_to: Recipient::try_from_base58_string("D1rrpsysCGCYXy9saP8y3kmNpGtJZUXN9SvFoUcqAsM9.9Ssso1ea5NfkbMASdiseDSjTN1fSWda5SgEVjdSN4CvV@GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN").unwrap(),
reply_to_hops: None,
reply_to_avg_mix_delays: None,
},
)
};
assert_eq!(connect.to_bytes().unwrap().len(), 107);
}
#[test]
fn check_size_of_data() {
let data = IpPacketRequest {
version: 4,
data: IpPacketRequestData::Data(DataRequest {
ip_packet: bytes::Bytes::from(vec![1u8; 32]),
}),
};
assert_eq!(data.to_bytes().unwrap().len(), 35);
}
#[test]
fn serialize_and_deserialize_data_request() {
let data = IpPacketRequest {
version: 4,
data: IpPacketRequestData::Data(DataRequest {
ip_packet: bytes::Bytes::from(vec![1, 2, 4, 2, 5]),
}),
};
let serialized = data.to_bytes().unwrap();
let deserialized = IpPacketRequest::from_reconstructed_message(
&nym_sphinx::receiver::ReconstructedMessage {
message: serialized,
sender_tag: None,
},
)
.unwrap();
assert_eq!(deserialized.version, 4);
assert_eq!(
deserialized.data,
IpPacketRequestData::Data(DataRequest {
ip_packet: bytes::Bytes::from(vec![1, 2, 4, 2, 5]),
})
);
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ tokio = { version = "1.24.1", features = [
"net",
"io-util",
] }
tokio-util = { version = "0.7.4", features = ["codec"] }
tokio-util = { workspace = true, features = ["codec"] }
url = { workspace = true }
thiserror = { workspace = true }
+27 -5
View File
@@ -6,9 +6,10 @@ use criterion::{criterion_group, criterion_main, Criterion};
use ff::Field;
use group::{Curve, Group};
use nym_coconut::{
aggregate_signature_shares, aggregate_verification_keys, blind_sign, elgamal_keygen,
prepare_blind_sign, prove_bandwidth_credential, setup, ttp_keygen, verify_credential,
Attribute, BlindedSignature, Parameters, Signature, SignatureShare, VerificationKey,
aggregate_signature_shares, aggregate_verification_keys, blind_sign, prepare_blind_sign,
prove_bandwidth_credential, setup, ttp_keygen, verify_credential,
verify_partial_blind_signature, Attribute, BlindedSignature, Parameters, Signature,
SignatureShare, VerificationKey,
};
use rand::seq::SliceRandom;
use std::ops::Neg;
@@ -175,8 +176,6 @@ fn bench_coconut(c: &mut Criterion) {
let binding_number = params.random_scalar();
let private_attributes = vec![serial_number, binding_number];
let _elgamal_keypair = elgamal_keygen(&params);
// The prepare blind sign is performed by the user
let (pedersen_commitments_openings, blind_sign_request) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
@@ -242,6 +241,29 @@ fn bench_coconut(c: &mut Criterion) {
.map(|keypair| keypair.verification_key())
.collect();
// verify a random partial blind signature
let rand_idx = 1;
let random_blind_signature = blinded_signatures.get(rand_idx).unwrap();
let partial_verification_key = verification_keys.get(rand_idx).unwrap();
group.bench_function(
&format!(
"verify_partial_blind_signature_{}_private_attributes_{}_public_attributes",
case.num_private_attrs, case.num_public_attrs
),
|b| {
b.iter(|| {
verify_partial_blind_signature(
&params,
&blind_sign_request,
&public_attributes,
random_blind_signature,
partial_verification_key,
)
})
},
);
// Lets bench worse case, ie aggregating all
let indices: Vec<u64> = (1..=case.num_authorities).collect();
// aggregate verification keys
+1
View File
@@ -14,6 +14,7 @@ pub use scheme::aggregation::aggregate_signature_shares;
pub use scheme::aggregation::aggregate_verification_keys;
pub use scheme::issuance::blind_sign;
pub use scheme::issuance::prepare_blind_sign;
pub use scheme::issuance::verify_partial_blind_signature;
pub use scheme::issuance::BlindSignRequest;
pub use scheme::keygen::ttp_keygen;
pub use scheme::keygen::KeyPair;
+172 -2
View File
@@ -3,12 +3,14 @@
use std::convert::TryFrom;
use std::convert::TryInto;
use std::ops::Neg;
use bls12_381::{G1Affine, G1Projective, Scalar};
use group::{Curve, GroupEncoding};
use bls12_381::{multi_miller_loop, G1Affine, G1Projective, G2Prepared, Scalar};
use group::{Curve, Group, GroupEncoding};
use crate::error::{CoconutError, Result};
use crate::proofs::ProofCmCs;
use crate::scheme::keygen::VerificationKey;
use crate::scheme::setup::Parameters;
use crate::scheme::BlindedSignature;
use crate::scheme::SecretKey;
@@ -318,6 +320,96 @@ pub fn blind_sign(
Ok(BlindedSignature(h, sig))
}
/// Verifies a partial blind signature using the provided parameters and validator's verification key.
///
/// # Arguments
///
/// * `params` - A reference to the cryptographic parameters.
/// * `blind_sign_request` - A reference to the blind signature request signed by the client.
/// * `public_attributes` - A reference to the public attributes included in the client's request.
/// * `blind_sig` - A reference to the issued partial blinded signature to be verified.
/// * `partial_verification_key` - A reference to the validator's partial verification key.
///
/// # Returns
///
/// A boolean indicating whether the partial blind signature is valid (`true`) or not (`false`).
///
/// # Remarks
///
/// This function verifies the correctness and validity of a partial blind signature using
/// the provided cryptographic parameters, blind signature request, blinded signature,
/// and partial verification key.
/// It calculates pairings based on the provided values and checks whether the partial blind signature
/// is consistent with the verification key and commitments in the blind signature request.
/// The function returns `true` if the partial blind signature is valid, and `false` otherwise.
pub fn verify_partial_blind_signature(
params: &Parameters,
blind_sign_request: &BlindSignRequest,
public_attributes: &[Attribute],
blind_sig: &BlindedSignature,
partial_verification_key: &VerificationKey,
) -> bool {
let num_private_attributes = blind_sign_request.private_attributes_commitments.len();
if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() {
return false;
}
// TODO: we're losing some memory here due to extra allocation,
// but worst-case scenario (given SANE amount of attributes), it's just few kb at most
let c_neg = blind_sig.1.to_affine().neg();
let g2_prep = params.prepared_miller_g2();
let mut terms = vec![
// (c^{-1}, g2)
(c_neg, g2_prep.clone()),
// (s, alpha)
(
blind_sig.0.to_affine(),
G2Prepared::from(partial_verification_key.alpha.to_affine()),
),
];
// for each private attribute, add (cm_i, beta_i) to the miller terms
for (private_attr_commit, beta_g2) in blind_sign_request
.private_attributes_commitments
.iter()
.zip(&partial_verification_key.beta_g2)
{
// (cm_i, beta_i)
terms.push((
private_attr_commit.to_affine(),
G2Prepared::from(beta_g2.to_affine()),
))
}
// for each public attribute, add (s^pub_j, beta_{priv + j}) to the miller terms
for (pub_attr, beta_g2) in public_attributes.iter().zip(
partial_verification_key
.beta_g2
.iter()
.skip(num_private_attributes),
) {
// (s^pub_j, beta_j)
terms.push((
(blind_sig.0 * pub_attr).to_affine(),
G2Prepared::from(beta_g2.to_affine()),
))
}
// get the references to all the terms to get the arguments the miller loop expects
let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::<Vec<_>>();
// since checking whether e(a, b) == e(c, d)
// is equivalent to checking e(a, b) • e(c, d)^{-1} == id
// and thus to e(a, b) • e(c^{-1}, d) == id
//
// compute e(c^1, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j})
multi_miller_loop(&terms_refs)
.final_exponentiation()
.is_identity()
.into()
}
#[cfg(test)]
pub fn sign(
params: &mut Parameters,
@@ -354,6 +446,7 @@ pub fn sign(
#[cfg(test)]
mod tests {
use super::*;
use crate::scheme::keygen::keygen;
#[test]
fn blind_sign_request_bytes_roundtrip() {
@@ -385,4 +478,81 @@ mod tests {
lambda
);
}
#[test]
fn successful_verify_partial_blind_signature() {
let params = Parameters::new(4).unwrap();
let private_attributes = params.n_random_scalars(2);
let public_attributes = params.n_random_scalars(2);
let (_commitments_openings, request) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
let validator_keypair = keygen(&params);
let blind_sig = blind_sign(
&params,
&validator_keypair.secret_key(),
&request,
&public_attributes,
)
.unwrap();
assert!(verify_partial_blind_signature(
&params,
&request,
&public_attributes,
&blind_sig,
&validator_keypair.verification_key()
));
}
#[test]
fn successful_verify_partial_blind_signature_no_public_attributes() {
let params = Parameters::new(4).unwrap();
let private_attributes = params.n_random_scalars(2);
let (_commitments_openings, request) =
prepare_blind_sign(&params, &private_attributes, &[]).unwrap();
let validator_keypair = keygen(&params);
let blind_sig =
blind_sign(&params, &validator_keypair.secret_key(), &request, &[]).unwrap();
assert!(verify_partial_blind_signature(
&params,
&request,
&[],
&blind_sig,
&validator_keypair.verification_key()
));
}
#[test]
fn fail_verify_partial_blind_signature_with_wrong_key() {
let params = Parameters::new(4).unwrap();
let private_attributes = params.n_random_scalars(2);
let public_attributes = params.n_random_scalars(2);
let (_commitments_openings, request) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
let validator_keypair = keygen(&params);
let validator2_keypair = keygen(&params);
let blind_sig = blind_sign(
&params,
&validator_keypair.secret_key(),
&request,
&public_attributes,
)
.unwrap();
// this assertion should fail, as we try to verify with a wrong validator key
assert!(!verify_partial_blind_signature(
&params,
&request,
&public_attributes,
&blind_sig,
&validator2_keypair.verification_key()
),);
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ repository = { workspace = true }
[dependencies]
bytes = "1.0"
tokio-util = { version = "0.7.4", features = ["codec"] }
tokio-util = { workspace = true, features = ["codec"] }
thiserror = { workspace = true }
nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] }
+2 -2
View File
@@ -8,8 +8,8 @@ edition = "2021"
[dependencies]
bytes = "1.0"
tokio = { version = "1.24.1", features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] }
tokio-util = { version = "0.7.4", features = [ "io" ] } # reason for getting this guy is to to able to port to tokio 1.X more quickly by being able to use
tokio = { workspace = true, features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] }
tokio-util = { workspace = true, features = [ "io" ] } # reason for getting this guy is to to able to port to tokio 1.X more quickly by being able to use
# their `read_buf` [from the util crate] replacement rather than having to rethink/reimplement `AvailableReader` with the new AsyncRead trait definition.
# In the long run, the dependency should probably get removed in favour of pure-tokio implementation, but for time being it's fine.
futures = { workspace = true }
+1 -1
View File
@@ -198,7 +198,7 @@ impl TunDevice {
async fn handle_tun_read(&self, packet: &[u8]) -> Result<(), TunDeviceError> {
let ParsedAddresses { src_addr, dst_addr } = parse_src_dst_address(packet)?;
log::debug!(
"iface: read Packet({src_addr} -> {dst_addr}, {} bytes)",
"iface: read Packet({dst_addr} <- {src_addr}, {} bytes)",
packet.len(),
);
-1
View File
@@ -20,7 +20,6 @@ bincode = "1.3.3"
#boringtun = "0.6.0"
boringtun = { workspace = true }
bytes = "1.5.0"
defguard_wireguard_rs = { path = "../../../wireguard-rs" }
dashmap = "5.5.3"
etherparse = "0.13.0"
futures = "0.3.28"
+64 -24
View File
@@ -17,34 +17,74 @@ use std::sync::Arc;
#[cfg(target_os = "linux")]
use nym_tun::tun_device;
use defguard_wireguard_rs::{host::Peer, InterfaceConfiguration, WGApi, WireguardInterfaceApi};
use nym_network_defaults::{WG_PORT, WG_TUN_DEVICE_ADDRESS};
use nym_tun::tun_task_channel;
use setup::PRIVATE_KEY;
/// Start wireguard device
/// Start wireguard UDP listener and TUN device
///
/// # Errors
///
/// This function will return an error if either the UDP listener of the TUN device fails to start.
#[cfg(target_os = "linux")]
pub async fn start_wireguard(
mut task_client: nym_task::TaskClient,
_gateway_client_registry: Arc<GatewayClientRegistry>,
) -> Result<WGApi, Box<dyn std::error::Error + Send + Sync + 'static>> {
let ifname = String::from("wg0");
let wgapi = WGApi::new(ifname.clone(), false)?;
wgapi.create_interface()?;
let interface_config = InterfaceConfiguration {
name: ifname.clone(),
prvkey: PRIVATE_KEY.to_string(),
address: WG_TUN_DEVICE_ADDRESS.to_string(),
port: WG_PORT as u32,
peers: vec![],
task_client: nym_task::TaskClient,
gateway_client_registry: Arc<GatewayClientRegistry>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
// TODO: make this configurable
// We can optionally index peers by their IP like standard wireguard. If we don't then we do
// plain NAT where we match incoming destination IP with outgoing source IP.
use nym_wireguard_types::tun_common::network_table::NetworkTable;
let peers_by_ip = Arc::new(tokio::sync::Mutex::new(NetworkTable::new()));
// Alternative 1:
let routing_mode = tun_device::RoutingMode::new_allowed_ips(peers_by_ip.clone());
// Alternative 2:
//let routing_mode = tun_device::RoutingMode::new_nat();
// Start the tun device that is used to relay traffic outbound
let config = tun_device::TunDeviceConfig {
base_name: nym_network_defaults::WG_TUN_BASE_NAME.to_string(),
ip: nym_network_defaults::WG_TUN_DEVICE_ADDRESS.parse().unwrap(),
netmask: nym_network_defaults::WG_TUN_DEVICE_NETMASK.parse().unwrap(),
};
wgapi.configure_interface(&interface_config)?;
let peer = std::env::var("NYM_PEER_PUBLIC_KEY").expect("NYM_PEER_PUBLIC_KEY must be set");
let mut peer = Peer::new(peer.as_str().try_into().unwrap());
peer.set_allowed_ips(vec!["10.1.0.2".parse().unwrap()]);
wgapi.configure_peer(&peer)?;
wgapi.configure_peer_routing(&vec![peer.clone()])?;
let (tun, tun_task_tx, tun_task_response_rx) = tun_device::TunDevice::new(routing_mode, config);
tun.start();
tokio::spawn(async move { task_client.recv().await });
// We also index peers by a tag
let peers_by_tag = Arc::new(tokio::sync::Mutex::new(wg_tunnel::PeersByTag::new()));
Ok(wgapi)
// If we want to have the tun device on a separate host, it's the tun_task and
// tun_task_response channels that needs to be sent over the network to the host where the tun
// device is running.
// The packet relayer's responsibility is to route packets between the correct tunnel and the
// tun device. The tun device may or may not be on a separate host, which is why we can't do
// this routing in the tun device itself.
let (packet_relayer, packet_tx) = packet_relayer::PacketRelayer::new(
tun_task_tx.clone(),
tun_task_response_rx,
peers_by_tag.clone(),
);
packet_relayer.start();
// Start the UDP listener that clients connect to
let udp_listener = udp_listener::WgUdpListener::new(
packet_tx,
peers_by_ip,
peers_by_tag,
Arc::clone(&gateway_client_registry),
)
.await?;
udp_listener.start(task_client);
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub async fn start_wireguard(
_task_client: nym_task::TaskClient,
_gateway_client_registry: Arc<GatewayClientRegistry>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
todo!("WireGuard is currently only supported on Linux")
}
+1 -1
View File
@@ -9,7 +9,7 @@ pub const WG_ADDRESS: &str = "0.0.0.0";
// The private key of the listener
// Corresponding public key: "WM8s8bYegwMa0TJ+xIwhk+dImk2IpDUKslDBCZPizlE="
pub(crate) const PRIVATE_KEY: &str = "AEqXrLFT4qjYq3wmX0456iv94uM6nDj5ugp6Jedcflg=";
const PRIVATE_KEY: &str = "AEqXrLFT4qjYq3wmX0456iv94uM6nDj5ugp6Jedcflg=";
// The AllowedIPs for the connected peer, which is one a single IP and the same as the IP that the
// peer has configured on their side.
+1 -1
View File
@@ -47,7 +47,7 @@ serde_json = "1.0.91"
thiserror = { workspace = true }
tokio = { version = "1", features = ["macros", "net","rt-multi-thread"] }
tokio-tungstenite = { workspace = true }
tokio-util = { version = "0.7.4", features = ["full"] }
tokio-util = { workspace = true, features = ["full"] }
toml = "0.7.0"
unsigned-varint = "0.7.1"
utoipa = { workspace = true, features = ["actix_extras"] }
+3 -5
View File
@@ -53,7 +53,7 @@ tokio = { workspace = true, features = [
] }
tokio-stream = { version = "0.1.11", features = ["fs"] }
tokio-tungstenite = { version = "0.20.1" }
tokio-util = { version = "0.7.4", features = ["codec"] }
tokio-util = { workspace = true, features = ["codec"] }
url = { workspace = true, features = ["serde"] }
zeroize = { workspace = true }
@@ -77,10 +77,8 @@ nym-statistics-common = { path = "../common/statistics" }
nym-task = { path = "../common/task" }
nym-types = { path = "../common/types" }
nym-validator-client = { path = "../common/client-libs/validator-client" }
nym-ip-packet-router = { path = "../service-providers/ip-packet-router" }
nym-wireguard = { path = "../common/wireguard", optional = true }
defguard_wireguard_rs = { path = "../../wireguard-rs", optional = true }
nym-ip-packet-router = { path = "../service-providers/ip-packet-router" }
[dev-dependencies]
tower = "0.4.13"
@@ -97,4 +95,4 @@ sqlx = { version = "0.5", features = [
] }
[features]
wireguard = ["nym-wireguard", "defguard_wireguard_rs"]
wireguard = ["nym-wireguard"]
+18 -13
View File
@@ -22,7 +22,6 @@ use crate::node::statistics::collector::GatewayStatisticsCollector;
use crate::node::storage::Storage;
use anyhow::bail;
use dashmap::DashMap;
use defguard_wireguard_rs::{WGApi, WireguardInterfaceApi};
use futures::channel::{mpsc, oneshot};
use log::*;
use nym_crypto::asymmetric::{encryption, identity};
@@ -201,10 +200,11 @@ impl<St> Gateway<St> {
mixnet_handling::Listener::new(listening_address, shutdown).start(connection_handler);
}
#[cfg(feature = "wireguard")]
async fn start_wireguard(
&self,
shutdown: TaskClient,
) -> Result<WGApi, Box<dyn Error + Send + Sync>> {
) -> Result<(), Box<dyn Error + Send + Sync>> {
// TODO: possibly we should start the UDP listener and TUN device explicitly here
nym_wireguard::start_wireguard(shutdown, Arc::clone(&self.client_registry)).await
}
@@ -476,6 +476,13 @@ impl<St> Gateway<St> {
});
}
self.start_client_websocket_listener(
mix_forwarding_channel.clone(),
active_clients_store.clone(),
shutdown.subscribe().named("websocket::Listener"),
Arc::new(coconut_verifier),
);
let nr_request_filter = if self.config.network_requester.enabled {
let embedded_nr = self
.start_network_requester(
@@ -496,7 +503,7 @@ impl<St> Gateway<St> {
if self.config.ip_packet_router.enabled {
let embedded_ip_sp = self
.start_ip_packet_router(
mix_forwarding_channel.clone(),
mix_forwarding_channel,
shutdown.subscribe().named("ip_service_provider"),
)
.await?;
@@ -513,17 +520,16 @@ impl<St> Gateway<St> {
.with_maybe_network_request_filter(nr_request_filter)
.start(shutdown.subscribe().named("http-api"))?;
self.start_client_websocket_listener(
mix_forwarding_channel,
active_clients_store,
shutdown.subscribe().named("websocket::Listener"),
Arc::new(coconut_verifier),
);
let wg_api = self
// Once this is a bit more mature, make this a commandline flag instead of a compile time
// flag
#[cfg(feature = "wireguard")]
if let Err(err) = self
.start_wireguard(shutdown.subscribe().named("wireguard"))
.await
.expect("Could not start wireguard");
{
// that's a nasty workaround, but anyhow errors are generally nicer, especially on exit
bail!("{err}")
}
info!("Finished nym gateway startup procedure - it should now be able to receive mix and client traffic!");
@@ -531,7 +537,6 @@ impl<St> Gateway<St> {
// that's a nasty workaround, but anyhow errors are generally nicer, especially on exit
bail!("{err}")
}
wg_api.remove_interface()?;
Ok(())
}
}
+2 -2
View File
@@ -33,8 +33,8 @@ rand = "0.7.3"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sysinfo = "0.27.7"
tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] }
tokio-util = { version = "0.7.3", features = ["codec"] }
tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] }
tokio-util = { workspace = true, features = ["codec"] }
toml = "0.5.8"
url = { workspace = true, features = ["serde"] }
cfg-if = "1.0.0"
@@ -24,7 +24,15 @@ struct MixnodeWithStakeAndPerformance {
impl MixnodeWithStakeAndPerformance {
fn to_selection_weight(&self) -> f64 {
let scaled_stake = self.total_stake * self.performance;
let scaled_performance = match self.performance.checked_pow(20) {
Ok(perf) => perf,
Err(overflow) => {
warn!("the node's performance ({}) has overflow while scaling it by the factor of 20: {overflow}. Setting it to 0 instead.", self.performance);
return 0.;
}
};
let scaled_stake = self.total_stake * scaled_performance;
stake_to_f64(scaled_stake)
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ nym-bin-common = { path = "../../../common/bin-common" }
# extra dependencies for libp2p examples
libp2p = { git = "https://github.com/ChainSafe/rust-libp2p.git", rev = "e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6", features = [ "identify", "macros", "ping", "tokio", "tcp", "dns", "websocket", "noise", "mplex", "yamux", "gossipsub" ]}
tokio-stream = "0.1.12"
tokio-util = { version = "0.7", features = ["codec"] }
tokio-util = { workspace = true, features = ["codec"] }
parking_lot = "0.12"
hex = "0.4"
@@ -27,6 +27,7 @@ nym-task = { path = "../../common/task" }
nym-tun = { path = "../../common/tun" }
nym-wireguard = { path = "../../common/wireguard" }
nym-wireguard-types = { path = "../../common/wireguard-types" }
rand = "0.8.5"
reqwest.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
@@ -30,6 +30,9 @@ pub enum IpPacketRouterError {
#[error("the entity wrapping the network requester has disconnected")]
DisconnectedParent,
#[error("received packet has an invalid version: {0}")]
InvalidPacketVersion(u8),
#[error("failed to deserialize tagged packet: {source}")]
FailedToDeserializeTaggedPacket { source: bincode::Error },
@@ -0,0 +1,38 @@
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr},
};
use crate::{ConnectedClient, TUN_DEVICE_ADDRESS};
// Find an available IP address in self.connected_clients
// TODO: make this nicer
fn generate_random_ip_within_subnet() -> Ipv4Addr {
let mut rng = rand::thread_rng();
// Generate a random number in the range 1-254
let last_octet = rand::Rng::gen_range(&mut rng, 1..=254);
Ipv4Addr::new(10, 0, 0, last_octet)
}
fn is_ip_taken(
connected_clients: &HashMap<IpAddr, ConnectedClient>,
tun_ip: Ipv4Addr,
ip: Ipv4Addr,
) -> bool {
connected_clients.contains_key(&ip.into()) || ip == tun_ip
}
// TODO: brute force approach. We could consider using a more efficient algorithm
pub(crate) fn find_new_ip(connected_clients: &HashMap<IpAddr, ConnectedClient>) -> Option<IpAddr> {
let mut new_ip = generate_random_ip_within_subnet();
let mut tries = 0;
let tun_ip = TUN_DEVICE_ADDRESS.parse::<Ipv4Addr>().unwrap();
while is_ip_taken(connected_clients, tun_ip, new_ip) {
new_ip = generate_random_ip_within_subnet();
tries += 1;
if tries > 100 {
return None;
}
}
Some(new_ip.into())
}
+297 -70
View File
@@ -1,8 +1,10 @@
#![cfg_attr(not(target_os = "linux"), allow(dead_code))]
use std::{
collections::HashMap,
net::{IpAddr, SocketAddr},
path::Path,
time::Duration,
};
use error::IpPacketRouterError;
@@ -11,6 +13,10 @@ use nym_client_core::{
client::mix_traffic::transceiver::GatewayTransceiver,
config::disk_persistence::CommonClientPaths, HardcodedTopologyProvider, TopologyProvider,
};
use nym_ip_packet_requests::{
DynamicConnectFailureReason, IpPacketRequest, IpPacketRequestData, IpPacketResponse,
StaticConnectFailureReason,
};
use nym_sdk::{
mixnet::{InputMessage, MixnetMessageSender, Recipient},
NymNetworkDetails,
@@ -25,6 +31,7 @@ pub use crate::config::Config;
pub mod config;
pub mod error;
mod generate_new_ip;
mod request_filter;
// The interface used to route traffic
@@ -32,6 +39,9 @@ pub const TUN_BASE_NAME: &str = "nymtun";
pub const TUN_DEVICE_ADDRESS: &str = "10.0.0.1";
pub const TUN_DEVICE_NETMASK: &str = "255.255.255.0";
const DISCONNECT_TIMER_INTERVAL: Duration = Duration::from_secs(10);
const CLIENT_INACTIVITY_TIMEOUT: Duration = Duration::from_secs(5 * 60);
pub struct OnStartData {
// to add more fields as required
pub address: Recipient,
@@ -160,6 +170,7 @@ impl IpPacketRouterBuilder {
tun_task_response_rx,
mixnet_client,
task_handle,
connected_clients: Default::default(),
};
log::info!("The address of this client is: {self_address}");
@@ -179,7 +190,7 @@ impl IpPacketRouterBuilder {
}
}
#[allow(unused)]
#[cfg_attr(not(target_os = "linux"), allow(unused))]
struct IpPacketRouter {
_config: Config,
request_filter: request_filter::RequestFilter,
@@ -187,77 +198,150 @@ struct IpPacketRouter {
tun_task_response_rx: nym_tun::tun_task_channel::TunTaskResponseRx,
mixnet_client: nym_sdk::mixnet::MixnetClient,
task_handle: TaskHandle,
connected_clients: HashMap<IpAddr, ConnectedClient>,
}
#[allow(unused)]
struct ConnectedClient {
nym_address: Recipient,
last_activity: std::time::Instant,
}
#[cfg_attr(not(target_os = "linux"), allow(unused))]
impl IpPacketRouter {
async fn run(mut self) -> Result<(), IpPacketRouterError> {
let mut task_client = self.task_handle.fork("main_loop");
while !task_client.is_shutdown() {
tokio::select! {
_ = task_client.recv() => {
log::debug!("IpPacketRouter [main loop]: received shutdown");
},
msg = self.mixnet_client.next() => {
if let Some(msg) = msg {
if let Err(err) = self.on_message(msg).await {
log::error!("Error handling mixnet message: {err}");
};
} else {
log::trace!("IpPacketRouter [main loop]: stopping since channel closed");
break;
};
},
packet = self.tun_task_response_rx.recv() => {
if let Some((_tag, packet)) = packet {
// Read recipient from env variable NYM_CLIENT_ADDR which is a base58
// string of the nym-address of the client that the packet should be
// sent back to.
//
// In the near future we will let the client expose it's nym-address
// directly, and after that, provide SURBS
let recipient = std::env::var("NYM_CLIENT_ADDR").ok().and_then(|addr| {
Recipient::try_from_base58_string(addr).ok()
});
if let Some(recipient) = recipient {
let lane = TransmissionLane::General;
let packet_type = None;
let input_message = InputMessage::new_regular(recipient, packet, lane, packet_type);
if let Err(err) = self.mixnet_client.send(input_message).await {
log::error!("IpPacketRouter [main loop]: failed to send packet to mixnet: {err}");
};
} else {
log::error!("NYM_CLIENT_ADDR not set or invalid");
}
} else {
log::trace!("IpPacketRouter [main loop]: stopping since channel closed");
break;
}
}
}
}
log::info!("IpPacketRouter: stopping");
Ok(())
}
async fn on_message(
async fn on_static_connect_request(
&mut self,
reconstructed: ReconstructedMessage,
) -> Result<(), IpPacketRouterError> {
log::debug!(
"Received message with sender_tag: {:?}",
reconstructed.sender_tag
connect_request: nym_ip_packet_requests::StaticConnectRequest,
) -> Result<Option<IpPacketResponse>, IpPacketRouterError> {
log::info!(
"Received static connect request from {sender_address}",
sender_address = connect_request.reply_to
);
let tagged_packet =
nym_ip_packet_requests::TaggedIpPacket::from_reconstructed_message(&reconstructed)
.map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket {
source: err,
})?;
let request_id = connect_request.request_id;
let requested_ip = connect_request.ip;
let reply_to = connect_request.reply_to;
// TODO: ignoring reply_to_hops and reply_to_avg_mix_delays for now
// Check that the IP is available in the set of connected clients
let is_ip_taken = self.connected_clients.contains_key(&requested_ip);
// Check that the nym address isn't already registered
let is_nym_address_taken = self
.connected_clients
.values()
.any(|client| client.nym_address == reply_to);
match (is_ip_taken, is_nym_address_taken) {
(true, true) => {
log::info!("Connecting an already connected client");
// Update the last activity time for the client
if let Some(client) = self.connected_clients.get_mut(&requested_ip) {
client.last_activity = std::time::Instant::now();
} else {
log::error!("Failed to update last activity time for client");
}
Ok(Some(IpPacketResponse::new_static_connect_success(
request_id, reply_to,
)))
}
(false, false) => {
log::info!("Connecting a new client");
self.connected_clients.insert(
requested_ip,
ConnectedClient {
nym_address: reply_to,
last_activity: std::time::Instant::now(),
},
);
Ok(Some(IpPacketResponse::new_static_connect_success(
request_id, reply_to,
)))
}
(true, false) => {
log::info!("Requested IP is not available");
Ok(Some(IpPacketResponse::new_static_connect_failure(
request_id,
reply_to,
StaticConnectFailureReason::RequestedIpAlreadyInUse,
)))
}
(false, true) => {
log::info!("Nym address is already registered");
Ok(Some(IpPacketResponse::new_static_connect_failure(
request_id,
reply_to,
StaticConnectFailureReason::RequestedNymAddressAlreadyInUse,
)))
}
}
}
async fn on_dynamic_connect_request(
&mut self,
connect_request: nym_ip_packet_requests::DynamicConnectRequest,
) -> Result<Option<IpPacketResponse>, IpPacketRouterError> {
log::info!(
"Received dynamic connect request from {sender_address}",
sender_address = connect_request.reply_to
);
let request_id = connect_request.request_id;
let reply_to = connect_request.reply_to;
// TODO: ignoring reply_to_hops and reply_to_avg_mix_delays for now
// Check if it's the same client connecting again, then we just reuse the same IP
// TODO: this is problematic. Until we sign connect requests this means you can spam people
// with return traffic
let existing_ip = self.connected_clients.iter().find_map(|(ip, client)| {
if client.nym_address == reply_to {
Some(*ip)
} else {
None
}
});
if let Some(existing_ip) = existing_ip {
log::info!("Found existing client for nym address");
// Update the last activity time for the client
if let Some(client) = self.connected_clients.get_mut(&existing_ip) {
client.last_activity = std::time::Instant::now();
} else {
log::error!("Failed to update last activity time for client");
}
return Ok(Some(IpPacketResponse::new_dynamic_connect_success(
request_id,
reply_to,
existing_ip,
)));
}
let Some(new_ip) = generate_new_ip::find_new_ip(&self.connected_clients) else {
log::info!("No available IP address");
return Ok(Some(IpPacketResponse::new_dynamic_connect_failure(
request_id,
reply_to,
DynamicConnectFailureReason::NoAvailableIp,
)));
};
self.connected_clients.insert(
new_ip,
ConnectedClient {
nym_address: reply_to,
last_activity: std::time::Instant::now(),
},
);
Ok(Some(IpPacketResponse::new_dynamic_connect_success(
request_id, reply_to, new_ip,
)))
}
async fn on_data_request(
&mut self,
data_request: nym_ip_packet_requests::DataRequest,
) -> Result<Option<IpPacketResponse>, IpPacketRouterError> {
log::info!("Received data request");
// We don't forward packets that we are not able to parse. BUT, there might be a good
// reason to still forward them.
@@ -271,11 +355,20 @@ impl IpPacketRouter {
src_addr,
dst_addr,
dst,
} = parse_packet(&tagged_packet.packet)?;
} = parse_packet(&data_request.ip_packet)?;
let dst_str = dst.map_or(dst_addr.to_string(), |dst| dst.to_string());
log::info!("Received packet: {packet_type}: {src_addr} -> {dst_str}");
// Check if there is a connected client for this src_addr. If there is, update the last activity time
// for the client. If there isn't, drop the packet.
if let Some(client) = self.connected_clients.get_mut(&src_addr) {
client.last_activity = std::time::Instant::now();
} else {
log::info!("Dropping packet: no connected client for {src_addr}");
return Ok(None);
}
// Filter check
if let Some(dst) = dst {
if !self.request_filter.check_address(&dst).await {
@@ -288,12 +381,146 @@ impl IpPacketRouter {
log::warn!("Ignoring filter check for packet without port number! TODO!");
}
// TODO: set the tag correctly. Can we just reuse sender_tag?
let peer_tag = 0;
// TODO: consider changing from Vec<u8> to bytes::Bytes?
// TODO: consider just removing the tag
let tag = 0;
self.tun_task_tx
.try_send((peer_tag, tagged_packet.packet.into()))
.try_send((tag, data_request.ip_packet.into()))
.map_err(|err| IpPacketRouterError::FailedToSendPacketToTun { source: err })?;
Ok(None)
}
async fn on_reconstructed_message(
&mut self,
reconstructed: ReconstructedMessage,
) -> Result<Option<IpPacketResponse>, IpPacketRouterError> {
log::debug!(
"Received message with sender_tag: {:?}",
reconstructed.sender_tag
);
// Check version of request
if let Some(version) = reconstructed.message.first() {
// The idea is that in the future we can add logic here to parse older versions to stay
// backwards compatible.
if *version != nym_ip_packet_requests::CURRENT_VERSION {
log::warn!("Received packet with invalid version");
return Err(IpPacketRouterError::InvalidPacketVersion(*version));
}
}
let request = IpPacketRequest::from_reconstructed_message(&reconstructed)
.map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket { source: err })?;
match request.data {
IpPacketRequestData::StaticConnect(connect_request) => {
self.on_static_connect_request(connect_request).await
}
IpPacketRequestData::DynamicConnect(connect_request) => {
self.on_dynamic_connect_request(connect_request).await
}
IpPacketRequestData::Data(data_request) => self.on_data_request(data_request).await,
}
}
async fn run(mut self) -> Result<(), IpPacketRouterError> {
let mut task_client = self.task_handle.fork("main_loop");
let mut disconnect_timer = tokio::time::interval(DISCONNECT_TIMER_INTERVAL);
while !task_client.is_shutdown() {
tokio::select! {
_ = task_client.recv() => {
log::debug!("IpPacketRouter [main loop]: received shutdown");
},
_ = disconnect_timer.tick() => {
let now = std::time::Instant::now();
let inactive_clients: Vec<IpAddr> = self.connected_clients.iter()
.filter_map(|(ip, client)| {
if now.duration_since(client.last_activity) > CLIENT_INACTIVITY_TIMEOUT {
Some(*ip)
} else {
None
}
})
.collect();
for ip in inactive_clients {
log::info!("Disconnect inactive client: {ip}");
self.connected_clients.remove(&ip);
}
},
msg = self.mixnet_client.next() => {
if let Some(msg) = msg {
match self.on_reconstructed_message(msg).await {
Ok(Some(response)) => {
let Some(recipient) = response.recipient() else {
log::error!("IpPacketRouter [main loop]: failed to get recipient from response");
continue;
};
let response_packet = response.to_bytes();
let Ok(response_packet) = response_packet else {
log::error!("Failed to serialize response packet");
continue;
};
let lane = TransmissionLane::General;
let packet_type = None;
let input_message = InputMessage::new_regular(*recipient, response_packet, lane, packet_type);
if let Err(err) = self.mixnet_client.send(input_message).await {
log::error!("IpPacketRouter [main loop]: failed to send packet to mixnet: {err}");
};
},
Ok(None) => {
continue;
},
Err(err) => {
log::error!("Error handling mixnet message: {err}");
}
};
} else {
log::trace!("IpPacketRouter [main loop]: stopping since channel closed");
break;
};
},
packet = self.tun_task_response_rx.recv() => {
if let Some((_tag, packet)) = packet {
// TODO: skip full parsing since we only need dst_addr
let Ok(ParsedPacket {
packet_type: _,
src_addr: _,
dst_addr,
dst: _,
}) = parse_packet(&packet) else {
log::warn!("Failed to parse packet");
continue;
};
let recipient = self.connected_clients.get(&dst_addr).map(|c| c.nym_address);
if let Some(recipient) = recipient {
let lane = TransmissionLane::General;
let packet_type = None;
let response_packet = IpPacketResponse::new_ip_packet(packet.into()).to_bytes();
let Ok(response_packet) = response_packet else {
log::error!("Failed to serialize response packet");
continue;
};
let input_message = InputMessage::new_regular(recipient, response_packet, lane, packet_type);
if let Err(err) = self.mixnet_client.send(input_message).await {
log::error!("IpPacketRouter [main loop]: failed to send packet to mixnet: {err}");
};
} else {
log::error!("IpPacketRouter [main loop]: no nym-address recipient for packet");
}
} else {
log::trace!("IpPacketRouter [main loop]: stopping since channel closed");
break;
}
}
}
}
log::info!("IpPacketRouter: stopping");
Ok(())
}
}
+45
View File
@@ -0,0 +1,45 @@
[package]
name = "dkg-manager"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { workspace = true }
bip39 = { workspace = true }
clap = { workspace = true }
futures = { workspace = true }
lazy_static = "1.4.0"
log = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
url = { workspace = true }
time = "0.3.30"
ratatui = { version = "0.24.0", features = ["serde", "macros"] }
crossterm = { version = "0.27.0", features = ["serde", "event-stream"] }
tui-input = { version = "0.8.0", features = ["serde"] }
tui-logger = { version = "0.10", features = ["tracing-support"] }
throbber-widgets-tui = "0.3.0"
# panic handler:
better-panic = "0.3.0"
color-eyre = "0.6.2"
strip-ansi-escapes = "0.2.0"
nym-validator-client = { path = "../../../common/client-libs/validator-client" }
nym-bin-common = { path = "../../../common/bin-common" }
nym-network-defaults = { path = "../../../common/network-defaults" }
nym-coconut-dkg-common = { path = "../../../common/cosmwasm-smart-contracts/coconut-dkg" }
nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common" }
+189
View File
@@ -0,0 +1,189 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::DkgState;
use nym_coconut_dkg_common::dealer::{ContractDealing, DealerDetails};
use nym_coconut_dkg_common::types::Epoch;
use nym_validator_client::nyxd::{cw4, cw_controllers};
use serde::{Serialize, Serializer};
use crate::components::basic_contract_info::BasicContractInfo;
use nym_coconut_dkg_common::verification_key::ContractVKShare;
use tokio::sync::mpsc::error::SendError;
use tokio::sync::mpsc::UnboundedSender;
use tui_logger::TuiWidgetEvent;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ContractsInfo {
pub dkg: DkgInfo,
pub group: GroupInfo,
pub bandwidth: BandwidthInfo,
pub multisig: MultisigInfo,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DkgInfo {
pub base: BasicContractInfo,
pub debug_state: DkgState,
pub epoch: Epoch,
pub threshold: Option<u64>,
pub dealers: Vec<DealerDetails>,
pub past_dealers: Vec<DealerDetails>,
pub epoch_dealings: Vec<ContractDealing>,
pub vk_shares: Vec<ContractVKShare>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct GroupInfo {
pub base: BasicContractInfo,
pub admin: cw_controllers::AdminResponse,
pub members: Vec<cw4::Member>,
pub total_weight: cw4::TotalWeightResponse,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct BandwidthInfo {
pub base: BasicContractInfo,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct MultisigInfo {
pub base: BasicContractInfo,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum Action {
Quit,
Error(String),
NextTab,
PreviousTab,
HomeAction(HomeAction),
LoggerAction(LoggerAction),
}
impl From<HomeAction> for Action {
fn from(value: HomeAction) -> Self {
Action::HomeAction(value)
}
}
impl From<LoggerAction> for Action {
fn from(value: LoggerAction) -> Self {
Action::LoggerAction(value)
}
}
impl From<TuiWidgetEvent> for Action {
fn from(value: TuiWidgetEvent) -> Self {
LoggerAction::WidgetKeyEvent(value).into()
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum HomeAction {
ToggleShowHelp,
StartInput,
ScheduleContractRefresh,
RefreshDkgContract(Box<ContractsInfo>),
ProcessInput(String),
SetLastContractError(String),
EnterNormal,
NextInputMode,
PreviousInputMode,
EnterProcessing,
ExitProcessing,
FinishContractUpdate,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LoggerAction {
WidgetKeyEvent(TuiWidgetEvent),
}
impl Serialize for LoggerAction {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
todo!()
}
}
#[derive(Clone)]
pub struct ActionSender(pub UnboundedSender<Action>);
impl ActionSender {
pub fn send(&self, action: Action) -> Result<(), SendError<Action>> {
self.0.send(action)
}
pub fn send_home_action(&self, action: HomeAction) -> Result<(), SendError<Action>> {
self.send(Action::HomeAction(action))
}
pub fn unchecked_send_home_action(&self, action: HomeAction) {
self.send_home_action(action)
.expect("failed to send home action")
}
pub fn unchecked_send(&self, action: Action) {
self.send(action).expect("failed to send action")
}
}
//
// impl<'de> Deserialize<'de> for Action {
// fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
// where
// D: Deserializer<'de>,
// {
// struct ActionVisitor;
//
// impl<'de> Visitor<'de> for ActionVisitor {
// type Value = Action;
//
// fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
// formatter.write_str("a valid string representation of Action")
// }
//
// fn visit_str<E>(self, value: &str) -> Result<Action, E>
// where
// E: de::Error,
// {
// match value {
// "Tick" => Ok(Action::Tick),
// "Quit" => Ok(Action::Quit),
// "ScheduleContractRefresh" => Ok(Action::ScheduleContractRefresh),
// "ToggleShowHelp" => Ok(Action::ToggleShowHelp),
// // "ProcessInput" => Ok(Action::ProcessInput),
// "EnterNormal" => Ok(Action::EnterNormal),
// data if data.starts_with("Error(") => {
// let error_msg = data.trim_start_matches("Error(").trim_end_matches(")");
// Ok(Action::Error(error_msg.to_string()))
// }
// data if data.starts_with("Resize(") => {
// let parts: Vec<&str> = data
// .trim_start_matches("Resize(")
// .trim_end_matches(")")
// .split(',')
// .collect();
// if parts.len() == 2 {
// let width: u16 = parts[0].trim().parse().map_err(E::custom)?;
// let height: u16 = parts[1].trim().parse().map_err(E::custom)?;
// Ok(Action::Resize(width, height))
// } else {
// Err(E::custom(format!("Invalid Resize format: {}", value)))
// }
// }
// _ => Err(E::custom(format!("Unknown Action variant: {}", value))),
// }
// }
// }
//
// deserializer.deserialize_str(ActionVisitor)
// }
// }
+242
View File
@@ -0,0 +1,242 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::action::ActionSender;
use crate::cli::Args;
use crate::components::chain_history::ContractChainHistory;
use crate::components::logger::Logger;
use crate::keybindings::KeyBindings;
use crate::nyxd::setup_nyxd_client;
use crate::utils::key_event_to_string;
use crate::{action::Action, components::home::Home};
use crossterm::event::KeyEvent;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Tabs};
use ratatui::Frame;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use tokio::sync::mpsc::UnboundedReceiver;
use tracing::{debug, error, info};
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Mode {
#[default]
Home,
ChainHistory,
Logger,
}
impl Mode {
pub fn next(&self) -> Self {
match self {
Mode::Home => Mode::ChainHistory,
Mode::ChainHistory => Mode::Logger,
Mode::Logger => Mode::Home,
}
}
pub fn previous(&self) -> Self {
match self {
Mode::Home => Mode::Logger,
Mode::ChainHistory => Mode::Home,
Mode::Logger => Mode::ChainHistory,
}
}
}
pub struct App {
pub keybindings: KeyBindings,
pub home: Home,
pub logger: Logger,
pub chain_history: ContractChainHistory,
pub action_tx: ActionSender,
pub should_quit: bool,
pub mode: Mode,
pub active_tab: usize,
pub tab_titles: Vec<&'static str>,
pub last_tick_key_events: Vec<KeyEvent>,
}
impl App {
pub async fn new(args: Args) -> anyhow::Result<(Self, UnboundedReceiver<Action>)> {
let (nyxd_client, upstream) = setup_nyxd_client(args)?;
let keybindings = KeyBindings::default();
let (action_tx, action_rx) = mpsc::unbounded_channel();
let action_sender = ActionSender(action_tx);
let home = Home::new(nyxd_client.clone(), upstream, action_sender.clone()).await?;
let chain_history = ContractChainHistory::new(nyxd_client);
let logger = Logger::new();
let mode = Mode::Home;
Ok((
Self {
keybindings,
home,
logger,
chain_history,
action_tx: action_sender,
should_quit: false,
mode,
last_tick_key_events: Vec::new(),
active_tab: 0,
tab_titles: vec![
"🥥 Contract Information 🥥",
"🔗 Transactions 🔗",
"📝 Logs 📝",
],
},
action_rx,
))
}
pub fn next_tab(&mut self) {
self.active_tab = (self.active_tab + 1) % self.tab_titles.len();
self.mode = self.mode.next();
}
pub fn previous_tab(&mut self) {
if self.active_tab > 0 {
self.active_tab -= 1;
} else {
self.active_tab = self.tab_titles.len() - 1;
}
self.mode = self.mode.previous();
}
pub fn action_sender(&self) -> ActionSender {
self.action_tx.clone()
}
pub fn draw(&mut self, f: &mut Frame<'_>) -> anyhow::Result<()> {
let frame_size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(0)])
.split(frame_size);
let block = Block::default();
f.render_widget(block, frame_size);
let titles = self
.tab_titles
.iter()
.map(|t| Line::from(Span::styled(*t, Style::default().bold())))
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title("Tabs"))
.select(self.active_tab)
.style(Style::default().cyan())
.highlight_style(Style::default().bold().light_cyan().on_black());
// .highlight_style(Style::default().bold().on_black());
f.render_widget(tabs, chunks[0]);
match self.active_tab {
0 => {
self.home.render_tick();
self.home.draw(f, chunks[1])?;
}
1 => self.chain_history.draw(f, chunks[1])?,
2 => self.logger.draw(f, chunks[1])?,
_ => unreachable!(),
};
f.render_widget(
self.history_widget(),
Rect {
x: frame_size.x + 1,
y: frame_size.height.saturating_sub(1),
width: frame_size.width.saturating_sub(2),
height: 1,
},
);
Ok(())
}
pub fn history_widget(&self) -> Block {
Block::default()
.title(
ratatui::widgets::block::Title::from(format!(
"{:?}",
&self
.last_tick_key_events
.iter()
.map(key_event_to_string)
.collect::<Vec<_>>()
))
.alignment(Alignment::Right),
)
.title_style(Style::default().bold())
}
pub fn key_event(&mut self, key: KeyEvent) -> anyhow::Result<()> {
if let Some(keymap) = self.keybindings.get(&self.mode) {
if let Some(action) = keymap.get(&vec![key]) {
info!("Got action: {action:?}");
self.action_tx.send(action.clone())?;
} else {
// If the key was not handled as a single key action,
// then consider it for multi-key combinations.
self.last_tick_key_events.push(key);
// Check for multi-key combinations
if let Some(action) = keymap.get(&self.last_tick_key_events) {
info!("Got action: {action:?}");
self.action_tx.send(action.clone())?;
}
}
}
if let Some(action) = self.home.handle_key_events(key)? {
self.action_tx.send(action)?;
}
Ok(())
}
pub fn tick(&mut self) {
self.last_tick_key_events.drain(..);
self.home.tick();
self.logger.tick();
}
pub fn update(&mut self, action: Action) -> anyhow::Result<()> {
debug!("handling action {action:?}");
let next = match action {
Action::Quit => {
self.should_quit = true;
None
}
Action::NextTab => {
self.next_tab();
None
}
Action::PreviousTab => {
self.previous_tab();
None
}
Action::Error(err) => {
error!("unhandled error action: {err}");
None
}
Action::HomeAction(home_action) => self.home.update(home_action)?,
Action::LoggerAction(logger_action) => self.logger.update(logger_action)?,
};
if let Some(action) = next {
self.action_sender().send(action)?
}
Ok(())
}
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
use nym_bin_common::bin_info;
use nym_validator_client::nyxd::AccountId;
use url::Url;
lazy_static::lazy_static! {
pub static ref PRETTY_BUILD_INFORMATION: String = bin_info!().pretty_print();
}
// Helper for passing LONG_VERSION to clap
fn pretty_build_info_static() -> &'static str {
&PRETTY_BUILD_INFORMATION
}
#[derive(Parser, Debug)]
#[command(author = "Nymtech", version, about, long_version = pretty_build_info_static())]
pub struct Args {
/// Path pointing to an env file that configures the environment.
#[clap(short, long)]
pub(crate) config_env_file: Option<std::path::PathBuf>,
#[clap(long)]
pub(crate) admin_mnemonic: bip39::Mnemonic,
#[clap(long)]
pub(crate) dkg_contract_address: Option<AccountId>,
#[clap(long)]
pub(crate) nyxd_validator: Option<Url>,
}
@@ -0,0 +1,84 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_contracts_common::ContractBuildInformation;
use nym_validator_client::nyxd::{cw2, Coin};
use ratatui::prelude::*;
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct BasicContractInfo {
pub name: String,
pub address: String,
pub balance: Coin,
pub cw2_version: Option<cw2::ContractVersion>,
pub build_info: Option<ContractBuildInformation>,
}
impl BasicContractInfo {
pub fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> anyhow::Result<()> {
// is it generic for any currency? no. but it's an internal tool so the approximation is good enough
let amount_nym = format!(
"{:.6}{}",
self.balance.amount as f64 / 1000000.,
self.balance.denom
);
let cw2_formatted = if let Some(cw2_version) = &self.cw2_version {
format!("{} {}", cw2_version.contract, cw2_version.version)
} else {
"<unspecified>".to_string()
};
let mut text = vec![
"Address (part might be hidden): ".into(),
Span::styled(&self.address, Style::default().yellow().bold()).into(),
Line::from(vec![
"Balance: ".into(),
Span::styled(amount_nym, Style::default().yellow().bold()),
]),
Line::from(vec![
"CW2 version: ".into(),
Span::styled(cw2_formatted, Style::default().yellow().bold()),
]),
];
if let Some(build_info) = &self.build_info {
text.push(Line::from(vec![
"build branch: ".into(),
Span::styled(&build_info.commit_branch, Style::default().yellow().bold()),
]));
text.push(Line::from(vec![
"build sha: ".into(),
Span::styled(&build_info.commit_sha, Style::default().yellow().bold()),
]));
} else {
text.push(Line::from(vec![
"Build Information: ".into(),
Span::styled(
"<unspecified>".to_string(),
Style::default().yellow().bold(),
),
]));
}
let block = Block::default()
.title(Line::from(vec![Span::styled(
&self.name,
Style::default().add_modifier(Modifier::BOLD),
)]))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded);
let paragraph = Paragraph::new(text)
.block(block)
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Left);
f.render_widget(paragraph, rect);
Ok(())
}
}
@@ -0,0 +1,36 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::action::Action;
use crate::nyxd::NyxdClient;
use ratatui::prelude::*;
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
pub struct ContractChainHistory {
pub nyxd_client: NyxdClient,
}
impl ContractChainHistory {
pub fn new(nyxd_client: NyxdClient) -> Self {
ContractChainHistory { nyxd_client }
}
pub fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> anyhow::Result<()> {
let block = Block::default()
.title("Contract Chain History")
.borders(Borders::ALL)
.border_type(BorderType::Rounded);
let paragraph = Paragraph::new("Unimplemented")
.block(block)
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Left);
f.render_widget(paragraph, rect);
Ok(())
}
pub fn update(&mut self, _action: ()) -> anyhow::Result<Option<Action>> {
todo!()
}
}
@@ -0,0 +1,151 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::action::DkgInfo;
use nym_coconut_dkg_common::dealer::ContractDealing;
use nym_coconut_dkg_common::types::DealerDetails;
use nym_coconut_dkg_common::verification_key::ContractVKShare;
use ratatui::prelude::*;
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
use std::collections::HashMap;
struct FullDealer<'a> {
info: &'a DealerDetails,
dealing: Option<&'a ContractDealing>,
vk_share: Option<&'a ContractVKShare>,
}
impl<'a> FullDealer<'a> {
fn new(info: &'a DealerDetails) -> Self {
FullDealer {
info,
dealing: None,
vk_share: None,
}
}
fn format(self) -> Vec<Line<'a>> {
let dealing = if let Some(_dealing) = self.dealing {
Span::styled("Submitted", Style::default().bold().green())
} else {
Span::styled("UNSUBMITTED", Style::default().bold().red())
};
let mut vk_share = if let Some(vk_share) = self.vk_share {
let verified = if vk_share.verified {
Span::styled(" share has been verified", Style::default().bold().green())
} else {
Span::styled(" share has been UNVERIFIED", Style::default().bold().red())
};
vec![
Line::from(vec![
" - VK share: ".into(),
Span::styled("Submitted", Style::default().bold().green()),
" for Epoch ".into(),
Span::styled(
vk_share.epoch_id.to_string(),
Style::default().bold().yellow(),
),
verified,
]),
Line::from(vec![
" - Assigned index: ".into(),
Span::styled(
vk_share.node_index.to_string(),
Style::default().bold().yellow(),
),
]),
]
} else {
vec![Line::from(vec![
" - VK share: ".into(),
Span::styled("UNSUBMITTED", Style::default().bold().red()),
])]
};
let mut lines = vec![
Line::from(vec![
Span::styled(format!("{}: ", self.info.address), Style::default().bold()),
"\tindex: ".into(),
Span::styled(
format!("{}", self.info.assigned_index),
Style::default().bold().yellow(),
),
"\t announce address: ".into(),
Span::styled(
self.info.announce_address.to_string(),
Style::default().bold().yellow(),
),
"\t BTE key: ".into(),
Span::styled(
format!("{}...", &self.info.bte_public_key_with_proof[..16]),
Style::default().bold().yellow(),
),
]),
Line::from(vec![" - dealing: ".into(), dealing]),
];
lines.append(&mut vk_share);
lines
}
}
// fn format_contract_bytes(bytes: &ContractSafeBytes) -> String {
// const MAX_LEN: usize = 32;
// let mut output = "0x".to_string();
// for byte in bytes.0.iter().take(MAX_LEN) {
// output.push_str(&format!("{byte:02X}"));
// }
// output.push_str("...");
// output
// }
pub fn draw_current_dealers_info(
info: &DkgInfo,
f: &mut Frame<'_>,
rect: Rect,
) -> anyhow::Result<()> {
let block = Block::default()
.title(Line::from(vec![Span::styled(
"Current Dealers",
Style::default().add_modifier(Modifier::BOLD),
)]))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded);
let mut dealers = HashMap::new();
for dealer in &info.dealers {
dealers.insert(dealer.address.clone(), FullDealer::new(dealer));
}
for dealing in &info.epoch_dealings {
if let Some(dealer) = dealers.get_mut(&dealing.dealer) {
dealer.dealing = Some(dealing)
}
}
for vk_share in &info.vk_shares {
if let Some(dealer) = dealers.get_mut(&vk_share.owner) {
dealer.vk_share = Some(vk_share)
}
}
let mut text = Vec::new();
// order dealers by their index
let mut dealers_vec = dealers.into_iter().map(|d| d.1).collect::<Vec<_>>();
dealers_vec.sort_by_key(|d| d.info.assigned_index);
for dealer_details in dealers_vec {
text.append(&mut dealer_details.format())
}
let paragraph = Paragraph::new(text)
.block(block)
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Left);
f.render_widget(paragraph, rect);
Ok(())
}
@@ -0,0 +1,96 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::action::DkgInfo;
use ratatui::prelude::*;
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
pub fn draw_dkg_info(info: &DkgInfo, f: &mut Frame<'_>, rect: Rect) -> anyhow::Result<()> {
let block = Block::default()
.title(Line::from(vec![Span::styled(
"DKG Information",
Style::default().add_modifier(Modifier::BOLD),
)]))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded);
let end =
OffsetDateTime::from_unix_timestamp(info.epoch.finish_timestamp.seconds() as i64).unwrap();
let remaining = end - OffsetDateTime::now_utc();
let threshold_str = if let Some(val) = info.threshold {
val.to_string()
} else {
"not yet determined".to_string()
};
let dealers = info.dealers.len();
let dealings = info.epoch_dealings.len();
let dealings_color = if dealings < dealers {
Color::Red
} else {
Color::Green
};
let shares = info.vk_shares.len();
let shares_color = if shares < dealings {
Color::Red
} else {
Color::Green
};
let text = vec![
Line::from(vec![
format!("DKG Epoch {} State: ", info.epoch.epoch_id).into(),
Span::styled(info.epoch.state.to_string(), Style::default().bold()),
]),
Line::from(vec![
"Epoch end time: ".into(),
Span::styled(
end.format(&Rfc3339).unwrap().to_string(),
Style::default().bold(),
),
" (".into(),
Span::styled(
remaining.whole_seconds().to_string(),
Style::default().light_green(),
),
" remaining)".into(),
]),
Line::from(vec![
"Threshold: ".into(),
Span::styled(threshold_str, Style::default().bold()),
]),
Line::from(vec![
"Registered Dealers: ".into(),
Span::styled(dealers.to_string(), Style::default().bold()),
" (".into(),
Span::styled(info.past_dealers.len().to_string(), Style::default().bold()),
" in the past)".into(),
]),
Line::from(vec![
"Submitted Dealings: ".into(),
Span::styled(
dealings.to_string(),
Style::default().bold().fg(dealings_color),
),
]),
Line::from(vec![
"Submitted VK Shares: ".into(),
Span::styled(shares.to_string(), Style::default().bold().fg(shares_color)),
]),
];
let paragraph = Paragraph::new(text)
.block(block)
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Left);
f.render_widget(paragraph, rect);
Ok(())
}
@@ -0,0 +1,78 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut_dkg_common::types::TimeConfiguration;
use ratatui::prelude::*;
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
pub fn draw_dkg_timeconfig(
config: TimeConfiguration,
f: &mut Frame<'_>,
rect: Rect,
) -> anyhow::Result<()> {
let block = Block::default()
.title(Line::from(vec![Span::styled(
"DKG Time Configuration",
Style::default().add_modifier(Modifier::BOLD),
)]))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded);
let paragraph = Paragraph::new(format_time_configuration(config))
.block(block)
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Left);
f.render_widget(paragraph, rect);
Ok(())
}
pub fn format_time_configuration<'a>(tc: TimeConfiguration) -> Vec<Line<'a>> {
vec![
Span::styled("Time Configuration", Style::default().bold()).into(),
Line::from(vec![
"Public Key Submission: ".into(),
Span::styled(
format!("{}secs", tc.public_key_submission_time_secs),
Style::default().yellow(),
),
]),
Line::from(vec![
"Dealing Exchange: ".into(),
Span::styled(
format!("{}secs", tc.dealing_exchange_time_secs),
Style::default().yellow(),
),
]),
Line::from(vec![
"Verification Key Submission: ".into(),
Span::styled(
format!("{}secs", tc.verification_key_submission_time_secs),
Style::default().yellow(),
),
]),
Line::from(vec![
"Verification Key Validation: ".into(),
Span::styled(
format!("{}secs", tc.verification_key_validation_time_secs),
Style::default().yellow(),
),
]),
Line::from(vec![
"Verification Key Finalization: ".into(),
Span::styled(
format!("{}secs", tc.verification_key_finalization_time_secs),
Style::default().yellow(),
),
]),
Line::from(vec![
"In Progress: ".into(),
Span::styled(
format!("{}secs", tc.in_progress_time_secs),
Style::default().yellow(),
),
]),
]
}
@@ -0,0 +1,68 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::action::GroupInfo;
use ratatui::prelude::*;
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
// TODO: move to separate component and get rid of that matching_admin bool
pub fn draw_group_info(
info: &GroupInfo,
f: &mut Frame<'_>,
rect: Rect,
matching_admin: bool,
) -> anyhow::Result<()> {
let block = Block::default()
.title(Line::from(vec![Span::styled(
"Group Information",
Style::default().add_modifier(Modifier::BOLD),
)]))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_type(BorderType::Rounded);
let group_admin = match &info.admin.admin {
None => Span::styled("<no admin>", Style::default().light_red().bold()),
Some(admin) => {
let style = if matching_admin {
Style::default().light_green().bold()
} else {
Style::default().light_red().bold()
};
Span::styled(admin, style)
}
};
let mut text = vec![
Line::from(vec!["Admin: ".into(), group_admin]),
Line::from(vec![
"Total Members: ".into(),
Span::styled(info.members.len().to_string(), Style::new().bold()),
" (total weight: ".into(),
Span::styled(
info.total_weight.weight.to_string(),
Style::default().yellow().bold(),
),
"):".into(),
]),
];
for member in &info.members {
text.push(Line::from(vec![
" - ".into(),
Span::styled(&member.addr, Style::default().bold()),
" (weight: ".into(),
Span::styled(member.weight.to_string(), Style::default().yellow()),
")".into(),
]))
}
let paragraph = Paragraph::new(text)
.block(block)
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Left);
f.render_widget(paragraph, rect);
Ok(())
}
@@ -0,0 +1,609 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::Frame;
use crate::action::{Action, ActionSender};
use crate::action::{ContractsInfo, HomeAction};
use crate::components::home::dealers_info::draw_current_dealers_info;
use crate::components::home::dkg_info::draw_dkg_info;
use crate::components::home::dkg_timeconfig::draw_dkg_timeconfig;
use crate::components::home::group_info::draw_group_info;
use crate::nyxd::NyxdClient;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{prelude::*, widgets::*};
use std::{collections::HashMap, time::Duration};
use throbber_widgets_tui::ThrobberState;
use tokio::time::Instant;
use tracing::error;
use tui_input::{backend::crossterm::EventHandler, Input};
use url::Url;
pub(crate) mod dealers_info;
pub(crate) mod dkg_info;
pub(crate) mod dkg_timeconfig;
pub(crate) mod group_info;
pub(crate) mod utils;
pub const REFRESH_RATE: Duration = Duration::from_secs(60);
#[derive(Default, Clone, PartialEq, Eq)]
pub enum InputState {
#[default]
Normal,
AddCW4Member {
address: String,
},
RemoveCW4Member,
AdvanceEpochState,
SurpassedThreshold,
Processing,
}
impl InputState {
// will consume provided characters
pub fn expects_text_input(&self) -> bool {
match self {
InputState::Normal => false,
InputState::AddCW4Member { .. } => true,
InputState::RemoveCW4Member => true,
InputState::Processing => false,
InputState::AdvanceEpochState => false,
InputState::SurpassedThreshold => false,
}
}
pub fn expects_user_input(&self) -> bool {
!matches!(self, InputState::Normal | InputState::Processing)
}
pub fn next(&self) -> Self {
match self {
InputState::Normal => InputState::Normal,
InputState::Processing => InputState::Processing,
InputState::AddCW4Member { .. } => InputState::RemoveCW4Member,
InputState::RemoveCW4Member => InputState::AdvanceEpochState,
InputState::AdvanceEpochState => InputState::SurpassedThreshold,
InputState::SurpassedThreshold => InputState::AddCW4Member {
address: "".to_string(),
},
}
}
pub fn previous(&self) -> Self {
match self {
InputState::Normal => InputState::Normal,
InputState::Processing => InputState::Processing,
InputState::AddCW4Member { .. } => InputState::SurpassedThreshold,
InputState::RemoveCW4Member => InputState::AddCW4Member {
address: "".to_string(),
},
InputState::AdvanceEpochState => InputState::RemoveCW4Member,
InputState::SurpassedThreshold => InputState::AdvanceEpochState,
}
}
}
pub struct Home {
pub show_help: bool,
pub last_contract_error_message: String,
pub refreshing: bool,
throbber_state: Option<throbber_widgets_tui::ThrobberState>,
pub nyxd_client: NyxdClient,
pub manager_address: String,
pub contracts: ContractsInfo,
pub upstream: String,
pub mode: InputState,
pub input: Input,
pub action_tx: ActionSender,
pub keymap: HashMap<KeyEvent, Action>,
pub last_contract_update: Instant,
}
impl Home {
pub async fn new(
nyxd_client: NyxdClient,
upstream: Url,
action_tx: ActionSender,
) -> anyhow::Result<Self> {
let manager_address = nyxd_client.address().await.to_string();
let contracts = nyxd_client.get_contract_update().await?;
Ok(Home {
show_help: false,
nyxd_client,
mode: Default::default(),
input: Default::default(),
action_tx,
keymap: Default::default(),
contracts,
last_contract_update: Instant::now(),
manager_address,
last_contract_error_message: "".to_string(),
refreshing: false,
upstream: upstream.to_string(),
throbber_state: None,
})
}
pub fn keymap(mut self, keymap: HashMap<KeyEvent, Action>) -> Self {
self.keymap = keymap;
self
}
pub fn tick(&mut self) {
// let state_end = OffsetDateTime::from_unix_timestamp(
// self.dkg_info.dkg_epoch.finish_timestamp.seconds() as i64,
// )
// .unwrap();
// let until_epoch_state_end = state_end - OffsetDateTime::now_utc();
// let epoch_should_move = until_epoch_state_end.as_seconds_f32() < -5.;
// let should_refresh = !self.mode.expects_text_input()
// && (self.last_contract_update.elapsed() >= REFRESH_RATE || epoch_should_move);
let should_refresh =
!self.mode.expects_text_input() && self.last_contract_update.elapsed() >= REFRESH_RATE;
if should_refresh && !self.refreshing {
self.last_contract_update = Instant::now();
self.refreshing = true;
self.schedule_contract_refresh()
}
if let Some(throbber) = &mut self.throbber_state {
throbber.calc_next()
}
}
pub fn render_tick(&mut self) {}
pub fn handle_input(&mut self, s: String) {
match &self.mode {
InputState::Normal | InputState::Processing => {
panic!("received input whilst it shouldn't have been possible!")
}
InputState::AddCW4Member { address } => {
if address.is_empty() {
self.mode = InputState::AddCW4Member { address: s };
self.input.reset();
} else {
let address_owned = address.clone();
self.mode = InputState::Processing;
self.input.reset();
self.schedule_add_cw4_member(address_owned, s)
}
}
InputState::RemoveCW4Member => {
self.mode = InputState::Processing;
self.input.reset();
self.schedule_remove_cw4_member(s)
}
InputState::AdvanceEpochState => {
self.mode = InputState::Processing;
self.input.reset();
self.schedule_call_advance_epoch_state();
}
InputState::SurpassedThreshold => {
self.mode = InputState::Processing;
self.input.reset();
self.schedule_call_surpassed_threshold();
}
}
}
pub fn schedule_call_advance_epoch_state(&self) {
let tx = self.action_tx.clone();
let client = self.nyxd_client.clone();
tokio::spawn(async move {
match client.try_advance_epoch_state().await {
Ok(_) => tx.unchecked_send_home_action(HomeAction::ScheduleContractRefresh),
Err(err) => tx.unchecked_send_home_action(HomeAction::SetLastContractError(
format!("failed to advance epoch state: {err}"),
)),
}
tx.unchecked_send_home_action(HomeAction::ExitProcessing)
});
}
pub fn schedule_call_surpassed_threshold(&self) {
let tx = self.action_tx.clone();
let client = self.nyxd_client.clone();
tokio::spawn(async move {
match client.try_surpass_threshold().await {
Ok(_) => {
tx.unchecked_send_home_action(HomeAction::ScheduleContractRefresh);
}
Err(err) => tx.unchecked_send_home_action(HomeAction::SetLastContractError(
format!("failed to surpass threshold: {err}"),
)),
}
tx.unchecked_send_home_action(HomeAction::ExitProcessing);
});
}
pub fn schedule_add_cw4_member(&self, member_address: String, member_weight_raw: String) {
let tx = self.action_tx.clone();
let client = self.nyxd_client.clone();
tokio::spawn(async move {
if let Ok(weight) = member_weight_raw.parse() {
match client.add_group_member(member_address, weight).await {
Ok(_) => {
tx.unchecked_send_home_action(HomeAction::ScheduleContractRefresh);
}
Err(err) => tx.unchecked_send_home_action(HomeAction::SetLastContractError(
format!("failed to add group member: {err}"),
)),
}
} else {
error!("could not parse '{member_weight_raw}' into a valid weight")
}
tx.unchecked_send_home_action(HomeAction::ExitProcessing);
});
}
pub fn schedule_remove_cw4_member(&self, member_address: String) {
let tx = self.action_tx.clone();
let client = self.nyxd_client.clone();
tokio::spawn(async move {
match client.remove_group_member(member_address).await {
Ok(_) => tx.unchecked_send_home_action(HomeAction::ScheduleContractRefresh),
Err(err) => tx.unchecked_send_home_action(HomeAction::SetLastContractError(
format!("failed to remove group member: {err}"),
)),
}
tx.unchecked_send_home_action(HomeAction::ExitProcessing)
});
}
pub fn schedule_contract_refresh(&self) {
let tx = self.action_tx.clone();
let client = self.nyxd_client.clone();
tokio::spawn(async move {
tx.unchecked_send_home_action(HomeAction::EnterProcessing);
match client.get_contract_update().await {
Ok(info) => {
tx.unchecked_send_home_action(HomeAction::RefreshDkgContract(Box::new(info)))
}
Err(err) => {
error!("failed to get dkg updates: {err}")
}
}
tx.unchecked_send_home_action(HomeAction::ExitProcessing);
tx.unchecked_send_home_action(HomeAction::FinishContractUpdate)
});
}
pub fn refresh_dkg_contract_info(&mut self, update_info: ContractsInfo) {
self.contracts = update_info;
}
// fn contracts_info_lines(&self) -> Vec<Line> {
// vec![]
// //
// // if !self.last_contract_error_message.is_empty() {
// // lines.push("".into());
// // lines.push("".into());
// // lines.push(Line::from(vec![
// // Span::styled(
// // "CONTRACT EXECUTION FAILURE: ",
// // Style::default().red().bold(),
// // ),
// // Span::styled(&self.last_contract_error_message, Style::default().white()),
// // ]))
// // }
// //
// // lines
// }
fn draw_main_widget(&mut self, f: &mut Frame<'_>, rect: Rect) -> anyhow::Result<()> {
let info_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Percentage(33), Constraint::Percentage(33)])
.split(rect);
let contract_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(33),
])
.split(info_chunks[0]);
draw_group_info(
&self.contracts.group,
f,
contract_chunks[0],
self.matching_admin(),
)?;
draw_dkg_info(&self.contracts.dkg, f, contract_chunks[1])?;
draw_dkg_timeconfig(
self.contracts.dkg.epoch.time_configuration,
f,
contract_chunks[2],
)?;
draw_current_dealers_info(&self.contracts.dkg, f, info_chunks[1])?;
// f.render_widget(self.main_widget(), rects[1]);
Ok(())
}
fn matching_admin(&self) -> bool {
if let Some(group_admin) = &self.contracts.group.admin.admin {
return group_admin == &self.manager_address;
}
false
}
fn admin_span<S: Into<String>>(&self, text: S) -> Span {
let style = if self.matching_admin() {
Style::default().light_green().bold()
} else {
Style::default().light_red().bold()
};
Span::styled(text.into(), style)
}
fn title_widget(&self) -> Block {
Block::default()
.title(Line::from(vec![
"Nym DKG Contract manager (managed via ".into(),
self.admin_span(&self.manager_address),
") @ ".into(),
Span::styled(&self.upstream, Style::new().light_blue()),
]))
.title_alignment(Alignment::Center)
.borders(Borders::ALL)
.border_style(match self.mode {
InputState::Processing => Style::default().fg(Color::Yellow),
_ => Style::default(),
})
.border_type(BorderType::Rounded)
}
fn input_widget(&self, scroll: usize) -> Paragraph {
let mode_name = match &self.mode {
InputState::Normal | InputState::Processing => Span::raw("Enter Input Mode "),
InputState::AddCW4Member { address } => {
if address.is_empty() {
Span::raw("Enter address of CW4 member to add to the group ")
} else {
Span::raw(format!("Enter voting weight of new member '{address}' "))
}
}
InputState::RemoveCW4Member => Span::raw("Enter address of CW4 member to remove it "),
InputState::AdvanceEpochState => {
Span::raw("press <ENTER> to attempt to advance the epoch state ")
}
InputState::SurpassedThreshold => {
Span::raw("press <ENTER> to attempt to surpass the threshold ")
}
};
Paragraph::new(self.input.value())
.style(if self.mode.expects_user_input() {
Style::default().fg(Color::Yellow)
} else {
Style::default()
})
.scroll((0, scroll as u16))
.block(
Block::default()
.borders(Borders::ALL)
.title(Line::from(vec![
mode_name,
Span::styled("(Press ", Style::default().fg(Color::DarkGray)),
Span::styled(
"/",
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Gray),
),
Span::styled(" to start, ", Style::default().fg(Color::DarkGray)),
Span::styled(
"ESC",
Style::default()
.add_modifier(Modifier::BOLD)
.fg(Color::Gray),
),
Span::styled(" to finish)", Style::default().fg(Color::DarkGray)),
])),
)
}
fn help_table(&self) -> Table {
let rows = vec![
Row::new(vec!["/", "Enter Input"]),
Row::new(vec!["ESC", "Exit Input"]),
Row::new(vec!["Enter", "Submit Input"]),
Row::new(vec!["<Ctrl-c>", "Quit"]),
Row::new(vec!["<Ctrl-d>", "Quit"]),
Row::new(vec!["<Ctrl-h>", "Open Help"]),
Row::new(vec!["<Ctrl-r>", "Force refresh contract state"]),
];
Table::new(rows)
.header(
Row::new(vec!["Key", "Action"])
.bottom_margin(1)
.style(Style::default().add_modifier(Modifier::BOLD)),
)
.widths(&[Constraint::Percentage(10), Constraint::Percentage(90)])
.column_spacing(1)
}
pub fn handle_key_events(&mut self, key: KeyEvent) -> anyhow::Result<Option<Action>> {
let action = match self.mode {
InputState::Normal | InputState::Processing => None,
_ => match key.code {
KeyCode::Esc => Some(Action::HomeAction(HomeAction::EnterNormal)),
KeyCode::Enter => Some(Action::HomeAction(HomeAction::ProcessInput(
self.input.value().to_string(),
))),
KeyCode::Left => Some(Action::HomeAction(HomeAction::PreviousInputMode)),
KeyCode::Right => Some(Action::HomeAction(HomeAction::NextInputMode)),
_ => {
self.input.handle_event(&crossterm::event::Event::Key(key));
None
}
},
};
Ok(action)
}
pub fn update(&mut self, action: HomeAction) -> anyhow::Result<Option<Action>> {
match action {
HomeAction::ToggleShowHelp => self.show_help = !self.show_help,
HomeAction::ScheduleContractRefresh => self.schedule_contract_refresh(),
HomeAction::RefreshDkgContract(update_info) => {
self.refresh_dkg_contract_info(*update_info)
}
HomeAction::ProcessInput(s) => self.handle_input(s),
HomeAction::SetLastContractError(err) => self.last_contract_error_message = err,
HomeAction::EnterNormal => {
self.mode = InputState::Normal;
self.last_contract_error_message = "".to_string();
}
HomeAction::StartInput => {
// make sure we're not already in the input mode
if !self.mode.expects_user_input() {
self.mode = InputState::AddCW4Member {
address: "".to_string(),
}
}
}
HomeAction::PreviousInputMode => {
self.mode = self.mode.previous();
self.last_contract_error_message = "".to_string();
}
HomeAction::NextInputMode => {
self.mode = self.mode.next();
self.last_contract_error_message = "".to_string();
}
HomeAction::EnterProcessing => {
self.mode = InputState::Processing;
self.throbber_state = Some(ThrobberState::default());
self.last_contract_error_message = "".to_string();
}
HomeAction::ExitProcessing => {
self.mode = InputState::Normal;
self.throbber_state = None;
}
HomeAction::FinishContractUpdate => {
self.refreshing = false;
}
}
Ok(None)
}
pub fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> anyhow::Result<()> {
let block = self.title_widget();
let inner_area = block.inner(rect);
f.render_widget(block, rect);
let rects = Layout::default()
.constraints(
[
Constraint::Min(8),
Constraint::Percentage(100),
Constraint::Min(3),
]
.as_ref(),
)
.split(inner_area);
let info_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(rects[0]);
self.contracts.bandwidth.base.draw(f, info_chunks[0])?;
self.contracts.dkg.base.draw(f, info_chunks[1])?;
self.contracts.group.base.draw(f, info_chunks[2])?;
self.contracts.multisig.base.draw(f, info_chunks[3])?;
self.draw_main_widget(f, rects[1])?;
// old input logic (I haven't touched it yet)
let width = rects[2].width.max(3) - 3; // keep 2 for borders and 1 for cursor
let scroll = self.input.visual_scroll(width as usize);
let input = self.input_widget(scroll);
f.render_widget(input, rects[2]);
if self.mode.expects_text_input() {
f.set_cursor(
(rects[2].x + 1 + self.input.cursor() as u16).min(rects[2].x + rects[2].width - 2),
rects[2].y + 1,
)
}
if let Some(throbber_state) = &mut self.throbber_state {
let full = throbber_widgets_tui::Throbber::default()
.label("Processing...")
.style(Style::default().yellow())
.throbber_style(Style::default().red().bold())
.throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE)
.use_type(throbber_widgets_tui::WhichUse::Spin);
f.render_stateful_widget(
full,
Rect {
x: rect.x + 2,
y: rect.height,
width: rect.width.saturating_sub(2),
height: 1,
},
throbber_state,
)
}
if self.show_help {
let rect = rect.inner(&Margin {
horizontal: 4,
vertical: 2,
});
f.render_widget(Clear, rect);
let block = Block::default()
.title(Line::from(vec![Span::styled(
"Key Bindings",
Style::default().add_modifier(Modifier::BOLD),
)]))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow));
f.render_widget(block, rect);
f.render_widget(
self.help_table(),
rect.inner(&Margin {
vertical: 4,
horizontal: 2,
}),
);
};
Ok(())
}
}
@@ -0,0 +1,2 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,69 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::action::{Action, LoggerAction};
use crossterm::event::KeyEvent;
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders};
use ratatui::Frame;
use std::collections::HashMap;
use tui_logger::{TuiLoggerLevelOutput, TuiLoggerSmartWidget, TuiWidgetState};
pub struct Logger {
state: TuiWidgetState,
pub keymap: HashMap<KeyEvent, Action>,
}
impl Logger {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Logger {
state: TuiWidgetState::new(),
keymap: Default::default(),
}
}
pub fn keymap(mut self, keymap: HashMap<KeyEvent, Action>) -> Self {
self.keymap = keymap;
self
}
pub fn tick(&mut self) {
//
}
pub fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> anyhow::Result<()> {
let block = Block::default().borders(Borders::ALL);
let inner_area = block.inner(rect);
f.render_widget(block, rect);
let tui_sm = TuiLoggerSmartWidget::default()
.style_error(Style::default().fg(Color::Red))
.style_warn(Style::default().fg(Color::Yellow))
.style_info(Style::default().fg(Color::Green))
.style_debug(Style::default().fg(Color::Cyan))
.style_trace(Style::default().fg(Color::Magenta))
.output_separator(':')
.output_timestamp(Some("%F %H:%M:%S%.3f".to_string()))
.output_level(Some(TuiLoggerLevelOutput::Long))
.output_target(true)
.output_file(true)
.output_line(true)
.state(&self.state);
f.render_widget(tui_sm, inner_area);
Ok(())
}
pub fn update(&mut self, action: LoggerAction) -> anyhow::Result<Option<Action>> {
match action {
LoggerAction::WidgetKeyEvent(event) => self.state.transition(&event),
};
Ok(None)
}
pub fn handle_key_events(&mut self, _key: KeyEvent) -> anyhow::Result<Option<Action>> {
Ok(None)
}
}
@@ -0,0 +1,9 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use ratatui::Frame;
pub mod basic_contract_info;
pub mod chain_history;
pub mod home;
pub mod logger;
@@ -0,0 +1,190 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::action::HomeAction;
use crate::{action::Action, app::Mode};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::collections::HashMap;
use std::ops::Deref;
use tui_logger::TuiWidgetEvent;
#[derive(Clone, Debug)]
pub struct KeyBindings(pub HashMap<Mode, HashMap<Vec<KeyEvent>, Action>>);
impl Deref for KeyBindings {
type Target = HashMap<Mode, HashMap<Vec<KeyEvent>, Action>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Default for KeyBindings {
fn default() -> Self {
let mut inner_home = HashMap::new();
let mut inner_logger = HashMap::new();
let mut inner_chain = HashMap::new();
// those are disgusting but can't be bothered to refactor it for global keybindings
// GLOBAL:
inner_home.insert(unchecked_keys("<tab>"), Action::NextTab);
inner_logger.insert(unchecked_keys("<tab>"), Action::NextTab);
inner_chain.insert(unchecked_keys("<tab>"), Action::NextTab);
inner_home.insert(unchecked_keys("<Ctrl-d>"), Action::Quit);
inner_home.insert(unchecked_keys("<Ctrl-c>"), Action::Quit);
inner_logger.insert(unchecked_keys("<Ctrl-d>"), Action::Quit);
inner_logger.insert(unchecked_keys("<Ctrl-c>"), Action::Quit);
inner_chain.insert(unchecked_keys("<Ctrl-d>"), Action::Quit);
inner_chain.insert(unchecked_keys("<Ctrl-c>"), Action::Quit);
// HOME
inner_home.insert(
unchecked_keys("<Ctrl-h>"),
HomeAction::ToggleShowHelp.into(),
);
inner_home.insert(
unchecked_keys("<Ctrl-r>"),
HomeAction::ScheduleContractRefresh.into(),
);
inner_home.insert(unchecked_keys("</>"), HomeAction::StartInput.into());
// LOGGER
inner_logger.insert(unchecked_keys("<space>"), TuiWidgetEvent::SpaceKey.into());
inner_logger.insert(unchecked_keys("<esc>"), TuiWidgetEvent::EscapeKey.into());
inner_logger.insert(
unchecked_keys("<pageup>"),
TuiWidgetEvent::PrevPageKey.into(),
);
inner_logger.insert(
unchecked_keys("<pagedown>"),
TuiWidgetEvent::NextPageKey.into(),
);
inner_logger.insert(unchecked_keys("<up>"), TuiWidgetEvent::UpKey.into());
inner_logger.insert(unchecked_keys("<down>"), TuiWidgetEvent::DownKey.into());
inner_logger.insert(unchecked_keys("<left>"), TuiWidgetEvent::LeftKey.into());
inner_logger.insert(unchecked_keys("<right>"), TuiWidgetEvent::RightKey.into());
inner_logger.insert(unchecked_keys("<+>"), TuiWidgetEvent::PlusKey.into());
inner_logger.insert(unchecked_keys("<->"), TuiWidgetEvent::MinusKey.into());
inner_logger.insert(unchecked_keys("<h>"), TuiWidgetEvent::HideKey.into());
inner_logger.insert(unchecked_keys("<f>"), TuiWidgetEvent::FocusKey.into());
let mut inner = HashMap::new();
inner.insert(Mode::Home, inner_home);
inner.insert(Mode::Logger, inner_logger);
inner.insert(Mode::ChainHistory, inner_chain);
KeyBindings(inner)
}
}
fn unchecked_keys(raw: &str) -> Vec<KeyEvent> {
parse_key_sequence(raw).expect("failed to parse the key sequence")
}
pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
return Err(format!("Unable to parse `{}`", raw));
}
let raw = if !raw.contains("><") {
let raw = raw.strip_prefix('<').unwrap_or(raw);
let raw = raw.strip_prefix('>').unwrap_or(raw);
raw
} else {
raw
};
let sequences = raw
.split("><")
.map(|seq| {
if let Some(s) = seq.strip_prefix('<') {
s
} else if let Some(s) = seq.strip_suffix('>') {
s
} else {
seq
}
})
.collect::<Vec<_>>();
sequences.into_iter().map(parse_key_event).collect()
}
fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
let raw_lower = raw.to_ascii_lowercase();
let (remaining, modifiers) = extract_modifiers(&raw_lower);
parse_key_code_with_modifiers(remaining, modifiers)
}
fn parse_key_code_with_modifiers(
raw: &str,
mut modifiers: KeyModifiers,
) -> Result<KeyEvent, String> {
let c = match raw {
"esc" => KeyCode::Esc,
"enter" => KeyCode::Enter,
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pageup" => KeyCode::PageUp,
"pagedown" => KeyCode::PageDown,
"backtab" => {
modifiers.insert(KeyModifiers::SHIFT);
KeyCode::BackTab
}
"backspace" => KeyCode::Backspace,
"delete" => KeyCode::Delete,
"insert" => KeyCode::Insert,
"f1" => KeyCode::F(1),
"f2" => KeyCode::F(2),
"f3" => KeyCode::F(3),
"f4" => KeyCode::F(4),
"f5" => KeyCode::F(5),
"f6" => KeyCode::F(6),
"f7" => KeyCode::F(7),
"f8" => KeyCode::F(8),
"f9" => KeyCode::F(9),
"f10" => KeyCode::F(10),
"f11" => KeyCode::F(11),
"f12" => KeyCode::F(12),
"space" => KeyCode::Char(' '),
"hyphen" => KeyCode::Char('-'),
"minus" => KeyCode::Char('-'),
"tab" => KeyCode::Tab,
c if c.len() == 1 => {
let mut c = c.chars().next().unwrap();
if modifiers.contains(KeyModifiers::SHIFT) {
c = c.to_ascii_uppercase();
}
KeyCode::Char(c)
}
_ => return Err(format!("Unable to parse {raw}")),
};
Ok(KeyEvent::new(c, modifiers))
}
fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
let mut modifiers = KeyModifiers::empty();
let mut current = raw;
loop {
match current {
rest if rest.starts_with("ctrl-") => {
modifiers.insert(KeyModifiers::CONTROL);
current = &rest[5..];
}
rest if rest.starts_with("alt-") => {
modifiers.insert(KeyModifiers::ALT);
current = &rest[4..];
}
rest if rest.starts_with("shift-") => {
modifiers.insert(KeyModifiers::SHIFT);
current = &rest[6..];
}
_ => break, // break out of the loop if no known prefix is detected
};
}
(current, modifiers)
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// based on https://github.com/ratatui-org/ratatui-async-template template
use crate::action::Action;
use crate::app::App;
use crate::cli::Args;
use crate::utils::{initialise_logger, initialize_panic_handler};
use clap::Parser;
use ratatui::layout::Rect;
use tokio::sync::mpsc::UnboundedReceiver;
use tracing::error;
pub mod action;
pub mod app;
pub mod cli;
pub mod components;
pub mod keybindings;
mod nyxd;
pub mod tui;
pub mod utils;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
initialize_panic_handler()?;
initialise_logger();
let args = Args::parse();
nym_network_defaults::setup_env(args.config_env_file.as_ref());
let (app, action_rx) = App::new(args).await?;
// app.run().await?;
run_app(app, action_rx).await
}
async fn run_app(mut app: App, mut action_rx: UnboundedReceiver<Action>) -> anyhow::Result<()> {
let mut tui = tui::Tui::new()?;
tui.enter()?;
loop {
// convert tui event to an application action
if let Some(event) = tui.next().await {
match event {
tui::Event::Tick => app.tick(),
tui::Event::Render => {
tui.draw(|f| {
if let Err(err) = app.draw(f) {
error!("failed to draw: {err:?}")
}
})?;
}
tui::Event::Resize(x, y) => {
tui.resize(Rect::new(0, 0, x, y))?;
tui.draw(|_f| {})?;
}
tui::Event::Key(key) => app.key_event(key)?,
_ => {}
}
}
// consume all emitted actions
while let Ok(action) = action_rx.try_recv() {
app.update(action)?
}
if app.should_quit {
break;
}
}
tui.exit()?;
Ok(())
}
+296
View File
@@ -0,0 +1,296 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::action::{BandwidthInfo, ContractsInfo, DkgInfo, GroupInfo, MultisigInfo};
use crate::cli::Args;
use crate::components::basic_contract_info::BasicContractInfo;
use crate::utils::zero_coin;
use futures::future::{join, join5};
use nym_coconut_dkg_common::types::Addr;
use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::nyxd::contract_traits::{
DkgQueryClient, DkgSigningClient, GroupQueryClient, GroupSigningClient, NymContractsProvider,
PagedDkgQueryClient, PagedGroupQueryClient,
};
use nym_validator_client::nyxd::cw4::Cw4Contract;
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::nyxd::{cw2, cw4, AccountId, CosmWasmClient};
use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient};
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use std::sync::Arc;
use tokio::sync::RwLock;
use url::Url;
#[derive(Clone)]
pub struct NyxdClient(Arc<RwLock<Inner>>);
struct Inner(DirectSigningHttpRpcNyxdClient);
impl Deref for Inner {
type Target = DirectSigningHttpRpcNyxdClient;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub struct DkgState {
pub mix_denom: String,
pub multisig_addr: Addr,
pub group_addr: Cw4Contract,
}
impl Inner {
// that's nasty, but it works
pub async fn get_dkg_state(&self) -> anyhow::Result<DkgState> {
let dkg_contract_address = &self
.dkg_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("dkg contract"))?;
let res = self
.query_contract_raw(dkg_contract_address, b"state".to_vec())
.await?;
Ok(serde_json::from_slice(&res).map_err(NyxdError::from)?)
}
pub async fn get_raw_cw2_version(
&self,
contract: &AccountId,
) -> anyhow::Result<cw2::ContractVersion> {
let res = self
.query_contract_raw(contract, b"contract_info".to_vec())
.await?;
Ok(serde_json::from_slice(&res).map_err(NyxdError::from)?)
}
}
impl NyxdClient {
pub async fn dkg_contract(&self) -> anyhow::Result<AccountId> {
Ok(self
.0
.read()
.await
.dkg_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("dkg contract"))?
.clone())
}
pub async fn group_contract(&self) -> anyhow::Result<AccountId> {
Ok(self
.0
.read()
.await
.group_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("group contract"))?
.clone())
}
pub async fn bandwidth_contract(&self) -> anyhow::Result<AccountId> {
Ok(self
.0
.read()
.await
.coconut_bandwidth_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("bandwidth contract"))?
.clone())
}
pub async fn multisig_contract(&self) -> anyhow::Result<AccountId> {
Ok(self
.0
.read()
.await
.multisig_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("multisig contract"))?
.clone())
}
pub async fn address(&self) -> AccountId {
self.0.read().await.address()
}
pub async fn dkg_update(&self) -> anyhow::Result<DkgInfo> {
let address = self.dkg_contract().await?;
let guard = self.0.read().await;
let balance_fut = guard.get_balance(&address, "unym".to_string());
let epoch_fut = guard.get_current_epoch();
let threshold_fut = guard.get_current_epoch_threshold();
let dealers_fut = guard.get_all_current_dealers();
let past_dealers_fut = guard.get_all_past_dealers();
let state_fut = guard.get_dkg_state();
let info_res = join5(
epoch_fut,
threshold_fut,
dealers_fut,
past_dealers_fut,
state_fut,
)
.await;
let dkg_epoch = info_res.0?;
let epoch_dealings_fut = guard.get_all_epoch_dealings(dkg_epoch.epoch_id);
let epoch_vk_shares_fut = guard.get_all_verification_key_shares(dkg_epoch.epoch_id);
let epoch_res = join(epoch_dealings_fut, epoch_vk_shares_fut).await;
Ok(DkgInfo {
base: BasicContractInfo {
name: "DKG Contract".to_string(),
address: address.to_string(),
balance: balance_fut.await?.unwrap_or(zero_coin()),
cw2_version: None,
build_info: None,
},
epoch: dkg_epoch,
threshold: info_res.1?,
dealers: info_res.2?,
past_dealers: info_res.3?,
debug_state: info_res.4?,
epoch_dealings: epoch_res.0?,
vk_shares: epoch_res.1?,
})
}
pub async fn group_update(&self) -> anyhow::Result<GroupInfo> {
let address = self.group_contract().await?;
let guard = self.0.read().await;
let balance_fut = guard.get_balance(&address, "unym".to_string());
let cw_version = guard.get_raw_cw2_version(&address);
let admin_fut = guard.admin();
let member_fut = guard.get_all_members();
let total_fut = guard.total_weight(None);
let res = join5(balance_fut, cw_version, admin_fut, member_fut, total_fut).await;
Ok(GroupInfo {
base: BasicContractInfo {
name: "CW4 Group Contract".to_string(),
address: address.to_string(),
balance: res.0?.unwrap_or(zero_coin()),
cw2_version: Some(res.1?),
build_info: None,
},
admin: res.2?,
members: res.3?,
total_weight: res.4?,
})
}
pub async fn bandwidth_update(&self) -> anyhow::Result<BandwidthInfo> {
let address = self.bandwidth_contract().await?;
let guard = self.0.read().await;
let balance_fut = guard.get_balance(&address, "unym".to_string());
Ok(BandwidthInfo {
base: BasicContractInfo {
name: "Coconut Bandwidth Contract".to_string(),
address: address.to_string(),
balance: balance_fut.await?.unwrap_or(zero_coin()),
cw2_version: None,
build_info: None,
},
})
}
pub async fn multisig_update(&self) -> anyhow::Result<MultisigInfo> {
let address = self.multisig_contract().await?;
let guard = self.0.read().await;
let balance_fut = guard.get_balance(&address, "unym".to_string());
let cw_version = guard.get_raw_cw2_version(&address);
let res = join(balance_fut, cw_version).await;
Ok(MultisigInfo {
base: BasicContractInfo {
name: "CW3 Flex Multisig".to_string(),
address: address.to_string(),
balance: res.0?.unwrap_or(zero_coin()),
cw2_version: Some(res.1?),
build_info: None,
},
})
}
pub async fn get_contract_update(&self) -> anyhow::Result<ContractsInfo> {
Ok(ContractsInfo {
dkg: self.dkg_update().await?,
group: self.group_update().await?,
bandwidth: self.bandwidth_update().await?,
multisig: self.multisig_update().await?,
})
}
pub async fn add_group_member(&self, addr: String, weight: u64) -> anyhow::Result<()> {
let member = cw4::Member { addr, weight };
// we need to have a write lock here so that we wouldn't accidentally send multiple transactions
// into the same block (and thus have invalid seq numbers)
self.0
.write()
.await
.update_members(vec![member], vec![], None)
.await?;
Ok(())
}
pub async fn remove_group_member(&self, address: String) -> anyhow::Result<()> {
// we need to have a write lock here so that we wouldn't accidentally send multiple transactions
// into the same block (and thus have invalid seq numbers)
self.0
.write()
.await
.update_members(vec![], vec![address], None)
.await?;
Ok(())
}
pub async fn try_advance_epoch_state(&self) -> anyhow::Result<()> {
// we need to have a write lock here so that we wouldn't accidentally send multiple transactions
// into the same block (and thus have invalid seq numbers)
self.0.write().await.advance_dkg_epoch_state(None).await?;
Ok(())
}
pub async fn try_surpass_threshold(&self) -> anyhow::Result<()> {
// we need to have a write lock here so that we wouldn't accidentally send multiple transactions
// into the same block (and thus have invalid seq numbers)
self.0.write().await.surpass_threshold(None).await?;
Ok(())
}
}
pub fn setup_nyxd_client(args: Args) -> anyhow::Result<(NyxdClient, Url)> {
let mut network_details = NymNetworkDetails::new_from_env();
if let Some(dkg_contract) = args.dkg_contract_address {
network_details.contracts.coconut_dkg_contract_address = Some(dkg_contract.to_string());
}
let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?;
let validator_endpoint = match args.nyxd_validator {
Some(endpoint) => endpoint,
None => network_details.endpoints[0].nyxd_url(),
};
Ok((
NyxdClient(Arc::new(RwLock::new(Inner(
DirectSigningHttpRpcNyxdClient::connect_with_mnemonic(
client_config,
validator_endpoint.as_ref(),
args.admin_mnemonic,
)?,
)))),
validator_endpoint,
))
}
+189
View File
@@ -0,0 +1,189 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crossterm::{
cursor,
event::{Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent},
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use futures::{FutureExt, StreamExt};
use ratatui::backend::CrosstermBackend as Backend;
use serde::{Deserialize, Serialize};
use std::{
ops::{Deref, DerefMut},
time::Duration,
};
use tokio::{
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
task::JoinHandle,
};
use tokio_util::sync::CancellationToken;
use tracing::error;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Event {
Init,
Error,
Tick,
Render,
FocusGained,
FocusLost,
Paste(String),
Key(KeyEvent),
Mouse(MouseEvent),
Resize(u16, u16),
}
pub struct Tui {
pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
pub task: JoinHandle<()>,
pub cancellation_token: CancellationToken,
pub event_rx: UnboundedReceiver<Event>,
pub event_tx: UnboundedSender<Event>,
}
impl Tui {
pub fn new() -> anyhow::Result<Self> {
let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
let (event_tx, event_rx) = mpsc::unbounded_channel();
let cancellation_token = CancellationToken::new();
let task = tokio::spawn(async {});
Ok(Self {
terminal,
task,
cancellation_token,
event_rx,
event_tx,
})
}
pub fn start(&mut self) {
let tick_delay = std::time::Duration::from_secs_f64(1.0 / 4.0);
let render_delay = std::time::Duration::from_secs_f64(1.0 / 30.0);
self.cancel();
self.cancellation_token = CancellationToken::new();
let _cancellation_token = self.cancellation_token.clone();
let _event_tx = self.event_tx.clone();
self.task = tokio::spawn(async move {
let mut reader = crossterm::event::EventStream::new();
let mut tick_interval = tokio::time::interval(tick_delay);
let mut render_interval = tokio::time::interval(render_delay);
_event_tx.send(Event::Init).unwrap();
loop {
let tick_delay = tick_interval.tick();
let render_delay = render_interval.tick();
let crossterm_event = reader.next().fuse();
tokio::select! {
_ = _cancellation_token.cancelled() => {
break;
}
maybe_event = crossterm_event => {
match maybe_event {
Some(Ok(evt)) => {
match evt {
CrosstermEvent::Key(key) => {
if key.kind == KeyEventKind::Press {
_event_tx.send(Event::Key(key)).unwrap();
}
},
CrosstermEvent::Mouse(mouse) => {
_event_tx.send(Event::Mouse(mouse)).unwrap();
},
CrosstermEvent::Resize(x, y) => {
_event_tx.send(Event::Resize(x, y)).unwrap();
},
CrosstermEvent::FocusLost => {
_event_tx.send(Event::FocusLost).unwrap();
},
CrosstermEvent::FocusGained => {
_event_tx.send(Event::FocusGained).unwrap();
},
CrosstermEvent::Paste(s) => {
_event_tx.send(Event::Paste(s)).unwrap();
},
}
}
Some(Err(_)) => {
_event_tx.send(Event::Error).unwrap();
}
None => {},
}
},
_ = tick_delay => {
_event_tx.send(Event::Tick).unwrap();
},
_ = render_delay => {
_event_tx.send(Event::Render).unwrap();
},
}
}
});
}
pub fn stop(&self) -> anyhow::Result<()> {
self.cancel();
let mut counter = 0;
while !self.task.is_finished() {
std::thread::sleep(Duration::from_millis(1));
counter += 1;
if counter > 50 {
self.task.abort();
}
if counter > 100 {
error!("Failed to abort task in 100 milliseconds for unknown reason");
break;
}
}
Ok(())
}
pub fn enter(&mut self) -> anyhow::Result<()> {
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?;
self.start();
Ok(())
}
pub fn exit(&mut self) -> anyhow::Result<()> {
self.stop()?;
if crossterm::terminal::is_raw_mode_enabled()? {
self.flush()?;
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
crossterm::terminal::disable_raw_mode()?;
}
Ok(())
}
pub fn cancel(&self) {
self.cancellation_token.cancel();
}
pub fn resume(&mut self) -> anyhow::Result<()> {
self.enter()?;
Ok(())
}
pub async fn next(&mut self) -> Option<Event> {
self.event_rx.recv().await
}
}
impl Deref for Tui {
type Target = ratatui::Terminal<Backend<std::io::Stderr>>;
fn deref(&self) -> &Self::Target {
&self.terminal
}
}
impl DerefMut for Tui {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.terminal
}
}
impl Drop for Tui {
fn drop(&mut self) {
self.exit().unwrap();
}
}
+127
View File
@@ -0,0 +1,127 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use nym_validator_client::nyxd::Coin;
use tracing::error;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, Layer};
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
let char;
let key_code = match key_event.code {
KeyCode::Backspace => "backspace",
KeyCode::Enter => "enter",
KeyCode::Left => "left",
KeyCode::Right => "right",
KeyCode::Up => "up",
KeyCode::Down => "down",
KeyCode::Home => "home",
KeyCode::End => "end",
KeyCode::PageUp => "pageup",
KeyCode::PageDown => "pagedown",
KeyCode::Tab => "tab",
KeyCode::BackTab => "backtab",
KeyCode::Delete => "delete",
KeyCode::Insert => "insert",
KeyCode::F(c) => {
char = format!("f({c})");
&char
}
KeyCode::Char(c) if c == ' ' => "space",
KeyCode::Char(c) => {
char = c.to_string();
&char
}
KeyCode::Esc => "esc",
KeyCode::Null => "",
KeyCode::CapsLock => "",
KeyCode::Menu => "",
KeyCode::ScrollLock => "",
KeyCode::Media(_) => "",
KeyCode::NumLock => "",
KeyCode::PrintScreen => "",
KeyCode::Pause => "",
KeyCode::KeypadBegin => "",
KeyCode::Modifier(_) => "",
};
let mut modifiers = Vec::with_capacity(3);
if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
modifiers.push("ctrl");
}
if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
modifiers.push("shift");
}
if key_event.modifiers.intersects(KeyModifiers::ALT) {
modifiers.push("alt");
}
let mut key = modifiers.join("-");
if !key.is_empty() {
key.push('-');
}
key.push_str(key_code);
key
}
pub fn initialize_panic_handler() -> anyhow::Result<()> {
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
.panic_section(format!(
"This is a bug. Consider reporting it at {}",
env!("CARGO_PKG_REPOSITORY")
))
.display_location_section(true)
.display_env_section(true)
.into_hooks();
eyre_hook.install()?;
std::panic::set_hook(Box::new(move |panic_info| {
if let Ok(mut t) = crate::tui::Tui::new() {
if let Err(r) = t.exit() {
error!("Unable to exit Terminal: {:?}", r);
}
}
let msg = format!("{}", panic_hook.panic_report(panic_info));
#[cfg(not(debug_assertions))]
{
eprintln!("{}", msg); // prints color-eyre stack trace to stderr
}
error!("Error: {}", strip_ansi_escapes::strip_str(msg));
#[cfg(debug_assertions)]
{
// Better Panic stacktrace that is only enabled when debugging.
better_panic::Settings::auto()
.most_recent_first(false)
.lineno_suffix(true)
.verbosity(better_panic::Verbosity::Full)
.create_panic_handler()(panic_info);
}
std::process::exit(1);
}));
Ok(())
}
pub fn initialise_logger() {
// tui_logger::set_default_level(log::LevelFilter::Trace);
let filter: EnvFilter = "trace,mio=warn,hyper=warn,tendermint_rpc=warn"
.parse()
.unwrap();
tracing_subscriber::registry()
.with(tui_logger::tracing_subscriber_layer().with_filter(filter))
.init();
}
pub fn zero_coin() -> Coin {
Coin::new(0, "unym")
}